前言
GC(Garbage Collection),内存的动态分配与内存回收技术的重要组成部分。
- 从整体上而言,我们要理解一下3个问题
- 哪些内存需要被回收
- 什么时候回收
- 如何回收
一、哪些内存需要被回收
-
程序计数器、虚拟机栈、本地方法栈
关于GC,我们不能只考虑具体的方法,要整体上明白GC的作用是实现内存管理,结合Java虚拟机内存管理知识,我们知道虚拟机栈,本地方法栈,程序计数器都是随线程的,大小都是在一开始就确定的,所以这几个区域的分配和回收都是确认的。他们在线程或方法开始的时候创建,在线程或方法结束的时候销毁 -
堆和方法区
这部分在事先我们是不知道的,我们不知道真正运行时会进入哪个分支,要创建哪些对象,所以这部分的是不确定的,它是分配和回收也是动态的,是GC需要关注的
在堆和方法区内如何判断对象要被回收?--即如何判断对象死亡了
- 引用计数法
引用时加1,容易理解,当问题是难以解决互相循环引用的问题 - 可达性分析---目前的方法
我们将一些量称之为root,从root的引用,不断延伸,引用链到最后节点,如果对象不在引用链上,则会被标记,它不可达- GC roots有哪些:虚拟机栈中引用的对象(意味着有线程正在引用)、 方法区中类的静态属性引用的对象(因为这个是属于类的,一开始就创建好的被引用)、方法去中常量池引用的对象(final的那些,为了速度快)、本地方法栈中JNI(即一般说的Native方法)引用的对象(线程正在引用)
-
Tip,这里的引用后来又扩展除了强引用、软引用、弱引用、虚引用
-
回收方法区
刚讲的是对象的回收,其实更进一步,虚拟机也可以对方法区进行回收,如某个常量“abc”是否没有被引用了等。这里判断回收的方法和之前不一样- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该
类的方法
目前很多框架大量运用反射、动态代理、CGLib等,自定义的ClassLoader都需要有这类卸载的功能,保证永久带不溢出
二、如何回收--各种垃圾收集算法
在收集的实现上,主要有一下几种
- 标记--清除 Mark-Sweep
这个是最基础的思路,标记哪些对象要被回收,然后回收。其他的都在对其的改进思路- 缺点 1.容易产生大量的不连续的内存碎片,导致大对象无法使用,需要再一次清理
- 缺点 2.效率问题,和复制算法相比,效率低
- 复制算法
为了提升效率,有了复制算法。将一块内存分为2块,每次使用其中一块,一块用完时,清理,将存活的移到另一块,再把这一块清空。
优点 1.简单、高校
缺点 1.内存可用缩小了一半
2.对象存活率较高时,效率很低
- tip 在实践中,由于新生代对象死亡很快,在现代的算法里,是将年轻代的内存划分为一块较大的Eden区和2块较小的Survivor区。默认8:1:1.每次用一个Eden和Survivor。复制时,把它们都复制到另一个Survivor中,将Eden和Survivor清理
-
标记-整理
从复制算法的优缺点可以看出,复制算法适合于年轻代,存活率低的场景。为了改进复制算法,提出了标记--整理算法
先标记不可达的对象,但后续不是清理,而是将存活的对象进行整理,移动,排整齐之后,清理掉边界以外的内存
image.png
优点:1.适合于老年代,这种对象存活率较高的内存区域
2.可以解决复制算法内存使用率低的问题
分代收集算法 Generation Collection
这个本身并没有新的算法,而仅仅是将内存划分为年代,根据不同年代的特点,选用不同的GC方法,以提升效率
总体来说:1. 年轻代对象朝生暮死,适合用复制算法
2. 年老代存活率高,且没有额外的空间,适合使用标记-清除或标记-整理算法
三、HotSpot虚拟机gc算法
我们都知道虚拟机有很多的gc策略,什么CMS,ParNew等等,但是虚拟机底层是如何执行这些策略的呢?
这就要涉及到最基本的几个概念
-
1.枚举跟节点
我们知道,现在对象回收都是采用可达性分析,而可达性分析的基础就是枚举跟节点。,枚举的特点是什么,是枚举时的东西是固定的,它不可以变化,否则无法完成枚举,因此所有的gc枚举跟节点必然会stop the world,这是难以避免的。
我们能做的,只是让这个过程尽可能得快,由于目前的主流Java虚拟机使用的都是准确式GC,虚拟机可以直接从对象中获得引用存放的信息,因此整个过程是很快的 - 安全点 Safe Point
我们并不是在线程的任何时刻都可以进行枚举跟节点,只是在“特定的位置”才行,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停
目前主流的都是轮询式,gc时,虚拟机设置一个标志位,其他线程轮询到这个标志位,进行安全点暂停,等待gc
tip:如何选择安全点?
是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、 循环跳转、 异常跳转等,所以具有
这些功能的指令才会产生Safepoint
- 安全点 Safe Point
- 安全区域 Safe Region
是安全点的扩展,如一些线程block或sleep状态,不会去轮询状态位,这时候状态相当于不会变化,这个区域就当做是安全区域
- 安全区域 Safe Region
四、具体的垃圾回收算法
重点了解CMS,实际使用,他的特点
我们所说的CMS实际上是一款老年代的收集器,一般采用Paraller New作为配合
Paraller New就是Serial的升级版,新生代GC时,多线程去GC,在多核下比Serial效果好
CMS(Concurrent Mark Sweep)
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,有些则目的是最大吞吐量。
从名字中可以看出,这是一个基于并发标记-清除策略的算法

初始标记-并发标记-重新标记-清除。
CMS是通过并发来缩短gc停顿时间,但再初始标记阶段,仍然要暂停所有线程,这无法避免
CMS的几个缺点
1.并发必然会对CPU敏感,默认采用25%的核进行gc回收,当核比较少时,对性能影响较大
- 浮动垃圾
多次标记的过程,因为清理时,用户线程才在运营,因此需要预留一部分给用户线程用,如现在默认设计92%,那么如果预留的空间不够怎么办,虚拟机会使用Serial Old算法(单线程标记-整理),从而使得整体停顿时间上升
3.标记-清除
导致碎片多,需要额外触发full-gc,或者进行整理,可以配置再多少次full gc后进行整理,而内存整理是无法并发的,因此停顿时间变长
五、内存分配和回收
了解了理论知识,我们来看一个完整的过程
1.对象优先在Eden区进行分配,我们知道年轻代采用复制算法,有2个survivor区用于复制。当Eden区不够时,触发young gc,young gc就是指发生再新生代的gc,特点是频繁,但比较快
-
大对象直接进入年老代
这是一个优化措施,避免触发复制,少一次young gc -
年龄大的对象进入年老代
虚拟机为每个对象设置了Age属性,挺过一次young gc,就加1,达到一定阈值就进入年老代,或同代的对象超过survivor也能进入年老代,这是一个优化
4.空间分配担保
我们知道young gc采用复制算法,可能一个survior不够,怎么办,必然会导致一部分对象进入老年代。那么就需要老年代进行空间担保,这有担保策略,根据历史平均值。如果最终老年代空间不够,则会导致full gc
GC日志的典型举例
youngGC日志

FullGC日志

网友评论