2017-02-22 122 views
7

UPD 21.11.2017: ошибка исправлена ​​в версии JDK, см comment from Vicente RomeroJava «для» реализации личных данных предотвращает мусор сбора

Резюме:

Если for заявление, если используется для любой Iterable реализации своей коллекции будет оставаться в памяти кучи до конца текущей области (метод, тело оператора) и не будет собираться мусором, даже если у вас нет других ссылок на коллекцию, и приложение должно выделить новую память.

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8175883

https://bugs.openjdk.java.net/browse/JDK-8175883

Пример:

Если у меня есть следующий код, который выделяет список больших строк с произвольным содержимым:

import java.util.ArrayList; 
public class IteratorAndGc { 

    // number of strings and the size of every string 
    static final int N = 7500; 

    public static void main(String[] args) { 
     System.gc(); 

     gcInMethod(); 

     System.gc(); 
     showMemoryUsage("GC after the method body"); 

     ArrayList<String> strings2 = generateLargeStringsArray(N); 
     showMemoryUsage("Third allocation outside the method is always successful"); 
    } 

    // main testable method 
    public static void gcInMethod() { 

     showMemoryUsage("Before first memory allocating"); 
     ArrayList<String> strings = generateLargeStringsArray(N); 
     showMemoryUsage("After first memory allocation"); 


     // this is only one difference - after the iterator created, memory won't be collected till end of this function 
     for (String string : strings); 
     showMemoryUsage("After iteration"); 

     strings = null; // discard the reference to the array 

     // one says this doesn't guarantee garbage collection, 
     // Oracle says "the Java Virtual Machine has made a best effort to reclaim space from all discarded objects". 
     // but no matter - the program behavior remains the same with or without this line. You may skip it and test. 
     System.gc(); 

     showMemoryUsage("After force GC in the method body"); 

     try { 
      System.out.println("Try to allocate memory in the method body again:"); 
      ArrayList<String> strings2 = generateLargeStringsArray(N); 
      showMemoryUsage("After secondary memory allocation"); 
     } catch (OutOfMemoryError e) { 
      showMemoryUsage("!!!! Out of memory error !!!!"); 
      System.out.println(); 
     } 
    } 

    // function to allocate and return a reference to a lot of memory 
    private static ArrayList<String> generateLargeStringsArray(int N) { 
     ArrayList<String> strings = new ArrayList<>(N); 
     for (int i = 0; i < N; i++) { 
      StringBuilder sb = new StringBuilder(N); 
      for (int j = 0; j < N; j++) { 
       sb.append((char)Math.round(Math.random() * 0xFFFF)); 
      } 
      strings.add(sb.toString()); 
     } 

     return strings; 
    } 

    // helper method to display current memory status 
    public static void showMemoryUsage(String action) { 
     long free = Runtime.getRuntime().freeMemory(); 
     long total = Runtime.getRuntime().totalMemory(); 
     long max = Runtime.getRuntime().maxMemory(); 
     long used = total - free; 
     System.out.printf("\t%40s: %10dk of max %10dk%n", action, used/1024, max/1024); 
    } 
} 

компиляции и запустите его с помощью ограниченный объем памяти, как это (180 Мб):

javac IteratorAndGc.java && java -Xms180m -Xmx180m IteratorAndGc 

и во время выполнения я есть:

Перед первой памяти распределения: 1251k от максимального 176640k

После первого распределения памяти: 131426k из макс 176640k

После итерации: 131426k от максимального 176640k

После того, как силы GC в теле метода: 110682k от максимального 176640k (почти ничего не собирали)

Try для выделения памяти в теле метода снова:

 !!!! Out of memory error !!!!:  168948k of max  176640k 

ГХ после того, как тела метода: 459k от максимального 176640k (мусор собирают)

Третье распределение вне метода всегда успешно: 117740k от максимального 163840k

Таким образом, внутри gcInMethod() Я попытался выделить список, перебрать его, отбросить ссылку на список, (необязательно) принудительно собрать мусор и снова разместить аналогичный список. Но я не могу выделить второй массив из-за нехватки памяти.

В то же время за пределами тела функции я могу успешно принудительно собрать сбор мусора (необязательно) и снова распределить тот же размер массива!

Чтобы избежать этого OutOfMemoryError внутри тела функции этого достаточно, чтобы удалить/комментировать только одну строку:

for (String string : strings); < - это зло !!!

