JVM内存管理
原文地址:https://www.cnblogs.com/dranawhite/articles/10616552.html
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
引子
公司的监控系统对每分钟内CMS垃圾回收次数做了监控,当1分钟内发生两次及以上的CMS GC时就会告警。前段时间发现到了某几个时间点会突然出现CMS GC大幅上涨的情况,后续GC次数又会慢慢回落,通过下载下来的GC日志发现,当时的GC情况是这样的:
2019-02-28T08:17:46.523+0800: 48485.679: [GC (Allocation Failure) 2019-02-28T08:17:46.524+0800: 48485.679: [ParNew (promotion failed): 3015488K->3015488K(3015488K), 4.8871257 secs]2019-02-28T08:17:51.411+0800: 48490.566: [CMS: 840496K->843775K(843776K), 1.9291020 secs] 3237853K->899424K(3859264K), [Metaspace: 125814K->125814K(1165312K)], 6.8172550 secs] [Times: user=10.04 sys=0.79, real=6.82 secs]
2019-02-28T08:17:53.342+0800: 48492.498: [GC (CMS Initial Mark) [1 CMS-initial-mark: 843775K(843776K)] 913567K(3859264K), 0.0491655 secs] [Times: user=0.00 sys=0.00, real=0.05 secs]
2019-02-28T08:17:53.392+0800: 48492.547: [CMS-concurrent-mark-start]
2019-02-28T08:17:54.014+0800: 48493.170: [CMS-concurrent-mark: 0.622/0.622 secs] [Times: user=1.50 sys=0.00, real=0.62 secs]
2019-02-28T08:17:54.014+0800: 48493.170: [CMS-concurrent-preclean-start]
2019-02-28T08:17:54.655+0800: 48493.810: [CMS-concurrent-preclean: 0.639/0.640 secs] [Times: user=1.24 sys=0.00, real=0.64 secs]
2019-02-28T08:17:54.655+0800: 48493.810: [CMS-concurrent-abortable-preclean-start]
2019-02-28T08:17:54.655+0800: 48493.810: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-02-28T08:17:54.656+0800: 48493.812: [GC (CMS Final Remark) [YG occupancy: 479871 K (3015488 K)]2019-02-28T08:17:54.656+0800: 48493.812: [Rescan (parallel) , 0.9560565 secs]2019-02-28T08:17:55.612+0800: 48494.768: [weak refs processing, 0.0000984 secs]2019-02-28T08:17:55.612+0800: 48494.768: [class unloading, 0.1195192 secs]2019-02-28T08:17:55.732+0800: 48494.887: [scrub symbol table, 0.0295023 secs]2019-02-28T08:17:55.762+0800: 48494.917: [scrub string table, 0.0025612 secs][1 CMS-remark: 843775K(843776K)] 1323647K(3859264K), 1.1079721 secs] [Times: user=2.16 sys=0.00, real=1.10 secs]
2019-02-28T08:17:55.765+0800: 48494.920: [CMS-concurrent-sweep-start]
2019-02-28T08:17:56.092+0800: 48495.247: [CMS-concurrent-sweep: 0.325/0.327 secs] [Times: user=0.42 sys=0.00, real=0.33 secs]
2019-02-28T08:17:56.092+0800: 48495.247: [CMS-concurrent-reset-start]
2019-02-28T08:17:56.094+0800: 48495.249: [CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-02-28T08:17:58.095+0800: 48497.250: [GC (CMS Initial Mark) [1 CMS-initial-mark: 843461K(843776K)] 1406103K(3859264K), 0.2357379 secs] [Times: user=0.88 sys=0.00, real=0.24 secs]
2019-02-28T08:17:58.331+0800: 48497.486: [CMS-concurrent-mark-start]
2019-02-28T08:17:58.910+0800: 48498.065: [CMS-concurrent-mark: 0.579/0.579 secs] [Times: user=1.12 sys=0.00, real=0.58 secs]
2019-02-28T08:17:58.910+0800: 48498.065: [CMS-concurrent-preclean-start]
2019-02-28T08:17:59.163+0800: 48498.318: [CMS-concurrent-preclean: 0.253/0.253 secs] [Times: user=0.00 sys=0.00, real=0.25 secs]
2019-02-28T08:17:59.163+0800: 48498.318: [CMS-concurrent-abortable-preclean-start]
2019-02-28T08:17:59.163+0800: 48498.318: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-02-28T08:17:59.164+0800: 48498.320: [GC (CMS Final Remark) [YG occupancy: 587358 K (3015488 K)]2019-02-28T08:17:59.164+0800: 48498.320: [Rescan (parallel) , 0.9429521 secs]2019-02-28T08:18:00.107+0800: 48499.263: [weak refs processing, 0.0000989 secs]2019-02-28T08:18:00.107+0800: 48499.263: [class unloading, 0.0454493 secs]2019-02-28T08:18:00.153+0800: 48499.308: [scrub symbol table, 0.0359367 secs]2019-02-28T08:18:00.189+0800: 48499.344: [scrub string table, 0.0028344 secs][1 CMS-remark: 843461K(843776K)] 1430820K(3859264K), 1.0275001 secs] [Times: user=2.43 sys=0.00, real=1.03 secs]
2019-02-28T08:18:00.192+0800: 48499.348: [CMS-concurrent-sweep-start]
2019-02-28T08:18:00.371+0800: 48499.526: [CMS-concurrent-sweep: 0.179/0.179 secs] [Times: user=0.00 sys=0.00, real=0.18 secs]
2019-02-28T08:18:00.371+0800: 48499.526: [CMS-concurrent-reset-start]
2019-02-28T08:18:00.373+0800: 48499.528: [CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
通过GC日志可以看到,JVM在新生代中分配空间,发生了分配失败后触发young gc;后续又发生了晋升失败,导致cms gc;但是老年代中的空间并没有被释放掉,于是不停的尝试触发cms gc;于是我们把当时的堆转储文件down下来,发现有大量的POI Excel对象占据内存空间,参考POI官网方案换用XSSF Event API解决问题。
JVM运行时内存分布
HotSpot虚拟机1.7之前一般把内存划分为以下区域:
-
PC计数器
该段内存类似于CPU中的CS:IP寄存器,用于标识JVM当前正在执行的字节码指令的地址,如果当前是native方法,则PC计数器的值是undefined。该段区域是线程独占的,每个线程都有自己的PC计数器。
-
栈
JVM中的栈分为Java栈和本地方法栈,Java栈是由一个个的栈帧组成,用于保存局部变量和一些中间结果;本地方法栈主要用于运行native方法。
下面细聊一下Java栈:Java栈是由一个个的栈帧组成,栈帧是用于支持JVM进行方法调用和方法执行的数据结构,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息,每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在JVM里面从入栈到出栈的过程。
栈帧的大概结构是这样:
图1 栈帧结构图
下面以代码为例解释下基于栈的解释器的运行过程:
原生代码如下:
public int foo(int a, int b) {
int sum = a + b;
return sum;
}
编译后的字节码如下:
public int foo(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=3
0: iload_1
1: iload_2
2: iadd
3: istore_3
4: iload_3
5: ireturn
LineNumberTable:
line 14: 0
line 15: 4
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/study/jvm/collection/Foo;
0 6 1 a I
0 6 2 b I
4 2 3 sum I
整段代码的执行过程如下:
图2 栈运行时结构图
0:iload1指令将局部变量表中的第2个元素(a)压入栈中
1:iload2指令将局部变量表中的第3个元素(b)压入栈中
2:iadd指令弹出栈中最顶层的两个元素,并相加(a + b),将结果保存到栈中
3:istore_3指令将栈顶的元素保存到局部变量表的第4个元素中
4:iload3指令将局部变量表中的第4个元素(sum)压入栈中
5:ireturn指令返回栈顶的元素(sum)
-
堆
HotSpot虚拟机中的分代算法将堆划分为年轻代和年老代两个区域。年轻代又划分为伊甸区(Eden)和两个幸存区(Survivor),默认Eden区和Survivor区大小比例为8:1。堆被所有的线程共享,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java对象分配的过程:
-
编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配,如果是在堆上分配,则进入选项B,如果没有开启逃逸分析选项,则进入选项B;
-
如果当前TLAB的存储足够,则直接在TLAB上分配对象,如果不足则进入C;
-
重新申请一个TLAB,并再次尝试存放当前对象,如果放不下则进入D;
-
在Eden区加锁,如果Eden的存储足够,则在Eden区创建对象,如果Eden区不足,则E;
-
执行一次Young GC,如果Eden区存储仍然不足,则直接晋升到老年代;
有关TLAB和逃逸分析,此处不做讨论,有兴趣的可以参考这篇文章:Java中的逃逸分析和TLAB以及Java对象分配。此处可以看到几乎所有的实例在创建时都是被分配在Eden区,部分大对象则直接分配到老年代;
有关内存分配的几个JVM参数:
-Xms: 最小堆内存
-Xmx: 最大堆内存
-Xmn: 年轻代内存
-
方法区
方法区主要用于存储运行时类信息,方法信息,以及常量池信息。方法区的默认大小是64M,若该区域出现OOM异常,通常是类加载过多导致的。该段内存在JDK1.8中被替换为MetaSpace。值得一提的是,在JDK1.6之前String常量池也是存放在该区域,不过JDK1.7之后将String常量池移到了堆内存中,有兴趣的可以参照这篇文章:深入解析String#intern
-
Code Cache代码缓存区
我们知道JVM发生JIT编译会将字节码文件编译为本地机器码,本地机器码就存储在该段内存区域中,可以通过 -XX:ReservedCodeCacheSize参数设置Code Cache空间大小。
如何判定一个对象是否存活
-
引用计数器
引用计数器是给每个对象设置一个计数器,当该对象被引用时,计数器+1,当引用失效时,计数器-1;如果计数器的值为0,则表明该对象不再被引用,可以被正常回收。引用计数器实现简单,但是不能够解决循环引用的问题。
例如,两个对象A和B,其中A引用B,B引用A,除此之外,没有被任何其它的对象引用这两个对象,理论上这两个对象都应该被标记为可回收对象,进行回收。但是由于两者的计数器值都不为0,所以这两个对象都不会进行回收。
-
根可达算法
根可达算法是通过一些GC Roots对象作为根节点,从根节点往下搜索,搜索过的路径称为引用链,当一个对象没有被GC Roots引用链链接的时候,说明这个对象可以被垃圾回收。
GC Roots对象包括以下几种:
-
局部变量表中引用的对象;
-
方法区中静态变量引用的对象;
-
方法区中常量引用的对象;
-
本地方法栈中引用的对象;
JVM中的垃圾回收算法
-
标记-清除算法
标记-清除算法分为两个阶段:标记阶段和清除阶段。在标记阶段,标记所有的从GC Roots可达的对象;在清除阶段清除所有未被标记的对象。标记-清除算法实现简单,但是会造成大量的内存碎片。
图3 标记-清除算法回收图
-
标记-整理算法
标记-整理算法类似于标记-清除算法,不过它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界之外的内存区域。
图4 标记-整理算法回收图
-
复制算法
复制算法将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块,当其中一块内存使用完后,就将还存活的对象复制到另一块内存上,然后将已经使用过的内存一次清理掉。复制算法在内存分配时不用考虑内存碎片的情况,但是内存消耗也变成了原来的两倍。
回收前内存如下:
图5 复制算法回收前内存图
回收后如下:
图6 复制算法回收后内存图
-
分代收集算法
分代收集算法其实是上面几种收集算法的组合,在HotSpot虚拟机中,按照被管理对象的生命周期的长短使用不同的算法对其进行管理。JVM中绝大部分的对象都是被分配到Eden中,Eden中的对象朝生夕死,存活周期很短,所以在这块区域采用复制算法,到Eden区内存占满之后,就触发垃圾回收,存活中的对象和其中From Survivor中的对象被移动到To Survivor区。在老年代内存中按照不同的方案使用标记-整理算法和标记-清除算法。
垃圾回收器
-
串行回收器
Serial New/Serial Old回收器分别是年轻代和老年代中的串行垃圾收集器。Serial垃圾收集器工作时只有一个线程,并且在垃圾回收过程中,会发生Stop The World,造成用户线程的停顿。
图7 Serial回收器运行示意图
-
并行回收器
ParNew/Parallel Old回收器分别是年轻代和老年代的并行垃圾回收器,是Serial回收器的并行版本。ParNew和Parallel Old回收器也会发生Stop The World暂停用户线程。
图8 ParNew/Parallel Old运行示意图
-
吞吐量优先回收器
Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,即CPU用于运行用户代码的时间与CPU总消耗时间的比值。Parallel Scavenge回收器提供了两个参数用于精准的控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能的保证内存回收花费的时间不超过设定值。GC停顿时间缩短是以牺牲吐吞量和新生代空间来换取的。
GCTimeRatio参数的值是一个大于0并且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。
-
CMS回收器
CMS回收器是一款以获取最短回收停顿时间(STD)为目标的收集器。CMS回收器大体可以分为四个阶段:
-
初始标记
在初始标记阶段,仅仅是标记一下GC Roots能直接关联到的对象,速度很快,在该阶段会发生STD。
-
并发标记
并发标记阶段,是标记GC Roots链路的过程,该阶段不会发生STD,GC线程和用户线程是并发执行的。
-
重新标记
重新标记阶段是为了修正并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,该阶段也会发生STD。
-
并发清除
并发清除阶段对标记过的对象进行回收;
图9 CMS运行示意图
CMS默认启动的回收线程数是(CPU数量 +3)/ 4,可以看出CPU数量越小,GC回收的代价越大。CMS是一款使用标记-清除算法的垃圾回收器,即每次回收完垃圾对象后,会产生大量的碎片化空间,如果空间碎片过多,在分配大对象时,由于没有足够的连续空间,不得不提前触发一次Full GC。CMS收集器提供了一个-XX:CMSFullGCsBeforeCompaction参数,用于设置执行多少次不压缩的Full GC后,执行一次带压缩的Full GC(默认值是0,表示每次进入Full GC时都会进行碎片整理)。
-
G1回收器
G1是一款面向服务端应用的垃圾收集器,HotSpot团队赋予它的使命是未来可以替换掉CMS收集器。G1收集器将整个Java堆划分为多个大小相等的独立区域,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
G1收集器的运作大致上也可以划分为以下几个步骤:
-
初始标记
初始标记阶段标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top At Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。
-
并发标记
类似CMS的并发标记阶段;
-
重新标记
类似CMS的再次标记阶段;
-
筛选回收
在筛选回收阶段会对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
G1回收器具有以下的特定:
-
并行与并发
G1充分利用多CPU和多核的优势,使用多个CPU来缩短STD的停顿时间;
-
分代收集
虽然G1是对整个堆进行管理,但是分代的概念在G1中依然得以保留,G1采用不同的方式去处理新创建的对象和已经存活了一段时间的对象以获取更好的收集效果;
-
空间整合
G1在整体上使用标记-整理算法,从局部(两个Region来看)是使用复制算法,这两种算法都意味着G1运作期间不会产生内存碎片;
-
可预测的停顿
G1除了追求低停顿之外,还能建立可预测的停顿时间模型,使用者可以明确的指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒;
参考资料
-
深入理解Java虚拟机:JVM高级特性与最佳实践 周志明
-
Java虚拟机规范(Java SE7)
-
Java虚拟机规范(Java SE8)

浙公网安备 33010602011771号