Использование gperftools для обнаружения утечек памяти Java в macOS

Java

В последние несколько дней, когда я устранял утечку памяти вне кучи, я видел, что многие люди упоминали артефакт gperftools, я хотел попробовать его и обнаружил, что его поддержка для macOS не очень дружелюбна. И большинство туториалов для C++, и операция компиляции и компоновки в нем просто ослепляет меня Java. Поэтому я здесь, чтобы организовать туториал по использованию версий для Mac и Java, чтобы все снова не наступили на яму.

1. Введение

gperftools — это набор инструментов анализа, предоставляемых Google, включая heap-profiler для обнаружения памяти в куче, heap-checker для анализа утечек памяти и cpu-profiler для мониторинга производительности процессора. Хорошо известно, что утечки памяти вне кучи трудно отследить.Используя инструменты анализа дампа, такие как MAT, вы можете начать только с самого большого или большинства объектов в куче, чтобы проанализировать, где происходит утечка. А gperftools заменяет вызов malloc собственным tcmalloc, который подсчитывает поведение всех выделений памяти и помогает нам быстрее найти утечку.

2. Установка

Просто установите его с помощью homebrew.

brew install gperftools

3. Используйте gperftools для обнаружения утечек памяти

1. Пример программы

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

кодовый адрес

public class NativeMemoryLeakDemo {

    public static void main(String[] args) throws IOException, FontFormatException {
        while (true) {
            test();
        }
    }

    private static void test() throws IOException, FontFormatException {
        Resource resource = new ClassPathResource("font/font.ttf");
        Font rawFont = Font.createFont(Font.TRUETYPE_FONT, resource.getFile());
        Font usedFont = rawFont.deriveFont(Font.PLAIN, 30);

        BufferedImage bufferedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2 = bufferedImage.createGraphics();
        g2.setFont(usedFont);
        g2.drawString("hello world", 16, 35);
    }
}

Сначала запускаем на некоторое время (Java8) со следующими параметрами ВМ

-XX:CMSInitiatingOccupancyFraction=80
-XX:CompressedClassSpaceSize=528482304
-XX:InitialHeapSize=3221225472
-XX:MaxDirectMemorySize=536870912
-XX:MaxHeapSize=3221225472
-XX:MaxMetaspaceSize=536870912
-XX:MaxNewSize=1157627904
-XX:MetaspaceSize=536870912
-XX:NewSize=1157627904
-XX:SurvivorRatio=8

Рис. 1. Общая память, занимаемая процессом

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

2. Используйте heap_profiler, чтобы найти места утечек памяти

1) Замените malloc на tcmalloc

открыть bash_profile

vi ~/.bash_profile

Указываем путь к библиотеке tcmalloc и добавляем ее в PATH

export DYLD_INSERT_LIBRARIES=<gperftools_lib_path>/lib/libtcmalloc_and_profiler.dylib

Где — это место установки gperftools на машине.Например, если я установил его в /usr/local/Cellar/gperftools/2.7/ с доморощенным пивом, то мой путь

export DYLD_INSERT_LIBRARIES=/usr/local/Cellar/gperftools/2.7/lib/libtcmalloc_and_profiler.dylib

Сохранить и применить конфигурацию (требуется перезапуск IDE)

source ~/.bash_profile

Примечание. Замена malloc здесь не запустит heap-profiler, однако, поскольку любой может запустить heap-profiler после добавления переменных среды, Google не рекомендует настраивать его в производственной среде.

2) Следить за выделением памяти

Импортируйте или создайте наш образец программы в Idea и добавьте переменные среды для запуска heap-profiler в настройках запуска.

HEAPPROFILE=<heap_output_path>

— это выходной адрес файла кучи. Например, чтобы вывести результат в файл memTrack в папке tmp, нужно

HEAPPROFILE=/tmp/memTrack

Рис. 2. Конфигурация запуска heap-profiler

Запустите программу, и вы увидите в журнале, что heap-profiler начинает отслеживать выделение памяти.Частота выборки по умолчанию составляет 100 МБ на выделение.

Рис. 3. Журнал heap-profiler

Вы также можете просмотреть журналы, выводимые heap-profiler в каталоге /tmp.

Рисунок 4. Вывод heap-profiler

3) Проанализируйте вывод

heap-profiler использует pprof для преобразования результатов в различные форматы, вот вывод txt и pdf соответственно

выходной текст

Выберите последнюю запись выборки memTrack.0026.heap, конвертируйте ее в txt-файл и выведите в папку ~/HeapFile

pprof $JAVA_HOME/bin/java --text /tmp/memTrack.0026.heap > ~/HeapFile/memTrack.txt

Результат относительно большой, здесь перехватывается вывод Java-части

