文章目录
红 \color{#FF0000}{红} 红 橙 \color{#FF7D00}{橙} 橙 黄 \color{#FF0000}{黄} 黄 绿 \color{#00FF00}{绿} 绿 蓝 \color{#0000FF}{蓝} 蓝 靛 \color{#00FFFF}{靛} 靛 紫 \color{#FF00FF}{紫} 紫
1 基础术语
-
年 轻 代 \color{#FF00FF}{年轻代} 年轻代
JVM 堆中一片区域, 用于存放对象用,内部分为 EDEN,SURVIVOR(包含 FROM和 TO 两份)区域,比例是8:2(FROM,TO 各占1) -
老 年 代 \color{#FF00FF}{老年代} 老年代
JVM 堆中一片区域,用于存放对象用,用于存放生命周期较长或者空间较大的对象(G1中有专门的大对
象区间) -
永 久 代 \color{#FF00FF}{永久代} 永久代
JDK8已经移除,之前是 JVM 规范中方法区的实现
-
GC Root
一个指针(引用),它保存了堆里面的对象(指向),而自己又不存储在堆中,那么它就可以是一个 ROOT,可以作为 GC Roots 的节点主要是全局性的引用(如常量或者静态属性引用的对象)与执行上下文(栈帧中的局部变量表)以及 JNI 本地方法栈中引用的对象 -
对象提升规则
虚拟机给每个对象定义了一个年龄计数器,对象每经过一次年轻代的垃圾回收然后存活下来就会加1,当达到一定年龄后(默认是15)会将对象提升放入到老年代中 -
Minor GC
年轻代的回收称之为Minor GC,年轻代的回收频率特别频繁,大多数对象都是在年轻代中创建并回收的 -
MajorGC/Full GC
年老代(老年代)的内存区域一般大于年轻代,所以年老代发生 GC 的频率会必年轻代少,对象从年老代消失
的时候我们称为MajorGC或者Full GC,Full GC 会占用大量时间导致程序一段时间内无响应 -
FullGC 一般发生在以下几种情况:
-
老
年
代
空
间
不
足
\color{#FF7D00}{老年代空间不足}
老年代空间不足
老年代只有在年轻代对象转入或者创建大对象,大数组的时候才会出现不足的情况,因为 Full GC 的资源消耗问题,所以我们尽量减少创建大对象和大数组,让对象尽量在年轻代就回收 -
永
久
代
空
间
满
\color{#FF7D00}{永久代空间满}
永久代空间满
当加载到类过多或者反射过多的类已经调用过多方法的时候永久代可能会被占满,可以通过设置更大的永
久代来解决,JDK8之后这些信息进入元空间,不需要我们再分配 -
整
体
空
间
不
足
\color{#FF7D00}{整体空间不足}
整体空间不足
此情况和1很像,年轻代回收的时候发现对象放不下,转而向老年代放,老年代暂时内存也不够放,这时候就会触发 FullGC -
对
象
提
升
的
平
均
大
小
大
于
老
年
代
的
剩
余
空
间
\color{#FF7D00}{对象提升的平均大小 大于老年代的剩余空间}
对象提升的平均大小大于老年代的剩余空间
此情况其实还是和前面很像,不过有区别,第一个 Minor GC 后,假设有 10M 内存对象被提升到了老年代,下一次 Minor GC 的时候会先判断老年代空间有没有10M,没有就触发 Full GC
- Mixed GC Event
混合 GC 事件,即所有的年轻代和一部分老年代一起回收,混合 GC 一定是跟随在 Minor GC 后端;
为什么是老年代的部分Region?。什么时候触发Mixed GC?回收部分老年代是参数 -
XX:MaxGCPauseMillis ,用来指定一个G1收集过程目标停顿时间,默认值200ms,当然这只是一个期望值。G1的强大之处在于他有一个停顿预测模型(Pause Prediction Model),他会有选择的挑选部分Region,去尽量满足停顿时间,关于G1的这个模型是如何建立的,这里不做深究。
Mixed GC的触发也是由一些参数控制。比如 XX:InitiatingHeapOccupancyPercent 表示老年代占整个堆大小的百分比,默认值是45%,达到该阈值就会触发一次Mixed GC - STW
STOP THE WORLD ,GC 事件/过程发生的时候 JVM 要停止你所有的程序的线程的执行,类似于你妈妈拖地的时候让你滚到一边站在那别动(哈哈),这样的设计的原则是因为垃圾回收器的任务就是对垃圾对象进行清理,为了让垃圾回收器可以有效的执行,大部分情况下会要求程序进入一个停顿状态,终止所有线程的执行,只有这样才不会产生新的垃圾,同时也保证了系统在某一时间的一致性,因为垃圾回收的时候会产生程序停顿的感觉,这种感觉叫STW - System.gc()
这个方法的作用主要是触发 FullGC,对老年代和年轻代进行回收,但是注意这个操作仅仅是请求垃圾回收的建议而已,JVM 不一定会立刻执行,而是对垃圾回收算法加权,使得垃圾回收更容易发生,就相当于你家里有垃圾你没丢,你妈妈冲你吼快去把垃圾扔掉,你可能会立刻去扔,你也可能翻翻身继续睡 - Region
在G1的垃圾回收算法中,堆内存采用了另外一种完全不同的方式进行组织,被划分为多个(默认2000多个)大小相同的内存块(Region),每个Region是逻辑连续的一段内存,在被使用时都充当一种角色,每次垃圾收集的时候只会处理几个区域,以此来控制垃圾回收产生的停顿时间,Region表示一个区域,每个区域里面的字符代表属于不同的分代内存空间类型
(E[Eden],O[Old],S[Survivor],H[Humongous])
,空白的分区不属于任何一个分代,G1可以在需要的时候将这个区域分给 O 或者 E 之类的 ,其中H是以往算法中没有的,它代表Humongous,表
示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。 Region的大小只能是1M、2M、4M、8M、16M或32M,比如-Xmx16g -Xms16g
,G1就会采用16G / 2048 = 8M 的Region. 如下图:
G1通过内存分区来避免内存碎片的问题,每个分区的大小是一样的,从逻辑来说,他们是连续的内存空
间,G1的并行全局标记阶段会决定整个堆区的活动对象,在标记完成后,G1 就知道哪些Region是空的了
2 常见垃圾收集算法
2.1 常见标记算法
常见的垃圾标记算法有两种,分别是引用计数器算法和可达性分析算法(根搜索算法)
2.1.1 引用计数器算法
引用计数算法很简单,它实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。
引用计数垃圾收集机制不一样,它只是在引用计数变化为0时即刻发生,而且只针对某一个对象以及它所依赖的其它对象。所以,我们一般也称呼引用计数垃圾收集为直接的垃圾收集机制。
但是这种引用计数算法有一个比较大的问题,那就是它不能处理环形数据 - 即如果有两个对象相互引用,那么这两个对象就不能被回收,因为它们的引用计数始终为1。这也就是我们常说的“内存泄漏”问题
算法特点
- 需要单独的字段存储计数器,增加了存储空间的开销;
- 每次赋值都需要更新计数器,增加了时间开销;
- 垃圾对象便于辨识,只要计数器为0,就可作为垃圾回收;
- 及时回收垃圾,没有延迟性;
- 不能解决循环引用的问题;
2.1.2 可达性分析算法
根搜索算法
的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链
(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明
此对象是不可用的。
那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种:
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
2.2 常见收集算法
当我们成功标记出存活和死亡对象后,GC 接下来就开始执行垃圾回收,释放内存,常见的收集算法有三种 标记-清除算法(Mark-Sweep) ,复制算法(Copying),标记-压缩算法(Mark-Compact)也有人称标记-整理算法
2.2.1 标记-清除算法
1 、概念
分为标记和清除两阶段:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象。
在标记阶段,collector
从mutator
根对象开始进行遍历,对从mutator根对象可以访问到的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象。
而在清除阶段,collector对堆内存(heap memory)从头到尾进行线性的遍历,如果发现某个对象没有标记为可达对象-通过读取对象的header信息,则就将其回收。
举个例子,老师在某个角落发现了一堆"闲书",他进教室后,让大家先停下来,然后开始问 这个是谁的,那个是谁的,然后把有人认领的书记下来,最后把没人要的书统统扔掉
回到算法本身,先说几个概念
首先是 mutator 和 collector ,这两个名词经常在垃圾收集算法中出现,collector指的就是垃圾收集器,而mutator是指除了垃圾收集器之外的部分,比如说我们应用程序本身。mutator的职责一般是NEW(分配内存),READ(从内存中读取内容),WRITE(将内容写入内存),而collector则就是回收不再使用的内存来供mutator进行NEW操作的使用。
第二个基本概念是关于mutator roots(mutator根对象),mutator根对象一般指的是分配在堆内存之外,可以直接被mutator直接访问到的对象,一般是指静态/全局变量以及Thread-Local变量(在Java中,存储在java.lang.ThreadLocal中的变量和分配在栈上的变量 - 方法内部的临时变量等都属于此类).
第三个基本概念是关于可达对象的定义,从mutator根对象开始进行遍历,可以被访问到的对象都称为是可达对象。这些对象也是mutator(你的应用程序)正在使用的对象
2 、图解
3 、缺点
1、标记阶段和清除阶段的效率都不高。
2、显而易见的,清除后产生了大量不连续的内存碎片,导致在程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。
2.2.2 标记-压缩算法
内存碎片一直是非移动垃圾回收器(指在垃圾回收时不进行对象的移动)的一个问题,比如说在前面的标记-清除垃圾回收器就有这样的问题。而标记-压缩垃圾回收算法能够有效的缓解这一问题。
- 算法原理
既然叫标记-压缩算法,那么它也分为两个阶段,一个是标记(mark),一个是压缩(compact). 其中标记阶段跟标记-清除算法中的标记阶段是一样的
而对于压缩阶段,它的工作就是移动所有的可达对象到堆内存的同一个区域中,使他们紧凑的排列在一起,从而将所有非可达对象释放出来的空闲内存都集中在一起,通过这样的方式来达到减少内存碎片的目的。
在压缩阶段,由于要移动可达对象,那么需要考虑移动对象时的顺序,一般分为下面三种:
- 任意顺序 - 即不考虑原先对象的排列顺序,也不考虑对象间的引用关系,随意的移动可达对象,这样可能会有内存访问的局部性问题。
- 线性顺序 - 在重新排列对象时,会考虑对象间的引用关系,比如A对象引用了B对象,那么就会尽可能的将A,B对象排列在一起。
- 滑动顺序 - 顾名思义,就是在重新排列对象时,将对象按照原先堆内存中的排列顺序滑动到堆的一端。
现在大多数的垃圾收集算法都是按照任意顺序或滑动顺序去实现的。
- 缺点
标记-压缩算法虽然缓解的内存碎片问题,但是它也引用了额外的开销,比如说额外的空间来保存迁移地址,需要遍历多次堆内存等。
2.2.3 复制算法
复制算法(半区复制算法)的目的也是为了更好的缓解内存碎片问题。对比于 标记-压缩算法 , 它不需要遍历堆内存那么多次,节约了时间,但是它也带来了一个主要的缺点,那就是相比于标记-清除和标记-压缩垃圾回收器,它的可用堆内存减少了一半。同时对于大对象,复制比标记的代价更大。所以半区复制算法更一般适合回收小的,存活期短的对象。
2.2.4 增量算法
incremental collection,在垃圾回收的时候,系统会出于 STW 状态,如果垃圾回收时间过长,应用程序会被挂起很久,严重影响用户体验或者系统等稳定性
增量算法的基本思想史如果一次性将所有垃圾都回收,需要造成系统长时间停顿,那么可以让垃圾回收线程和程序线程教程执行,垃圾回收只收集一小片区域的内存空间,然后切换到应用程序,依次反复,直到垃圾收集完成,使用这种方式能减少系统停顿时间,不过因为线程上下文的切换,导致垃圾回收总体成本较高,系统吞吐量下降
本质上,增量算法基础上还是传统的复制算法和标记清除算法
2.2.5 分代收集算法
“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收。方法区永久代,回收方法同老年代。注:JDK8移除了永久代
- 对象分类
分代搜集算法是针对对象的不同特性,而使用适合的算法,这里面并没有实际上的新算法产生。与其说分代搜集算法是第四个算法,不如说它是对前三个算法的实际应用。
首先我们来探讨一下对象的不同特性,接下来和各位来一起给这些对象选择GC算法。
内存中的对象按照生命周期的长短大致可以分为三种,以下命名均为个人的命名。
1、夭折对象(新生代
):朝生夕灭的对象,通俗点讲就是活不了多久就得死的对象。
例子:某一个方法的局域变量、循环内的临时变量等等。
2、老不死对象(老年代
):这类对象一般活的比较久,岁数很大还不死,但归根结底,老不死对象也几乎早晚要死的,但也只是几乎而已。
例子:缓存对象、数据库连接对象、单例对象(单例模式)等等。
3、不灭对象(永久代
):此类对象一般一旦出生就几乎不死了,它们几乎会一直永生不灭,记得,只是几乎不灭而已。例子:String池中的对象(享元模式)、加载过的类信息等等 Hotspot虚拟机通过永久代实现方法区。注:改变参考上文的备注Java内存分配机制
Java内存分配机制
这里所说的内存分配,主要指的是在堆上的分配,一般的,对象的内存分配都是在堆上进行。Java内存分配和回收的机制概括的说,就是:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。
GC主要有YoungGC,OldGC,FullGC(还有G1中独有的Mixed GC,收集整个young区以及部分Old区)YoungGC:回收Eden区,有些地方称之为Minor GC,或者简称YGC
OldGC:回收Old区,只单独回收Old区的只有CMS GC,且是CMS的concurrent collection模式。FullGC:收集整个GC堆,也称之为Major GC。
当有人说“Major GC”的时候一定要问清楚他想要指的是上面的FullGC还是OldGC。对这个GC的误解最大,尤其最常用的ParNew+CMS组合,很多人误解FullGC可能是受到jstat结果的影响。
如果配置了CMS垃圾回收器,那么jstat中的FGC并不表示就一定发生了FullGC,很有可能是发生了CMS GC,而且每发生一次CMS GC,jstat中的FGC就会+2(因为CMS GC时初始化标记和重新标记都会STW,所以FGC的值会+2,可以通过让JVM按照预期GC提供的代码验证)
事实上,FullGC的触发条件比较苛刻,判断是否发生了FullGC最好通过GC日志,所以强烈建议生产环境开启GC日志,它的价值远大于它对性能的影响
3. GC触发的过程
当创建一个新对象的时候需要为新对象申请空间,在伊甸园区申请,但是需要判断伊甸园区的空间是否充足,充足则申请成功,如果不充足则触发MinorGC回收不活跃的对象(不经常使用的对象)【采用的算法是复制算法】,回收之后再判断伊甸园区空间是否充足,充足则申请成功。【 gc 时把存活的对象从一块空间(From space)复制到另外一块空间(To space),再把原先的那块内存(From space)清理干净,最后调换 From space 和 To space 的逻辑角色(这样下一次 gc 的时候还可以按这样的方式进行),当启动MinorGC 的时候会停止应用线程,启动GC回收线程,这个线程是一个守护线程】
2. 如果依然不充足,则判断存活区空间是否充足将伊甸园区的部分活跃对象移到存活区,伊甸园的空间申请成功。
3. 如果存活区的空间不足则判断老年代,如果充足则将存活区的部分活跃对象保存到老年代,之后将伊甸园区的活跃对象保存到存活区,空间申请成功。
4. 如果老年代的空间不充足则触发Full GC(MajorGC)让Jvm进行完全彻底的垃圾回收,之后再判断老年代的空间是否充足,充足则将存活区的活跃对象移到老年代,再将伊甸园区的活跃对象移到存活区,空间申请成功。如果触发Full GC之后老年代的空间依然不足则会出现OOM(Out Of Memory)错误。这种活跃对象逐步往上移动的过程叫做对象的晋升。
以上就是对象的创建与GC的基本关系过程