Спасибо за память

Изучаем, как JVM использует системную память в Windows и Linux

Исключения java.lang.OutOfMemoryError могут появляться не только из-за нехватки памяти в куче Java™. При нехватке системной памяти также могут появляться исключения OutOfMemoryError, которые не получится устранить с помощью обычных приемов отладки. В этой статье рассказывается о том, что такое системная память, как исполняющая среда Java ее использует, как проявляется ее нехватка и как отлаживать исключения OutOfMemoryError, вызванные нехваткой системной памяти В Windows® и Linux®. Эту статью дополняет другая статья, раскрывающая те же самые темы для систем AIX®.

Эндрю Холл, инженер-программист, IBM

Эндрю Холл работает в IBM в Java Technology Centre с 2004 года. Первые два года он проработал в команде системного тестирования, затем 18 месяцев в сервисной команде Java, где занимался отладкой проблем с системной памятью на множестве платформ. Сейчас он работает разработчиком в команде надежности, доступности и удобности сервисов Java. В свободное время он любит читать, фотографировать и показывать фокусы.



24.05.2011

«Куча», в которой выделяется место для каждого объекта в Java, - это область памяти, с которой разработчик при написании Java-приложения контактирует наиболее тесно. JVM была спроектирована таким образом, чтобы изолировать нас от внутренностей операционной системы, поэтому когда идет речь о памяти, естественно думать о куче Java. Несомненно, вы сталкивались с исключением OutOfMemoryError , вызванным утечкой объектов или тем, что размер кучи был задан недостаточно большим для того, чтобы вместить все ваши данные, и возможно даже изучили несколько приемов отладки подобных сценариев. Но по мере увеличения нагрузки и объема данных, обрабатываемых приложением, могут начать появляться исключения OutOfMemoryError которые нельзя устранить с помощью стандартных приемов. Такие исключения возникают даже тогда, когда в куче Java есть свободное место. Чтобы понять, что происходит, необходимо разобраться в том, как работает исполняющая среда Java (JRE).

Приложения Java запускаются в виртуальном окружении исполняющей среды Java, а сама исполняющая среда является программой, написанной на таком языке как C, использующей ресурсы операционной системы, в том числе системную память. Системная память – это память, доступная для процесса исполняющей среды, а память в куче Java – это память, используемая приложением Java. В системной памяти хранятся все виртуальные ресурсы приложения Java, такие как куча и потоки, а также данные, необходимые для работы самой виртуальной машины. Это значит, что ограничения системной памяти, накладываемые аппаратным обеспечением и операционной системой (ОС), влияют на возможности приложения.

