Изящно завершение референта SoftReference - PullRequest
7 голосов
/ 28 октября 2009

Я использую поисковую библиотеку, которая рекомендует держать объект дескриптора поиска открытым, для этого может быть полезен кеш запросов. Со временем я заметил, что кэш имеет тенденцию к раздуванию (несколько сотен мегабайт и продолжает расти), и OOM начали набирать обороты. Нет никакого способа установить ограничения этого кеша и не планировать, сколько памяти он может использовать. Поэтому я увеличил предел Xmx , но это только временное решение проблемы.

В конце концов, я думаю сделать этот объект референтом из java.lang.ref.SoftReference. Таким образом, если в системе не хватает свободной памяти, объект будет отключен, а новый будет создан по требованию. Это уменьшит скорость после нового старта, но это гораздо лучшая альтернатива, чем удар по OOM.

Единственная проблема, которую я вижу в SoftReferences, заключается в том, что не существует чистого способа получить окончательную версию их референтов. В моем случае перед уничтожением дескриптора поиска мне нужно закрыть его, иначе в системе могут не хватить файловых дескрипторов. Очевидно, что я могу обернуть этот дескриптор в другой объект, написать на нем финализатор (или подключить к ReferenceQueue / PhantomReference) и отпустить. Но, эй, каждая статья на этой планете советует не использовать финализаторы, и особенно - против финализаторов для освобождения файловых дескрипторов (например, Effective Java ed. II, стр. 27).

Так что я несколько озадачен. Должен ли я тщательно игнорировать все эти советы и идти дальше. Иначе, есть ли другие жизнеспособные альтернативы? Заранее спасибо.

РЕДАКТИРОВАТЬ # 1: Текст ниже был добавлен после тестирования некоторого кода в соответствии с предложением Тома Хоутина. Мне кажется, что либо предложение не работает, либо я что-то упустил. Вот код:

class Bloat {  // just a heap filler really
   private double a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;

   private final int ii;

   public Bloat(final int ii) {
      this.ii = ii;
   }
}

// as recommended by Tom Hawtin
class MyReference<T> extends SoftReference<T> {
   private final T hardRef;

   MyReference(T referent, ReferenceQueue<? super T> q) {
      super(referent, q);
      this.hardRef = referent;
   }
}

//...meanwhile, somewhere in the neighbouring galaxy...
{
   ReferenceQueue<Bloat> rq = new ReferenceQueue<Bloat>();
   Set<SoftReference<Bloat>> set = new HashSet<SoftReference<Bloat>>();
   int i=0;

   while(i<50000) {
//      set.add(new MyReference<Bloat>(new Bloat(i), rq));
      set.add(new SoftReference<Bloat>(new Bloat(i), rq));

//      MyReference<Bloat> polled = (MyReference<Bloat>) rq.poll();
      SoftReference<Bloat> polled = (SoftReference<Bloat>) rq.poll();

      if (polled != null) {
         Bloat polledBloat = polled.get();
         if (polledBloat == null) {
           System.out.println("is null :(");
         } else {
           System.out.println("is not null!");
         }
      }
      i++;
   }
}