, а затем результат выглядит следующим образом:

Перед первой выделения памяти: 1251k от максимального 176640k

После первого выделения памяти: 131409k от максимального 176640k

После итерации: 131409k от максимального 176640k

После усилия GC в корпусе метода: 497k макс. 176640k (сбор мусора!)

Попытка выделить память в теле метода снова:

После вторичного распределения памяти: 115541k от максимального 163840k

ГХ после тела метода: 493k от максимального 163840k (! Мусор собирают)

третьего распределения вне метода всегда успешно: 121300k от максимального 163840k

Таким образом, без для итерируя мусор успешно собран после отбрасывания ссылки на строки и назначается второй раз (внутри тела функции) и выделен третий раз (вне метода).

Мое предположение:

для синтаксиса конструкция компилируется

Iterator iter = strings.iterator(); 
while(iter.hasNext()){ 
    iter.next() 
} 

(и я проверил это декомпиляции javap -c IteratorAndGc.class)

и выглядит ITER ссылочных пребывания в рамках до конца. У вас нет доступа к ссылке, чтобы свести его на нет, и GC не может выполнить сбор.

Может быть, это нормальное поведение (может быть, даже указано в JAVAC, но я не нашел), но ИМХО, если компилятор создает некоторые экземпляры он должен заботиться о отбрасывая их из сферы после использования.

Вот как я рассчитывать на реализацию for заявления:

Iterator iter = strings.iterator(); 
while(iter.hasNext()){ 
    iter.next() 
} 
iter = null; // <--- flush the water! 

Используется компилятор и среда выполнения версии Java:

javac 1.8.0_111 

java version "1.8.0_111" 
Java(TM) SE Runtime Environment (build 1.8.0_111-b14) 
Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode) 

Примечание:

  • Q uestion не о стиле программирования, передовой практике, соглашениях и т. д., вопрос об эффективности платформы Java .

  • речь идет не о System.gc() поведения (вы можете удалить все Gc вызовов из примера) - во втором распределении струн JVM должны освободить dicarded памяти.

Reference to the test java class, Online compiler to test (но этот ресурс имеет только 50 Мб динамической памяти, поэтому используйте N = 5000)

+2

Вы неправильно понимаете, как работает GC. Нет никакой гарантии, что GC будет собирать что-либо после одного звонка. – Andremoniy

+0

Черт, прочитайте описание, PLS! вопрос не о вызове GC !!! «вопрос не о поведении System.gc() (вы можете удалить все вызовы gc из примера) - во время выделения второй строки JVM должна освободить дисковое пространство». Вопрос о реализации «для». – radistao

+0

@ Andremoniy pls, прочитайте описание и удалите флаг дублирования - вопрос о реализации «для», а не о вызове System.gc() – radistao

ответ

0

Наконец, Oracle/Open ДКД ошибка принимается и не утвержден (не исправить до сих пор):

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8175883

https://bugs.openjdk.java.net/browse/JDK-8175883

Цитируя комментарии из нитей:

Этот является проблемой, воспроизводимой как на 8, так и на 9

Существуют некоторая проблема программа сохраняет свою собственную неявную автоматически сгенерированный ссылки на блок памяти до следующего неявного использования и его память в настоящее время заблокировано, что вызывает Oom