Это одна из двух статей, раскрывающих одну и ту же тему для разных платформ. Из обеих статей вы узнаете о том, что такое системная память, как исполняющая среда Java ее использует, как выглядят ситуации с нехваткой системной памяти и как отлаживать подобные исключения OutOfMemoryError. В данной статье эта тема рассматривается для операционных систем Windows и Linux без акцента на какой-либо конкретной исполняющей среде. Во второй статье проблема рассматривается для случая операционной системы AIX и IBM® Developer Kit для Java. (Приводимая в ней информация о реализации исполняющей среды Java от IBM также справедлива и для платформ отличных от AIX. Поэтому если вы используете IBM Developer Kit для Java на Linux или 32-разрядную исполняющую среду Java от IBM для Windows, вторая статья вам также может оказаться полезной.

Обзор устройства системной памяти

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

Аппаратные ограничения

Многие накладываемые на процесс ограничения обусловлены аппаратным обеспечением, а не операционной системой. Каждый компьютер имеет процессор и некоторое количество памяти произвольного доступа (RAM), также называемой физической памятью. Процессор интерпретирует поток данных как инструкции для выполнения, он имеет один или несколько обрабатывающих блоков, выполняющих целочисленные вычисления, вычисления с плавающей точкой, а также более сложные вычисления. Процессор имеет определенное количество регистров – очень быстрых элементов памяти, используемых для хранения данных при выполнении вычислений. Размер регистра определяет максимальное число, которое процессор может использовать в отдельном вычислении.

Процессор соединяется с физической памятью посредством шины памяти. Количество памяти, к которой можно обращаться, ограничено размером физического адреса (адреса, используемого процессором для указания на физическую память). Например, 16-разрядный адрес может указывать на адреса от 0x0000 до 0xFFFF, что составляет 2^16 = 65536 уникальных участков памяти. Если каждый адрес указывает на блок памяти размером 1 байт, 16-разрядный физический адрес позволяет процессору адресовать 64КБ памяти.

Процессоры классифицируют по количеству бит (разрядов). Как правило, это количество обозначает размер регистров процессора, хотя есть и исключения – например 31-разрядная система 390 – где 31 разряд обозначает размер физического пространства адресов. Для рабочих станций и серверных платформ это число обычно равно 31, 32 или 64, а для встроенных устройств и микропроцессоров оно может снижаться до 4. Размер физического адреса может совпадать с размером регистра, хотя может быть как больше, так и меньше его. Большинство 64-разрядных процессоров могут выполнять 32-разрядные программы, если они находятся под управлением подходящей операционной системы.

В таблице 1 перечислены размеры регистров и физических адресов для некоторых популярных архитектур Windows и Linux:

Таблица 1. Размер регистров и физических адресов для некоторых популярных архитектур процессоров
АрхитектураРазмер регистров (в разрядах)Размер физических адресов (в разрядах)
(Современные) Intel® x863232
и 36 при наличии Physical Address Extension - расширения физических адресов (Pentium Pro и выше)
x86 6464В настоящее время 48 разрядов (в дальнейшем планируется увеличение)
PPC646450 разрядов на POWER 5
31-разрядная 3903231
64-разрядная 3906464

Операционные системы и виртуальная память

Если бы вы писали приложение, работающее непосредственно на процессоре без участия операционной системы, вы бы могли использовать всю память, которую процессор может адресовать (если предположить, что имеется достаточное количество физической RAM-памяти). Но, для того, чтобы иметь удовольствие воспользоваться такими возможностями, как многозадачность и аппаратные абстракции, почти все используют для выполнения своих программ какую-либо операционную систему.

В многозадачной ОС, такой как Windows или Linux системные ресурсы, в том числе и память, используются больше, чем одной программой. Каждой программе необходимо выделить для работы участки физической памяти. Можно спроектировать такую операционную систему, в которой каждой программе будет разрешено работать с выделенной ей физической памятью напрямую, и некоторые встроенные операционные системы работают именно таким образом. Однако это не очень практично в окружении, состоящем из множества программ, совместная работа которых не протестирована, так как любая программа может повредить память, используемую другой программой или самой операционной системой.

Виртуальная память позволяет множеству процессов совместно использовать физическую память, не давая возможность повредить чужие данные. В операционных системах с виртуальной памятью (таких как Windows, Linux и множество других) каждая программа имеет свое собственное виртуальное адресное пространство – логический участок адресов, размер которых определяется размером адресов операционной системы (т.е. 31, 32 или 64 бита для рабочей станции или серверной платформы). Виртуальное адресное пространство процесса может отображаться в физическую память, файл или какое-либо другое устройство хранения. Операционная система может перемещать данные из физической памяти в область подкачки (файл подкачки в Windows или раздел подкачки в Linux) и, при необходимости, наоборот, для обеспечения наилучшего использования физической памяти. Когда программа запрашивает память по виртуальному адресу, операционная система при содействии аппаратного обеспечения определяет физическое местонахождение виртуального адреса. Адрес может находиться в физической RAM-памяти, файле или разделе/файле подкачки. Если запрошенный участок памяти ранее был перемещен в область подкачки, то перед использованием он загружается обратно в физическую память. На рисунке 1 показано, как работает виртуальная память, отображая участки адресного пространства процесса на совместно используемые ресурсы:

Рисунок 1. Отображение адресного пространства процесса посредством виртуальной памяти в физические ресурсы
Virtual memory mapping

Каждая программа выполняется в виде процесса. В Windows и Linux процесс представляет собой коллекцию информации об управляемых операционной системой ресурсах (таких, как информация о файле или сокете), как правило, одном виртуальном адресном пространстве (на некоторых архитектурах более чем одном), и как минимум одном потоке выполнения.

Размер виртуального адресного пространства процесса может быть меньше, чем физическое адресное пространство процессора. 32-разрядная архитектура Intel x86 изначально имела 32-разрядный физический адрес, позволявший процессору адресовать 4ГБ памяти. Позднее была добавлена возможность расширения физических адресов (PAE или Physical Address Extension), которая расширяет размер физического адресного пространства до 36 разрядов и позволяет устанавливать и адресовать до 64ГБ памяти RAM. PAE позволяет операционным системам отображать 32-разрядные виртуальные адресные пространства размером 4ГБ на большие интервалы физических адресов, но не позволяет каждому процессу иметь адресное пространство размером 64 ГБ. Это значит, что если вы установите более 4ГБ памяти в 32-разрядный сервер Intel, вы не сможете работать напрямую со всей памятью в рамках одного процесса.

Функциональность AWE (Address Windowing Extensions или расширения адресных окон) позволяет процессу в Windows отображать часть своего 32-разрядного адресного пространства в виде скользящего окна в область памяти большего размера. Linux использует подобную технологию для отображения участков памяти в виртуальное адресное пространство. Это значит, что хотя напрямую адресовать более 4ГБ памяти нельзя, работа с участками памяти большего размера все же возможна.

Пространство ядра и пространство пользователя

Хотя каждый процесс имеет свое собственное адресное пространство, как правило, программа не может использовать его полностью. Адресное пространство разделяется на пространство пользователя и пространство ядра. Ядро – это главная программа операционной системы, предоставляющая средства взаимодействия с аппаратным обеспечением, программами-планировщиками, а также такие сервисы как сеть и виртуальная память.

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

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

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

По умолчанию 32-разрядная система Windows имеет 2ГБ пространства пользователя и 2ГБ пространства ядра. В некоторых версиях Windows размер пространства пользователя можно увеличить до 3ГБ и соответственно уменьшить пространство ядра до 1ГБ, добавив переключатель /3GB в конфигурацию загрузки и перекомпоновав приложения с переключателем /LARGEADDRESSAWARE. На 32-разрядных системах Linux пространство пользователя по умолчанию имеет размер 3ГБ, а пространство ядра – 1ГБ. Некоторые дистрибутивы Linux предоставляют ядро hugemem, поддерживающее 4ГБ пользовательского пространства. Для достижения этого ядру выделяется свое собственное адресное пространство, используемое при выполнении системных вызовов. За преимущества, использования большого пользовательского пространства приходится платить более медленными системными вызовами. Операционной системе приходится копировать данные между адресными пространствами и сбрасывать отображения адресного пространства процесса каждый раз при выполнении системного вызова. На рисунке 2 показана разметка адресного пространства 32-разрядной системы Windows:

Рисунок 2. Разметка адресного пространства 32-разрядной системы Windows
Windows 32 bit address spaces

На рисунке 3 показана структура адресного пространства на 32-разрядных системах Linux:

Рисунок 3. Разметка адресного пространства 32-разрядной системы Linux
Linux 32 bit address spaces

Отдельное адресное пространство ядра также используется в 31-разрядной системе Linux 390. Здесь разделение небольшого (2-гигабайтного) адресного пространства является нежелательным, однако архитектура 390 может одновременно работать с несколькими адресными пространствами без издержек производительности.

Адресное пространство процесса должно содержать в себе все, что нужно программе, в том числе саму программу и используемые ей разделяемые библиотеки (.dll для Windows и .so файлы для Linux). Разделяемые библиотеки не только забирают свободное пространство у программы, они также могут фрагментировать адресное пространство и уменьшать количество памяти, которую можно выделить в виде непрерывного блока. Это становится заметным в программах, работающих на Windows x86 с 3ГБ пользовательского пространства. DLL-библиотеки устроены таким образом, что при загрузке они отображаются в память адресного пространства, расположенную по заранее определенному адресу, если он не занят. Если же адрес занят, то они перебазируются в какое-либо другое место. При проектировании Windows NT, в котором пользовательское пространство имело размер 2ГБ, имело смысл размещать системные библиотеки вблизи границы двух гигабайт, оставляя, таким образом, большую часть пользовательской памяти свободной для использования приложением. Но при расширении пространства пользователя до 3ГБ, системные разделяемые библиотеки все также загружаются на границе 2ГБ, теперь уже в середине адресного пространства. Поэтому, хотя общий размер пространства пользователя составляет 3ГБ, невозможно выделить непрерывный блок памяти размером 3ГБ.

Использование переключателя /3GB в Windows сокращает размер адресного пространства ядра наполовину. В некоторых сценариях 1ГБ пространства ядра может полностью исчерпаться, что чревато замедлением операций ввода/вывода или проблемами при создании пользовательских сеансов. Хотя переключатель /3GB может быть чрезвычайно полезен в некоторых приложениях, для любого окружения, в котором он будет использоваться, следует провести тщательное нагрузочное тестирование перед разворачиванием приложения. Больше информации о переключателе /3GB, его достоинствах и недостатках, вы можете найти по ссылке в разделе Ресурсы.

Утечки системной памяти или чрезмерное использование системной памяти приводят к различным проблемам в зависимости от того, что закончится раньше: адресное пространство или физическая память. Исчерпание адресного пространства обычно происходит только с 32-разрядными процессами, так как максимум в 4ГБ довольно легко использовать полностью. 64-разрядные процессы имеют адресное пространство размером в сотни или тысячи гигабайт, которое сложно заполнить полностью, даже если постараться. При исчерпании адресного пространства процесса Java исполняющая среда Java начинает проявлять странные симптомы, описываемые далее в этой статье. При работе в системе, где размер адресного пространства превышает размер физической памяти, утечки памяти или чрезмерное использование системной памяти принуждают операционную систему перемещать определенную часть адресного пространства процесса в область подкачки. Доступ к адресу памяти, находящемуся в области подкачки, осуществляется гораздо медленнее, чем чтение резидентного (находящегося в физической памяти) адреса, так как в этом случае операционная система должна считать данные с жесткого диска. Могут возникать ситуации, когда заканчивается и вся физическая память, и область подкачки. В Linux это приводит к срабатыванию функции ядра OOM (out-of-memory)-killer, которая принудительно завершает процесс, использующий наибольшее количество памяти. В Windows это приводит к сбоям выделения памяти, точно также как в случае исчерпания адресного пространства.

Если вы пытаетесь использовать больше виртуальной памяти, чем имеющееся количество физической памяти, проблема станет проявляться задолго до того, как процесс будет принудительно завершен. Система начнет «метаться» и большая часть времени будет уходить на копирование данных в область подкачки и из нее. Когда это происходит, производительность компьютера и отдельных приложений становится настолько плохой, что пользователь не сможет не заметить проблему. Когда куча памяти Java перемещается в область подкачки, производительность сборщика мусора становится настолько ужасной, что порой может казаться, что приложение зависло. Если на одной машине одновременно работают несколько исполняющих сред Java, физической памяти должно быть достаточно для размещения всех куч памяти Java.


Как исполняющая среда Java использует системную память

Исполняющая среда Java – это процесс операционной системы, на который действуют ограничения аппаратного обеспечения и операционной системы, о которых я рассказал в предыдущем разделе. Исполняющая среда обеспечивает средства работы, предоставляемые какому-то неизвестному пользовательскому коду. Поэтому невозможно предсказать, какие ресурсы потребуются исполняющей среде в каждой ситуации. Каждое действие, предпринимаемое приложением Java в управляемом окружении Java, потенциально может повлиять на требования к ресурсам, получаемым посредством исполняющей среды. В этом разделе рассказывается, как и почему приложения Java используют системную память.

Куча памяти Java и сборка мусора

Куча в Java – область памяти, в которой происходит выделение места для всех объектов приложения. Большинство реализаций Java SE имеют одну логическую кучу, хотя некоторые специальные исполняющие среды Java, например реализующие спецификацию Java реального времени (Real Time Specification for Java или RTSJ), имеют несколько куч. Одна физическая куча может быть разбита на разделы, в зависимости того, какой алгоритм сборки мусора (garbage collection или GC) используется для управления памятью кучи. Эти разделы обычно реализуются как непрерывные участки системной памяти, находящиеся под управлением менеджера памяти Java (который включает в себя сборщика мусора).

Размером кучи можно управлять из командной строки с помощью параметров -Xmx и -Xms (mx – определяет максимальный размер кучи, ms - изначальный размер). Хотя логическая куча (активно используемая область памяти) может расти и уменьшаться в зависимости от количества находящихся в куче объектов и от работы сборщика мусора, количество используемой системной памяти остается постоянным и зависит от значения -Xmx, задающего максимальный размер кучи. Большинство алгоритмов сборки мусора полагаются на то, что куча представляет собой непрерывный участок памяти, поэтому после создания кучи увеличить ее размер уже невозможно. Вся память кучи должна быть зарезервирована заранее.

Резервирование системной памяти это не то же самое, что ее выделение. При резервировании системной памяти не выделяется какой-либо физической памяти. Хотя выделение блоков адресного пространства не приведет к исчерпанию физических ресурсов, оно не позволяет использовать эту память для других целей. Утечка, вызванная резервированием никогда не используемой памяти также серьезна, как и утечка выделенной памяти.

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

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

Just-in-time (JIT) компилятор

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

Также как для работы обычного статического компилятора, такого как gcc, для компиляции байткода требуется системная память, но, кроме того, также входные (байткод) и выходные данные (исполняемый код) компилятора JIT также должны храниться в системной памяти. Приложения Java, содержащие много JIT-скомпилированных методов используют больше системной памяти, чем менее крупные приложения.

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

Приложения Java состоят из классов, которые определяют структуры объектов и логику методов. Кроме того, они используют классы библиотеки Java (такие как java.lang.String), а также могут использовать сторонние библиотеки. Пока эти классы используются, они должны храниться в памяти.

То, каким образом хранятся классы, зависит от реализации. В JDK от Sun используется область кучи PermGen (permanent generation или постоянного порождения). В реализации Java 5 от IBM для хранения каждого загрузчика класса и самого класса заранее выделяется блок системной памяти. В современных исполняющих средах Java имеются технологии такие, как совместное использование классов, которые могут требовать отображения областей разделяемой памяти в адресное пространство. Чтобы понять, как эти технологии влияют на использование системной памяти в вашей реализации исполняющей среды Java, нужно читать техническую документацию исполняющей среды. Однако есть универсальные закономерности, работающие во всех реализациях.

На самом простом уровне, больше классов используют больше памяти. (Это может значить как то, что использование системной памяти просто будет расти, так и то, что вам придется явно переопределить размер области памяти – такой как PermGen или кэш разделяемых классов, чтобы вместить все нужные классы.) Помните, что место необходимо не только вашему приложению: инфраструктуры, серверы приложений, сторонние библиотеки и сама исполняющая среда Java содержат классы, загружаемые по требованию и занимающие место.

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

  • Куча Java не содержит ссылок на объект java.lang.ClassLoader представляющий этот загрузчик класса.
  • Куча Java не содержит ссылок на объекты java.lang.Class представляющие классы, загруженные этим загрузчиком.
  • Куча Java не содержит ни одной ссылки на объекты классов, загруженные этим загрузчиком

Стоит заметить, что три загрузчика классов по умолчанию, которые исполняющая среда Java создает для всех приложений, — bootstrap, extension и application никогда не могут отвечать этим критериям. Поэтому все системные классы (такие как java.lang.String) или любые классы приложения, загруженные с помощью загрузчика application , не могут быть выгружены во время выполнения.

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

Также классы могут генерироваться во время выполнения без вашего участия. Многие приложения J2EE используют технологию JavaServer Pages (JSP) для создания Web-страниц. Инфраструктура JSP генерирует класс для каждой выполняемой .jsp страницы, который существует все время жизни загрузившего его загрузчика класса, обычно совпадающее со временем жизни всего Web-приложения.

Другой распространенный способ генерации классов Java – с помощью отражения (reflection). Механизм отражения в разных реализациях Java работает по-разному, но в реализациях от Sun и IBM используется метод, о котором я сейчас расскажу.

При использовании API java.lang.reflect исполняющая среда Java должна соединить методы отражающего объекта (такого как java.lang.reflect.Field) с объектом или классом, на который осуществляется отражение. Это может реализовываться с помощью метода доступа к атрибутам класса (аксессора) Java Native Interface (JNI), который почти не требует настройки, но довольно медлителен в работе, или посредством динамического построения класса во время выполнения для каждого объекта, на который вы хотите осуществить отражение. Последний метод требует длительной настройки, но работает быстрее, что делает его идеальным для приложений, часто осуществляющих отражение на определенный класс.

Для отражения класса первые несколько раз исполняющая среда Java использует метод чтения (аксессор) JNI. После нескольких раз использования этот аксессор может «дорасти» до аксессора байткода, создание которого включает в себя построение класса и его загрузку посредством нового загрузчика. Частое применение отражений может привести к созданию множества классов доступа и загрузчиков классов. Хранение ссылок на отражающие объекты приводит к тому, что эти классы остаются в памяти и продолжают занимать место. Так как создание аксессоров байткода – довольно медленная операция, исполняющая среда Java может кэшировать эти аксессоры для последующего использования. Также объекты отражения могут кэшироваться в приложениях и инфраструктурах, что еще больше увеличивая использование системной памяти.

JNI

JNI позволяет платформенно-зависимому коду (приложениям, написанным на таких языках, как C и C++) вызывать методы Java, а также наоборот. Исполняющая среда Java сама по себе в значительной степени полагается на код JNI при реализации некоторых функций библиотеки классов, обеспечивающих, например файловый и сетевой ввод/вывод. Использование системной памяти средой выполнения Java может увеличиваться за счет приложения JNI тремя способами:

  • Платформенно-зависимый код для приложения JNI компилируется в разделяемую библиотеку или исполняемый файл, загружаемый в адресное пространство процесса. Большие платформенно-зависимые приложения только лишь от того, что они загружены, могут занимать значительные участки адресного пространства процесса.
  • Платформенно-зависимый код должен разделять адресное пространство с исполняющей средой Java. Любое выделение системной памяти или отображение памяти, выполняемое платформенно-зависимым кодом, забирает память у исполняющей среды Java.
  • Некоторые функции JNI могут в своей работе использовать системную память. Функции GetTypeArrayElements и GetTypeArrayRegion могут копировать данные из кучи Java в буферы системной памяти для обработки платформенно-зависимым кодом. Делается ли копия данных или нет – зависит от реализации исполняющей среды (IBM Developer Kit для Java 5.0 и выше делают платформенно-зависимую копию). Доступ подобным образом к большим объемам данных из кучи Java может требовать также пропорционально большого количества системной памяти.

NIO

Новые классы ввода/вывода NIO (new I/O), добавленные в Java 1.4, представляют собой новый способ осуществления ввода/вывода, основанный на каналах и буферах. Наряду с буферами ввода/вывода основанными на памяти в куче Java, в NIO есть поддержка прямых объектов ByteBuffer (выделяемых с помощью метода java.nio.ByteBuffer.allocateDirect()), основанных на использовании напрямую системной памяти, а не кучи Java. Прямые объекты ByteBuffer можно передавать напрямую в функции системных библиотек ОС для осуществления ввода/вывода, что делает их в некоторых сценариях значительно быстрее, так как они могут избегать копирования данных между кучей Java и системной памятью.

С тем, где хранятся данные прямых объектов ByteBuffer, легко может возникнуть путаница. Приложение все также использует объект в куче Java для управления операциями ввода вывода, но сам буфер, хранящий данные, находится в системной памяти – а объект, находящийся в куче Java содержит только ссылку на этот буфер. Непрямой объект ByteBuffer хранит данные в массиве byte[] в куче Java. На рисунке 4 показана разница между прямыми и непрямыми объектами ByteBuffer:

Рисунок 4. Топология памяти прямых и непрямых объектов java.nio.ByteBuffer
ByteBuffer memory arrangements

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

Возможно возникновение патологической ситуации, когда системная память заполняется и для одного или нескольких прямых буферов ByteBuffer могла бы быть выполнена сборка мусора (которая бы освободила некоторое количество системной памяти), однако куча памяти Java практически пуста и поэтому сборки мусора не происходит.

Потоки

Каждому потоку приложения требуется память для хранения его стека (области памяти, используемой для хранения локальных переменных и поддержания состояния при вызове функций). То есть, для работы каждого потока Java необходимо некоторое место в памяти. В зависимости от реализации поток Java может иметь раздельные системные и Java стеки. Помимо места для хранения стека каждому потоку требуется системная память для хранения локальных данных потока и внутренних структур данных.

Размер стека зависит от реализации Java и архитектуры. Некоторые реализации позволяют задавать размер стека потоков. Как правило, размер стека потока составляет от 256KB до 756KB.

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


Как я могу распознать нехватку системной памяти?

Исполняющая среда Java по-разному борется с ситуациями, когда заканчивается место в куче Java, и когда заканчивается системная память, хотя оба случая могут проявлять себя похожими симптомами. Приложение Java функционирует с очень большим трудом при исчерпании места в куче Java, так как приложению трудно что-либо сделать без создания объектов. Признаками исчерпания места в куче Java являются низкая производительность сборщика мусора и появление исключений OutOfMemoryError.

Но, когда приложение Java уже запустилось и находится в стабильном состоянии, оно может продолжать функционировать и с полностью исчерпанной системной памятью. Оно не обязательно будет показывать какое-либо странное поведение, так как действия, требующие выделения системной памяти происходят гораздо реже, чем действия, требующие выделения памяти в куче Java. В разных реализациях JVM выделение системной памяти требуется при различных действиях, однако в качестве популярных примеров можно привести: запуск потока, загрузку класса, а также определенные операции файлового и сетевого ввода/вывода.

Поведение системы при нехватке системной памяти не так однозначно, как при нехватке памяти в куче Java, так как не существует единого места управления выделением системной памяти. Если в куче Java все выделения памяти происходят под контролем системы управления памятью, то попытаться выделить системную память и получить ошибку может любой платформенно-зависимый код, находящийся внутри JVM, библиотек классов Java или кода приложения. Код, пытающийся выделить память, может обработать эту ошибку так, как захочет разработчик: может пробросить исключение OutOfMemoryError через интерфейс JNI, вывести сообщение об ошибке на экран, подождать и попробовать еще раз или сделать что-нибудь другое.

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


Примеры нехватки системной памяти

Чтобы помочь вам посмотреть, как ваша реализация Java реагирует на нехватку системной памяти, в примерах кода (см. Загрузки) приведено несколько программ Java, приводящих к нехватке системной памяти несколькими способами. Программы в примерах занимают все адресное пространство с помощью платформенно-зависимых библиотек, написанных на C, а затем пытаются выполнить какое-либо действие, требующее выделения системной памяти. Примеры поставляются в уже собранном виде, однако в html-файле README, находящемся в корневой директории каждого пакета, приводятся также инструкции по компиляции.

Класс com.ibm.jtc.demos.NativeMemoryGlutton содержит метод gobbleMemory(), который вызывает в цикле malloc до практически полного исчерпания системной памяти. По окончании своей работы он выводит в стандартный поток ошибок количество выделенных байт памяти:

Allocated 1953546736 bytes of native memory before running out

Каждая демо-программа запускалась в исполняющей среде Java от Sun и от IBM в 32-разрядной системе Windows. Бинарные файлы были протестированы на следующих системах:

  • Linux x86
  • Linux PPC 32
  • Linux 390 31
  • Windows x86

Для выполнения приложения использовалась следующая версия исполняющей среды Java от Sun:

java version "1.5.0_11"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_11-b03)
Java HotSpot(TM) Client VM (build 1.5.0_11-b03, mixed mode)