Если я запускаю приведенный выше фрагмент с -Xmx10m и SoftReferences (как в коде выше), я получаю напечатанные тонны is null :(. Но если я заменяю код на MyReference (раскомментируя две строки с MyReference и комментируя с SoftReference), я всегда получаю OOM.

Как я понял из совета, наличие жесткой ссылки внутри MyReference не должно предотвращать попадание объекта ReferenceQueue, верно?

Ответы [ 4 ]

7 голосов
/ 28 октября 2009

Для конечного числа ресурсов: подкласс SoftReference. Мягкая ссылка должна указывать на охватывающий объект. Сильная ссылка в подклассе должна ссылаться на ресурс, поэтому он всегда сильно доступен. При прочтении ReferenceQueue poll ресурс может быть закрыт и удален из кэша. Кэш должен быть освобожден правильно (если SoftReference сам по себе сборщик мусора, он не может быть помещен в очередь на ReferenceQueue).

Будьте осторожны, чтобы у вас было только конечное количество ресурсов, не выпущенных в кеш - исключите старые записи (действительно, вы можете отбросить мягкие ссылки с помощью кеша if finite, если это подходит вашей ситуации). Обычно дело в том, что это не ресурс памяти, который более важен, и в этом случае кеша удаления LRU без экзотических эталонных объектов должно быть достаточно.

(Мой ответ # 1000. Отправлено из лондонского DevDay.)

5 голосов
/ 04 декабря 2009

Ответ Томса правильный, однако код, добавленный к вопросу, не совпадает с предложенным Томом. То, что предлагал Том, больше похоже на это:

class Bloat {  // just a heap filler really
    public Reader res;
    private double a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;

    private final int ii;

    public Bloat(final int ii, Reader res) {
       this.ii = ii;
       this.res = res;
    }
 }

 // as recommended by Tom Hawtin
 class MySoftBloatReference extends SoftReference<Bloat> {
    public final Reader hardRef;

    MySoftBloatReference(Bloat referent, ReferenceQueue<Bloat> q) {
       super(referent, q);
       this.hardRef = referent.res;
    }
 }

 //...meanwhile, somewhere in the neighbouring galaxy...
 {
    ReferenceQueue<Bloat> rq = new ReferenceQueue<Bloat>();
    Set<SoftReference<Bloat>> set = new HashSet<SoftReference<Bloat>>();
    int i=0;

    while(i<50000) {
        set.add(new MySoftBloatReference(new Bloat(i, new StringReader("test")), rq));

        MySoftBloatReference polled = (MySoftBloatReference) rq.poll();

        if (polled != null) {
            // close the reference that we are holding on to
            try {
                polled.hardRef.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        i++;
    }
}

Обратите внимание, что большая разница в том, что жесткая ссылка на объект, который должен быть закрыт. Окружающий объект может и будет собирать мусор, поэтому вы не попадете в OOM, однако у вас все еще есть шанс закрыть ссылку. Как только вы выйдете из цикла, это также будет сбор мусора. Конечно, в реальном мире вы, вероятно, не сделаете res членом публичного экземпляра.

Тем не менее, если вы держите открытые ссылки на файлы, вы рискуете исчерпать их, прежде чем исчерпаете память. Вы, вероятно, также захотите иметь кэш LRU, чтобы держать не более 10000 * 500 открытых файлов. Они также могут относиться к типу MyReference, чтобы при необходимости их можно было собирать мусором.

Чтобы немного пояснить, как работает MySoftBloatReference, базовый класс, то есть SoftReference, все еще содержит ссылку на объект, занимающий всю память. Это объект, который необходимо освободить, чтобы предотвратить появление OOM. Однако, если объект освобожден, вам все равно нужно освободить ресурсы, которые использует Bloat, то есть Bloat использует два типа ресурсов, память и дескриптор файла, оба эти ресурса должны быть освобождены, или вы запускаете из одного или другого ресурса. SoftReference справляется с нагрузкой на ресурс памяти, освобождая этот объект, однако вам также необходимо освободить другой ресурс, дескриптор файла. Поскольку Bloat уже был освобожден, мы не можем использовать его для освобождения соответствующего ресурса, поэтому MySoftBloatReference хранит жесткую ссылку на внутренний ресурс, который необходимо закрыть. Как только информация о том, что Bloat освобожден, то есть, когда ссылка появляется в ReferenceQueue, MySoftBloatReference также может закрыть связанный ресурс через жесткую ссылку, которая у него есть.

РЕДАКТИРОВАТЬ: Обновлен код, чтобы он компилировался при броске в класс. Он использует StringReader, чтобы проиллюстрировать концепцию закрытия Reader, который используется для представления внешнего ресурса, который необходимо освободить. В этом конкретном случае закрытие этого потока по сути является запретом и поэтому не требуется, но показывает, как это сделать, если это необходимо.

2 голосов
/ 06 декабря 2009

Ahm.
(Насколько я знаю) Вы не можете держать палку с обоих концов. Либо вы держите свою информацию, либо вы отпускаете ее.
Однако ... вы можете придерживаться некоторой ключевой информации, которая позволит вам завершить работу. Конечно, ключевая информация должна быть значительно меньше, чем «реальная информация», и не должна содержать реальной информации в своем графе достижимых объектов (в этом вам могут помочь слабые ссылки).
Опираясь на существующий пример (обратите внимание на ключевое информационное поле):

public class Test1 {
    static class Bloat {  // just a heap filler really
        private double a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z;

        private final int ii;

        public Bloat(final int ii) {
            this.ii = ii;
        }
    }

    // as recommended by Tom Hawtin
    static class MyReference<T, K> extends SoftReference<T> {
        private final K keyInformation;

        MyReference(T referent, K keyInformation, ReferenceQueue<? super T> q) {
            super(referent, q);
            this.keyInformation = keyInformation;
        }

        public K getKeyInformation() {
            return keyInformation;
        }
    }

    //...meanwhile, somewhere in the neighbouring galaxy...
    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Bloat> rq = new ReferenceQueue<Bloat>();
        Set<SoftReference<Bloat>> set = new HashSet<SoftReference<Bloat>>();
        int i = 0;

        while (i < 50000) {
            set.add(new MyReference<Bloat, Integer>(new Bloat(i), i, rq));

            final Reference<? extends Bloat> polled = rq.poll();

            if (polled != null) {
                if (polled instanceof MyReference) {
                    final Object keyInfo = ((MyReference) polled).getKeyInformation();
                    System.out.println("not null, got key info: " + keyInfo + ", finalizing...");
                } else {
                    System.out.println("null, can't finalize.");
                }
                rq.remove();
                System.out.println("removed reference");
            }

Edit:
Я хочу уточнить, что «либо держи свою информацию, либо отпускай». Предполагая, что у вас был какой-то способ хранения вашей информации. Это вынудило бы GC снять отметки с ваших данных, в результате чего данные будут действительно очищены только после того, как вы закончите с этим, во втором цикле GC. Это возможно - и это именно то, для чего finalize (). Поскольку вы заявили, что не хотите, чтобы происходил второй цикл, вы не можете хранить свою информацию (если a -> b, то! B ->! A). что означает, что вы должны отпустить.

Edit2:
На самом деле, произойдет второй цикл - но для ваших «ключевых данных», а не для ваших «основных данных раздувания». Фактические данные будут удалены в первом цикле.

Edit3:
Очевидно, что реальное решение будет использовать отдельный поток для удаления из ссылочной очереди (не опрашивать (), удалять (), блокировать выделенный поток).

0 голосов
/ 07 декабря 2009

@ Павел - большое спасибо за ответ и разъяснения.

@ Ran - Я думаю, что в вашем текущем коде i ++ отсутствует в конце цикла. Кроме того, вам не нужно выполнять rq.remove () в цикле, поскольку rq.poll () уже удаляет верхнюю ссылку, не так ли?

Несколько баллов:

1) Мне пришлось добавить оператор Thread.sleep (1) после цикла i ++ в цикле (для обоих решений Пола и Рана), чтобы избежать OOM, но это не имеет отношения к общей картине и также зависит от платформы. Моя машина имеет четырехъядерный процессор и работает под управлением Sun Linux 1.6.0_16 JDK.

2) Посмотрев на эти решения, я думаю, что буду придерживаться финализаторов. Книга Блоха приводит следующие причины:

  • нет никакой гарантии, что финализаторы будут выполнены быстро, поэтому никогда не делайте что-либо критичное по времени в финализаторе - и нет никаких гарантий для SoftRereferences!
  • Никогда не зависите от финализатора для обновления критического постоянного состояния - я не
  • существует серьезное ухудшение производительности при использовании финализаторов - в моем худшем случае я буду финализировать примерно один объект в минуту или около того. Я думаю, что могу жить с этим.
  • используйте попробовать / наконец - о да, я определенно буду!

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

3) К сожалению, невозможно разделить точки между Полом, Томом и Раном :( Надеюсь, Том не возражает, так как у него их уже много :) Судить между Полом и Раном было гораздо сложнее - я думаю, что оба ответа работают и верны. Я только устанавливаю флаг согласия на ответ Пола, потому что он был оценен выше (и имеет более подробное объяснение), но решение Рана совсем не плохое и, вероятно, было бы моим выбором, если бы я решил реализовать его с помощью SoftReferences. Спасибо, ребята!

...