Типичные случаи утечки памяти в Java

Большинству разработчиков известно, что сборщик мусора в Java не является универсальным механизмом, позволяющим программисту полностью забыть о правилах использования памяти и о том, в каких случаях осуществляется его работа. Ниже описаны типичные случаи утечки памяти в java-приложениях, встречающиеся повсеместно.
Итак, о чём должен помнить каждый java-программист.

Типичная ситуация утечки памяти:

image
Сборщик мусора периодически собирает неиспользуемые объекты, но мы видим, что график использования кучи уверенно и верно ползёт вверх.

Чем может быть вызвана данная ситуация?

Строковые операции

Наиболее частая ситуация, когда возникают утечки памяти в Java-приложениях.
Для понимания. из-за чего происходят проблемы при работе со строками, вспомним, что в Java при выполеннии таких операций, как вызов метода substring() у строки, возвращается экземпляр String лишь и изменёнными значениями переменных length и offset — длины и смещения char-последовательности. При этом, если мы получаем строку длиной 5000 символов и хотим лишь получить её префикс, используя метод substring(), то 5000 символов будут продолжать храниться в памяти.
Для систем, которые получают и обрабатывают множество сообщений, это может быть серьёзной проблемой.
Для того, чтобы избежать данную проблему, можно использовать два варианта:

String prefix = new String(longString.substring(0,5)); //первый вариант
String prefix = longString.substring(0,5).intern(); //второй вариант


Важное замечание ко второму варианту с intern-строками от Zorkus: интернированные строки хранятся не в heapspace, а в permgen space. Сборка мусора в нем происходит по отдельным правилам, не так как в heap-e / young/tenured memory pools.

Аналогично надо помнить, что схожая проблема возникает при использовании метода split().

ObjectInputStream и ObjectOutputStream

Классы ObjectInputStream и ObjectOutputStream хранят ссылки на все объекты, с которыми они работали, чтобы передавать их вместо копий. Это вызывает утечку памяти при непрерывнои использовании (к примеру, при сетевом взаимодействии).
Для решения этой проблемы необходимо периодически вызывать метод reset().

Потоки и их стек

Каждый экземпляр класса Thread в Java выделяет память для своего стека (по умолчанию, это 512 Кб; изменяется с помощью параметра -Xss). Неоптимизированные приложения, использующие множество потоков, могут привести к необоснованно высокому потреблению памяти.

Нестатичные внутренние классы

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

Паттерн Observer и связанные с ним угрозы

Часто ситуация с утечкой памяти возникает при использовании паттерна Observer (Наблюдатель). Как известно, Observer хранит список своих слушателей, которые подписаны на оповещения об определенных действиях. При этом, если мы больше не используем некий класс, который является подписчиком этого наблюдателя, то GC не сможет “собрать” его, поскольку ссылка на него хранится в самом экземпляре Observer.

Singleton

Как только экземпляр-синглтон был инициализирован, он остаётся в памяти на всё время жизни приложения. Как следствие, данный экземпляр не сможет быть собран сборщиком. Данный паттерн стоит применять лишь тогда, когда это обосновано реальными требованию к постоянному хранению в памяти.

ThreadLocal-переменные

Ссылка на ThreadLocal-переменную используется связанным с ней потоком. В большинстве серверов приложений потоки переиспользуются в пулах, следовательно ThreadLocal-данные не будут собраны GC. Если приложение само не заботится об очищении этих значений, это повлечёт серьёзную утечку памяти.

Изменяемые статичные объекты

Также частым случаям утечки памяти в Java-приложениях служит неправильное использование static. Статичная переменная хранится своим классом, а как следствие, его загрузчиком (classloader). По причине внешнего использования увеличивается шанс, что сборщик мусора не соберёт данный экземпляр. Также зачастую в static-переменных кэшируется информация или же хранятся состояния, используемые несколькими потоками. Отдельным примером являются статичные коллекции. Хорошим же тоном при архитектурном проектировании служит полное избегание изменяемых статичных объектов — зачастую существует лучшая альтернатива.

Создание объектов

Рассмотрим два случая.
Случай 1:

Elem e;
e = new HeavyElem();
e = new HeavyElem();


Случай 2:

Elem e;
e = new HeavyElem();
e = null;
e = new HeavyElem();


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

Загрузчики классов

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