Версия Java от IBM была следующей:

java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build pwi32devifx-20071025 (SR
6b))
IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 Windows XP x86-32 j9vmwi3223-2007100
7 (JIT enabled)
J9VM - 20071004_14218_lHdSMR
JIT  - 20070820_1846ifx1_r8
GC   - 200708_10)
JCL  - 20071025

Пытаемся запустить поток при нехватке системной памяти

Класс com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation пытается запустить поток при заполненном адресном пространстве процесса. Это распространенный способ обнаружения нехватки системной памяти, так как многие приложения в процессе своей работы запускают потоки.

Результат работы примера StartingAThreadUnderNativeStarvation в исполняющей среде Java от IBM выглядит так:

Allocated 1019394912 bytes of native memory before running out
JVMDUMP006I Processing Dump Event "systhrow", detail 
"java/lang/OutOfMemoryError" - Please Wait.
JVMDUMP007I JVM Requesting Snap Dump using 'C:\Snap0001.20080323.182114.5172.trc'
JVMDUMP010I Snap Dump written to C:\Snap0001.20080323.182114.5172.trc
JVMDUMP007I JVM Requesting Heap Dump using 'C:\heapdump.20080323.182114.5172.phd'
JVMDUMP010I Heap Dump written to C:\heapdump.20080323.182114.5172.phd
JVMDUMP007I JVM Requesting Java Dump using 'C:\javacore.20080323.182114.5172.txt'
JVMDUMP010I Java Dump written to C:\javacore.20080323.182114.5172.txt
JVMDUMP013I Processed Dump Event "systhrow", detail "java/lang/OutOfMemoryError".
java.lang.OutOfMemoryError: ZIP006:OutOfMemoryError, ENOMEM error in ZipFile.open
   at java.util.zip.ZipFile.open(Native Method)
   at java.util.zip.ZipFile.<init>(ZipFile.java:238)
   at java.util.jar.JarFile.<init>(JarFile.java:169)
   at java.util.jar.JarFile.<init>(JarFile.java:107)
   at com.ibm.oti.vm.AbstractClassLoader.fillCache(AbstractClassLoader.java:69)
   at com.ibm.oti.vm.AbstractClassLoader.getResourceAsStream(AbstractClassLoader.java:113)
   at java.util.ResourceBundle$1.run(ResourceBundle.java:1101)
   at java.security.AccessController.doPrivileged(AccessController.java:197)
   at java.util.ResourceBundle.loadBundle(ResourceBundle.java:1097)
   at java.util.ResourceBundle.findBundle(ResourceBundle.java:942)
   at java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:779)
   at java.util.ResourceBundle.getBundle(ResourceBundle.java:716)
   at com.ibm.oti.vm.MsgHelp.setLocale(MsgHelp.java:103)
   at com.ibm.oti.util.Msg$1.run(Msg.java:44)
   at java.security.AccessController.doPrivileged(AccessController.java:197)
   at com.ibm.oti.util.Msg.<clinit>(Msg.java:41)
   at java.lang.J9VMInternals.initializeImpl(Native Method)
   at java.lang.J9VMInternals.initialize(J9VMInternals.java:194)
   at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:764)
   at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:758)
   at java.lang.Thread.uncaughtException(Thread.java:1315)
