JAVA内存深度分析报告

本文涉及的产品
文档翻译,文档翻译 1千页
文本翻译,文本翻译 100万字符
语种识别,语种识别 100万字符
简介: JAVA内存深度分析报告


最近在对云主机的内存占用优化中,又有了新的认识,网上对JAVA的native memory 的认知五花八门,对direct memory 的获取的有偏差,今天我们来好好理一理,以便我们对JAVA 内存有个更好的认知。

理论部分:

首先,JAVA应用的内存到底包含了哪些部分?下图可以完整的表达,

从上图我们可以认识到,JAVA 应用包含Heap Memory,Non-heap Memory,以及Direct Memory.

1.Heap Memory(堆内存)

这块主要是有 Eden Space(伊甸园), Survivor Space (幸存者区), Old Gen (老年代)组成。

这块数据是可以监控到的,这个后面再提。

2.Non-heap Memory(堆外内存)

包含了Metaspace(元数据区或者叫元空间),Code cache(代码缓存,JIT产生的汇编指令所占的空间), Compressed Class Space 是 Metaspace 的一部分,默认大小为 1G,这三块属于可观察部分,这个也有相对应的API可以获取到。

我们看下metaSpace的JDK默认值,

[root@VM-16-13-centos ~]# java -XX:+PrintFlagsFinal -version | grep MetaspaceSize
   size_t InitialBootClassLoaderMetaspaceSize      = 4194304                                   {product} {default}
   size_t MaxMetaspaceSize                         = 18446744073709547520                      {product} {default}
   size_t MetaspaceSize                            = 21807104                               {pd product} {default}

其中MaxMetaspace可以认为是无上限的值。

代码缓存如果 -XX:ReservedCodeCacheSize >= 240m,则代码分成三个区域:

其中 Code cache包含以下三个区域:

  • CodeHeap ‘non-nmethods’ JVM 自己用的代码
  • CodeHeap ‘non-profiled nmethods’, 完全优化的机器码
  • CodeHeap ‘profiled nmethods’ 部分优化的机器码
    如果 -XX:ReservedCodeCacheSize < 240m,则它们存在一起 不进行区分
    另一部分,Thread Stack(线程堆栈),GC(执行GC逻辑所需的空间),internal,Symbol(有关符号的分配,如同String table和常量池),Other (Direct Memory),这部分暂时还没找到用API获取的方式,但也是能通过其他方式获取到。

3.Direct Memory(直接内存)

直接内存是和java.nio.DirectByteBuffer类相关联的,常被用在第三方库,比如nio和gzip。

这里有个疑问,经过观察,上面堆外内存的Other 和Direct Memory 是一样的,那么,上面那张图,那为何要独立分出这一部分来?

实验部分:

接下去我们通过实验来相互印证一些数据和想法。

准备环境:

JDK:

openjdk version "11.0.16.1" 2022-08-16
OpenJDK Runtime Environment TencentKonaJDK (build 11.0.16.1+2)
OpenJDK 64-Bit Server VM TencentKonaJDK (build 11.0.16.1+2, mixed mode)

启动参数:

java -server  -Xms256M -Xmx256M -XX:+UseG1GC -XX:MaxMetaspaceSize=1024m -XX:NativeMemoryTracking=detail  -jar game.jar

在这里,我们分配256MB的堆大小,同时,使用G1作为我们的GC算法,并且启动了本地内存追踪XX:NativeMemoryTracking=detail ,

1.Platform MXBeans API 监控快照

max heap memory[G1 Eden Space,G1 Survivor Space,G1 Old Gen] 256 MB,used 169 MB [91, 3, 75],
max non-heap[CodeHeap 'non-nmethods',CodeHeap 'non-profiled nmethods',CodeHeap 'profiled nmethods',Compressed Class Space,Metaspace]  memory  2280 MB,used 165 MB [1, 7, 26, 14, 116], committed 172 MB,
max direct memory 256 MB,used 4 MB

通过每分钟的内存监控打印,我们可以看到,

堆的使用空间为G1 Eden Space,G1 Survivor Space,G1 Old Gen 相加,总量的调用API为:

ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();

非堆空间(可监控的) 为CodeHeap ‘non-nmethods’,CodeHeap ‘non-profiled nmethods’,CodeHeap ‘profiled nmethods’,Compressed Class Space,Metaspace相加,总量的调用API 为:ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage();

