原视频链接:不存在的网站,翻译此视频并发布已获得原作者同意。
目录
Java进程的内存占用[译] Part 1 - AndreiPangin
JVM 选项“-Xmx4g”是否意味着该进程将最多消耗 4 GB 的 物理内存?当然不是。还有什么可以占用虚拟内存以及占用多少虚拟内存?
当在共享环境或资源有限的容器中运行 Java 时,这个问题变得尤为重要。很多地方都会发生内存消耗:从代码和库到JVM和操作系统。虽然 Java 内存泄漏通常很容易从堆转储中发现,但native memory泄漏就很难搞懂了。
在本次会议期间,我们将讨论哪些结构会影响 JVM 占用空间。我们将研究native memory泄漏的真实案例,并探索可用于内存分配分析的工具。
Andrei Pangin【应该是俄罗斯人】 领导 Odnoklassniki 社交网络的开发,专注于高性能 Java 服务器。他之前从事 HotSpot JVM 方面的工作,这成为他最喜欢的主题和专业领域。 Andrei 也是 Stack Overflow 上的顶级#JVM 回答者和 Async-profiler 的作者。
容器化应用
之前我们是一个Java应用放在一个服务器,很方便管理,但是从资源使用的角度看,并不高效。所以我们开始构建自己的云服务器,让多个应用程序共享一个服务器的资源,但是资源隔离成了问题,怎样避免一个程序占用了所有资源,其他程序饿死的情况发生?
容器可以解决这个问题,通过设置resources配置即可。
但是到了Java,它自己就有设置内存的方式,即 -Xmx4G,我们就可以认为Java将使用最多4G的物理内存,当然JVM本身也可能用掉一些额外的不太多的内存,用于各类其他用途。这只是我们期望的情况。如果一切都这么顺利,我就没必要演讲了。
你会发现系统突然过来,Kill掉了Java进程,虽然不是很常见,但因为它达到了资源配额上限,所以被kill掉了。
Container support?
不过没事,Java不是有了新选项:-XX:+UseContainerSupport吗?那我就升级下JDK,加上这个选项,问题解决!
不好意思,并没有!事实上,这个选项其实没啥效果,仅仅是让Java进程能够看到容器里的CPU数量,和用于计算默认堆内存的容器内存大小。JVM甚至不会满足于这两个限制,所以问题还是有,内存杀手开始了表演,操作系统出来洗地。
事实上,很多人都在问这个问题,为什么Java用了更多资源以及如何限制,所以这让我有了做这个演讲的动力。
Java进程的内存占用
我们一起来找下答案。一般最简单的方式是用top命令,从这里可以看出java是内存使用率最高的进程。
在这里,VIRT显示20.747g,看起来很吓人,但其实这不是实际内存占用,这只是进程的地址空间的总量;
RES或者RSS(Residence set size)才是实际物理内存占用,只有9.022g。
pmap
但是,这9个G是怎么来的呢?在Linux,有一个很有用的工具,pmap命令,可以用来展示Java进程完整的内存映射。这里我们只讲Linux系统,其他操作系统的Java也差不多。
内存映射是一个很长的表格,展示了进程拥有的每一个地址空间范围,这其中我们能看到地址空间范围的长度和对应的RSS内存大小占用。有一些区域是映射给了磁盘上的jar文件或者so共享库文件,其他是匿名的,代表的是内存而不是文件。
高亮的这一行,是一个4GB的地址范围,很显然这是Java 堆,那其他的呢?
JVM运行时
为了搞清楚其他内存消耗的源头,我们需要先了解下JVM同志的工作职责,很显然,JVM同志不可能只是用来收集垃圾的,还做了很多其他工作,比如加载类、编译字节码、管理线程等等,这些活动都需要额外的内存。
Native Memory Tracking
好在JVM是可以告诉我们他用了多少内存,通过启用功能:Native Memory Tracking(简称NMT,-XX:NativeMemoryTracking=[summary|detail],JDK7开始提供),就可以了!不过,为什么这个功能不是默认启用的?因为凡事皆有代价,通过其文档可以看到,启用后会有额外的5~10%的性能开销,另外,对每个已分配的本地内存快,NMT会向所有malloc内存加上额外2个字的header信息。
另外要注意的是,NMT只关注JVM分配的内存,例如说你使用了第三方的JNILibrary,那么这个Library使用的内存,NMT是跟踪不了的。
当这个选项开启后,你可以使用**jcmd PID VM.native_memory [detail]**命令,随时查看NMT报告。
NMT 概要
NMT报告包含多个部分,每个部分展示了每个JVM子系统的细节。
顶部的Total committed,就是当前JVM所使用的大小。
内存可以有两种统计方式:1. 系统分配器:malloc实现,2:JVM直接向操作系统申请内存,通过mmap系统调用实现。
JVM运行时 - Java堆
最明显的部分莫过于Java堆内存,因为Java对象放在这里。然而还有一部分叫:GC,为啥它会在这?
Java堆和GC
拿G1举例,Java堆被分割为了一个个的Region,这些Region可能是老年代、Eden、Survivor,所有这些区的大小是通过-Xmx控制的。但是为了管理Java堆,GC算法本身会需要额外的内存,而这些内存是不被Xmx所限制的。
例如:
1. 遍历对象以找到可以访问到的对象,GC使用其他的结构:off-heap,即非堆结构:标记位图(Mark bitmap)标记了它所访问的对象;
2. 遍历算法本身需要一些内存,称为:Mark stacks(标记栈);
3. 最后,最重要的也是最大的部分,是Remembered sets(RSets),包含了Region引用,另外不能直接设置RSets的大小,但是间接的,可以设置Region大小(-XX:G1HeapRegionSize)来影响RSets的大小。
GC内存开销
不同的GC算法需要使用不同的结构,但是多数情况下,它们都需要额外的内存开销,我这里做了一个实验,测量它们的开销。
另外不同的JDK版本、GC参数、应用程序也会产生不通的结果。但这样做的目的在于,确认了GC会产生额外的内存开销,以便我们之后在分配内存时,提前准备好一部分额外内存给GC使用。
堆大小
在Java里面,我们知道-Xmx设置堆内存最大值,-Xms设置内存最小值,当这两个值设置为一样,理所当然的,Java进程是不是正好就占用这么多内存呢?通过top命令可以看到,java进程的RES值很小,不到4GB。那么,为何?
这是因为操作系统在物理内存中获取内存页时,其获取方式是惰性的,一开始,并没有使用堆内存,所以并没有消耗物理内存,但是这种惰性访问,会导致在运行时出现Page Fault,这肯定是非常不好的。
这里有一个JVM选项:-XX:+AlwaysPreTouch,可以占用规定的堆内存大小。如果我们是在生产应用中,即Xms等于Xmx,可以看到进程的确占用了4G的物理内存。
我发现很多开发者误以为-Xms选项是最小堆内存大小的含义,但其实不是,即使你把-Xmx设置为和-Xms一样,堆内存大小仍然会调整,甚至会低于Xms。如果你真的不希望这样,那么可以选择关闭:-XX:-AdaptiveSizePolicy。
Adaptive size policy
AdaptiveSizePolicy是默认启用的,它允许堆内存进行扩大和缩小。有两个选项用于主动的调整其扩大和缩小:-XX:MinHeapFreeRatio、-XX:MaxHeapFreeRatio。
然而AdaptiveSizePolicy是很有用的,因为它可以让JVM归还内存给操作系统。对于一些应用会很有用,举例说桌面应用程序,比如你在运行IDE,或者编译一个大的项目,IDE用了很多内存,没关系,当它闲置的时候,再把内存归还回来就行了!
分配和归还内存听起来很厉害,所以我设置了另外的实验,看看不同的GC算法在committed内存方面各有什么特点,我的测试使用最大堆2G,初始化堆32M,测试将会加载很多数据,接着在GC中被丢弃。
各GC的归还内存对比
首先是Parallel GC,完全没有归还内存给操作系统,committed都没降下去。
CMS 可以归还内存,但也仅仅是在调用了几次System.gc()后才实现。
G1 在commit内存时非常激进,进程启动后的使用量很大,但是却能在Full GC后立刻归还所有内存。而到了JDK12,有了一个新特性,可以让G1不只在Full GC后才归还,而是常规的concurrent cycle(并发处理周期)也能做到。
不过,在JDK12之前,能够做到在concurrent cycle(并发处理周期)就归还内存的垃圾回收器,只有Shenandoah GC,这里特指完全不需要Full GC的情况下。