K0319java.lang.OutOfMemoryError: Failed to fork OS thread
   at java.lang.Thread.startImpl(Native Method)
   at java.lang.Thread.start(Thread.java:979)
   at com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation.main(
StartingAThreadUnderNativeStarvation.java:22)

В вызове java.lang.Thread.start() делается попытка выделить в ОС память для нового потока. Память выделить не удается, и это приводит к возбуждению исключения OutOfMemoryError. Строки JVMDUMP оповещают пользователя, что исполняющая среда Java сгенерировала стандартную отладочную информацию для OutOfMemoryError.

При попытке обработать первое исключение OutOfMemoryError возникает второе исключение :OutOfMemoryError, ENOMEM error in ZipFile.open. Многократные исключения OutOfMemoryError часто встречаются при заполнении адресного пространства процесса. Возможно, самым распространенным признаком нехватки системной памяти является сообщение Failed to fork OS thread.

Примеры, поставляемые с этой статьей, генерируют массу исключений OutOfMemoryError Скорее всего, в ваших приложениях при нехватке системной памяти все будет выглядеть не так сурово. Отчасти, это связано с тем, что здесь заполняется практически вся системная память, и в отличие от реальных приложений она не освобождается впоследствии. В реальных приложениях при возбуждении исключения OutOfMemoryError закрываются потоки, что понижает давление на системную память и дает исполняющей среде шанс обработать ошибку. Тривиальная природа сценариев примеров также означает, что целые разделы библиотеки классов (например, система безопасности) не успевают проинициализироваться. Их инициализация начинается исполняющей средой Java при попытке обработать ситуацию нехватки системной памяти. В реальных приложениях вы, возможно, встретитесь с некоторыми из показанных здесь ошибок, однако маловероятно, что вы увидите их все вместе.

