Подтвердить что ты не робот

Являются ли невидимые ссылки еще актуальными в недавних JVM?

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

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

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

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


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

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

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

Это, по-видимому, является источником идеи о том, что обнуление ссылки на переменные помогло бы сборщику мусора, даже если переменная должна была выйти из области видимости.

4b9b3361

Ответ 1

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

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

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

... эффективная реализация 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 не создает невидимые переменные, когда цикл заканчивается, потому что маловероятно, что они будут снова объявлены вне цикла.

Любые мысли

Ответ 3

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

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

Ответ 4

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

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

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

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

  • Создание другой новой локальной переменной после того, как локальная переменная выходит из области действия, повторно использует свое место в фрейме стека. В следующем примере объект 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");
            }
        }
    }