做者简介html
杨俊明,携程云客服平台研发部软件技术专家。从事IT行业10余年,腾讯云+社区、阿里云栖社区、华为云社区认证专家。近年来主要研究分布式架构、微服务、Java技术等方向。前端
java的内存布局以及GC原理”是java开发人员绕不开的话题,也是面试中常见的高频问题之一。java
java发展历史上出现过不少垃圾回收器,各有各的适应场景,不少网上的旧文章已经跟不上的变化。本文详细介绍了java的内存布局以及各类垃圾回收器的原理(包括的ZGC),但愿阅读完后,你们对这方面的知识再也不陌生,有所收获,同时也欢迎你们留言讨论。ios
1、JVM运行时内存布局c++
按java 8虚拟机规范的原始表达:(jvm)Run-Time Data Areas, 暂时翻译为“jvm运行时内存布局”。git
从概念上大体分为6个(逻辑)区域,参考下图。注:Method Area中还有一个常量池区,图中未明确标出。github
这6块区域按是否被线程共享,能够分为两大类:web
一类是每一个线程所独享的:面试
1)PC Register:也称为程序计数器, 记录每一个线程当前执行的指令信。eg:当前执行到哪一条指令,下一条该取哪条指令。算法
2)JVM Stack:也称为虚拟机栈,记录每一个栈帧(Frame)中的局部变量、方法返回地址等。注:这里出现了一个新名词“栈帧”,它的结构以下:
线程中每次有方法调用时,会建立Frame,方法调用结束时Frame销毁。
3)Native Method Stack:本地(原生)方法栈,顾名思义就是调用操做系统原生本地方法时,所须要的内存区域。
上述3类区域,生命周期与Thread相同,即:线程建立时,相应的区域分配内存,线程销毁时,释放相应内存。
另外一类是全部线程共享的:
1)Heap:即鼎鼎大名的堆内存区,也是GC垃圾回收的主站场,用于存放类的实例对象及Arrays实例等。
2)Method Area:方法区,主要存放类结构、类成员定义,static静态成员等。
3)Runtime Constant Pool:运行时常量池,好比:字符串,int -128~127范围的值等,它是Method Area中的一部分。
Heap、Method Area 都是在虚拟机启动时建立,虚拟机退出时释放。
注:Method Area 区,虚拟机规范只是说必需要有,可是具体怎么实现(好比:是否须要垃圾回收? ),交给具体的JVM实现去决定,逻辑上讲,视为Heap区的一部分。因此,若是你看见相似下面的图,也不要以为画错了。
上述6个区域,除了PC Register区不会抛出StackOverflowError或OutOfMemoryError ,其它5个区域,当请求分配的内存不足时,均会抛出OutOfMemoryError(即:OOM),其中thread独立的JVM Stack区及Native Method Stack区还会抛出StackOverflowError。
最后,还有一类不受JVM虚拟机管控的内存区,这里也提一下,即:堆外内存。
能够经过Unsafe和NIO包下的DirectByteBuffer来操做堆外内存。如上图,虽然堆外内存不受JVM管控,可是堆内存中会持有对它的引用,以便进行GC。
提一个问题:整体来看,JVM把内存划分为“栈(stack)”与“堆(heap)”两大类,为什么要这样设计?
我的理解,程序运行时,内存中的信息大体分为两类,一是跟程序执行逻辑相关的指令数据,这类数据一般不大,并且生命周期短;一是跟对象实例相关的数据,这类数据可能会很大,并且能够被多个线程长时间内反复共用,好比字符串常量、缓存对象这类。
将这两类特色不一样的数据分开管理,体现了软件设计上“模块隔离”的思想。比如咱们一般会把后端service与前端website解耦相似,也更便于内存管理。
2、GC垃圾回收原理
2.1 如何判断对象是垃圾 ?
有两种经典的判断方法,借用网友的图(文中最后有给出连接):
引用计数法,思路很简单,可是若是出现循环引用,即:A引用B,B又引用A,这种状况下就很差办了,因此JVM中使用了另外一种称为“可达性分析”的判断方法:
仍是刚才的循环引用问题(也是某些公司面试官可能会问到的问题),若是A引用B,B又引用A,这2个对象是否能被GC回收?
答案:关键不是在于A、B之间是否有引用,而是A、B是否能够一直向上追溯到GC Roots。若是与GC Roots没有关联,则会被回收,不然将继续存活。
上图是一个用“可达性分析”标记垃圾对象的示例图,灰色的对象表示不可达对象,将等待回收。
2.2 哪些内存区域须要GC ?
在第一部分JVM内存布局中,咱们知道了thread独享的区域:PC Regiester、JVM Stack、Native Method Stack,其生命周期都与线程相同(即:与线程共生死),因此无需GC。线程共享的Heap区、Method Area则是GC关注的重点对象。
2.3 经常使用的GC算法
1)mark-sweep 标记清除法
如上图,黑色区域表示待清理的垃圾对象,标记出来后直接清空。该方法简单快速,可是缺点也很明显,会产生不少内存碎片。
2)mark-copy 标记复制法
思路也很简单,将内存对半分,老是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,而后左侧所有清空。避免了内存碎片问题,可是内存浪费很严重,至关于只能使用50%的内存。
3)mark-compact 标记-整理(也称标记-压缩)法
避免了上述两种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(相似于windows的磁盘碎片整理),保证它们占用的空间连续,这样就避免了内存碎片问题,可是整理过程也会下降GC的效率。
4)generation-collect 分代收集算法
上述三种算法,每种都有各自的优缺点,都不完美。在现代JVM中,每每是综合使用的,通过大量实际分析,发现内存中的对象,大体能够分为两类:有些生命周期很短,好比一些局部变量/临时对象,而另外一些则会存活好久,典型的好比websocket长链接中的connection对象,以下图:
纵向y轴能够理解分配内存的字节数,横向x轴理解为随着时间流逝(伴随着GC),能够发现大部分对象其实至关短命,不多有对象能在GC后活下来。所以诞生了分代的思想,以Hotspot为例(JDK 7):
将内存分红了三大块:年青代(Young Genaration),老年代(Old Generation),永久代(Permanent Generation),其中Young Genaration更是又细为分eden,S0,S1三个区。
结合咱们常用的一些jvm调优参数后,一些参数能影响的各区域内存大小值,示意图以下:
注:jdk8开始,用MetaSpace区取代了Perm区(永久代),因此相应的jvm参数变成-XX:MetaspaceSize 及 -XX:MaxMetaspaceSize。
以Hotspot为例,咱们来分析下GC的主要过程:
刚开始时,对象分配在eden区,s0(即:from)及s1(即:to)区,几乎是空着。
随着应用的运行,愈来愈多的对象被分配到eden区。
当eden区放不下时,就会发生minor GC(也被称为young GC),第1步固然是要先标识出不可达垃圾对象(即:下图中的黄色块),而后将可达对象,移动到s0区(即:4个淡蓝色的方块挪到s0区),而后将黄色的垃圾块清理掉,这一轮事后,eden区就成空的了。
注:这里其实已经综合运用了“【标记-清理eden】 + 【标记-复制 eden->s0】”算法。
随着时间推移,eden若是又满了,再次触发minor GC,一样仍是先作标记,这时eden和s0区可能都有垃圾对象了(下图中的黄色块),注意:这时s1(即:to)区是空的,s0区和eden区的存活对象,将直接搬到s1区。而后将eden和s0区的垃圾清理掉,这一轮minor GC后,eden和s0区就变成了空的了。
继续,随着对象的不断分配,eden空可能又满了,这时会重复刚才的minor GC过程,不过要注意的是,这时候s0是空的,因此s0与s1的角色其实会互换,即:存活的对象,会从eden和s1区,向s0区移动。而后再把eden和s1区中的垃圾清除,这一轮完成后,eden与s1区变成空的,以下图。
对于那些比较“长寿”的对象一直在s0与s1中挪来挪去,一来很占地方,并且也会形成必定开销,下降gc效率,因而有了“代龄(age)”及“晋升”。
对象在年青代的3个区(edge,s0,s1)之间,每次从1个区移到另1区,年龄+1,在young区达到必定的年龄阈值后,将晋升到老年代。下图中是8,即:挪动8次后,若是还活着,下次minor GC时,将移动到Tenured区。
下图是晋升的主要过程:对象先分配在年青代,通过屡次Young GC后,若是对象还活着,晋升到老年代。
若是老年代,最终也放满了,就会发生major GC(即Full GC),因为老年代的的对象一般会比较多,由于标记-清理-整理(压缩)的耗时一般会比较长,会让应用出现卡顿的现象,这也是为何不少应用要优化,尽可能避免或减小Full GC的缘由。
注:上面的过程主要来自oracle官网的资料,可是有一个细节官网没有提到,若是分配的新对象比较大,eden区放不下,可是old区能够放下时,会直接分配到old区(即没有晋升这一过程,直接到老年代了)。
下图引自阿里出品的《码出高效-Java开发手册》一书,梳理了GC的主要过程。
3、垃圾回收器
不算出现的神器ZGC,历史上出现过7种经典的垃圾回收器。
这些回收器都是基于分代的,把G1除外,按回收的分代划分,横线以上的3种:Serial ,ParNew, Parellel Scavenge都是回收年青代的,横线如下的3种:CMS,Serial Old, Parallel Old 都是回收老年代的。
3.1 Serial 收集器
单线程用标记-复制算法,快刀斩乱麻,单线程的好处避免上下文切换,早期的机器,大可能是单核,也比较实用。但执行期间,会发生STW(Stop The World)。
3.2 ParNew 收集器
Serial的多线程版本,一样会STW,在多核机器上会更适用。
3.3 Parallel Scavenge 收集器
ParNew的升级版本,主要区别在于提供了两个参数:-XX:MaxGCPauseMillis 较大垃圾回收停顿时间;-XX:GCTimeRatio 垃圾回收时间与总时间占比,经过这2个参数,能够适当控制回收的节奏,更关注于吞吐率,即总时间与垃圾回收时间的比例。
3.4 Serial Old 收集器
由于老年代的对象一般比较多,占用的空间一般也会更大,若是采用复制算法,得留50%的空间用于复制,至关不划算,并且由于对象多,从1个区,复制到另1个区,耗时也会比较长,因此老年代的收集,一般会采用“标记-整理”法。从名字就能够看出来,这是单线程(串行)的, 依然会有STW。
3.5 Parallel Old 收集器
一句话:Serial Old的多线程版本。
3.6 CMS 收集器
全称:Concurrent Mark Sweep,从名字上看,就能猜出它是并发多线程的。这是JDK 7中普遍使用的收集器,有必要多说一下,借一张网友的图说话:
相对3.4 Serial Old收集器或3.5 Parallel Old收集器而言,这个明显要复杂多了,分为4个阶段:
1)Inital Mark 初始标记:主要是标记GC Root开始的下级(注:仅下一级)对象,这个过程会STW,可是跟GC Root直接关联的下级对象不会不少,所以这个过程其实很快。
2)Concurrent Mark 并发标记:根据上一步的结果,继续向下标识全部关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,可是其它工做线程并不会阻塞,没有STW。
3)Remark 再标志:为啥还要再标记一次?由于第2步并无阻塞其它工做线程,其它线程在标识过程当中,颇有可能会产生新的垃圾。
试想下,高铁上的垃圾清理员,从车箱一头开始吆喝“有须要扔垃圾的乘客,请把垃圾扔一下”,一边工做一边向前走,等走到车箱另外一头时,刚才走过的位置上,可能又有乘客产生了新的空瓶垃圾。因此,要彻底把这个车箱清理干净的话,她应该喊一下:全部乘客不要再扔垃圾了(STW),而后把新产生的垃圾收走。固然,由于刚才已经把收过一遍垃圾,因此此次收集新产生的垃圾,用不了多长时间(即:STW时间不会很长)。
4)Concurrent Sweep:并行清理,这里使用多线程以“Mark Sweep-标记清理”算法,把垃圾清掉,其它工做线程仍然能继续支行,不会形成卡顿。
等等,刚才咱们不是提到过“标记清理”法,会留下不少内存碎片吗?确实,可是也没办法,若是换成“Mark Compact标记-整理”法,把垃圾清理后,剩下的对象也顺便排整理,会致使这些对象的内存地址发生变化,别忘了,此时其它线程还在工做,若是引用的对象地址变了,就天下大乱了。
另外,因为这一步是并行处理,并不阻塞其它线程,因此还有一个副使用,在清理的过程当中,仍然可能会有新垃圾对象产生,只能等到下一轮GC,才会被清理掉。
虽然仍不完美,可是从这4步的处理过程来看,以往收集器中最让人诟病的长时间STW,经过上述设计,被分解成二次短暂的STW,因此从整体效果上看,应用在GC期间卡顿的状况会大大改善,这也是CMS一度十分流行的重要缘由。
3.7 G1 收集器
G1的全称是Garbage-First,为何叫这个名字,呆会儿会详细说明。鉴于CMS的一些不足以外,好比: 老年代内存碎片化,STW时间虽然已经改善了不少,可是仍然有提高空间。G1就横空出世了,它对于heap区的内存划思路很新颖,有点算法中分治法“分而治之”的味道。
以下图,G1将heap内存区,划分为一个个大小相等(1-32M,2的n次方)、内存连续的Region区域,每一个region都对应Eden、Survivor 、Old、Humongous四种角色之一,可是region与region之间不要求连续。
注:Humongous,简称H区是专用于存放超大对象的区域,一般>= 1/2 Region Size,且只有Full GC阶段,才会回收H区,避免了频繁扫描、复制/移动大对象。
全部的垃圾回收,都是基于1个个region的。JVM内部知道,哪些region的对象最少(即:该区域最空),老是会优先收集这些region(由于对象少,内存相对较空,确定快),这也是Garbage-First得名的由来,G便是Garbage的缩写, 1即First。
G1 Young GC
young GC前:
young GC后:
理论上讲,只要有一个Empty Region(空区域),就能够进行垃圾回收。
因为region与region之间并不要求连续,而使用G1的场景一般是大内存,好比64G甚至更大,为了提升扫描根对象和标记的效率,G1使用了二个新的辅助存储结构:
Remembered Sets:简称RSets,用于根据每一个region里的对象,是从哪指向过来的(即:谁引用了我),每一个Region都有独立的RSets。(Other Region -> Self Region)。
Collection Sets :简称CSets,记录了等待回收的Region集合,GC时这些Region中的对象会被回收(copied or moved)。
RSets的引入,在YGC时,将年青代Region的RSets作为根对象,能够避免扫描老年代的region,能大大减轻GC的负担。注:在老年代收集Mixed GC时,RSets记录了Old->Old的引用,也能够避免扫描全部Old区。
Old Generation Collection(也称为 Mixed GC)
按oracle官网文档描述分为5个阶段:Initial Mark(STW) -> Root Region Scan -> Cocurrent Marking -> Remark(STW) -> Copying/Cleanup(STW && Concurrent)
注:也有不少文章会把Root Region Scan省略掉,合并到Initial Mark里,变成4个阶段。
存活对象的“初始标记”依赖于Young GC,GC 日志中会记录成young字样。
2019-06-09T15:24:37.086+0800: 500993.392: [GC pause (G1 Evacuation Pause) (young), 0.0493588 secs]
[Parallel Time: 41.9 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 500993393.7, Avg: 500993393.7, Max: 500993393.7, Diff: 0.1]
[Ext Root Scanning (ms): Min: 1.5, Avg: 2.2, Max: 4.4, Diff: 2.8, Sum: 17.2]
[Update RS (ms): Min: 15.8, Avg: 18.1, Max: 18.9, Diff: 3.1, Sum: 144.8]
[Processed Buffers: Min: 110, Avg: 144.9, Max: 163, Diff: 53, Sum: 1159]
[Scan RS (ms): Min: 4.7, Avg: 5.0, Max: 5.1, Diff: 0.4, Sum: 39.7]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 16.4, Avg: 16.5, Max: 16.6, Diff: 0.2, Sum: 132.0]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 4.9, Max: 7, Diff: 6, Sum: 39]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3]
[GC Worker Total (ms): Min: 41.7, Avg: 41.8, Max: 41.8, Diff: 0.1, Sum: 334.1]
[GC Worker End (ms): Min: 500993435.5, Avg: 500993435.5, Max: 500993435.5, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.2 ms]
[Other: 7.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 4.3 ms]
[Ref Enq: 0.1 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.1 ms]
[Free CSet: 0.6 ms]
[Eden: 1340.0M(1340.0M)->0.0B(548.0M) Survivors: 40.0M->64.0M Heap: 2868.2M(12.0G)->1499.8M(12.0G)]
[Times: user=0.35 sys=0.00, real=0.05 secs]
并发标记过程当中,若是发现某些region全是空的,会被直接清除。
进入从新标记阶段。
并发复制/清查阶段。这个阶段,Young区和Old区的对象有可能会被同时清理。GC日志中,会记录为mixed字段,这也是G1的老年代收集,也称为Mixed GC的缘由。
2019-06-09T15:24:23.959+0800: 500980.265: [GC pause (G1 Evacuation Pause) (mixed), 0.0885388 secs]
[Parallel Time: 74.2 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 500980270.6, Avg: 500980270.6, Max: 500980270.6, Diff: 0.1]
[Ext Root Scanning (ms): Min: 1.7, Avg: 2.2, Max: 4.1, Diff: 2.4, Sum: 17.3]
[Update RS (ms): Min: 11.7, Avg: 13.7, Max: 14.3, Diff: 2.6, Sum: 109.8]
[Processed Buffers: Min: 136, Avg: 141.5, Max: 152, Diff: 16, Sum: 1132]
[Scan RS (ms): Min: 42.5, Avg: 42.9, Max: 43.1, Diff: 0.5, Sum: 343.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Object Copy (ms): Min: 14.9, Avg: 15.2, Max: 15.4, Diff: 0.5, Sum: 121.7]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Termination Attempts: Min: 1, Avg: 8.2, Max: 11, Diff: 10, Sum: 66]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
[GC Worker Total (ms): Min: 74.0, Avg: 74.0, Max: 74.1, Diff: 0.1, Sum: 592.3]
[GC Worker End (ms): Min: 500980344.6, Avg: 500980344.6, Max: 500980344.6, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.5 ms]
[Other: 13.9 ms]
[Choose CSet: 4.1 ms]
[Ref Proc: 1.8 ms]
[Ref Enq: 0.1 ms]
[Redirty Cards: 0.2 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.1 ms]
[Free CSet: 5.6 ms]
[Eden: 584.0M(584.0M)->0.0B(576.0M) Survivors: 28.0M->36.0M Heap: 4749.3M(12.0G)->2930.0M(12.0G)]
[Times: user=0.61 sys=0.00, real=0.09 secs]
上图是,老年代收集完后的示意图。
经过这几个阶段的分析,虽然看上去不少阶段仍然会发生STW,可是G1提供了一个预测模型,经过统计方法,根据历史数据来预测本次收集,须要选择多少个Region来回收,尽可能知足用户的预期停顿值(-XX:MaxGCPauseMillis参数可指定预期停顿值)。
注:若是Mixed GC仍然效果不理想,跟不上新对象分配内存的需求,会使用Serial Old GC(Full GC)强制收集整个Heap。
小结:与CMS相比,G1有内存整理过程(标记-压缩),避免了内存碎片;STW时间可控(能预测GC停顿时间)。
3.8 ZGC (截止目前史上较好的GC收集器)
在G1的基础上,作了不少改进(JDK 11开始引入)
3.8.1 动态调整大小的Region
G1中每一个Region的大小是固定的,建立和销毁Region,能够动态调整大小,内存使用更高效。
3.8.2 不分代,干掉了RSets
G1中每一个Region须要借助额外的RSets来记录“谁引用了我”,占用了额外的内存空间,每次对象移动时,RSets也须要更新,会产生开销。
注:ZGC没有为止,没有实现分代机制,每次都是并发的对全部region进行回收,不象G1是增量回收,因此用不着RSets。不分代的带来的可能性能降低,会用下面立刻提到的Colored Pointer && Load Barrier来优化。
3.8.3 带颜色的指针 Colored Pointer
这里的指针相似java中的引用,意为对某块虚拟内存的引用。ZGC采用了64位指针(注:目前只支持Linux 64位系统),将42-45这4个bit位置赋予了不一样的含义,即所谓的颜色标志位,也换为指针的metadata。
finalizable位:仅finalizer(类比c++中的析构函数)可访问;
remap位:指向对象当前()的内存地址,参考下面提到的relocation;
marked0 && marked1 位:用于标志可达对象;
这4个标志位,同一时刻只会有1个位置是1。每当指针对应的内存数据发生变化,好比内存被移动,颜色会发生变化。
3.8.4 读屏障 Load Barrier
传统GC作标记时,为了防止其它线程在标记期间修改对象,一般会简单的STW。而ZGC有了Colored Pointer后,引入了所谓的读屏障,当指针引用的内存正被移动时,指针上的颜色就会变化,ZGC会先把指针更新成状态,而后再返回。(你们能够回想下java中的volatile关键字,有殊途同归之妙),这样仅读取该指针时可能会略有开销,而不用将整个heap STW。
3.8.5 重定位 relocation
如上图,在标记过程当中,先从Roots对象找到了直接关联的下级对象1,2,4。
而后继续向下层标记,找到了5,8对象, 此时已经能够断定 3,6,7为垃圾对象。
若是按常规思路,通常会将8从最右侧的Region移动或复制到中间的Region,而后再将中间Region的3干掉,最后再对中间Region作压缩compact整理。但ZGC作得更高明,它直接将4,5复制到了一个空的新Region就完事了,而后中间的2个Region直接废弃,或理解为“释放”,作为下次回收的“新”Region。这样的好处是避免了中间Region的compact整理过程。
最后,指针从新调整为正确的指向(即:remap),并且上一阶段的remap与下一阶段的mark是混在一块儿处理的,相对更高效。
Remap的流程图以下:
3.8.6 多重映射 Multi-Mapping
这个优化,说实话没彻底看懂,只能谈下本身的理解(若是有误,欢迎指正)。虚拟内存与实际物理内存,OS会维护一个映射关系,才能正常使用。以下图:
zgc的64位颜色指针,在解除映射关系时,代价较高(须要屏蔽额外的42-45的颜色标志位)。考虑到这4个标志位,同1时刻,只会有1位置成1(以下图),另外finalizable标志位,永远不但愿被解除映射绑定(可不用考虑映射问题)。
因此剩下3种颜色的虚拟内存,能够都映射到同1段物理内存。即映射复用,或者更通俗点讲,原本3种不一样颜色的指针,哪怕0-41位彻底相同,也须要映射到3段不一样的物理内存,如今只须要映射到同1段物理内存便可。
3.8.7 支持NUMA架构
NUMA是一种多核服务器的架构,简单来说,一个多核服务器(好比2core),每一个cpu都有属于本身的存储器,会比访问另外一个核的存储器会慢不少(相似于就近访问更快)。
相对以前的GC算法,ZGC首次支持了NUMA架构,申请堆内存时,判断当前线程属是哪一个CPU在执行,而后就近申请该CPU能使用的内存。
小结:革命性的ZGC通过上述一堆优化后,每次GC整体卡顿时间按官方说法<10ms。注:启用zgc,须要设置-XX:+UnlockExperimentalVMOptions -XX:+UseZGC。
4、实战练习
前面介绍了一堆理论,最后来作一个小的练习,下面是一段模拟OOM的测试代码,咱们在G一、CMS这二种经常使用垃圾回收器上试验一下。
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class OOMTest {
public static void main(String[] args) {
OOMTest test = new OOMTest();
//heap区OOM测试
//test.heapOOM();
//虚拟机栈和本地方法栈溢出
//test.stackOverflow();
//metaspace OOM测试
//test.metaspaceOOM();
//堆外内存 OOM测试
//test.directOOM();
}
/**
* heap OOM测试
*/
public void heapOOM() {
List<OOMTest> list = new ArrayList<>();
while (true) {
list.add(new OOMTest());
}
}
private int stackLength = 1;
public void stackLeak() {
stackLength += 1;
stackLeak();
}
/**
* VM Stack / Native method Stack 溢出测试
*/
public void stackOverflow() {
OOMTest test = new OOMTest();
try {
test.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + test.stackLength);
throw e;
}
}
public void genString() {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add("string-" + i);
i++;
}
}
/**
* metaspace/常量池 OOM测试
*/
public void metaspaceOOM() {
OOMTest test = new OOMTest();
test.metaspaceOOM();
}
public void allocDirectMemory() {
final int _1MB = 1024 * 1024;
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = null;
try {
unsafe = (Unsafe) unsafeField.get(null);
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
while (true) {
unsafe.allocateMemory(_1MB);
}
}
/**
* 堆外内存OOM测试
*/
public void directOOM() {
OOMTest test = new OOMTest();
test.allocDirectMemory();
}
}
4.1 openjdk 11.0.3 环境:+ G1回收
4.1.1 验证heap OOM
把main方法中的test.heapOOM()行,注释打开,而后命令行下运行:
java -Xmx10M -XX:+UseG1GC -Xlog:gc* -Xlog:gc:gc.log -XX:+HeapDumpBeforeFullGC OOMTest.java
最后会输出:
[1.892s][info][gc ] GC(42) Concurrent Cycle 228.393ms
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3689)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:242)
at java.base/java.util.ArrayList.add(ArrayList.java:485)
at java.base/java.util.ArrayList.add(ArrayList.java:498)
at oom.OOMTest.heapOOM(OOMTest.java:37)
at oom.OOMTest.main(OOMTest.java:16)
[1.895s][info][gc,heap,exit ] Heap
其中 OutOfMemoryError:Java heap space即表示heap OOM。
4.1.2 验证stack溢出
把main方法中的test.stackOverflow()行,注释打开,而后命令行下运行:
java -Xmx20M -Xss180k -XX:+UseG1GC -Xlog:gc* -Xlog:gc:gc.log -XX:+HeapDumpBeforeFullGC OOMTest.java
最后会输出:
[0.821s][info][gc ] GC(4) Pause Young (Normal) (G1 Evacuation Pause) 12M->7M(20M) 5.245ms
[0.821s][info][gc,cpu ] GC(4) User=0.00s Sys=0.00s Real=0.00s
stack length:1699
Exception in thread "main" java.lang.StackOverflowError
at oom.OOMTest.stackLeak(OOMTest.java:45)
at oom.OOMTest.stackLeak(OOMTest.java:45)
其中 StackOverflowError 即表示stack栈区内存不足,致使溢出。
4.1.3 验证metaspace OOM
把main方法中的test.metaspaceOOM()行,注释打开,而后命令行下运行:
java -Xmx20M -XX:MaxMetaspaceSize=10M -XX:+UseG1GC -Xlog:gc* -Xlog:gc:gc.log -XX:+HeapDumpBeforeFullGC OOMTest.java
最后会输出:
[0.582s][info][gc,metaspace,freelist,oom]
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
[0.584s][info][gc,heap,exit ] Heap
其中 OutOfMemoryError: Metaspace 即表示Metaspace区OOM。
4.1.4 验证堆外内存OOM
把main方法中的test.directOOM()行,注释打开,而后命令行下运行:
最后会输出:
[0.842s][info][gc,cpu ] GC(4) User=0.06s Sys=0.00s Real=0.01s
Exception in thread "main" java.lang.OutOfMemoryError
at java.base/jdk.internal.misc.Unsafe.allocateMemory(Unsafe.java:616)
...
其中OutOfMemoryError行并无输出具体哪一个区(注:堆外内存不属于JVM内存中的任何一个区,因此没法输出),但紧接着有一行jdk.internal.misc.Unsafe.allocateMemory 能够看出是“堆外内存直接分配”致使的异常。
4.2 openjdk 1.8.0_212 + CMS回收
jdk1.8下,java命令没法直接运行.java文件,必须先编译,即:
javac OOMTest.java -encoding utf-8
(注:-encoding utf-8 是为了防止中文注释javac没法识别)成功后,会生成OOMTest.class文件, 而后再能够参考下面的命令进行测试。
4.2.1 heap OOM测试:
java -Xmx10M -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError OOMTest
4.2.2 验证stack溢出
java -Xmx10M -Xss128k -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError OOMTest
4.2.3 验证metaspace OOM
java -Xmx20M -XX:+UseConcMarkSweepGC -XX:MaxMetaspaceSize=10M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError OOMTest
4.2.4 验证堆外内存OOM
java -Xmx20M -XX:+UseConcMarkSweepGC -XX:MaxDirectMemorySize=10M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError OOMTest
4.3 GC日志查看工具
生成的gc日志文件,能够用开源工具GCViewer查看,这是一个纯java写的GUI程序,使用很简单,File→Open File 选择gc日志文件便可。目前支持CMS/G1生成的日志文件,另外若是GC文件过大时,可能打不开。
GCViewer能够很方便的统计出GC的类型,次数,停顿时间,年青代/老年代的大小等,还有图表显示,很是方便。
参考文章:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
https://blog.csdn.net/heart_mine/article/details/79495032
https://www.programcreek.com/2013/04/jvm-run-time-data-areas/
https://javapapers.com/core-java/java-jvm-run-time-data-areas/
https://javapapers.com/core-java/java-jvm-memory-types/
https://cloud.tencent.com/developer/article/1152616
https://www.jianshu.com/p/17e72bb01bf1
http://calvin1978.blogcn.com/articles/directbytebuffer.html
https://www.cnkirito.moe/nio-buffer-recycle/
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
http://inbravo.github.io/html/jvm.html
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html
https://segmentfault.com/a/1190000009783873
https://segmentfault.com/a/1190000016551339
https://www.team-bob.org/things-about-java-garbage-collection-1/2/
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html
https://tech.meituan.com/2016/09/23/g1.html
https://mp.weixin.qq.com/s/KUCs_BJUNfMMCO1T3_WAjw
https://www.baeldung.com/jvm-zgc-garbage-collector
http://xxfox.perfma.com/jvm/
https://wiki.openjdk.java.net/display/zgc/Main
http://cr.openjdk.java.net/~pliden/slides/ZGC-FOSDEM-2018.pdf
http://www.ishenping.com/ArtInfo/43701.html
http://likehui.top/2019/04/11/ZGC-%E7%89%B9%E6%80%A7%E8%A7%A3%E8%AF%BB/