Работа этого же примера в исполняющей среде Java от Sun выглядит так:

Allocated 1953546736 bytes of native memory before running out
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
   at java.lang.Thread.start0(Native Method)
   at java.lang.Thread.start(Thread.java:574)
   at com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation.main(
StartingAThreadUnderNativeStarvation.java:22)

Хотя трассировка стека вызовов и сообщение об ошибке немного отличаются, в сущности, поведение то же самое: не удается выделить системную память и возбуждается исключение java.lang.OutOfMemoryError Единственным отличием исключений OutOfMemoryError в этом сценарии от исключений, возбуждаемых при заполненной куче Java, является текст сообщения.

Пытаемся выделить память для прямого объекта ByteBuffer при нехватке системной памяти

Класс com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation пытается выделить память для прямого (т.е. использующего напрямую системную память) объекта java.nio.ByteBuffer при заполненном адресном пространстве процесса. Работа приложения в исполняющей среде Java от IBM выглядит так

Allocated 1019481472 bytes of native memory before running out
JVMDUMP006I Processing Dump Event "uncaught", detail
"java/lang/OutOfMemoryError" - Please Wait.
JVMDUMP007I JVM Requesting Snap Dump using 'C:\Snap0001.20080324.100721.4232.trc'
JVMDUMP010I Snap Dump written to C:\Snap0001.20080324.100721.4232.trc
JVMDUMP007I JVM Requesting Heap Dump using 'C:\heapdump.20080324.100721.4232.phd'
JVMDUMP010I Heap Dump written to C:\heapdump.20080324.100721.4232.phd
JVMDUMP007I JVM Requesting Java Dump using 'C:\javacore.20080324.100721.4232.txt'
JVMDUMP010I Java Dump written to C:\javacore.20080324.100721.4232.txt
JVMDUMP013I Processed Dump Event "uncaught", detail "java/lang/OutOfMemoryError".
Exception in thread "main" java.lang.OutOfMemoryError: 
Unable to allocate 1048576 bytes of direct memory after 5 retries
   at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:167)
   at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:303)
   at com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation.main(
   DirectByteBufferUnderNativeStarvation.java:29)
Caused by: java.lang.OutOfMemoryError
   at sun.misc.Unsafe.allocateMemory(Native Method)
   at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:154)
   ... 2 more

В этом сценарии возбуждается исключение OutOfMemoryError, и для произошедшей ошибки генерируется стандартная отладочная информация. Исключение OutOfMemoryError достигает верхушки стека основного потока и выводится в stderr.

При работе приложения в исполняющей среде Java от Sun в консоль выводится следующая информация:

Allocated 1953546760 bytes of native memory before running out
Exception in thread "main" java.lang.OutOfMemoryError
   at sun.misc.Unsafe.allocateMemory(Native Method)
   at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:99)
   at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
   at com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation.main(
DirectByteBufferUnderNativeStarvation.java:29)

Способы и техники отладки

При появлении исключения java.lang.OutOfMemoryError или ошибки о нехватке памяти, в первую очередь необходимо определить, какая именно память закончилась. Самый легкий способ сделать это – проверить, заполнена ли куча Java. Если место в куче Java есть, следует проанализировать использование системной памяти.

Проверяем документацию производителя

Руководства, приводимые в этой статье, представляют собой общие принципы отладки сценариев нехватки системной памяти. Производитель вашей исполняющей среды Java может предлагать свои собственные инструкции отладки, которые следует выполнить при обращении в службу поддержки. Если вы намерены сообщить производителю исполняющей среды (в том числе и IBM) о проблеме, всегда проверяйте их документацию для отладки и диагностики, чтобы узнать какие шаги вам следует для этого предпринять.

Проверяем кучу Java

В разных реализациях Java проверить использование кучи можно разными методами. В реализациях Java 5 и 6 от IBM при возбуждении исключения OutOfMemoryError генерируется файл javacore, в котором содержится необходимая информация. Файл javacore обычно генерируется в рабочей директории процесса Java и имеет имя вида javacore.date.time.pid.txt. Открыв файл в текстовом редакторе, в нем можно увидеть раздел, выглядящий так:

0SECTION       MEMINFO subcomponent dump routine
NULL           =================================
1STHEAPFREE    Bytes of Heap Space Free: 416760 
1STHEAPALLOC   Bytes of Heap Space Allocated: 1344800

В этой секции показано, как много место было свободно в куче Java на момент создания файла javacore. Обратите внимание, что значения здесь выводятся в шестнадцатеричной форме. Если исключение OutOfMemoryError было сгенерировано из-за невозможности выделить память для какого-либо объекта, то в разделе с трассировкой сборщика мусора будет показано следующее:

1STGCHTYPE     GC History  
3STHSTTYPE     09:59:01:632262775 GMT j9mm.80 -   J9AllocateObject() returning NULL!
32 bytes requested for object of class 00147F80

Текст J9AllocateObject() returning NULL! означает, что процедура выделения памяти в куче Java завершилась неудачно и поэтому будет возбуждено исключение OutOfMemoryError.

Исключение OutOfMemoryError также может генерироваться, из-за того, что сборщик мусора начинает запускаться слишком часто (признак того, что куча заполнена, и работа приложения Java продвигается очень медленно или не продвигается вообще). В этом случае значение Heap Space Free, скорее всего, будет очень маленьким, и трассировки сборщика мусора будут содержать одно из следующих сообщений:

1STGCHTYPE     GC History  
3STHSTTYPE     09:59:01:632262775 GMT j9mm.83 -     Forcing J9AllocateObject()
to fail due to excessive GC
1STGCHTYPE     GC History  
3STHSTTYPE     09:59:01:632262775 GMT j9mm.84 -     Forcing 
J9AllocateIndexableObject() to fail due to excessive GC

В реализации JVM от Sun при исчерпании места в куче Java возбуждается исключение, в тексте которого явно указывается, что место закончилось именно в куче Java:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

В реализациях от IBM и Sun в сборщике мусора предусмотрен параметр verbose, генерирующий трассировочные данные, из которых видно, насколько заполненной была куча на каждом цикле сборки мусора. Эту информацию можно изобразить графически, например, с помощью инструмента GCMV из пакета инструментов мониторинга и диагностики для Java от IBM (see Ресурсы), чтобы посмотреть, растет ли размер кучи Java.

Измерение использования системной памяти

Если вы определили, что ошибка не была вызвана исчерпанием места в куче Java, следующим шагом является профилирование использования системной памяти.

Поставляемый вместе с Windows инструмент PerfMon позволяет отслеживать и записывать многие метрики операционной системы и процессов, в том числе использование системной памяти (см. Ресурсы). Он позволяет отслеживать в реальном времени счетчики, а также сохранять их в файл журнала для последующего изучения. Для отображения количества используемого адресного пространства используйте счетчик Private Bytes. Если его значение приближается к верхней границе пространства пользователей (как ранее обсуждалось от 2 до 3ГБ), следует ожидать появления проблем с нехваткой памяти.

В Linux нет эквивалента PerfMon, однако есть несколько альтернативных инструментов. Посмотреть количество используемой приложением системной памяти можно с помощью таких инструментов командной строки, как ps, top, и pmap. И хотя получить картину использования памяти процессом в определенный момент может быть полезным, гораздо лучшее понимание происходящего можно получить из графика изменения использования системной памяти во времени. Такой график можно построить, например, с помощью GCMV.

Изначально GCMV был написан для построения графиков данных из журналов сборщика мусора, чтобы при настройке сборщика мусора пользователи могли видеть, как их изменения влияют на использование кучи Java и производительность GC. В дальнейшем GCMV был расширен, и теперь с его помощью можно строить графики на основе других данных, в том числе информации о системной памяти Linux и AIX. GCMV поставляется в виде модуля расширения к ISA (IBM Support Assistant).

