Являются ли невидимые ссылки все еще проблемой в недавних JVM? - PullRequest
23 голосов
/ 07 ноября 2008

Я читал Производительность платформы Java (к сожалению, ссылка, кажется, исчезла из Интернета, так как я изначально задал этот вопрос), и раздел A.3.3 меня обеспокоил.

Я работал над предположением, что переменная, которая выпала из области видимости, больше не будет считаться корнем GC, но эта статья, по-видимому, противоречит этому.

У последних JVM, в частности версии 1.6.0_07 Sun, все еще есть это ограничение? Если так, то у меня много кода для анализа ...

Я задаю вопрос, потому что статья написана в 1999 году - иногда все меняется, особенно в мире GC.

<Ч />

Поскольку бумаги больше нет в наличии, я хотел бы перефразировать проблему. В документе подразумевается, что переменные, которые были определены внутри метода, будут считаться корнем GC до тех пор, пока метод не завершится, а не до окончания блока кода. Поэтому установка переменной на ноль была необходима, чтобы позволить объекту, на который ссылаются, собирать мусор.

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

Код из выбранного ответа хорошо иллюстрирует проблему. В версии JVM, на которую есть ссылка в документе, объект foo не может быть подвергнут сборке мусора, когда он выпадает из области видимости в конце блока try. Вместо этого JVM будет держать открытую ссылку до конца метода main (), даже при том, что никому не удастся использовать эту ссылку.

Похоже, что это происхождение идеи о том, что обнуление ссылки на переменную поможет сборщику мусора, даже если переменная вот-вот выпадет из области видимости.

Ответы [ 4 ]

6 голосов
/ 07 ноября 2008

Этот код должен очистить его:

public class TestInvisibleObject{
  public static class PrintWhenFinalized{
    private String s;
    public PrintWhenFinalized(String s){
      System.out.println("Constructing from "+s);
      this.s = s;
    }
    protected void finalize() throws Throwable {
      System.out.println("Finalizing from "+s);
    }   
  }
  public static void main(String[] args) {
    try {
        PrintWhenFinalized foo = new PrintWhenFinalized("main");
    } catch (Exception e) {
        // whatever
    }
    while (true) {
      // Provoke garbage-collection by allocating lots of memory
      byte[] o = new byte[1024];
    } 
  }
}

На моей машине (jdk1.6.0_05) она печатает:

Построение из основного

Завершение от основного

Похоже, проблемы были исправлены.

Обратите внимание, что использование System.gc () вместо цикла не приводит к тому, что объект по какой-то причине собирается.

2 голосов
/ 07 ноября 2008

В статье говорится, что:

... эффективная реализация JVM вряд ли обнулит ссылку когда он выходит за рамки

Я думаю, что это происходит из-за таких ситуаций:

public void doSomething() {  
    for(int i = 0; i < 10 ; i++) {
       String s = new String("boo");
       System.out.println(s);
    }
}

Здесь та же ссылка используется "эффективной JVM" в каждом объявлении String s, но в куче будет 10 новых строк, если GC не включится.

В примере статьи я думаю, что ссылка на foo хранится в стеке, потому что "эффективная JVM" считает , что весьма вероятно, что будет создан другой объект foo и, если так, он будет использовать та же ссылка. Мысли ???

public void run() {
    try {
        Object foo = new Object();
        foo.doSomething();
    } catch (Exception e) {
        // whatever
    }
    while (true) { // do stuff } // loop forever
}

Я также выполнил следующий тест с профилированием:

public class A {

    public static void main(String[] args) {
        A a = new A();  
        a.test4();
    }

    public void test1() {  
        for(int i = 0; i < 10 ; i++) {
           B b = new B();
           System.out.println(b.toString());
        }
        System.out.println("b is collected");
    }

    public void test2() {
        try {
            B b = new B();
            System.out.println(b.toString());
        } catch (Exception e) {
        }
        System.out.println("b is invisible");
    }

    public void test3() {
        if (true) {
            B b = new B();
            System.out.println(b.toString());
        }
        System.out.println("b is invisible");
    }

    public void test4() {
        int i = 0;
        while (i < 10) {
            B b = new B();
            System.out.println(b.toString());
            i++;
        }
        System.out.println("b is collected");
    }

    public A() {
    }

    class B {
        public B() {
        }

        @Override
        public String toString() {
            return "I'm B.";
        }
    }
}

и пришли к выводам:

teste1 -> b собрано

teste2 -> b невидим

teste3 -> b невидим

teste4 -> b собрано

... поэтому я думаю, что в циклах JVM не создает невидимые переменные, когда цикл заканчивается, поскольку маловероятно, что они будут снова объявлены вне цикла.

Есть мысли ??

1 голос
/ 23 августа 2013

Проблема все еще там. Я проверил это с Java 8 и смог доказать это.

Вы должны отметить следующие вещи:

  1. Единственный способ форсировать гарантированную сборку мусора - это попробовать выделение, которое заканчивается OutOfMemoryError, поскольку JVM требуется для освобождения неиспользуемых объектов перед выбросом. Это, однако, не выполняется, если запрашиваемая сумма слишком велика для успеха, т. Е. Превышает адресное пространство. Хорошей стратегией является попытка увеличить распределение до получения OOME.

  2. Гарантированный GC, описанный в пункте 1, не гарантирует завершение. Время, когда вызывается метод finalize (), не указано, они могут вообще никогда не вызываться. Поэтому добавление метода finalize () в класс может помешать его экземплярам собираться, поэтому finalize не является хорошим выбором для анализа поведения GC.

  3. Создание другой новой локальной переменной после того, как локальная переменная вышла из области видимости, будет повторно использовать ее место в кадре стека. В следующем примере объект a будет собран, так как его место в кадре стека занято локальной переменной b. Но b действует до конца основного метода, поскольку нет другой локальной переменной, которая могла бы занять его место.

    import java.lang.ref.*;
    
    public class Test {
        static final ReferenceQueue<Object> RQ=new ReferenceQueue<>();
        static Reference<Object> A, B;
        public static void main(String[] s) {
            {
                Object a=new Object();
                A=new PhantomReference<>(a, RQ);
            }
            {
                Object b=new Object();
                B=new PhantomReference<>(b, RQ);
            }
            forceGC();
            checkGC();
        }
    
        private static void forceGC() {
            try {
                for(int i=100000;;i+=i) {
                  byte[] b=new byte[i];
                }
            } catch(OutOfMemoryError err){ err.printStackTrace();}
        }
    
        private static void checkGC() {
            for(;;) {
                Reference<?> r=RQ.poll();
                if(r==null) break;
                if(r==A) System.out.println("Object a collected");
                if(r==B) System.out.println("Object b collected");
            }
        }
    }
    
1 голос
/ 07 ноября 2008

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

Я бы совсем не удивился, если бы он был незафиксирован в данный момент, но я не думаю, что он будет настолько значительным, насколько вы, похоже, боитесь.

...