Total: 2544.9 MB
  2541.9  99.9%  99.9%   2541.9  99.9% 0x00007fff6f5bb1bd
     0.0   0.0% 100.0%    298.4  11.7% _JavaMain
     0.0   0.0% 100.0%      0.0   0.0% _Java_com_apple_eawt_Application_nativeInitializeApplicationDelegate
     0.0   0.0% 100.0%      0.0   0.0% _Java_java_awt_image_BufferedImage_initIDs
     0.0   0.0% 100.0%      0.0   0.0% _Java_java_awt_image_ColorModel_initIDs
     0.0   0.0% 100.0%      0.0   0.0% _Java_java_awt_image_Raster_initIDs
     0.0   0.0% 100.0%      0.0   0.0% _Java_java_awt_image_SampleModel_initIDs
     0.0   0.0% 100.0%      0.0   0.0% _Java_java_io_UnixFileSystem_checkAccess
     0.0   0.0% 100.0%      0.1   0.0% _Java_java_io_UnixFileSystem_getBooleanAttributes0
     0.0   0.0% 100.0%      0.3   0.0% _Java_java_lang_ClassLoader_00024NativeLibrary_load
     0.0   0.0% 100.0%      0.1   0.0% _Java_java_lang_ClassLoader_defineClass1
     0.0   0.0% 100.0%      0.1   0.0% _Java_java_lang_ClassLoader_findBootstrapClass
     0.0   0.0% 100.0%      0.0   0.0% _Java_java_lang_Class_forName0
     0.0   0.0% 100.0%      0.2   0.0% _Java_java_lang_System_initProperties
     0.0   0.0% 100.0%      0.0   0.0% _Java_java_net_Inet6Address_init
     0.0   0.0% 100.0%      0.0   0.0% _Java_java_net_NetworkInterface_init
     0.0   0.0% 100.0%      0.0   0.0% _Java_java_net_PlainSocketImpl_initProto
     0.0   0.0% 100.0%      0.0   0.0% _Java_java_net_PlainSocketImpl_socketConnect
     0.0   0.0% 100.0%      0.9   0.0% _Java_java_util_zip_Inflater_inflateBytes
     0.0   0.0% 100.0%      0.2   0.0% _Java_java_util_zip_Inflater_init
     0.0   0.0% 100.0%      0.0   0.0% _Java_java_util_zip_ZipFile_getEntry
     0.0   0.0% 100.0%      0.4   0.0% _Java_java_util_zip_ZipFile_open
     0.0   0.0% 100.0%      0.0   0.0% _Java_sun_awt_CGraphicsEnvironment_registerDisplayReconfiguration
     0.0   0.0% 100.0%      0.5   0.0% _Java_sun_awt_image_BufImgSurfaceData_initRaster
     0.0   0.0% 100.0%      0.1   0.0% _Java_sun_font_CFontManager_loadNativeDirFonts
     0.0   0.0% 100.0%      0.0   0.0% _Java_sun_font_StrikeCache_freeIntMemory
     0.0   0.0% 100.0%      0.4   0.0% _Java_sun_font_T2KFontScaler_createScalerContextNative
     0.0   0.0% 100.0%    764.7  30.0% _Java_sun_font_T2KFontScaler_getGlyphImageNative
     0.0   0.0% 100.0%      0.0   0.0% _Java_sun_font_T2KFontScaler_initIDs
     0.0   0.0% 100.0%   1751.7  68.8% _Java_sun_font_T2KFontScaler_initNativeScaler
     0.0   0.0% 100.0%      0.0   0.0% _Java_sun_java2d_SurfaceData_initIDs
     0.0   0.0% 100.0%      0.0   0.0% _Java_sun_java2d_loops_GraphicsPrimitiveMgr_initIDs
     0.0   0.0% 100.0%      0.4   0.0% _Java_sun_java2d_opengl_CGLGraphicsConfig_getOGLCapabilities
     0.0   0.0% 100.0%      0.0   0.0% _Java_sun_java2d_opengl_OGLRenderQueue_flushBuffer

Вы можете видеть, что первая строка — это общая память, занимаемая всей программой, а использование памяти каждым методом записывается в порядке стека вызовов (единица измерения: МБ).

  • Первый столбец — используемая прямая память.
  • Четвертый столбец — это общая память, используемая процессом и всеми вызываемыми им методами.
  • Второй и пятый столбцы — это процентное соотношение памяти первого и четвертого столбцов к общей памяти процесса соответственно.
  • Третий столбец представляет собой накопление данных во втором столбце.

Поскольку gperftools — это инструмент для C++, видно, что полную информацию для мониторинга нельзя получить в Java. Но мы все еще можем обнаружить, что метод _Java_sun_font_T2KFontScaler_initNativeScaler занимает больше всего памяти через четвертый столбец.Глядя на код, вы можете видеть, что этот метод изменен ключевым словом native, что указывает на то, что, вероятно, выделенная здесь память не была освобождена. с помощью JVM. Выполните поиск, и вы обнаружите, что именно выделенная здесь память удерживается объектом Font2D и в конечном итоге вызвала утечку.

выходной pdf

pprof также поддерживает графический вывод статистических результатов в pdf, чтобы мы могли более интуитивно находить место, которое занимает больше всего памяти. Здесь также используйте memTrack.0026.heap, конвертируйте его в формат pdf и выводите в папку ~/HeapFile

pprof $JAVA_HOME/bin/java --pdf /tmp/memTrack.0026.heap > ~/HeapFile/memTrack.pdf

После этого вы можете увидеть сгенерированный файл PDF в папке ~/HeapFile. Картинка относительно большая, и здесь перехвачена только ее часть.

Рис. 5 Ссылка на выделение памяти

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

Примечание: Если вы столкнулись со следующими ошибками при выводе pdf, вам необходимо установить соответствующие зависимости

dot: not found    需要安装graphviz
brew install graphviz

ps2pdf: command not found    需要安装ghostscript
brew install ghostscript

4. Резюме

Видно, что gperftools действительно является артефактом для устранения утечек памяти, предоставляя нам возможность отслеживать память вне кучи. Жаль, что это инструмент, предназначенный для C++, а многие другие функции кажутся недоступными для Java. В этот раз я сначала изучу здесь, а потом посмотрю, какие еще функции можно использовать под Java.

официальная документация