Изящная доработка референта SoftReference

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

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

Единственная проблема, которую я вижу в SoftReferences, заключается в том, что нет чистого способа окончательного оформления их референтов. В моем случае, прежде чем уничтожить дескриптор поиска, мне нужно закрыть его, иначе в системе могут закончиться файловые дескрипторы. Очевидно, я могу обернуть этот дескриптор в другой объект, написать на него финализатор (или зацепиться за ReferenceQueue/PhantomReference) и отпустить. Но эй, каждая статья на этой планете советует не использовать финализаторы, и особенно - финализаторы для освобождения файловых дескрипторов (например, Effective Java ed. II, page 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, верно?


person mindas    schedule 28.10.2009    source источник


Ответы (4)


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

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, чтобы гарантировать, что вы храните не более тыкает пальцем в воздух 500 открытых файлов. Они также могут иметь тип MyReference, чтобы при необходимости их также можно было удалить сборщиком мусора.

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

EDIT: Обновлен код, чтобы он компилировался при попадании в класс. Он использует StringReader, чтобы проиллюстрировать концепцию закрытия Reader, который используется для представления внешнего ресурса, который необходимо освободить. В этом конкретном случае закрытие этого потока фактически не является операцией и поэтому не требуется, но показывает, как это сделать, если это необходимо.

person Paul Wagland    schedule 03.12.2009
comment
Можно ли исправить ваш код, чтобы он компилировался? Например. Конструктор MyReference принимает референтный аргумент Bloat и должен присвоить его hardRef, но hardRef имеет совершенно другой тип (ResourceThatMustBeClosed). Кроме того, не могли бы вы уточнить, почему Bloat по-прежнему необходим, когда у нас есть ResourceThatMustBeClosed? P.S. Я бы не был таким нуждающимся, если бы к этому вопросу не прилагались бонусные баллы: P - person mindas; 04.12.2009
comment
Я обновил код и (надеюсь) добавил четкое объяснение того, как это работает? Если нет, дайте мне знать... - person Paul Wagland; 04.12.2009
comment
Код фиксируется, чтобы он компилировался. Просто бросьте его в пустой класс и добавьте соответствующий импорт. - person Paul Wagland; 05.12.2009

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

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

(Мой ответ № 1000. Опубликовано с Лондонского дня разработчиков.)

person Tom Hawtin - tackline    schedule 28.10.2009
comment
Я удивлен, что это было отдаленно когерентным (так ли это?) после часа или около того сна, дня в затемненной комнате (с плохим обслуживанием кофе, хотя работающий Wi-Fi) и попытки слушать говорящего. Но это нужно было сделать. - person Tom Hawtin - tackline; 28.10.2009
comment
Том, не могли бы вы опубликовать (или отредактировать этот) более подробный ответ, в конечном итоге сопровождаемый некоторым (псевдо) кодом? У меня тоже был трудный день, может быть, завтра я пойму это лучше, но сейчас, к сожалению, я, похоже, не в состоянии. - person Marius Burz; 29.10.2009
comment
@marcob Я думаю, что аналогичную реализацию можно найти здесь: javaspecialists.eu/archive/Issue015.html Возможно, вы захотите добавить дженерики поверх него, так как это, похоже, было закодировано еще в 2001 году. Изначально я хотел использовать мягкие значения с MapMaker для коллекций Google, но не смог найти там возможность подключить пользовательскую логику завершения. Я отправил сообщение в список рассылки google-collections и посмотрю, смогу ли я что-нибудь из него извлечь. - person mindas; 29.10.2009
comment
Прочитав это, я стараюсь избегать этого сайта: при очень внимательном осмотре я обнаружил большую разницу между фантомными и слабыми ссылками. Обе освобождаются довольно быстро, но фантомная ссылка ставится в очередь ссылок до того, как ее референт будет очищен, тогда как слабая ссылка ставится в очередь после очистки референта. Это доказывает явное непонимание того, что представляют собой эти ссылки и как они работают. Так что спасибо, но нет. - person Marius Burz; 30.10.2009

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

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");
            }

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

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

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

person Ran Biron    schedule 06.12.2009
comment
Забыл упомянуть - запуск этого примера с -Xmx 10mb не дает OOM и перечисляет все виды чисел (предполагаемая ключевая информация). - person Ran Biron; 06.12.2009

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

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

Несколько моментов:

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

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

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

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

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

person mindas    schedule 07.12.2009
comment
i++ - да, вероятно, не прошел через копирование/вставку. нет необходимости в remove() - правильно. У меня не хватает примерно половины ссылок. - person Ran Biron; 07.12.2009