深入JVM虚拟机系列
1、深入JVM虚拟机系列——内存结构分析(入门)
2、深入JVM虚拟机系列——垃圾回收(进阶)
3、待更新…
前言
本篇文章用图文并茂的形式让大家了解JVM,本文使用到的知识来自《深入了解jvm虚拟机》《Java 编程思想》以及维基百科一、垃圾回收器的分类(总共10种)
-
按垃圾收集器运行方式分类
- 串行
- serial 收集器
- serial old 收集器
- 并行
- Parallel Scavenge收集器(吞吐收集器)
- parallel old收集器
- par new 收集器
- 并发
- CMS 收集器
- G1 收集器
- 串行
-
按回收区域分配
- 年轻代
- Serial
- ParNew
- Parallel Scavenge
- 老年代
- CMS
- Serial Old
- Parallel Old
- 老年代与年轻代都会参与
- G1
- ZGC
- 年轻代
如下图:
本次我们只分析以G1收集器为分界线左边的收集器,因为右边的收集器实际上在市场上的占有率极低可以说忽略不计,因为他们一般是实验室的产物,一般只应用于理论或者某些特定的场景。
1、serial收集器
serial收集器是最基本的垃圾收集器,使用复制算法,曾经是jdk1.3.1之前的唯一新生代垃圾回收器。serial是一个单线程的回收器,它不但只会使用一个CPU或者一个线程执行垃圾回收操作,而且在它进行回收垃圾时必须暂停所有的工作线程知道垃圾回收结束。
虽然serial收集器在收集垃圾时会暂停所有的工作线程,但是它简单高效,对于限定的单个CPU环境来讲没有线程交互的开销,可以获得最高的单线程垃圾回收效率,所以serial回收器依旧是虚拟机运行在client模式下默认的新生代垃圾回收器。
serial收集器运行图
2、parNew收集器
parNew垃圾收集器其实就是serial的多线程版本,也是使用复制算法,除了使用多线程进行垃圾回收之外,其余的行为和serial几乎一致。
parNew收集器默认开启与CPU数目相同的线程数进行垃圾回收,当然也可以通过参数设置:-XX:ParallelGCThreads 参数来配置,除此之外parNew收集器还是很多Java虚拟机在server模式下的新生代默认垃圾收集器。
parNew收集器垃圾回收运行图
3、parallel Scavenge收集器(多线程复制算法、高效)
parallel Scavenge收集器也是一个新生代垃圾收集器,同样是使用复制算法,也是一个多线程垃圾回收器。它重点关注的是程序达到一个可控制的吞吐量(CPU运行用户线程/CPU总消耗时间,即吞吐量=CPU运行用户线程时间/CPU运行用户时间+垃圾回收时间)。高吞吐量可以最高效的利用CPU时间,尽快的完成程序的运算任务。然后就是一个parallel Scavenge收集器拥有自己的一个自适应调节策略,这个也是它与parNew收集器最大的区别。
parallel scavenge收集器运行图
4、serial old收集器
serial old收集器是serial收集器的老年代回收版本,也是单线程,不过使用的是标记整理方法。这个收集器也是虚拟机运行在client模式下的默认old区域的收集器。
5、parallel old收集器
parallel old收集器是parallel Scavenge收集器的老年代版本,使用多线程的标记整理方法,在jdk1.6才开始提供使用。
在jdk1.6之前虚拟机使用parallel Scavenge收集器对年轻代进行回收,serial old对年老代进行回收,虽然保证了年轻代的吞吐量优先,但是无法保证整体的吞吐量。这个时候为了解决整体吞吐量不一致的问题就诞生了parallel old收集器,parallel old收集器正是为了在老年代提供吞吐量优先的垃圾收集器。如果系统对吞吐量要求比较高,就建议使用年轻代—parallel Scavenge收集器 ,老年代——parallel old收集器这样的搭配来保证整体吞吐量。
6、CMS收集器(多线程标记清除算法)
concurrent Mark sweep(CMS)收集器是一种老年代的垃圾收集器,主要目标时获取最短的垃圾回收时间,和其他使用标记整理法的老年垃圾回收器不同的是它使用标记清除算法。最短的垃圾收集停顿时间可以给用户带来最优的用户体验。
CMS收集器的工作模式较比其他的收集器更为复杂,整个过程分为4个阶段:
- 初始标记:只是标记下GC Roots关联的对象,速度非常快
- 并发标记:对GC root进行跟踪的过程,与用户线程一起工作不暂停用户线程。
- 重新标记:为了修正在并发标记过程中因为用户程序继续而导致标记变动的一些对象的标记,进行该操作需要停止用户线程。
- 并发清除:清除GC Root不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户一起并发工作,所以总的来看CMS收集器的内存回收和用户线程是一起并发执行的。
CMS收集器回收垃圾的过程图:
7、G1(Garbage first)收集器
Garbage first收集器是目前垃圾收集器理论发展的最前的结果,也就是说最新的一款。相比与CMS收集器G1收集器有两个最明显的改动点:
- 使用标记整理法进行垃圾收集不产生内存碎片
- 可以非常精确的控制停顿时间,在不牺牲吞吐量的前提下实现低停顿垃圾回收。
G1收集器避免全区域垃圾回收,他把收集区域划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每一次根据所允许的时间,优先回收垃圾多的区域。正是由于区域划分和优先级区域回收机制,确保了G1收集器可以在有限的时间内获得最高的垃圾回收效率。
二、垃圾收集器的搭配
server模式下:
1. serial + serial old
2. scavenge /parNew +serial old
3. scavenge +parallel old
4、STW(stop the work)停止用户线程
当项目发生GC 用户线程会进入到一个JVM设置的所谓的save point (安全点),此时所有的用户进程被挂起,此时GC线程执行对垃圾进行回收操作,直到GC线程执行结束,所有的用户线程才能继续执行任务。当然这种GC方式运行结构图只适用于串行和并行的收集器,对于并发方式的收集器此图并不适用。
看完上面的是不是有种醍醐灌顶的柑橘?别急下面来重头戏!!!
三、垃圾回收算法
- 复制算法
- 标致-清除(mark sweep)法
- 标志-整理(mark collect)法
- 分代收集算法
1、复制算法
图示:
-
原理
当一块内存用完时,就将还存活的对象复制到另外一块内存区域中,然后把使用的内存空间进行内存回收,不许考虑内存碎片化等复杂情况,只需要移动堆指针,按顺序分配内存,实现简单,运行高效。 -
特点:
主要回收对象是内存堆区域中的年轻代 -
针对内存空间
Eden以及survivor区域中的一块 -
优缺点
- 好处
- 效率高于标志-清除算法
- 不会产生过多碎片
- 好处
-
缺点
- 占用资源较多,每次回收都必须空出一块同样的空闲内存区域进行存储复制后的存活对象
- 当可回收对象较少时,复制算法执行的效率就会变得很低
2、标志-清除算法
图示:
-
原理
- 标记(Mark)
- 在标记阶段,collector从mutator根对象开始进行遍历,对从mutator根对象可以访问到的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象。
- 清除(回收(sweep))
- 在清除阶段,collector对堆内存(heap memory)从头到尾进行线性的遍历,如果发现某个对象没有标记为可达对象-通过读取对象的header信息,则就将其回收。
- 标记(Mark)
-
特点:
回收对象是内存堆区域中的old代 -
针对内存空间
回收对象是内存堆区域中的old -
优缺点
-
好处
- 资源占用较复制算法少
-
缺点
- 标记阶段和清除阶段的效率都不高。
- 显而易见的,清除后产生了大量不连续的内存碎片,导致在程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。
-
3、标志-整理算法
图示:
-
原理
-
标记(Mark)
- 在标记阶段,collector从mutator根对象开始进行遍历,对从mutator根对象可以访问到的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象。
-
整理(collect)
- 而在清除阶段,会将存活对象都向一端移动,然后直接清理掉端边界以外的内存。
-
特点:
回收对象是内存堆区域中的old代 -
针对内存空间
回收对象是内存堆区域中的old -
优缺点
-
好处
- 自带整理功能,这样不会产生大量不连续的内存空间,适合老年代的大对象存储。
-
缺点
- 不适合生命周期短的对象
-
4、分代收集算法
图示:
分代收集算法并非直接作用于内存进行回收,而是对垃圾回收方法论进行控制,后面引入jvm 自适应技术对不同区域的内存垃圾进行回收算法控制。
5、自适应技术
四、垃圾回收
- 对象创建,JVM Eden区域开辟内存空间Eden区域
- Eden区域内存不足发生轻量GC(young GC)此时回收区域是年轻代包括Eden区域以及survivor from区域,使用复制算法将存活对象复制到survivor to 区域(此时内存区域占比为8+1:1)因为收集的效率达到98%(hotspot实验室数据)所以survivor to区域是可以存储的下的。
- 存活的对象会循环此操作,直到对象的分代年龄达到15(CMS是6)就会分配到old区域
1、什么是存活对象?——存活依据是什么
1、引用计数算法
-
堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。 当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1
图示:
-
优缺点
- 好处:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
- 缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.
-
循环引用
-
例子
class Node { Node next; } Node a = new Node(); Node b = new Node(); a.next = b; b.next = a;
-
代码中,a对象引用了b对象,b对象也引用了a对象,这种情况下a对象和b对象就形成了循环引用
-
2、对象可达性分析算法
-
根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点, 找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点
图示:
-
优缺点
-
好处:避免了循环引用导致的对象无法回收
-
缺点:复杂,需要从末尾节点一个一个分析是否拥有连接节点或者是否拥有跟节点
-
GC root 跟节点
-
什么对象可以被当为GC root 根节点
- 虚拟机栈中的引用的对象(本地变量表)
- 方法区中的静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中的引用的对象