目录
一.GC简介
垃圾收集 GC(Garbage Collection)是 Java 非常重要的核心技术之一,Java 开发中程序员不需要关心对象的内存分配和资源释放,这些都由 GC 来完成,这使得 Java 开发者只需要将注意力集中在业务逻辑的处理上。
学习 GC 需要从以下 4 个方面入手:
- 如何判断某个对象是垃圾,需要被回收?
- 垃圾回收算法。
- 不同内存区域的回收方式。
- 垃圾收集器的分类
二.如何判断对象是垃圾
Java 对象被判定为垃圾的标准:没有被其他对象引用,判断方法有两种:
1、引用计数算法
通过判断对象的引用数量来决定是否要被回收,每一个对象实例都有一个计数器,被引用则+1,完成引用则-1。
什么是完成引用?
当该对象的引用超过了生命周期,或者引用指向了其他对象,在某方法中定义一个对象的引用变量,方法结束之后变量被虚拟机栈自动释放,则改对象的引用也就结束了,所以任何一个引用计数为 0 的对象是可以被当作垃圾回收的。
当发生循环引用时,会出现问题。
2、可达性分析算法
通过判断对象的引用链是否可达来决定对象是否要被回收,这个算法的基本思想就是通过一系列的称为 GC Root 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Root 没有任何引用链相连的话,则证明此对象是不可达的,即认为它不可用
怎么让一个被判定为垃圾的对象不被回收?
一个类实现finalize方法,当这个类的某个对象第一次被判定为垃圾的时候,GC的时候就可能会调用这个对象的finalize方法,相当于给他一次起死回生的机会(如果调用过finalize就不会再调用了)。
finalize方法是由finalizer这个线程去执行的,而这个线程的优先级非常低,每次GC的时候也不一定会执行到这个方法。所以这个方法有很大的不确定性。
3.什么对象可以作为 GC Root ?
- 虚拟机栈中的引用对象
- 方法区中的常量引用对象
- 方法区中的类静态属性引用对象
- 本地方法栈中的引用对象
- 活跃线程中的引用对象
三.垃圾回收算法
1)标记清除算法:
标记:先进行扫描,将已存活的对象进行标记;
清除:对堆内存进行遍历,回收没有标记的对象;
缺点:清楚后会产生大量不连续的内存碎片,也就是碎片化问题;这个问题可能导致后续创建较大的对象时无法找到足够的连续空间继而再次触发垃圾回收。
2)复制算法:
将可用内存分为对象面和空闲面,在对象面上创建对象,若对象面满,就将还存活的对象复制到空闲面,然后将对象面的所有对象清楚。
这个算法不会产生碎片化的问题,特别适用新生代,因为新生代的对象的存活率很低大部分都是"朝生夕死的",所以所需复制的对象也很少,所以效率会很高。
3)标记-整理算法
标记:先进行扫描,对存活的对象进行标记。
整理:移动所有存活的对象(有标记的对象),按内存地址依次排列,然后将末端地址以后的内存全部回收。
在标记-清除的基础上完成了移动,解决了内存碎片的问题,但是成本更高,适用于对象存活率较高的场景
4)分代收集算法:
是一种组合的回收机制,也是 GC 的主流回收算法,将不同生命周期的对象分配到堆中的不同区域,采用不同的垃圾回收算法,提高 JVM 垃圾回收效率。
年轻代:使用 Minor GC 进行回收,采用复制算法,年轻代分为 Eden 区和 Survivor 区。
老年代:存放生命周期较长的对象,使用标记-清理算法或者标记-整理算法进行回收。
四.垃圾收集器的分类
Jdk1.8默认的垃圾收集器时Parallel+ParalleIOld
1.年轻代常见的垃圾收集器
1、Serial 收集器(复制算法):单线程收集,进行垃圾收集时,必须暂停所有工作线程(Stop-the-World )。
STW:停止工作线程
2、ParNew 收集器(复制算法):多线程收集。GC的时候也会STW,但是因为是多线程GC所以速度上会比Serial快。
3、Parallel Scavenge 收集器(复制算法):多线程收集,更关注系统的吞吐量。
Serial 收集器和 ParNew 收集器更关注用户线程停顿时间,停顿时间越短,响应速度越快,用户体验越好,适用于直接与用户交互的程序。
Parallel Scavenge 收集器更关注系统的吞吐量,可提升 CPU 的效率,尽快完成运算任务,适合在后台运行,不需要太多交互的程序。
2.老年代常见的垃圾收集器
1、Serial Old 收集器(标记-整理算法):单线程收集,进行垃圾收集时,必须暂停所有工作线程。
2、Parallel Old 收集器(标记-整理算法):多线程收集,吞吐量优先。
3、CMS 收集器(标记-清除算法):垃圾回收线程和用户线程几乎可以同时工作,追求最短停顿时间。
回收步骤:
- 初始标记:标记直接与GCRoot相连的对象(会STW)。所以速度会很快,停顿时间很短。
- 并发标记:从上一步的对象开始再进行遍历(可以和用户线程并发执行)。
- 重新标记:因为上—步会产生浮动垃圾,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间要短。(STW)。
- 并发清除:在这一步还是会产生浮动垃圾。在整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS 收集器的内存回收过程是与用户线程一起并发执行的
缺点:存在空间碎片,牺牲了吞吐量,gc不完全(会有浮动垃圾),对CPU要求高
只有CMS收集器用的是标记清楚算法,只有CMS的MajoyGC不会触发MinorGC
4、Garbage First(G1) 收集器(复制+标记-整理算法):并发和并行,使用多个 CPU 来缩短 Stop-the-World 的停顿时间,与用户线程并发执行,并且可采用不同的方式去处理新产生的对象。同时有利于空间整合,基于标记-整理算法,可以解决内存碎片的问题。可以通过参数设置最短停顿时间。
G1对我们的堆内存做了重新的布局分成了大小相同的regions(原本的布局不适合筛选回收),G1可以知道每个regions里空间的利用率,所以可以进行一个筛选回收。
回收步骤:
- 初始标记:标记直接与GCRoot相连的对象(会STW)。所以速度会很快,停顿时间很短。
- 并发标记:从上一步的对象开始再进行遍历(可以和用户线程并发执行)。
- 最终标记:修正在并发标记阶段因为用户程序的并发执 行导致变动的数据,需暂停用户线程,(STW)。
- 筛选回收:对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间(参数可配)制定回收计划(STW)。
任何一种 GC 算法中都会发生,当 Stop-the-World 发生时,除了 GC 的线程以外, 所有线程都处于等待状态,直到 GC 任务完成,多数 GC 优化就是通过减少 Stop-the-World 发生的时间来提高程序性能。
判断是否需要使用 G1 收集器?
- 50%以上的堆被存活对象占用
- 对象分配和晋升的速度变化非常大
- 垃圾回收时间比较长
JVM调优一般会关注
吞吐量:吞吐量=用户业务代码执行的时间/(业务代码执行的时间+垃圾收集的时间)。吞吐量其实就是CPU的利用效率。
停顿时间:垃圾收集器进行垃圾回收终端应用执行响应的时间,停顿时间越少用户体验越好。
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验;高吞吐量则可以高效地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
五.什么情况下对象会从新生区到老年区
1)对象经历了15次Minor GC依旧存活 (默认值 -XX:MaxTenuringThreshold = 15);
2)再幸存区(Survivor)同龄对象大小的和超过幸存区50%空间,那么大于或等于这个年龄的对象就会到老年区。无须等到16岁;
3)新生代满进行Minor GC后,Eden区和Survivor区依然存活的对象无法放入到Survivor中,则会通过分配担保机制提前转移到老年代中;
4)对象体积过大,新生代无法容纳,这时这个对象就会绕过新生代, 直接在老年代分配;
GC的分代年龄存在对象头中