(это доказывает @vanza's expectation см this example from the JDK developer)

Согласно спецификации, это не должно произойти

(это ответ на мой вопрос: , если компилятор создает некоторый instanc эс он должен заботиться о отбрасывая их из сферы после использования)

UPD 21.11.2017: ошибки исправлена ​​в версии JDK см comment from Vicente Romero

5

Единственную соответствующую часть усовершенствована для заявления, здесь, является дополнительной локальной ссылкой на объект.

Ваш пример может быть сведено к

public class Example { 
    private static final int length = (int) (Runtime.getRuntime().maxMemory() * 0.8); 

    public static void main(String[] args) { 
     byte[] data = new byte[length]; 
     Object ref = data; // this is the effect of your "foreach loop" 
     data = null; 
     // ref = null; // uncommenting this also makes this complete successfully 
     byte[] data2 = new byte[length]; 
    } 
} 

Эта программа также не будет с OutOfMemoryError. Если вы удалите объявление ref (и его инициализацию), он завершится успешно.

Первое, что вам нужно понять, это то, что scope не имеет никакого отношения к сбору мусора. Scope is a compile time concept that defines where identifiers and names in a program's source code can be used to refer to program entities.

Garbage collection is driven by reachability. Если виртуальная машина может определить, что объект не может быть доступа к любому потенциальному непрерывному вычислению с любой живой нити, то он будет считать это правом для сбора мусора. Кроме того, System.gc() бесполезен, поскольку JVM будет выполнять большую коллекцию, если не может найти место для размещения нового объекта.

Поэтому возникает вопрос: почему не может JVM определить, что byte[] объект больше не доступен, если хранить его во второй локальной переменной?

У меня нет ответа на это. Различные алгоритмы сбора мусора (и JVM) могут вести себя по-разному в этом отношении. Кажется, что JVM не помещает объект как недоступный, когда вторая запись в таблице локальных переменных имеет ссылку на этот объект.


Вот другой сценарий, в котором виртуальная машина ведет себя не так, как вы migth ожидали в отношении сбора мусора:

+0

в примере вы сохраняете ссылку на массив байтов в объекте _ref_, не так ли? Вот почему он не может быть собран. Если я удаляю объявление _ref_ или устанавливаю на ** null ** - память может быть освобождена («делает это успешно», как вы говорите. Но я думаю, что компилятор или JVM не должны создавать никаких «мертвых» ссылок, которые заблокировать память, но больше не использовать. – radistao

+0

спасибо за ссылку «OutOfMemoryError, когда незащищенный блок кода закомментирован» - я также попытаюсь найти в ссылках байт-кода для цикла 'for' (но не так много опыта чтения декомпилированные коды) – radistao

+1

@radistao Вам не нужно читать байт-код. JLS определяет, как [расширенный для оператора] (http://docs.oracle.com/javase/specs/jls/se8/html/jls- 14.html # jls-14.14.2). Существует дополнительная ссылка на «Итератор» (который ссылается на ваш объект), как и вы предложили. –

4

Так что это на самом деле интересный вопрос которые могли бы извлечь выгоду из немного иной формулировки. Более конкретно, сосредоточение внимания на сгенерированном байт-коде вместо этого устранило бы большую путаницу. Итак, давайте сделаем это.

Учитывая этот код:

List<Integer> foo = new ArrayList<>(); 
for (Integer i : foo) { 
    // nothing 
} 

Это сгенерированный байткод:

0: new   #2     // class java/util/ArrayList 
    3: dup   
    4: invokespecial #3     // Method java/util/ArrayList."<init>":()V 
    7: astore_1  
    8: aload_1  
    9: invokeinterface #4, 1   // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; 
    14: astore_2  
    15: aload_2  
    16: invokeinterface #5, 1   // InterfaceMethod java/util/Iterator.hasNext:()Z 
    21: ifeq   37 
    24: aload_2  
    25: invokeinterface #6, 1   // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; 
    30: checkcast  #7     // class java/lang/Integer 
    33: astore_3  
    34: goto   15 

Итак, играть по игре:

  • магазин новый список в локальной переменной 1 (» foo ")
  • Храните итератор в локальной переменной 2
  • Для каждого элемента, хранить элемент в локальной переменной 3

Следует отметить, что после того, как петли, нет никакой очистки от всего, что было использовано в цикле. Это не ограничивается итератором: последний элемент все еще сохраняется в локальной переменной 3 после окончания цикла, хотя в коде нет ссылки на него.

Итак, прежде чем идти «что это неправильно, неправильно, неправильно», давайте посмотрим, что происходит, когда я добавить этот код после этого кода выше:

byte[] bar = new byte[0]; 

Вы получаете этот байткод после цикла:

37: iconst_0  
    38: newarray  byte 
    40: astore_2  

О, посмотрите на это. Новая заявленная локальная переменная хранится в той же «локальной переменной», что итератор. Итак, теперь ссылка на итератор исчезла.

Обратите внимание, что это отличается от кода Java, который вы считаете эквивалентным. Фактическая Java эквивалент, который генерирует тот же байт-код, это:

List<Integer> foo = new ArrayList<>(); 
for (Iterator<Integer> i = foo.iterator(); i.hasNext();) { 
    Integer val = i.next(); 
} 

И еще нет очистки. Почему это?

Ну, здесь мы находимся в зоне угадывания, если только это не указано в спецификации JVM (не проверено). В любом случае, чтобы выполнить очистку, компилятор должен был бы генерировать дополнительный байт-код (2 инструкции, aconst_null и astore_<n>) для каждой переменной, выходящей за пределы области видимости. Это означало бы, что код работает медленнее; и чтобы этого избежать, возможно, в JIT должны быть добавлены сложные оптимизации.

Итак, почему ваш код не работает?

Вы попадаете в аналогичную ситуацию, как указано выше. Итератор выделяется и хранится в локальной переменной 1. Затем ваш код пытается выделить новый массив строк и, поскольку локальная переменная 1 больше не используется, она будет храниться в той же локальной переменной (проверьте байт-код). Но распределение происходит до назначения, поэтому есть ссылка на итератор, поэтому памяти нет.

Если добавить эту строку перед try блока, все работает, даже если вы удалите System.gc() вызов:

int i = 0; 

Итак, похоже, разработчики JVM сделали выбор (генерировать меньше/более эффективного байткод вместо явно обнуляющих переменные, выходящие за рамки), и у вас есть написанный код, который не очень хорошо себя ведет в предположениях, сделанных ими о том, как люди пишут код.Учитывая, что я никогда не видел эту проблему в реальных приложениях, мне кажется незначительной.

+0

, это выглядит для меня наиболее применимым ответом: «автоматически сгенерированный идентификатор» для ссылки итератора сохраняет память до следующего использования идентификатора. – radistao

+0

, но хотя: 1) если это угловой регистр - он должен быть учтен (хороший разработчик пропускает угловые случаи?); 2) платформа (компилятор + виртуальная машина) не должна работать неопределенным образом, когда переменная может быть или не может быть повторно использована, таким образом, память освобождается или нет; 3) выбор для «более эффективного» жертвоприношения стабильности и детерминизма кажется не лучшим выбором; 4) этот «угловой случай», который я нашел в своем рабочем приложении, когда после большого анализа XML и итерации я вышел из состояния нехватки памяти. – radistao