Для построения в GCMV графика использования системной памяти сначала необходимо собрать информацию об использовании системной памяти с помощью какого-либо скрипта. Включенный в GCMV анализатор системной памяти Linux зачитывает данные команды ps, перемежаемые временными метками. Скрипт, собирающий данные в корректной форме включен в документацию GCMV. Чтобы найти этот скрипт:

  1. Загрузите и установите ISA версии 4 (или выше), а также модуль расширения GCMV.
  2. Запустите ISA.
  3. Чтобы открыть меню справки, нажмите Help >> Help Contents (в строке меню).
  4. Найдите инструкции по работе с системной памятью Linux в панели слева по следующему пути: Tool:IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer >> Using the Garbage Collection and Memory Visualizer >> Supported Data Types >> Native memory >> Linux native memory .

На рисунке 5 показано местонахождение скрипта в справочном файле ISA. Если в справочном файле отсутствует пункт, посвященный GCMV, скорее всего модуль расширения GCMV у вас не установлен.

Рисунок 5. Местонахождение в справке ISA скрипта для сбора данных об использовании системной памяти Linux
IBM Support Assistant Help File

Представленный в справке GCMV скрипт использует команду, работающую только с новыми версиями программы ps. В некоторых старых дистрибутивах Linux команда, указанная в справочном файле не будет генерировать информацию в правильном формате. Чтобы проверить, подходит ли скрипт для вашего дистрибутива, запустите команду ps -o pid,vsz=VSZ,rss=RSS. Если ваша версия ps поддерживает новый синтаксис аргументов командной строки, результат её работы будет выглядеть так:

  PID    VSZ   RSS
 5826   3772  1960
 5675   2492   760

Если же ваша версия ps не поддерживает новый синтаксис, результат ее работы будет выглядеть иначе:

  PID VSZ,rss=RSS
 5826        3772
 5674        2488

Если у вас установлена старая версия ps, измените скрипт, заменив в нем строку

ps -p $PID -o pid,vsz=VSZ,rss=RSS

на

ps -p $PID -o pid,vsz,rss

Скопируйте скрипт из панели справки в файл (в нашем примере memscript.h), узнайте идентификатор Java процесса (PID), который вы хотите отслеживать (в нашем примере 1234) и запустите его:

./memscript.sh 1234 > ps.out

Данные об использовании системной памяти будут записываться в файл журнала ps.out. Для построения графика на основе записанных данных выполните следующее:

  1. В ISA из выпадающего меню Launch Activity выберите Analyze Problem.
  2. В верхней части панели Analyze Problem выберите вкладку Tools.
  3. Выберите IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer.
  4. Нажмите кнопку Launch в нижней части панели инструментов.
  5. Нажмите кнопку Browse и укажите расположение файла журнала. Нажмите OK для запуска GCMV.

Теперь, когда у вас есть информация об изменении использования системной памяти с течением времени, необходимо решить, вызвана ли проблема утечкой системной памяти, или же приложение пытается сделать слишком много в располагаемом пространстве. Даже в хорошо работающих Java- приложениях использование системной памяти не является постоянным с момента запуска. Некоторые системы исполняющей среды Java – в частности компилятор JIT и загрузчики классов инициализируются в процессе работы приложения, что может увеличить потребление системной памяти. Рост использования памяти затухает после фазы инициализации, однако, если в вашем сценарии использование памяти на начальном этапе приближается к верхней границе адресного пространства, этой загрузочной фазы может быть достаточно для возникновения нехватки памяти. На рисунке 6 приведен пример графика системной памяти, построенного с помощью GCMV для нагрузочного теста Java. Загрузочная фаза на графике выделена цветом.

Рисунок 6. Построенный с помощью GCMV график использования системной памяти Linux, показывающий разминочную фазу
GCMV native memory plot

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

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

  • Уменьшить использование системной памяти Сокращение кучи Java – хорошая отправная точка.
  • Ограничить использование системной памяти. Если использование системной памяти возрастает при увеличении нагрузки, найдите способ оптимизировать нагрузку или механизм выделения ресурсов для новых заданий.
  • Увеличьте размер доступного адресного пространства. Это можно сделать, настроив ОС (например, увеличив пространство пользователя с помощью переключателя /3GB в Windows или переключившись на ядро hugemem в Linux), поменяв платформу (обычно Linux имеет больше пользовательского пространства, чем Windows) или перейдя на 64-разрядную операционную систему.

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

При обнаружении утечки у вас есть ограниченный набор возможных действий. Можно увеличить размер пользовательского пространства (чтобы выделить больше места, куда памяти можно утекать), но это лишь отсрочит время возникновения нехватки памяти. Если вы располагаете достаточным объемом физической памяти и адресного пространства, вы можете допустить существование утечки, при условии, что приложение будет перезагружаться до исчерпания всего адресного пространства процесса.

Что использует мою системную память?

Когда вы определили, что столкнулись с нехваткой системной памяти, возникает логичный вопрос: «что использует всю эту память»? Ответить на этот вопрос довольно сложно, так как Windows и Linux не хранят информации о том, каким именно кодом был выделен тот или иной участок памяти.

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

  • Куча Java занимает не менее значения -Xmx.
  • Каждому потоку Java необходимо место для стека. Размер стека зависит от реализации, но с настройками по умолчанию каждый поток может занимать до 756КБ системной памяти.
  • Прямые объекты ByteBuffer занимают не менее памяти не меньше, чем указывается в вызове функции allocate().

Если полученное общее значение намного меньше размера пользовательского пространства, это не означает, что вы в безопасности. Многие другие компоненты окружения выполнения Java могут использовать достаточно памяти, чтобы вызвать проблемы. Однако если согласно вашим вычислениям вы приближаетесь к верхней границе пользовательского пространства, вполне вероятно, что вы столкнетесь с нехваткой системной памяти. Если вы подозреваете, что в вашем приложении есть утечка памяти, или хотите понять, куда именно уходит системная память, вам могут помочь несколько инструментов.

Microsoft предлагает инструменты UMDH (user-mode dump heap) и LeakDiag для отладки утечек системной памяти в Windows (см. Ресурсы). Оба инструмента работают похожим образом: записывают, какой код выделил каждый конкретный участок памяти, и предоставляют способ определить, каким кодом был выделен участок памяти, не освобожденный впоследствии. За инструкциями по использованию UMDH я рекомендую вам обратиться к статье Umdhtools.exe: How to use Umdh.exe to find memory leaks on Windows" (см. Ресурсы). А в этой статье я сфокусируюсь на том, как выглядит результат работы UMDH при проверке приложения с утечкой JNI.

В пакете примеров к данной статье имеется приложение Java, называемое LeakyJNIApp. В нем вызывается в цикле JNI метод с утечкой системной памяти. Программа UMDH делает снимок кучи системной памяти и трассировок системных стеков, показывающих, каким именно кодом был выделен каждый участок памяти. Сделав два таких снимка и проанализировав различия с помощью UMDH, можно получить отчет о росте использования системной памяти за данный интервал времени.

Для LeakyJNIApp файл отличий содержит следующую информацию:

// _NT_SYMBOL_PATH set by default to C:\WINDOWS\symbols
//
// Each log entry has the following syntax:
//
// + BYTES_DELTA (NEW_BYTES - OLD_BYTES) NEW_COUNT allocs BackTrace TRACEID
// + COUNT_DELTA (NEW_COUNT - OLD_COUNT) BackTrace TRACEID allocations
//     ... stack trace ...
//
// where:
//
//     BYTES_DELTA - increase in bytes between before and after log
//     NEW_BYTES - bytes in after log
//     OLD_BYTES - bytes in before log
//     COUNT_DELTA - increase in allocations between before and after log
//     NEW_COUNT - number of allocations in after log
//     OLD_COUNT - number of allocations in before log
//     TRACEID - decimal index of the stack trace in the trace database
//         (can be used to search for allocation instances in the original
//         UMDH logs).
//

+  412192 ( 1031943 - 619751)    963 allocs     BackTrace00468

Total increase == 412192

Здесь важной является строка + 412192 ( 1031943 - 619751) 963 allocs BackTrace00468. В ней показано, что в одной из обратных трассировок было сделано 963 выделения системной памяти в сумме 412192 байта, которая не была освобождена впоследствии. Заглянув в один из файлов снимков, можно соотнести BackTrace00468 с неким участком кода. Первый файл содержит следующую информацию о BackTrace00468:

000000AD bytes in 0x1 allocations (@ 0x00000031 + 0x0000001F) by: BackTrace00468
        ntdll!RtlpNtMakeTemporaryKey+000074D0
        ntdll!RtlInitializeSListHead+00010D08
        ntdll!wcsncat+00000224
        leakyjniapp!Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod+000000D6

Отсюда видно, что утечка происходит в методе Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod модуля leakyjniapp.dll.

На момент написания статьи Linux не имеет эквивалента UMDH или LeakDiag. Но способы для отладки утечек системной памяти все же есть. Большинство доступных в Linux отладчиков использования памяти можно разделить на следующие категории:

  • Отладчики уровня препроцессора. Для работы этих инструментов необходимо скомпилировать заголовок инструмента в тестируемый исходный код. Можно перекомпилировать собственные JNI библиотеки с одним из этих инструментов для отслеживания утечек памяти в вашем коде. Однако, подобные инструменты не могут найти утечки памяти внутри JVM, если у вас нет ее кода (и даже при наличии кода, скомпилировать подобный инструмент в такой большой проект как JVM наверняка будет сложной и трудоемкой задачей). Примером подобного инструмента является Dmalloc (см. Ресурсы).
  • Отладчики цуровня компоновщика. В таких инструментах требуется перекомпоновка бинарных файлов с отладочной библиотекой. Как и инструменты предыдущей категории, эти инструменты подходят для работы с отдельными JNI библиотеками, но не годятся для всей исполняющей среды Java, так как вряд ли производитель исполняющей среды поддерживает возможность запуска модифицированных бинарных файлов. Примером инструмента такого рода является ccmalloc (см. Ресурсы).
  • Отладчики уровня компоновщика исполняемой среды. В этих инструментах используется переменная окружения LD_PRELOAD для предварительной загрузки библиотеки, заменяющей стандартные процедуры работы с памятью инструментированными версиями. Эти инструменты не требуют повторной компиляции или компоновки исходного кода, но многие из них плохо работают с исполняющими средами Java. Исполняющая среда Java – сложная система, которая может использовать память и потоки необычными способами, что может вводить в заблуждение подобные инструменты. Стоит поэкспериментировать с одним или несколькими подобными инструментами, чтобы посмотреть, будут ли они работать в ваших сценариях. Примером инструмента такого рода является NJMAD (см. Ресурсы).
  • Отладчики, основанные на эмуляторах. Модуль memcheck инструмента Valgrind – единственный представитель отладчиков памяти этого типа (см. Ресурсы). Он эмулирует процессор подобно тому, как исполняющая среда Java эмулирует виртуальную машину (JVM). Под ним можно запускать Java, однако сильное снижение производительности (в 10-30 раз) делает весьма затруднительным запуск подобным образом больших и сложных приложений Java. В настоящее время Valgrind доступен на Linux x86, AMD64, PPC 32 и PPC 64. Если вы используете Valgrind, попытайтесь предварительно сузить проблему до наименьшего возможного сценария (по возможности исключив из него всю среду выполнения Java).

Для простых сценариев, в которых падение производительности приемлемо, memcheck из Valgrind является самым простым и дружелюбным среди доступных бесплатных инструментов. Так же как UMDH в Windows, он может предоставить полную трассировку стека кода, в котором происходит утечка памяти.

Приложение LeakyJNIApp является достаточно простым для того, чтобы запускать его под Valgrind. Модуль memcheck из Valgrind по окончании выполнения программы может распечатывать сводную информацию об утечках памяти. По умолчанию, программа LeakyJNIApp выполняется бесконечно, чтобы она завершилась по истечении определенного времени, нужно передать желаемое время выполнения в секундах в виде аргумента командной строки.

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

Для трассировки приложения LeakyJNIApp с помощью memcheck из Valgrind, используйте следующую команду (одной строкой):

valgrind --trace-children=yes --leak-check=full 
java -Djava.library.path=. com.ibm.jtc.demos.LeakyJNIApp 10

Параметр --trace-children=yes командует Valgrind отслеживать любые процессы, запущенные загрузчиком Java. Некоторые версии загрузчика Java повторно исполняют сами себя (они перезапускают сами себя, чтобы вступили в силу вновь заданные переменные окружения). Если не задать параметр --trace-children, вы можете потерять трассировку фактически работающей исполняющей среды Java.

Параметр --leak-check=full командует распечатывать в конце выполнения программы полные трассировки стеков кода с утечками памяти, вместо вывода лишь сводной информации о состоянии памяти.

Во время работы Valgrind печатает множество предупреждений и ошибок (из которых большинство не представляет интереса в данном контексте), а в конце выводит список стеков вызовов с утечками памяти, упорядочивая их по возрастанию объема утечки. Конец итоговой секции, распечатанной Valgrind для LeakyJNIApp, запущенного на Linux x86 выглядит так:

==20494== 8,192 bytes in 8 blocks are possibly lost in loss record 36 of 45
==20494==    at 0x4024AB8: malloc (vg_replace_malloc.c:207)
==20494==    by 0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod
(in /home/andhall/LeakyJNIApp/libleakyjniapp.so)
==20494==    by 0x535CF56: ???
==20494==    by 0x46423CB: gpProtectedRunCallInMethod 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x46441CF: signalProtectAndRunGlue 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x467E0D1: j9sig_protect 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9prt23.so)
==20494==    by 0x46425FD: gpProtectAndRun 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x4642A33: gpCheckCallin 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x464184C: callStaticVoidMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x80499D3: main 
(in /usr/local/ibm-java2-i386-50/jre/bin/java)
==20494== 
==20494== 
==20494== 65,536 (63,488 direct, 2,048 indirect) bytes in 62 blocks are definitely 
lost in loss record 42 of 45
==20494==    at 0x4024AB8: malloc (vg_replace_malloc.c:207)
==20494==    by 0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod 
(in /home/andhall/LeakyJNIApp/libleakyjniapp.so)
==20494==    by 0x535CF56: ???
==20494==    by 0x46423CB: gpProtectedRunCallInMethod 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x46441CF: signalProtectAndRunGlue 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x467E0D1: j9sig_protect 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9prt23.so)
==20494==    by 0x46425FD: gpProtectAndRun 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x4642A33: gpCheckCallin 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x464184C: callStaticVoidMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x80499D3: main 
(in /usr/local/ibm-java2-i386-50/jre/bin/java)
==20494== 
==20494== LEAK SUMMARY:
==20494==    definitely lost: 63,957 bytes in 69 blocks.
==20494==    indirectly lost: 2,168 bytes in 12 blocks.
==20494==      possibly lost: 8,600 bytes in 11 blocks.
==20494==    still reachable: 5,156,340 bytes in 980 blocks.
==20494==         suppressed: 0 bytes in 0 blocks.
==20494== Reachable blocks (those to which a pointer was found) are not shown.
==20494== To see them, rerun with: --leak-check=full --show-reachable=yes

Во второй строке стеков показано, что утечка памяти происходит в методе com.ibm.jtc.demos.LeakyJNIApp.nativeMethod().

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

В настоящее время отладка утечек системной памяти в Linux с помощью бесплатных инструментов является, по сравнению с Windows, более трудной задачей. В то время как UMDH позволяет отлаживать утечки системной памяти в Windows in situ (от начала и до конца), на Linux, возможно, стоит провести традиционную отладку, а не полагаться на то, что инструмент решит проблему за вас. Вот некоторые предлагаемые нами шаги отладки:

  • Выявите сценарий. Создайте отдельное окружение, в котором вы можете воспроизводить утечку системной памяти. Это существенно упростит отладку.
  • Сократите сценарий насколько это возможно. Пробуйте заменять функции заглушками, чтобы идентифицировать участок кода, виновный в утечке памяти. Если у вас есть собственные JNI библиотеки, также попробуйте заменить их заглушками, чтобы определить, не в них ли причина утечки.
  • Сократите размер кучи Java. Вероятно, что куча Java является самым крупным потребителем виртуального адресного пространства процесса. Сократив размер кучи Java, вы можете освободить некоторое количество системной памяти для других нужд.
  • Отслеживайте изменение размера процесса. Имея график изменения использования системной памяти с течением времени, можно сопоставить его с данными о нагрузке приложения и сборке мусора. Если скорость утечки пропорциональна уровню нагрузки, можно предположить, что утечка вызвана чем-то, что происходит при каждой транзакции или операции. Если размер памяти процесса значительно падает после сборки мусора, можно предположить, что вы имеете дело не с утечкой, а с созданием объектов, для которых выделяется системная память (например, объектов ByteBuffer). Количество памяти, занимаемое объектами, использующими системную память, можно уменьшить, сократив размер кучи Java (таким образом, заставляя более часто запускать сборку мусора) или самостоятельно организовав для них некий кэш объектов, вместо того, чтобы полагаться при их удалении на сборщика мусора.

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