Как же их избежать?


А теперь для закрепления несколько советов, как избежать проблемы с утечками памяти:
1. Используйте профайлеры. Профайлер помогает увидеть, какие объекты располагаются в куче (а также просмотреть их прямо по экземплярам), что позволит на ранних стадиях отловить утечки
2. Осторожнее используйте строковые операции, особенно в случаях, когда программа работает над обработкой множества текстовых данных.
3. Всегда внимательно следите, нужны ли вам нестатичные внутренние классы, статичные переменные
4. Очищайте коллекции объектов после того, как данные были обработаны и не нуждаются в дальнейшем использовании

Пишите хороший код и не забывайте о правилах обращения с памятью!
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 80

  • UFO just landed and posted this here
      +12
      Не путаю. Утечка памяти — процесс неконтролируемого уменьшения доступной для использования памяти. В приведённых выше случаях при указанных условиях сборщик мусора не соберёт эти объекты.
        +5
        Проблема утечек памяти в яве довольно не тривиальна и очень запутана. В добавление к этому материалу будет полезно почитать материалы с IBM. Статьи старые, но думаю, что все еще актуальные:
        Java theory and practice: Plugging memory leaks with weak references
        Java theory and practice: Plugging memory leaks with soft references
          0
          Очень полезные источники, спасибо. Они ещё долго будут актуальными.
        +5
        Про особенность работы со строками не знал. Спасибо!
          +3
          Автор, советую про про строки добавить бы одно уточнение. Интернированные строки хранятся не в heapspace, они хранятся в permgen space. И сборка мусора в нем происходит по отдельным правилам, не так как в heap-e / young/tenured memory pools.
            0
            Спасибо за полезное уточнение, добавил в пост.
          +1
          Желтоватый заголовок получился. Для этой статьи мне кажется больше подошло бы название вроде «Используйте память экономно» или что-то вроде этого, а то создается впечатление что у джавы где-то в натив коде есть утечки, на которые программист никак не может повлиять. А за статью спасибо. Познавательно
            0
            Спасибо за полезную критику, первая статья :) Думаю, что также было бы уместно использовать «Типичные случаи утечки памяти в Java-приложениях» и «при Java-разработке».
              0
              Кроме замечения по строкам, я бы добавил к пункту про потоки — если у вас столько много активных потоков, что потребляемый ими стек начинает беспокоить, смотрите в сторону NIO и отделения пула потоков от пула connections/IO streems через channels/selectors.
                0
                Можно еще и с размером стека поиграться, дефолтные 512кб это много, иногда даже очень много, тут можно хорошо поиграться с параметром -Xss
                  0
                  Угу, согласен. Но хотя… сколько активных потоков у вас будет? Ну пусть 100, это 50мб оверхеда по памяти. У вас их 2000? Наверное лучше подумать а зачем вам так много.
                    0
                    Например если у меня сервер с архитектурной где на каждого клиента по потоку — там и больше может быть. Некоторые утверждают что это во первых быстрее, во вторых проще для разработчика.

                    Например www.mailinator.com/tymaPaulMultithreaded.pdf
                      0
                      Ну проще для разработчика-то конечно. Насчет быстрее, я бы сказал зависит от того с какой частотой и в каких объемах приходят данные от каждого клиента (потока). Если там данных условно «часто и много» с точки зрения обработки и нагрузки на процессор — да, наверное один thread/connection и синхронный IO будет лучше, если условно «приходят с перерывами и поток большую часть времени простаивает» — я бы выбрал NIO, хотя для программирования он будет сложнее.
                        0
                        Возможно для такой архитектуры больше подошел бы тот же Erlang с его легковесными процессами и tail-call оптимизацией?
                          0
                          Возможно, но есть очень много но — например найти человека, а то и не одного который бы смог написать это + интегрировать с текущим кодом.

                          Ту реализацию такого сервера я видел, получилась очень простой и действительно очень быстрой при 300-500 коннектах с хоть и не частым и при этом большим объемом IO и тяжелой бизнес логикой.
                            0
                            У этого Но есть и преимущество — такие люди будут востребованы ещё долго, а людей которые об этом знают ещё мало :)
                              0
                              Не вижу ни одного преимущества. Представьте такую ситуацию, вы являетесь работодателем и делаете какой-то клевый продукт и у вас вся база кода с пятью разработчиками на яве, примерно 80% затрат — это зп сотрудникам. Теперь перед вами встает задача написать к этому продукту сервер, то вам врядли придет в голову идея найти, а главное нанять 2-х человек с зп в полтора раза большей нежели у ваших программистов что бы написать сервер, при этом половину уже реализованного кода придется переписывать на другом языке.

                              Это самоубийство. Сервер будет писаться силами разработчиков, это круто если кто-то из них в курсе что такое seda и может быстро на той же mina/netty поднять за недельку что-то подобное что нам нужно, а если только парочка из них в курсе что такое сокеты, а уж про конкареннси им надо в гугл ходить…
                                0
                                К сожалению я слабо представляю продукт, к которому неожиданно надо написать сервер — не это ли требование будет самоубийством? Не будет ли это уже новым продуктом?
                                  0
                                  Если из 5-ти разработчиков 1-2 слышали о concurrency в java, то выбор «нанять-найти» или «использовать-что-есть» далеко не очевиден. Возможно в таком случае дешевле будет сначала «обучить», а потом уже пустить за разработку сервера.
                                  Впрочем, это всё вопросы управления уже готовым продуктом. Я же говорю о том, когда вопрос выбора технологий и подбора команды стоит до начала разработки.
                  –10
                  Не вижу ничего желтого в заголовке. Когда только java появилась внимательно присматривался у этому языку и отказался от его использования.
                  1. Слишком много областей видимости и они порождают хаос в голове разработчика (тем кто на ней пишет задайте себе вопрос, а сколько их?).
                  2. Нет множественного наследования (разработчики лукавят, что это плохо, просто им это сложно сделать).
                  3. У нас нет goto, но зато есть break и continue с метками (это просто бред какой-то).
                  4. Главный принцип языка java — переносимость (а не скорость, хотя все производители про нее только и орут), java везде должна работать одинаково (вынужден ставить по несколько версий, поскольку одни приложения работают под одной, другие под другой, в результате вообще видя написан java использую только при отсутствии альтернативы).

                  Такая работа с памятью, это просто очередной гвоздь в крышку…. Я конечно понимаю не хотели пускать Microsoft и это правильно, но за счет такого….
                    +2
                    Толсто. Но если хотите спорить — давайте продолжим.
                      0
                      Чтобы спорить ответьте аргументированно на мои пункты.
                      1….
                      2….
                      3….
                      4….

                      Если грамотно, я смолчу, значит Вы разумный человек, если нет то…. Вообщем ответьте, моего ответа не будет, а я Вашего жду.
                        0
                        1. java.sun.com/docs/books/jls/second_edition/html/names.doc.html#103228 — вы про это, что ли? Но в любом случае, хаос в голове разработчика обычно порождают не они.

                        2. Это правда. А зачем вам множественное наследование? Есть интерфейсы, абстрактные классы и обычные классы.

                        3. Строго говоря, ни goto, ни брейки не нужны для полноты языка по тьюрингу (точнее, нужен или гото, или оператор цикла). Смысл в том, что с помощью goto вы могли бы перейти откуда угодно куда угодно, а break/continue работают изнутри циклов, что как бы, делает их менее повсеместными и как бы ограниченными для применения, хотя в определенных случаях их все конечно используют. Я собственно, их не особенно люблю.

                        4. Переносимость между чем, прежде всего? Между Операционками? Есть. Между архитектурами хардварными? Есть.

                        У нас сложное серверное приложение работает на Win XP/Vista/7/Redhad/Debian/Solaris, да, по настоящему работает. Про UI-ную часть? Ну тут многие используют нативные фреймворки, типа SWT, как эклипс, но некоторые пишут переносимые и нормально работающие сложные UI приложения на чистом Swing — IntelliJ IDEA, например.

                        Переносимость между виртуальными машинами? Мм, наверное не очень. Хорошая между Hotspot/JRockit, похуже дело обстоит с apache harmony/gcj, остальные не смотрел.

                        До тех пор, пока вы не используете недокументированные опции конкретной имплементации JVM, и не используете внутренние API, все переносится нормально. Если используете — да, все к чертям. Да, в сложных проектах довольно часто приходится такие вещи использовать, поэтому переносимость между виртуальными машинами требует доп. усилий.

                        JVM- абстракция. Как и любая сложная абстракция, она течет (http://www.joelonsoftware.com/articles/LeakyAbstractions.html). Так же, как течет SQL, который вроде бы тоже, по задумке, чисто декларативный язык.

                        Переносимость между версиями языка? Очень хорошая. До сих пор поддерживается бинарный байткод, скомпилированный для ранних версий JVM, 1.2 например. Где вы такое увидите в .NET, например?

                        А теперь почему я сказал «толсто», не смотря на то, что по сути многие ваши замечания верны… А альтернатива-то какая? Вы используете написанное на яве в последнюю очередь, отлично, а что вы используете в первую, вторую и третью, и почему?
                  +11
                  Второй случай является более правильным, поскольку в большинстве случаев это явно укажет сборщику на сбор неиспользванного экземпляра первоначально созданного HeavyElem.


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

                  зы. источник тоже укажите, пожалуйста: iwillgetthatjobatgoogle.tumblr.com/post/12591334729/java-memory-leaks
                    +12
                    Это мой блог :) Статью изначально писал для хабра, а в блоге перевод на английский язык для его читателей.
                    Насчёт случая использования — согласен.
                      +2
                      Работаете в Google?
                        +3
                        Нет, хотя одержим этой идеей. Блог завёл месяц назад и веду его, параллельно освежая знания. Надеюсь, в дальнейшем он станет хорошим источником для подготовки к IT-собеседованиям. Хотя не хотел его освещать тут, внимательный tulskiy случайно выдал его :)
                          +2
                          Дык, он у вас в профили висит, я сам не заметил места роботы.
                          Одержимость хорошая, пост про работу в Google от Steve Yegge plus.google.com/110981030061712822816/posts/UgCL6YRwgbR, может быть не читали еще.
                            0
                            Ыы, сорри что выдал блог :) А вообще статья очень интересная, некоторых фактов не знал, спасибо.
                      0
                      Спасибо, в избранное! Буду сверяться )
                      • UFO just landed and posted this here
                          0
                          Присоединяюсь к вопросу, весьма интересно.
                            0
                            Скажу честно, данное утверждение увидел в статье «Leaking Memory in Java» по ссылке blog.xebia.com/2007/10/04/leaking-memory-in-java
                            В посте говорится, что первый случай вызовет OutOfMemoryError.
                            Цитата про второй случай: «the second creation of the FatObject will trigger a full GC and the GC will be able to clear enough memory (since the old reference has been nulled)».
                            Согласен с комментарием выше о том, что время между созданиями объектов должно играть роль. Также как и момент, в который будет происходить выполнение этой операции. Конечное поведение зависит от настроек и реализации используемого GC.
                              0
                              Честно сказать и по ссылке не очень-то убедительно.

                              Will be fine, because the second creation of the FatObject will trigger a full GC and the GC will be able to clear enough memory (since the old reference has been nulled).

                              Собственно, с чего бы создание нового объекта дергало GC?
                                0
                                Может дергать, если для этого объекта не хватает памяти, вот вам более простой пример, запускать с -XX:+PrintGCDetails -Xmx64m, сборщики можно любые как дефолтные, так и связку ParNew + ConcMarkSweepGC:

                                    static class Foo {
                                
                                        byte[] b = new byte[32*1024*1024];
                                    }
                                
                                    public static void main(String[] args) throws Exception {
                                
                                        for(int i = 0; i < 3; i++) {
                                            Foo f = new Foo();
                                        }
                                    }
                                


                                В логах должны быть 3 красивые записи о сборке мусора.

                                  0
                                  А теперь читаем про DoEscapeAnalysis.

                                  public class TestMem {
                                  public static void main(String[] args) throws Exception {
                                  System.err.println("create 1");
                                  Test2 e = new Test2();

                                  e = null;

                                  System.err.println("create 2");
                                  e = new Test2();
                                  }

                                  public static class Test2 {
                                  private char mas[] = new char[50000000];
                                  }
                                  }


                                  размер mas подобрать под собственные настройки jvm, и в зависимости от наличия или отсутсвия
                                  e = null
                                  мы любуемся одной из двух надписей:
                                  1.
                                  create 1
                                  create 2

                                  2.
                                  create 1
                                  create 2
                                  Exception in thread «main» java.lang.OutOfMemoryError: Java heap space
                                    0
                                    Про DoEscapeAnalysis в курсе (правда пришлось освежить память что же это все таки такое). Параметр довольно интересный, но не встречал что бы его кто-то использовал кроме как для синтетических тестов — «посмотрите как оно круто работает». Может потому, что ситуации в реальной системе чуточку сложнее нежели «сейчас я тут насоздаю 100500 объектов в этом методе».

                                    Кстати, если бы этот разговор состоялся бы около двух месяцев назад, я бы смог предоставить достаточно неплохую статистику как он может помочь\навредить в реальном приложении которое использует много-много памяти и живет месяцами.
                                      0
                                      а можно поподробней когда он может оказаться вреден?

                                      по поводу только синтетики, то этот флаг уже очень давно стоит по умолчанию, и чтобы показать как jvm не может его приходится ручками отключать.
                                        0
                                        Поподробней когда он может оказаться вреден ничего сказать не могу — флаг прошел мимо меня. Но судя по поведению, у нас он был выключен — мелкие локальные объекты очень хорошо так плодились до подчистки их парньюшечкой судя по профайлеру
                                  +3
                                  Ситуация возможна если у вас этот объект занимает БОЛЬШЕ ПОЛОВИНЫ памяти, так как действия для первого и второго случая:

                                  первый:
                                  1. выделили память под первый объект
                                  2. выделяем память под второй
                                  3. памяти нету, запускаем gc
                                  4. так как объект1 еще доступен, то сколько бы не освободили мелочи на второй объект памяти не хватает
                                  5. OOM

                                  второй:
                                  1. выделили память под первый объект
                                  2. занулили указатель на переменную, первый объект уже недоступен и при необходимости, может быть очищен
                                  3. выделяем память под второй
                                  5. памяти нету, запускаем gc
                                  6. так как объект1 уже недоступен, то очищаем его, памяти на второй объект хватает
                                  7. вселенское счастье

                                  НО если оказывается что у этого объекта переопределен finalize, то зануляй не зануляй, но OOM мы получим, так как все объекты с finalize собираются в 2 прохода:
                                  1й проход — вызываем метод finalize
                                  2й проход — действительной удаляем уже элемент из кучи

                                  Так что увлекаться финализацией тоже не рекомендуется по пустякам, только когда это действительно необходимо, иначе можно поймать OOM почти на ровном месте.

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

                                  Да и сами сановские инженеры не рекомендуют занулять указатели, jit сам умеет определять когда к элементу был последний доступ, и если дальше по коду обращений к данной переменной нету, то он может ее удалить даже посреди метода, все равно она уже не использутся, так что тут лучше совет — Каждой переменной свое имя.

                                  Автору:
                                  Дополнительно полезно почитать более свежий материал.

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

                                  P.S. статья достаточно полезная, хотя по некоторым пунктам очень спорная.
                                    0
                                    Спасибо за статью по оптимизационным трюкам, очень хороший материал.
                                    По поводу поведения в обоих случаях — важные дополнения. Только не понял насчёт объектов с переопределённым finalize() — чем он помешает сборке объекта?
                                      0
                                      Тем то, что сборка таких объектов производится в 2 этапа. На первой сборке выполняется finalize() и ставиться флаг, что объект финализирован, на второй — удаляется сам объект без выполнения finalize() если после его выполнения объект не стал достижим.
                                        +1
                                        ничем не помешает, вот только память вернется в хип только после второго gc:

                                        1. создали объект с finalize
                                        2. закончилась память, дернули gc
                                        3. gc дернул метод finalize у нашего объекта, удалил обычные объекты
                                        4. продолжаем работать, опять закончилась память, дернули gc
                                        5. gc видит наш объект и finalize уже отработан, удаляет объект и возвращает память

                                        таким образом для сборки объекта с finalize должно пройти 2 gc вместо одного.

                                        В случае с примером выше: имея объект с finalize мы дергаем gc один раз и достаточно памяти не получаем, так как объект еще не удалился, получаем oom.

                                        Но изменяя пример на:
                                        e = null;
                                        System.gc();
                                        System.gc();


                                        Мы гарантировано собираем даже такие объекты и пример отрабатывает без oom, вот только в продакшен коде за дерганье gc надо руки отрывать, так что работать с finalize нужно очень осторожно.
                                          0
                                          Теперь понятно, благодарю :)
                            • UFO just landed and posted this here
                                0
                                Имею ввиду использование потоками ThreadLocal-переменных, описанных в классах. Каждый поток будет иметь свою независимо инициализированную копию данной переменной (соответственно, и уникальную ссылку на неё), что в случае с пулами потоков и невнимательном обращении с хранимыми в ThreadLocal-данными приведёт нас к указанной проблеме.
                                +1
                                Не совсем понял к каким именно утечкам могут привести операции над строками? К замедлению работы программы они точно привести могут, но к утечкам?
                                  +6
                                  У автора очень своеобразное понимание термина «утечка памяти», не обращайте внимание.
                                    0
                                    Вернёмся к приведённому в посте примеру с системой обработки сообщений.
                                    Допустим, мы ведём обработку поступающих к нам сообщений, созданных по специально выбранному протоколу. Для простоты представим, что мы выполняем операцию получения префикса строк для данных сообщений и кэшируем эти значения.
                                    Что произойдет при кэшировании десятка тысяч таких коротких префиксов при получении их через substring()? В памяти будут храниться все данные, содержащиеся во всех исходных сообщениях. И если их длина значительно превосходит суммурную длину всех префиксов в кэше, то именно на эту величину у нас и создастся утечка памяти.
                                    Пример абстрактный, но точно отражает проблему. Мы используем нужные нам данные, заставляя перманентно храниться в памяти избыточные данные большего размера.
                                      +1
                                      Во-первых, операция substring() никакой новой памяти не выделяет — кроме памяти самого экземпляр объекта java.lang.String. Благодаря иммутабельности строк, новая, полученная в результате операции substring(), строка наследует символьный буфер родительской строки.

                                      Во-вторых, даже если ввести модифицирующую операцию (например toLowerCase) никакого «кэширования» не происходит. Экземпляры строк создаются и затем уничтожаются также как и обычные экземляры других классов. Единственное «кэширование» которое приходит на ум в данном случае — это принудительный вызов метода java.lang.String#intern() — но зачем?
                                        0
                                        Совершенно верно, я это все ниже расписал :) И даже в своем блоге отдельный пост на эту тему написал — потому что на самом деле, многие на этом могут спотыкаться.
                                          +1
                                          Да, новой памяти не выделяет, но и старую не отдаёт.
                                          String автор = ВойнаИМир.substring(12).
                                          Всё как вы сказали, но вместо пары байт на автора в памяти будет вся война и мир и GC её не соберёт. Со стороны будет выглядеть как утечка…
                                            0
                                            substring(0,12) конечно же, но Вы поняли, для примера же…
                                              0
                                              Да, вы правы — такая проблема существует, однако мы все равно имеем фиксированный расход по памяти строго кратный O(n), что не совсем вяжется с моими понятием определения «утечка».
                                          0
                                          Замедление программы вызывает частые GC, которые происходят из-за недостатка памяти, в следствие постоянно торчащих там объектов, которые GC не может грохнуть
                                          0
                                          Интересная статья, хотя все вещи довольно очевидны.
                                          Про «правильность» зануления ссылок с Вами, боюсь, не согласен Джошуа Блох=) Да и я тоже. Некрасиво это смотрится, к тому же
                                            +4
                                            Спасибо за статью. Маленький комментарий по поводу workaround для substring. Intern возможно и решит проблему утечки памяти heap, но повальное его использование не по назначению, а ваш пример как раз сюда попадает, может вызвать утечку памяти в PermGen который обычно значительно меньше и вообще не всегда собирается GC.
                                              0
                                              Очень важное замечание, спасибо.
                                                +2
                                                «A nasty variant of this blooper is when the substring is later interned by calling String.intern(). On some VMs, this means the large char[] object is now held onto forever by the VM's intern pool. Kiss that memory good-bye, because there's no way to free it again.» © wiki.eclipse.org/Performance_Bloopers
                                              0
                                              У меня очень маленький опыт использования Java, пару раз писал маленькие приложения. Помню намучился я с SAX анализатором. В разных версиях jre(1.7, 1.6) код работал по разному. При этом в 1.7 как раз и была проблема с превышением допустимого значения heap.
                                                0
                                                Вообще говоря, как раз в SAX при корректном использовании не должно быть проблем с этим, это в DOM-парсерах могут быть проблемы. SAX же не строит в памяти дерево документов, он проходит по нему использую более-менее константный объем памяти.
                                                  0
                                                  абсолютно верно, именно по этому и был выбран подход с SAX анализатором, так как нужно было читать большой файл построчно. Но на практике возникла проблема.
                                                +6
                                                String prefix = longString.substring(0,5).intern();
                                                И это в статье об утечках…
                                                  +1
                                                  У автора какое-то странное понятие об утечках памяти. На самом деле что бы память в яве действительно потекла, надо, я даже не знаю что сделать*. То, что в мире явы зовут утечками, это просто неверная бизнес логика и банальное отсутствие знаний о том как все таки устроена память в яве и как работает сборщик мусора.

                                                  Некоторые вон, передавая в метод класс в конце метода приравнивают ссылку на него в null при этом оставляя комментарий в вида //объект больше не нужен, очистим память

                                                  * на классическую утечку похожа следующая ситуация:

                                                  static class Foo {
                                                      static int sequence;
                                                      int id = ++sequence;
                                                  
                                                      @Override
                                                      public int hashCode() {
                                                  //        return id;
                                                          return ++sequence;
                                                      }
                                                  }
                                                  
                                                  public static void main(String[] args) throws Exception {
                                                      HashSet<Foo> foos = new HashSet<Foo>();
                                                          Foo foo = new Foo();
                                                      foos.add(foo);
                                                  
                                                      System.out.println(foos.remove(foo));
                                                      System.out.println(foos.size());
                                                  }
                                                  


                                                  Запустив эти несколько строчек мы увидим что-то вроде:

                                                  false
                                                  1
                                                  


                                                  Тут мы получим что-то похоже на «утечку» потому как уже добраться из кода до элемента в сете будет достаточно проблематично (не берем в расчет полную итерацию), но даже в такой тяжелой ситуации у нас всегда есть выход — вызов метода clear()
                                                    +1
                                                    Насчет substing, смотрите код этого метода в JDK. Итак.

                                                    public String substring(int beginIndex, int endIndex) {
                                                    if (beginIndex < 0) {
                                                    throw new StringIndexOutOfBoundsException(beginIndex);
                                                    }
                                                    if (endIndex > count) {
                                                    throw new StringIndexOutOfBoundsException(endIndex);
                                                    }
                                                    if (beginIndex > endIndex) {
                                                    throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
                                                    }
                                                    return ((beginIndex == 0) && (endIndex == count)) ? this :
                                                    new String(offset + beginIndex, endIndex - beginIndex, value);
                                                    }


                                                    Смотрим теперь по коду конструктор с такой же сигнатурой, видим:

                                                    // Package private constructor which shares value array for speed.
                                                    String(int offset, int count, char value[]) {
                                                    this.value = value;
                                                    this.offset = offset;
                                                    this.count = count;
                                                    }


                                                    Вывод — при вызове метода Substring, новый объект String конечно создается, но массив символов, используемый для хранения данных, не копируется. В новом объекте String будут просто использованы другие значения для offset/lenght, и ссылка на тот же самый массив символов относительно которых эти offset/length и берутся.
                                                      0
                                                      в этом то и проблема:
                                                      1. на вход подается строка из 10000 символов
                                                      2. вы получаете префикс с 2го по 5й символ
                                                      3. создается новая строка с проставленными offset, count, value
                                                        0
                                                        извиняюсь, отправилось рано, продолжим:

                                                        4. в value попадают все 10000 символов
                                                        5. сам префикс сохраняем в какой коллекции
                                                        5. повторяем итерациюю 1000 раз

                                                        Ожидаемый размер занятой памяти примерно = 4(символа префикса) * 1000 (итераций)
                                                        на практике = 10000 (размер первоначальной строки) * 1000 (итераций)

                                                        В то время как делая new String(str.substring(1,4)) мы получаем новую строку на 4 символа, а старая на 10000 символов свободно может уйти под gc.
                                                          0
                                                          Хм?

                                                          Ожидаемый объем памяти — 4(символа префикса) * 1000 (итераций). На практике — 1000 (потому что массив value- один), + 1000 * (разные offset/prefix), ну и плюс, если уж смотреть в глубь, 8 (по моему) байт на object descriptor) и выравнивание интов по 4х или 8-байтному смещению при создании новых объектов String.
                                                            0
                                                            Т.е. я согласен с тем, что если вы один раз вызвали substring чтобы вырезать маленькую подстроку, то он у вас создает доп. ссылку на цельный оригинальный массив, который будет из-за этого держаться в памяти. Но когда вы потом будете 1000 раз вызывать substring на таких строках, массив value[] не будет дублироваться, он будет один, со ссылками на него из 1000 разных мелких объектов.

                                                            Я не так понял вашу мысль, может?
                                                              +1
                                                              Вероятно автор подразумевает что substring будет вызываться все время на разных строках, а не на одной и той же. Иначе смысл действительно теряется…
                                                                0
                                                                Немного не так: на вход у вас каждый раз новые строки, соответсвенно ради хранения нескольких символов вы будете держать в памяти весь массив на 10000 элементов.

                                                                Если у вас одна большая строка и множество вызовов substring от нее, тогда да, получаем даже выигрыш по памяти, так как value[] не дублируется.
                                                                  0
                                                                  Да, именно. В приведённом мной примере я подразумевал, что мы обрабатываем множество различных сообщений, применяя к их строковому представлению метод substring(), допустим, для получения префикса.
                                                                  Для одного char-массива постоянные вызовы substring() — это наоборот хорошо.
                                                                  Для каждого случая — свой подход. Однако, как заметили выше, не каждый разработчик знает про эту особенность java.lang.String. Поэтому я не мог не упомянуть этот случай. Поскольку в общем виде мы приходим к тому, что «привязываемся» к некому набору данных, используя лишь его ограниченную часть.
                                                                  При единичных вызовах substring() к строкам большой длины стоит, конечно же, использовать конструктор.
                                                            0
                                                            вы же как раз приводите пример подтверждающий слова автора
                                                            this.value = value; — вот именно в этом месте и «утечка памяти»
                                                            если оригинальная строка удаляется то это value остаётся в памяти а оно никому в целом виде не надобно
                                                            0
                                                            Я бы посоветовал вообще вот такой вариант, с явным указанием области видимости. Тут как минимум всегда видно, что именно за объект лежит в e.
                                                            {
                                                            Elem e = new HeavyElem();
                                                            }
                                                            {
                                                            Elem e = new HeavyElem();
                                                            }

                                                            Впрочем подобный код все равно намекает, что скорее всего тут необходим рефакторинг, скажем экстракт метода.
                                                              0
                                                              блин
                                                              ну причем тут область видимости?
                                                              речь же совсем про другое
                                                              прочитайте тот параграф еще раз потом каменты которые уже были про тот параграф
                                                                0
                                                                При том, что присваивание переменной null ничего явно не укажет сборщику мусора. А вот отсутствие ссылок — укажет, что объект ушел из области видимости. Попробуйте донести свою мысль еще раз. Я всего лишь хотел донести простую мысль, что переприсваивание локальной переменной значения это плохой стиль, всегда есть более прямые способы донести свою мысль до кода.
                                                                  0
                                                                  там речь не шла про код который идёт подряд
                                                                  речь шла про ситуацию когда надо большущий объект создать
                                                                  а потом когда нибудь его понадобится пересоздать
                                                                  да даже и не обязательно его

                                                                  как вы собираетесь с помощью области видимости удалять ссылки на объект который полдня живёт, потому умирает в другом потоке и потом еще в другом пересоздается?
                                                              +1
                                                              class Leak {
                                                                  static Leak instance;
                                                              
                                                                  Leak() {
                                                                      instance = this;
                                                                      throw new Error();
                                                                  }
                                                              }
                                                              
                                                                0
                                                                Этот пример не совсем про утечку, instance все таки будет иметь ссылку на созданный объект, с ним даже можно будет работать (правда насколько плодотворно — тот еще вопрос).

                                                                Если изменить в вашем примере конструктор на инициализатор:

                                                                class Leak {
                                                                static Leak instance;

                                                                {
                                                                instance = this;
                                                                throw new Error();
                                                                }
                                                                }


                                                                То получим ошибку говорящую о том, что инициализатор должен выполниться нормально.

                                                                Но, если все таки случиться такое что в инициализаторе будет брошено исключение, объект так же создаться, до него можно будет добраться, но состояние опять будет сломано или же он будет удален с ближайшей сборкой мусора.

                                                                Only users with full accounts can post comments. Log in, please.