2.MetaSpace 快照:

jcmd 18228 GC.heap_info
18228:
 garbage-first heap   total 262144K, used 205311K [0x00000000f0000000, 0x0000000100000000)
  region size 1024K, 126 young (129024K), 3 survivors (3072K)
 Metaspace       used 118903K, capacity 121481K, committed 121632K, reserved 1146880K
  class space    used 14438K, capacity 15451K, committed 15564K, reserved 1040384K

这里的 class Space 14438K,Metaspace 118903K 和API 的 14M,116M 基本能对应上。

3.Native Memory 快照:

jcmd 18228  VM.native_memory summary scale=MB     
18228:
Native Memory Tracking:
Total: reserved=1795MB, committed=520MB
-                 Java Heap (reserved=256MB, committed=256MB)
                            (mmap: reserved=256MB, committed=256MB) 
-                     Class (reserved=1123MB, committed=122MB)
                            (classes #22297)
                            (  instance classes #20922, array classes #1375)
                            (malloc=3MB #54279) 
                            (mmap: reserved=1120MB, committed=119MB) 
                            (  Metadata:   )
                            (    reserved=104MB, committed=104MB)
                            (    used=102MB)
                            (    free=2MB)
                            (    waste=0MB =0.00%)
                            (  Class space:)
                            (    reserved=1016MB, committed=15MB)
                            (    used=14MB)
                            (    free=1MB)
                            (    waste=0MB =0.00%)
-                    Thread (reserved=80MB, committed=8MB)
                            (thread #79)
                            (stack: reserved=79MB, committed=8MB)
-                      Code (reserved=245MB, committed=42MB)
                            (malloc=3MB #14293) 
                            (mmap: reserved=242MB, committed=39MB) 
-                        GC (reserved=48MB, committed=48MB)
                            (malloc=7MB #20188) 
                            (mmap: reserved=42MB, committed=42MB) 
-                  Internal (reserved=1MB, committed=1MB)
                            (malloc=1MB #2072) 
-                     Other (reserved=4MB, committed=4MB)
                            (malloc=4MB #22) 
-                    Symbol (reserved=26MB, committed=26MB)
                            (malloc=22MB #633541) 
                            (arena=4MB #1)
-    Native Memory Tracking (reserved=12MB, committed=12MB)
                            (tracking overhead=11MB)

让我们一段一段的分析NMT的输出。

先插入一个常识:

reserved :操作系统已经为该进程“保留”的。所谓的保留,更加接近一种记账的概念,就是操作系统承诺最多可以给你多少资源,到时候并不一定能申请的到。
used:你当前正在使用的有多少资源。
committed:进程已经申请进的内存,可能在被使用,有可能闲置着。used<=committed
3.1 分配的内存
Native Memory Tracking:
Total: reserved=1795MB, committed=520MB

reserved显示了我们应用程序能够预定的最大内存。相对地,committed内存是当前已经申请进JAVA进程的内存。

尽管分配了256 MB的堆内存,但是我们应用程序全部预定内存大约1.7 GB,远远大于堆内存。类似,使用内存大约520 MB,也大于256 MB。

3.2 堆

NMT打印我们的堆内存,确实像我们前面设置的一样:

Java Heap (reserved=256MB, committed=256MB)
(mmap: reserved=256MB, committed=256MB)

256MB的保留和使用内存,符合我们设置的情况。

3.3 元数据区(Metaspace)

下面是加载的类的类元数据的信息:

Class (reserved=1123MB, committed=122MB)
(classes #22297)
(  instance classes #20922, array classes #1375)
(malloc=3MB #54279) 
(mmap: reserved=1120MB, committed=119MB) 
(  Metadata:   )
(    reserved=104MB, committed=104MB)
(    used=102MB)
(    free=2MB)
(    waste=0MB =0.00%)
(  Class space:)
(    reserved=1016MB, committed=15MB)
(    used=14MB)
(    free=1MB)
(    waste=0MB =0.00%)

大约1123MB的预定内存和122 MB的使用空间区加载22297个类。这里的used MetaData 102MB 和used Class space 14MB 没有和API获得的完全对应上 [Metaspace,Compressed Class Space] memory 2280 MB,used 165 MB [116,14] .这是为什么?

3.4 线程空间(Thread)

下面是线程的内存分配信息:

Thread (reserved=80MB, committed=8MB)
(thread #79)
(stack: reserved=79MB, committed=8MB)

总共,8MB的内存被分配到了79个线程——大约每个栈有0.1MB的内存。

3.5 代码缓存(Code Cache)

让我们看看JIT产生的汇编指令所占的空间:

Code (reserved=245MB, committed=42MB)
(malloc=3MB #14293) 
(mmap: reserved=242MB, committed=39MB)

这个也在API 中反映, [CodeHeap ‘non-nmethods’,CodeHeap ‘non-profiled nmethods’,CodeHeap ‘profiled nmethods’] memory [1, 7, 26] 共 34MB,

当前,大约42MB的空间被会缓存了,并且能使用的空间大约在245MB。

3.6 GC

下面是NMT报告的G1GC的内存使用情况:

GC (reserved=48MB, committed=48MB)
(malloc=7MB #20188) 
(mmap: reserved=42MB, committed=42MB)

我们能看到,大约48MB的空间是G1可以保留和使用的。

3.7 其他(Other)
Other (reserved=4MB, committed=4MB)
(malloc=4MB #22)

回顾下我们API监控的日志“max direct memory 256 MB,used 4 MB”

从Other部分可以看到其值跟Direct Memory使用的值一致,改变ByteBuffer.allocateDirect的值再重新查看,可以发现Other部分跟着改变;因而初步断定Other部分应该是可以反映direct memory的使用大小

3.8 符号(Symbol)

下面是NMT打印有关符号的分配,如同String table和常量池:

Symbol (reserved=26MB, committed=26MB)
(malloc=22MB #633541) 
(arena=4MB #1)

大约26MB被分配给Symbol使用。

4.NMT 监控时间段

NMT允许我们追踪在一段时间内的内存改变情况。首先我们应该标记当前我们应用程序的状态作为基线:

$ jcmd VM.native_memory baseline

Baseline succeeded

1

2

然后,过一段时间,我们就能够比较出当前内存与基线之间的差别:

$ jcmd VM.native_memory summary.diff

1

现在,使用+和-标记,就能够告诉我们在这段时间内内存的使用情况:

总结

我们通过API ,MetaData , Native memory 快照,让我们对JAVA内存结构有了更深度的认识,以便以后在分析问题时,能有更客观的数据分析的基础。

参考文献:

Java memory management

Native Memory Tracking in JVM

目录
相关文章
|
11天前
|
缓存 easyexcel Java
Java EasyExcel 导出报内存溢出如何解决
大家好,我是V哥。使用EasyExcel进行大数据量导出时容易导致内存溢出,特别是在导出百万级别的数据时。以下是V哥整理的解决该问题的一些常见方法,包括分批写入、设置合适的JVM内存、减少数据对象的复杂性、关闭自动列宽设置、使用Stream导出以及选择合适的数据导出工具。此外,还介绍了使用Apache POI的SXSSFWorkbook实现百万级别数据量的导出案例,帮助大家更好地应对大数据导出的挑战。欢迎一起讨论!
103 1
|
20天前
|
存储 Java
【编程基础知识】 分析学生成绩:用Java二维数组存储与输出
本文介绍如何使用Java二维数组存储和处理多个学生的各科成绩,包括成绩的输入、存储及格式化输出,适合初学者实践Java基础知识。
55 1
|
4天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
47 9
|
3天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
11 2
|
4天前
|
Java 关系型数据库 数据库
面向对象设计原则在Java中的实现与案例分析
【10月更文挑战第25天】本文通过Java语言的具体实现和案例分析,详细介绍了面向对象设计的五大核心原则:单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。这些原则帮助开发者构建更加灵活、可维护和可扩展的系统,不仅适用于Java,也适用于其他面向对象编程语言。
8 2
|
6天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
20 1
|
8天前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。
|
9天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
16天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
18天前
|
存储 Java
Java内存模型
【10月更文挑战第11天】Java 内存模型(JMM)是 Java 虚拟机规范中定义的多线程内存访问机制,解决内存可见性、原子性和有序性问题。它定义了主内存和工作内存的概念,以及可见性、原子性和有序性的规则,确保多线程环境下的数据一致性和操作正确性。使用 `synchronized` 和 `volatile` 等同步机制可有效避免数据竞争和不一致问题。
29 3