前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM内存布局及GC知识回顾

JVM内存布局及GC知识回顾

作者头像
菩提树下的杨过
发布2019-06-15 17:37:10
6140
发布2019-06-15 17:37:10
举报

一、JVM运行时内存布局

java 8虚拟机规范的原始表达:(jvm)Run-Time Data Areas, 暂时翻译为"jvm运行时内存布局"。

从概念上大致分为6个(逻辑)区域,参考下图(注:Method Area中还有一个常量池区,图中未明确标出)

这6块区域按是否被线程共享,可以分为二大类:

一类是每个线程所独享的:

  1. PC Register:也称为程序计数器, 记录每个线程当前执行的指令信息(eg:当前执行到哪一条指令,下一条该取哪条指令)
  2. JVM Statck: 也称为虚拟机栈,记录每个栈帧(Frame)中的局部变量、方法返回地址等。注:这里出现了一个新名词“栈帧”,它的结构如下:

线程中每次有方法调用时,会创建Frame,方法调用结束时Frame销毁。

  1. 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解耦类似),也更便于内存管理。

二、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算法:

a. mark-sweep 标记清除法

如上图,黑色区域表示待清理的垃圾对象,标记出来后直接清空。该方法很简单快速,但是缺点也很明显,会产生很多内存碎片。

b. mark-copy 标记复制法

思路也很简单,将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。避免了内存碎片问题,但是内存浪费很严重,相当于只能使用50%的内存。

c. mark-compact 标记-整理(也称标记-压缩)法

避免了上述二种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于windows的磁盘碎片整理),保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低GC的效率。

d. 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的主要过程。

三、垃圾回收器

不算最新出现的神器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四种角色之一(注:Humongous,简称H区是专用于存放超大对象的区域,通常>= 1/2 Region Size,且只有Full GC阶段,才会回收H区,避免了频繁扫描、复制/移动大对象),但是region与region之间不要求连续。所有的垃圾回收,都是基于1个个region的。JVM内部知道,哪些region的对象最少(即:该区域最空),总是会优先收集这些region(因为对象少,内存相对较空,肯定快),这也是Garbage-First得名的由来,G即是Garbage的缩写, 1即First(第1)。

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)

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字样。

(上图),并发标记过程中,如果发现某些region全是空的,会被直接清除。

(上图)进入重新标记阶段。

(上图)并发复制/清查阶段。这个阶段,Young区和Old区的对象有可能会被同时清理。GC日志中,会记录为mixed字段,这也是G1的老年代收集,也称称为Mixed GC的原因。

上图是,老年代收集完后的示意图。

通过这几个阶段的分析,虽然看上去很多阶段仍然会发生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位指针,将42-45这4个bit位置赋予了不同的含义(即:所谓的颜色标志位)

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会维护一个映射关系,才能正常使用。如下图:

传统指针方式下,每1段虚拟内存与每1段物理内存,都是一一对应的,解除每一条映射关系,代价较高(即:有开销)。但是ZGC中的颜色指针,因为始终同一时间只有1位是1(即:颜色位是互斥的,1个指针上的颜色,不可能同时为多种颜色),另外finalizable颜色的指针,永远不希望被解除映射绑定。 所以剩下3种颜色的虚拟内存,可以都映射到同1段物理内存(即:映射复用)。

3.8.7 支持NUMA架构

NUMA是一种多核服务器的架构,简单来讲,一个多核服务器(比如:2core),每个cpu都有属于自己的存储器,会比访问另一个核的存储器会慢很多(类似于就近访问更快)。相对之前的GC算法,ZGC首次支持了NUMA架构,申请堆内存时,判断当前线程属是哪个CPU在执行,然后就近申请该CPU能使用的内存。

小结:革命性的ZGC经过上述一堆优化后,每次GC总体卡顿时间按官方说法<10ms。

参考文章:

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-06-11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档