3

Как уже указывалось в других ответах, понятие переменных областей не известно во время выполнения. В скомпилированных файлах классов локальные переменные представляют собой только места в кадре стека (адресованные индексом), к которым выполняются записи и чтения. Если у нескольких переменных есть области дизъюнкции, они могут использовать один и тот же индекс, но формального объявления их не существует. Только запись нового значения отбрасывает старый.

Итак, есть три способа, как ссылка проводится в локальном переменном хранении можно считать неиспользованной:

  1. Место хранения заменяется новым значением
  2. Метод выхода материала
  3. Нет последующий код считывает значение

Должно быть очевидно, что третий пункт является самым сложным для проверки, следовательно, он не всегда применяется, но когда оптимизатор начинает свою работу, он может приводят к неожиданностям в другом направлении, как описано в «Can java finalize an object when it is still in scope?» и «finalize() called on strongly reachable object in Java 8».

В вашем случае приложение выполняется очень коротко и, вероятно, не оптимизировано, что может привести к тому, что ссылки не будут распознаны как неиспользуемые из-за пункта 3, если точки 1 и 2 не применяются.

Вы можете легко убедиться, что это так. Когда вы измените строку

ArrayList<String> strings2 = generateLargeStringsArray(N); 

в

ArrayList<String> strings2 = null; 
strings2 = generateLargeStringsArray(N); 

OutOfMemoryError уходит. Причина в том, что место хранения, содержащее Iterator, использованное в предыдущем цикле for, не было перезаписано в этот момент. Новая локальная переменная strings2 будет повторно использовать хранилище, но это проявляется только тогда, когда на нее фактически записывается новое значение. Таким образом, инициализация с nullдо, вызывающая generateLargeStringsArray(N), перезапишет ссылку Iterator и позволяет собирать старый список.

В качестве альтернативы вы можете запустить программу в исходной форме, используя опцию -Xcomp. Это приводит к компиляции всех методов. На моей машине он имел заметное замедление загрузки, но из-за анализа использования переменных, OutOfMemoryError также ушел.

Наличие приложения, которое выделяет столько памяти (по сравнению с максимальным размером кучи) во время инициализации, то есть когда большинство методов интерпретируется, является необычным угловым случаем. Обычно большинство горячих методов достаточно скомпилированы до того, как потребление памяти будет высоким. Если вы неоднократно сталкиваетесь с этим угловым случаем в реальном приложении, то -Xcomp может работать на вас.

0

Просто суммировать ответы:

Как @ Sotirios-delimanolis упоминается in his comment о The enhanced for statement - мое предположение явно определено: для сахара заявление составляется в Iterator с hasNext() - next() звонки:

#i - это автоматически сгенерированный идентификатор, отличный от любых других идентификаторов (автоматически сгенерированных или иных), которые находятся в области (§6.3) в точке, где en выведено для утверждения.

Как тогда @vanza showed in his answer: это автоматически генерируется идентификатор может быть или может быть не переопределены позже. Если он переопределен - память может быть выпущена, а если нет - память больше не будет выпущена.

На данный момент у меня нет ответа на вопрос: Если Java-компилятор или JVM создает некоторые неявные ссылки, не следует ли потом позже отказаться от этих ссылок? Есть ли гарантия, что одна и та же автогенерированная ссылка итератора будет повторно использоваться при следующих вызовах перед следующим распределением памяти? Разве это не должно быть правило: те, кто выделяет память, заботятся о ее освобождении? Я бы сказал - это должен заботиться об этом. В противном случае поведение не определено (оно может упасть до OutOfMemoryError или может не быть - кто знает ...)

Да, мой пример - это угловой регистр (ничего не инициализировано между итератором for), но это doesn 't средний невозможно кейс. И это не означает, что этот случай трудно достичь - вполне вероятно, что он работает в ограниченной среде памяти с некоторыми большими данными и перераспределяет память сразу же, как она была использована. Я нашел этот случай в своем рабочем приложении, где я разбираю большой XML, который «ест» более половины памяти.

(и вопрос не только об итераторе и for циклах, предположим, что это обычная проблема: компилятор или JVM иногда не очищают собственные неявные ссылки).

+0

Ну, как указано в моем ответе, если вы действительно сталкиваетесь с этим необычным сценарием и имеете это давление памяти, есть опция, позволяющая вам торговать некоторыми циклами ЦП для их исправления. Ваш случай также необычен тем, что он создает большой список, итерации над ним один раз и затем его отбрасывает. Как правило, списки имеют гораздо более продолжительный срок службы, чем итератор, и это не то, что повреждает итератор. – Holger

+0

Если даже это угловой регистр, его следует обработать правильно. Когда вы создаете приложение, вы пропускаете обработку и тестирование угловых случаев? Такие вещи, как приложение Java, должны работать предсказуемым образом даже в редких случаях. И не следует ретранслировать «ссылка может быть или не может быть повторно использована другими вызовами». Существует также ** никаких гарантий, что эта автоматически сгенерированная ссылка будет повторно использована ** и память будет выпущена! Я нашел этот случай в своей практике, поэтому я размещаю проблему здесь. – radistao

+0

Я боюсь, если вы предположите, что «приложение Java должно работать предсказуемым образом», и мы говорим только о потреблении памяти и производительности, Java может быть неправильной для вас. Вы не можете предположить мгновенное освобождение памяти, вы не можете предсказать максимальную глубину рекурсии, и нет гарантии о производительности. В спецификации нет гарантии, которая была бы нарушена здесь. Кроме того, как уже было сказано, ваш код работает так, как ожидалось, если вы запустите его с опцией '-Xcomp'. – Holger

2

Спасибо за сообщение об ошибке. Мы исправили эту ошибку, см. JDK-8175883. Как отметил здесь, в случае усиливается для, Javac был генерации синтетических переменных, так что для следующего кода:

void foo(String[] data) { 
    for (String s : data); 
} 

Javac был приблизительно генерации:

for (String[] arr$ = data, len$ = arr$.length, i$ = 0; i$ < len$; ++i$) { 
    String s = arr$[i$]; 
} 

как упомянуто выше этого перевода подход предполагает что синтетическая переменная arr $ содержит ссылку на массив данные, что мешает GC собирать массив, если он больше не упоминается внутри метода. Эта ошибка была исправлена ​​путем генерации этого кода:

String[] arr$ = data; 
String s; 
for (int len$ = arr$.length, i$ = 0; i$ < len$; ++i$) { 
    s = arr$[i$]; 
} 
arr$ = null; 
s = null; 

Идея заключается в том, чтобы установить нулевое значение любого синтетические переменного ссылочного типа, созданный JAVAC перевести петлю. Если бы мы говорили о массиве примитивного типа, то последнее присваивание null не генерируется компилятором. Ошибка была исправлена ​​в репо JDK repo

+0

Спасибо за обновление! – radistao

+0

уверен, что @radistao, np –