Устраняем ограничения: переход на 64-разрядную ОС

В 32-разрядной среде выполнения Java довольно просто исчерпать всю системную память, так как адресное пространство относительно мало. Пользовательское пространство от 2 до 4ГБ, предоставляемое 32-разрядными ОС зачастую меньше объема имеющейся физической памяти и многие современные приложения, работающие с большими объемами данных, могут легко заполнить все доступное пространство.

Если ваше приложение не может поместиться в 32-разрядном адресном пространстве, можно многократно увеличить размер пользовательского пространства, перейдя на 64-разрядную исполняющую среду Java. При работе на 64-разрядной ОС 64-разрядная исполняющая среда Java позволяет использовать огромные кучи памяти Java и забыть о проблемах с нехваткой места в адресном пространстве. В таблице 2 перечислены пользовательские пространства, доступные в настоящее время в 64-разрядных ОС:

Талица 2. Размеры пользовательского пространства в 64-разрядных ОС
ОСРазмер пользовательского пространства по умолчанию
Windows x86-648192GB
Windows Itanium7152GB
Linux x86-64500GB
Linux PPC641648GB
Linux 390 644EB

Тем не менее, переход на 64-разрядную систему не является универсальным решением для всех проблем, связанных с системной памятью, Вам все так же нужно иметь достаточное количество физической памяти для хранения всех ваших данных. Если исполняющая среда Java не будет помещаться в физической памяти, производительность приложения будет неприемлемо низкой, так как ОС будет вынуждена постоянно копировать данные в область подкачки и из нее. По той же причине переход на 64-разрядную систему не является решением проблемы утечки памяти – вы просто предоставляете больше места, куда память может утекать, покупая своему приложению отсрочку перед вынужденной перезагрузкой.

Не всегда возможно использовать 32-разрядный платформенно-зависимый код в 64-разрядной исполняющей среде; любой платформенно-зависимый код (библиотеки JNI, агенты интерфейса инструментов JVM (JVM Tool Interface или JVMTI), интерфейса профилирования JVM (JVM Profiling Interface или JVMPI) и интерфейса отладки JVM (JVM Debug Interface или JVMDI)) должны быть перекомпилированы для 64-разрядной среды. Производительность 64-разрядной исполняющей среды также может быть ниже, чем производительность аналогичной 32-разрядной версии на том же железе. 64-разрядная исполняющая среда использует 64-разрядные указатели (ссылки на адреса системной памяти), поэтому объект в 64-разрядной исполняющей среде занимает больше места, чем, объект, содержащий те же самые данные в 32-разрядной среде. Больший размер объектов означает, что для хранения того же объема данных в куче Java требуется больше места, при одинаковой производительности сборщика мусора. Это делает кэши аппаратного обеспечения и ОС менее эффективными. К удивлению, больший размер кучи Java не обязательно означает более долгую работу сборщика мусора, так как количество «живых» данных в куче может и не увеличиться, поэтому некоторые алгоритмы сборки мусора более эффективны с большими кучами Java.

В некоторых современных исполняющих средах Java имеются технологии для для уменьшения «распухания» 64-разрядных объектов и улучшения производительности. Такая функциональность работает за счет использования более коротких ссылок в 64-разрядных исполняющих средах. Это называется сжатыми ссылками в реализациях от IBM и сжатыми объектами (oops) в реализациях от Sun.

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


Заключение

Понимание устройства системной памяти играет важную роль при проектировании и написании больших Java-приложений. Этим часто пренебрегают, так как чтобы его понять, приходится вникать в детали работы аппаратного обеспечения и ОС, от которых исполняющая среда Java по замыслу должна нас изолировать. Однако JRE – это процесс, который должен работать в окружении, определяемом этими деталями. Чтобы выжать максимум производительности из своего Java-приложения, необходимо понимать, как приложение влияет на использование системной памяти в исполняющей среде.

Нехватка системной памяти может походить на нехватку памяти в куче Java, но для отладки и решения проблем с нехваткой системной памяти необходим другой набор инструментов. Ключевыми факторами в устранении неполадок с системной памятью являются понимание того, какие ограничения накладываются на системную память со стороны аппаратного обеспечения и ОС, на которых работает ваше Java-приложение, и умение работать с инструментами ОС для отслеживания использования системной памяти. Комбинируя эти знания и умения вы сможете решать одни из самых сложных задач, которые могут возникнуть в ваших Java-приложениях.


Загрузка

ОписаниеИмяРазмер
Примеры кода, использующего системную памятьj-nativememory-linux.zip115КБ

Ресурсы

Научиться

  • Ознакомьтесь с оригиналом статьи: "Thanks for the memory" (EN, developerWorks, апрель 2009 г.).
  • "Garbage collection with the IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer" (EN, Холли Каминс, developerWorks, октябрь 2007 г.): узнайте, как загрузить, установить и использовать GCMV для анализа подробных отладочных данных о сборке мусора.
  • "Don't forget about memory" (EN, Эмма Шеферд и др., developerWorks, ноябрь 2004 г.): научитесь отслеживать использование памяти вашими Java-приложениями в Windows с помощью PerfMon и других инструментов.
  • "Analyzing memory usage with LeakDiag" (EN, блог Дмитрия Еремова, февраль 2005 г.): инструмент LinkDiag перехватывает вызовы функций выделения памяти, записывает для каждого из них стек вызова и заносит в журнал информацию о каждом выделении памяти, группируя ее по стекам вызовов.
  • "Umdhtools.exe: How to use Umdh.exe to find memory leaks on Windows" (EN, центр помощи и поддержки Microsoft, апрель 2007 г.): познакомьтесь с UMDH - другим инструментом от Microsoft, похожим на LeakDiag.
  • "Windows Java address space" (EN, Фил Викерс и Амар Девеговда, IBM центр Java-технологий, декабрь 2005 г.): узнайте, как максимизировать размер адресного пространства в Java от IBM на 32-разрядных системах Windows.
  • "The Support Authority: Introducing the IBM Guided Activity Assistant" (EN, Дейв Дрегер и др., developerWorks, май 2007 г.): познакомьтесь с правилами, которые помогут вам отлаживать распространенные проблемы, в том числе ситуации с нехваткой памяти Java.
  • "Kernel address space consequences of the /3GB switch" (EN, Рэймонд Чен, The Old New Thing, август 2004 г.): небольшое обсуждение сокращения пространства ядра Windows с помощью переключателя /3GB.
  • Summary of the recent spate of /3GB articles (EN, Рэймонд Чен, The Old New Thing, август 2004 г.): Ссылки на некоторые статьи и записи в блогах о переключателе /3GB в Windows.
  • Guided Debugging for Java: (EN) в SDK от Java для IBM имеются руководства, которые могут помочь вам решить распространенные проблемы, возникающие при программировании на Java.

Получить продукты и технологии

  • Valgrind: загрузите инфраструктуру инструментирования Valgrind, включающую в себя, в том числе, детектор ошибок работы с памятью.
  • Dmalloc: загрузите библиотеку Debug Malloc.
  • ccmalloc: загрузите библиотеку ccmalloc для отладки работы с памятью.
  • NJAMD: загрузите NJAMD (Not Just Another Malloc Debugger или непросто еще один отладчик Malloc) - библиотеку для отладки работы с памятью.
  • IBM Monitoring and Diagnostic Tools for Java: Посетите страницу инструментов для Java от IBM.
  • IBM Support Assistant (ISA): Эта бесплатная инфраструктура сопровождения содержит такие инструменты, как GCMV (Garbage Collection and Memory Visualizer) и IBM Guided Activity Assistant, которые могут вам помочь в отладке проблем с нехваткой системной памяти.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Технология Java
ArticleID=660436
ArticleTitle=Спасибо за память
publish-date=05242011