4 回答
TA贡献1770条经验 获得超3个赞
Java进程使用的虚拟内存远远超出了Java堆。您知道,JVM包含许多子系统:垃圾收集器,类加载,JIT编译器等,所有这些子系统都需要一定量的RAM才能运行。
JVM不是RAM的唯一消费者。本机库(包括标准Java类库)也可以分配本机内存。而本机内存跟踪甚至无法看到这一点。Java应用程序本身也可以通过直接ByteBuffers使用堆外内存。
那么什么需要Java进程中的内存?
JVM部件(主要通过本机内存跟踪显示)
Java堆
最明显的部分。这是Java对象所在的位置。堆占用了
-Xmx
大量的内存。垃圾收集器
GC结构和算法需要额外的内存用于堆管理。这些结构是Mark Bitmap,Mark Stack(用于遍历对象图),Remembered Sets(用于记录区域间引用)等。其中一些是直接可调的,例如
-XX:MarkStackSizeMax
,其他一些依赖于堆布局,例如,较大的是G1区域(-XX:G1HeapRegionSize
),较小的是记忆集。GC内存开销因GC算法而异。
-XX:+UseSerialGC
并且-XX:+UseShenandoahGC
开销最小。G1或CMS可以轻松使用总堆大小的10%左右。代码缓存
包含动态生成的代码:JIT编译的方法,解释器和运行时存根。它的大小受限于
-XX:ReservedCodeCacheSize
(默认为240M)。关闭-XX:-TieredCompilation
以减少编译代码的数量,从而减少代码缓存的使用。编译器
JIT编译器本身也需要内存来完成它的工作。通过关闭分层编译或减少编译器线程的数量,可以再次减少这种情况:
-XX:CICompilerCount
。类加载
类元数据(方法字节码,符号,常量池,注释等)存储在称为Metaspace的堆外区域中。加载的类越多 - 使用的元空间就越多。总使用量可以受限
-XX:MaxMetaspaceSize
(默认为无限制)和-XX:CompressedClassSpaceSize
(默认为1G)。符号表
JVM的两个主要哈希表:Symbol表包含名称,签名,标识符等,String表包含对实习字符串的引用。如果本机内存跟踪指示String表占用大量内存,则可能意味着应用程序过度调用
String.intern
。主题
线程堆栈也负责占用RAM。堆栈大小由
-Xss
。每个线程的默认值是1M,但幸运的是事情并没有那么糟糕。操作系统懒惰地分配内存页面,即在第一次使用时,因此实际内存使用量将低得多(通常每个线程堆栈80-200 KB)。我编写了一个脚本来估计RSS有多少属于Java线程堆栈。还有其他JVM部件可以分配本机内存,但它们通常不会在总内存消耗中发挥重要作用。
直接缓冲
应用程序可以通过调用显式请求堆外内存ByteBuffer.allocateDirect
。默认的堆外限制等于-Xmx
,但可以用它覆盖-XX:MaxDirectMemorySize
。Direct ByteBuffers包含在Other
NMT输出部分(或Internal
JDK 11之前)。
通过JMX可以看到使用的直接内存量,例如在JConsole或Java Mission Control中:
除了直接的ByteBuffers,还可以有MappedByteBuffers
- 映射到进程虚拟内存的文件。NMT不跟踪它们,但MappedByteBuffers也可以占用物理内存。而且没有一种简单的方法来限制它们可以承受多少。您可以通过查看进程内存映射来查看实际使用情况:pmap -x <pid>
Address Kbytes RSS Dirty Mode Mapping ... 00007f2b3e557000 39592 32956 0 r--s- some-file-17405-Index.db 00007f2b40c01000 39600 33092 0 r--s- some-file-17404-Index.db ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
本地图书馆
加载的JNI代码System.loadLibrary
可以根据需要分配尽可能多的堆外内存,而无需JVM端的控制。这也涉及标准的Java类库。特别是,未封闭的Java资源可能成为本机内存泄漏的来源。典型的例子是ZipInputStream
或DirectoryStream
。
JVMTI代理,特别是jdwp
调试代理 - 也可能导致过多的内存消耗。
此答案描述了如何使用async-profiler配置本机内存分配。
分配器问题
进程通常直接从OS(通过mmap
系统调用)或使用malloc
- 标准libc分配器请求本机内存。反过来,malloc
要求OS使用大块内存mmap
,然后根据自己的分配算法管理这些块。问题是 - 该算法可能导致碎片和过多的虚拟内存使用。
jemalloc
,替代分配器,通常看起来比常规libc更智能malloc
,因此切换到jemalloc
可能导致更小的空闲。
结论
没有保证估计Java进程的完整内存使用量的方法,因为有太多因素需要考虑。
Total memory = Heap + Code Cache + Metaspace + Symbol tables + Other JVM structures + Thread stacks + Direct buffers + Mapped files + Native Libraries + Malloc overhead + ...
可以通过JVM标志缩小或限制某些内存区域(如代码缓存),但许多其他内存区域完全不受JVM控制。
设置Docker限制的一种可能方法是在进程的“正常”状态下观察实际内存使用情况。有研究Java内存消耗问题的工具和技术:Native Memory Tracking,pmap,jemalloc,async-profiler。
TA贡献1827条经验 获得超8个赞
内存的详细用法由本机内存跟踪(NMT)详细信息(主要是代码元数据和垃圾收集器)提供。除此之外,Java编译器和优化器C1 / C2使用摘要中未报告的内存。
使用JVM标志可以减少内存占用(但存在影响)。
必须通过测试应用程序的预期负载来完成Docker容器大小调整。
每个组件的详细信息
所述共享类空间可以在容器内被禁用,因为类不会被另一个JVM进程共享。可以使用以下标志。它将删除共享类空间(17MB)。
-Xshare:off
所述垃圾收集器串行具有在更长的暂停时间期间垃圾的成本最小的内存占用收集处理(参照在一个画面GC之间阿列克谢Shipilëv比较)。可以使用以下标志启用它。它可以节省使用的GC空间(48MB)。
-XX:+UseSerialGC
所述C2编译器可以用下面的标志被禁用,以减少用于决定是否优化与否的方法分析数据。
-XX:+TieredCompilation -XX:TieredStopAtLevel=1
代码空间减少了20MB。而且,JVM外部的内存减少了80MB(NMT空间和RSS空间之间的差异)。优化编译器C2需要100MB。
的C1和C2的编译器可以用下面的标志被禁用。
-Xint
JVM外部的内存现在低于总提交空间。代码空间减少了43MB。请注意,这会对应用程序的性能产生重大影响。禁用C1和C2编译器会减少170 MB的内存使用量。
使用Graal VM编译器(替换C2)可以减少内存占用。它增加了20MB的代码内存空间,并从外部JVM内存减少了60MB。
JVM的Java内存管理文章提供了不同内存空间的一些相关信息。Oracle在Native Memory Tracking文档中提供了一些细节。有关高级编译策略和禁用C2中的编译级别的更多详细信息将代码高速缓存大小减少了5倍。有关为什么JVM报告的内存比Linux进程驻留集大小更多的一些细节?当两个编译器都被禁用时。
添加回答
举报