关于jvm和GC还是自己写一篇文章来强化下把。
一、JVM 运行时内存布局
1、先盗两张图再说,方便理解和记忆。参考链接会附在下面
2、左图 6 块区域按是否被线程共享,可以分为两大类,即右图所示。
1)PC Register:也称为程序计数器(有的叫寄存器), 记录每个线程当前执行的指令信。eg:当前执行到哪一条指令,下一条该取哪条指令。
2)JVM Stack:也称为虚拟机栈,记录每个栈帧(Frame)中的局部变量、方法返回地址等。栈也是JAVA虚拟机自动管理的(不是由gc)。栈类似一个集合(不过是有固定的容量),是由很多元素(专业术语:栈帧)组合起来的。在我们码代码的时候,每调用一个方法,在运行的时候,JAVA虚拟机就会自动在内存中分配对应的一块空间,那么这块空间就是一个栈帧,也就自然属于栈了,而当方法调用结束后,对应的栈帧就会被释放掉,那么因为JAVA程序从main方法开始执行,那么JAVA虚拟机就会在内存中分配一块main方法的栈帧,如果main方法里没有调用其它方法,那么,main方法调用结束后,会把main方法栈帧释放掉,此时,栈里没有了栈帧,程序结束。此段话是抄的,讲的通俗易懂。
补充一点,在方法中的局部变量,如果是基本类型,会把值直接存储在栈。如果是引用类型,会把其对象存储在堆,而把这个对象的引用(指针)存储在栈。类的成员变量在不同对象中各不相同,都有自己的存储空间(成员变量在堆中的对象中),基本类型和引用类型的成员变量都在这个对象的空间中,作为一个整体存储在堆。而类的方法却是该类的所有对象共享的,只有一套,对象使用方法的时候方法才被压入栈,方法不使用则不占用内存。
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 区的一部分。
最后,还有一类不受 JVM 虚拟机管控的内存区,这里也提一下,即:堆外内存。可以通过 Unsafe 和 NIO 包下的 DirectByteBuffer 来操作堆外内存。如上图,虽然堆外内存不受 JVM 管控,但是堆内存中会持有对它的引用,以便进行 GC。
提一个问题:总体来看,JVM 把内存划分为“栈 (stack)”与“堆 (heap)”两大类,为何要这样设计?
个人理解,程序运行时,内存中的信息大致分为两类,一是跟程序执行逻辑相关的指令数据(线程上下文),这类数据通常不大,而且生命周期短;一是跟对象实例相关的数据,这类数据可能会很大,而且可以被多个线程长时间内反复共用,比如字符串常量、缓存对象这类。
上面就是jvm的内存模型的简单叙述了。下面在来说说GC原理,有大佬写的文章太棒了,我就直接摘抄。
二、GC 垃圾回收原理
在学习Java GC 之前,我们需要记住一个单词:stop-the-world 简称STW。它会在任何一种GC算法中发生。stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行。当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。GC优化很多时候就是减少stop-the-world 的发生。
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)根搜索算法(补充)
根搜索算法是从离散数学中的图论引入的,程序把所有引用关系看作一张图,从一个节点GC ROOT 开始,寻找对应的引用节点,找到这个节点后,继续寻找这个节点的引用节点。当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。应该可以想想出来,就是根据爹妈往下找孩子,剩下没有关系的孤儿都是孤儿院(GC)收集对象。基本所有GC算法都引用根搜索算法这种概念
目前Java中可以作为GC ROOT的对象有:
1、虚拟机栈中引用的对象(本地变量表)
2、方法区中静态属性引用的对象
3、方法区中常亮引用的对象
4、本地方法栈中引用的对象(Native对象)
5)generation-collect 分代收集算法
将内存分成了三大块:年青代(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 区即老年代。
如果老年代,最终也放满了,就会发生 major GC(即 Full GC),由于老年代的的对象通常会比较多,因为标记 - 清理 - 整理(压缩)的耗时通常会比较长,会让应用出现卡顿的现象,这也是为什么很多应用要优化,尽量避免或减少 Full GC 的原因。
注:如果分配的新对象比较大,eden 区放不下,但是 old 区可以放下时,会直接分配到 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 中广泛使用的收集器,有必要多说一下,借一张网友的图说话:
Serial ,ParNew, Parellel Scavenge 都是回收年青代的。CMS,Serial Old, Parallel Old 都是回收老年代的。
1)Inital Mark 初始标记:主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
2)Concurrent Mark 并发标记:根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。
3)Remark 再标志:为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
试想下,高铁上的垃圾清理员,从车厢一头开始吆喝“有需要扔垃圾的乘客,请把垃圾扔一下”,一边工作一边向前走,等走到车厢另一头时,刚才走过的位置上,可能又有乘客产生了新的空瓶垃圾。所以,要完全把这个车厢清理干净的话,她应该喊一下:所有乘客不要再扔垃圾了(STW),然后把新产生的垃圾收走。当然,因为刚才已经把收过一遍垃圾,所以这次收集新产生的垃圾,用不了多长时间(即:STW 时间不会很长)。
4)Concurrent Sweep:并行清理,这里使用多线程以“Mark Sweep- 标记清理”算法,把垃圾清掉,其它工作线程仍然能继续支行,不会造成卡顿。
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 区。
文章内容基本是摘抄自链接:https://www.infoq.cn/article/3WyReTKqrHIvtw4frmr3