目录
一、四种引用方式
1.1 强引用
1.2 软引用(SoftReference)
1.3 弱引用(WeakReference)
1.4 虚引用(PhantomReference)
二、如何判断对象是垃圾
2.1 引用计数法
2.2 根可达性分析
三、垃圾回收算法
3.1 标记-清除(mark-sweep)
3.2 标记-整理(mark-compact)
3.3 标记-复制(mark-copy)
四、垃圾收集器
4.1 分类及特点简述
4.1.1 串行
4.1.2 吞吐量优先
4.1.3 响应时间优先
4.2 串行垃圾回收器详述
4.2.1 Serial
4.2.2 Serial-Old
4.2.3 流程图
4.3 吞吐量优先垃圾回收器详述
4.3.1 JVM相关参数
4.3.2 流程图
4.4、响应时间优先垃圾回收器详述
4.4.1 JVM相关参数
4.4.2 流程图
4.3.3 CMS的特点
五、G1垃圾回收器
5.1 相关JVM参数
5.2 特点
5.3 G1新生代垃圾回收
5.4 G1老年代垃圾回收
一、四种引用方式
1.1 强引用
只有所有 GC Roots对象都不通过【强引用】引用该对象,该对象才可以被回收。
1.2 软引用(SoftReference)
- 仅有软引用引用该对象时,在垃圾回收后,若内存仍不足时再次发出的垃圾回收,回收软引用对象
- 可以配合引用队列来释放软引用自身
1.3 弱引用(WeakReference)
- 仅有弱引用引用该对象时,每次发生垃圾回收的时候,无论内存是否充足都会回收掉这部分对象
- 可以配合引用队列来释放软引用自身
1.4 虚引用(PhantomReference)
- 必须配合引用队列来使用,主要配合ByteBuffer的使用,被引用对象回收时,会将虚引用存入队列,由Reference Handler线程调用虚引用相关方法释放直接内存
二、如何判断对象是垃圾
2.1 引用计数法
某个对象只要有一处引用关系,该对象的引用次数就加1,如果一个对象的引用次数为0,则说明该对象是垃圾。
优势:实现简单,效率较高
弊端:如果有一对对象之间形成了相互引用,但是这两个对象都已经没有被其它对象所引用了,正常情况下,这一对对象应该被作为垃圾回收掉,但是因为形成了相互引用导致无法被回收。
2.2 根可达性分析
通过GC Root对象开始向下寻找,寻找不到的对象即说明没有被引用,那么这些没有被引用的对象被认定为垃圾。
目前,如下对象可以作为GC Root对象:
1. 栈桢中的本地变量表所引用的对象
2. 本地方法栈中的对象
3. 类静态属性引用的对象
4. 方法区常量引用的对象
5. 存活的Thread对象
6. Bootstrap CloassLoader
7. 通过Bootstrap ClassLoader、Extention ClassLoader加载的类
8. 正在被Synchronized锁定的对象
三、垃圾回收算法
根据前面的描述,知道了哪些对象是垃圾需要被回收,那么回收是按照什么样的算法呢?
3.1 标记-清除(mark-sweep)
很好理解,即在GC的放生时候,先对所有对象进行根可达性分析,借此标记所有的垃圾对象;所有对象标记完毕之后会进行清理操作。
因此,总体来说,就是先标记再清除。
弊端;标记清除之后会产生大量不连续的内存碎片,碎片太多可能会导致程序运行过程中需要分配较大对象时,无法满足分配要求导致GC操作。
3.2 标记-整理(mark-compact)
该回收算法操作过程基本等同于标记-清除算法只不过,第二步有点区别,该种方式会在清除的过程中进行整理操作,这是最大的不同。
优势:最终不会出现若干空间碎片而导致的空间浪费。
弊端:在整理过程中带来的计算不可小觑。
3.3 标记-复制(mark-copy)
该种方式与前两种有较大的区别:
该种方式会将存储区分成两个部分,分别为From、To,其中From区域中可能存在着对象,而To区域始终为空,用做下一次接受数据做准备。
分别有两个指针指向这两个区域:From-Pointer、To-Pointer,
优点:这种算法非常适合早生夕死的对象
缺点:始终有一块内存区域是未使用的,造成空间的浪费。
四、垃圾收集器
垃圾收集器,就是实现了上述三种垃圾回收算法的具体实现
在Java中不同的“代”所保存的对象特点都不一样,因此Java在垃圾回收的选择上并没有偏爱一种,只采用一种算法方式,而是根据不同“代”对象的特点,采取不同的垃圾回收器(垃圾回收算法)。
4.1 分类及特点简述
4.1 串行
特点:
- 单线程
- 堆内存小,适合于个人电脑
4.2 吞吐量优先
特点:
- 多线程
- 堆内存较大,适合多核CPU
- 让单位时间内,STW的时间最短,即一段时间中,垃圾回收的时间与总运行时间的占比,占比越小越好,说明大多数时间都在处代码逻辑
4.3 响应时间优先
特点:
- 多线程
- 堆内存较大,适合多核CPU
- 尽可能让单次STW时间最短,只是单单的追求每次垃圾回收时间短即可,并不在意一段时间内发生了多少次垃圾回收。
4.2 串行垃圾回收器详述
JVM开关:-XX:+UseSerialGC = Serial + SerialOld
4.2.1 Serial
- 工作在新生代
- 采用复制算法
- 单线程
4.2.2 Serial-Old
- 老年代
- 采用标记-整理算法
- 单线程
4.2.3 流程图
4.3 吞吐量优先垃圾回收器详述
JDK1.8默认开启的,使用的算法本身与Serial是一致的,只是处理线程不一样而已。
4.3.1 JVM相关参数
-
-XX+UseParallelGC
- 工作在新生代
- 复制算法
-
-XX+UseParallelOldGC
- 工作在老年代
- 标记-整理算法
-
-XX:+UseAdaptiveSizePolicy
动态调整Eden与Survivor的比例
-
-XX:GCTimeRatio=ratio(默认为99)
占比 = 1/(1+radio)
表示希望在当前运行总时间中,GC的时间与总时间的占比小于等于上面的公式值。
-
-XX:MaxGCPauseMills=ms(默认200ms)
单次垃圾回收的时间
-
-XX:ParallelGCThread=n
指定并行的垃圾处理线程数,默认为CPU核数
因此在垃圾回收的过程中可能会导致CPU一下子跑满
4.3.2 流程图
4.4、响应时间优先垃圾回收器详述
4.4.1 JVM相关参数
-
-XX:+UseParNewGC
工作在新生代
-
-XX:+UseConcMarkSweepGC
工作在老年代,若垃圾回收失败时,则会退回到SerialOld垃圾回收。
采用标记清除算法;
可以并发执行,即在某些垃圾回收阶段,垃圾回收线程可以与用户线程一起执行。
-
-XX:ConGCThread=n
指定在并发收集的过程中,可以使用n个线程处理垃圾回收
-
-XX:CMSInitiationOccupancyFraction=precent
在下面的分析中,会介绍到浮动垃圾的概念,因此为了更加及时的清除浮浮动垃圾,在老年代的空间占用达到了Precent的时候,就会触发老年代的垃圾回收。
-
-XX:+CMSScavengeBeforeRemark
因为老年代的CMS涉及到重新标记,重新标记就是在判断得到的垃圾对象是否又被重新引用。那么CMS会从新生代开始,使用根可达性分析(因为根可达性分析是不可逆的,也就是说无法通过某个对象直接查看其是否被引用,就好像二叉查找树一样,必须先从根对象开始往下寻找,看是否能找到该对象),而新生代中的对象较多,一个一个的从新生代对象进行更可达性分析,势必会拖慢响应分析,因此在重新标记之前,若改参数开关已打开,会先进行一次新生代的垃圾回收。
-
-XX:ParallelGCThreads=n
指定并行的垃圾处理线程数,默认为CPU核数。
4.4.2 流程图
上图是:CMS垃圾回收器在老年代GC的工作流程图:
- 初始标记:标记根对象
- 会引发STW
- 根对象较少,因此标记很快
- 并发标记:从根对象出发去标记剩余对象
- 不会引发STW
- 此处的标记可以与用户线程并发执行
- 重新标记:从第二步中得到的垃圾对象进行重新扫描
- 会引入STW
- 为了避免在并发标记的过程中,因为用户线程的继续执行导致部分垃圾对象又重新被引用
- 并发清理:对第三步最后得到的垃圾对象进行使用标记清除算法
- 不会引入STW
- 此处可以与用户线程并发执行
4.3.3 CMS的特点
在某些阶段垃圾清理线程可以与用户线程并发执行
最大限度减少STW时间
-
在并发标记的过程中,由垃圾对象重新变成有用对象则需要在重新标记中进行最后一步的筛选,但是有一种情况,无法处理,即:
在并发标记的过程中,用户线程新产生的浮动垃圾,则无法被识别,需要等到下一次垃圾回收才能清除掉
在整个清理的过程中,只有初始标记与重新标记需要进行STW,其余步骤都可以与用户线程并发的执行
-
CMS老年代垃圾回收器有可能会退化到SerialOld老年代垃圾回收器:
因为某个对象无法存入内存时,且经过了新生代垃圾回收之后,仍然无法存入,此时需要进行老年代的垃圾回收,在老年代垃圾回收之后仍然无法存入,则此时认为并发失败,会退回到SerialOld进行一次老年代垃圾回收,若清理之后仍然无法存入,则会报错老年代OOM。
五、G1垃圾回收器
5.1 相关JVM参数
-
-XX:UseG1GC
开启使用G1垃圾回收器
-
-XX:G1HeapRegionSize=n(默认是2048)
指定整个堆被划分成N个Region,因此每个Region的大小为
堆空间 / N
,但是有一条硬性规定,即每个Region的大小必须是 2 的整数倍。两个
初始
Region数公式:新生代的Region个数 = 5% * N
老年代的Region个数 = 95% * N
而对于新生代而言,在以往,新生代中进一步被划分为Eden区、Survivor(From、To)区,且三者的比值依然为:8:1:1,这样的划分在G1中同样的适用,只不过这样的划分不会出现在同一个Region中,而是Eden在某个独立的新生代Region中,From、To都各占一个独立的Region。
-
-XX:InitaingHeapOccupancyPercent=45%
老年代的Region个数达到n%,即会触发老年代垃圾回收。
-
--XX:G1MixedGCuntTarget=n
在混合模式下的垃圾回收,在并发清理阶段分为n次进行
-
-XX:MaxGCPauseMillis=ms
指定的垃圾回收最长时间,如果时间很短,G1就只会回收那些回收价值最高的Region,这是为了能够达到这个暂停目标。
5.1 特点
-
全年代垃圾回收器
G1负责全年代的垃圾回收,不像前面几种垃圾回收器,如:Serial(新生代)需要配合SerialOld(老年代)、CMS(老年代)需要配合ParNew(新生代)
-
G1对堆的划分方式不一样
在G1之前,堆空间被“切”成两份,分别是:新生代、老年代,
而G1并没有按照传统方式去划分,G1将整个堆空间划分为一个一个相同大小的Region,而每个Region要么属于新生代,要么属于老年代,而每个新生代或老年代的Region在物理上不是一块连续的物理地址。而每个Region也不一定在整个项目周期中完全属于新生代或老年代而是处于一种动态变化的过程中。
-
每次回收某个代并不会全部进行扫描回收
回收价值:这是每个Region中带有的属性,Calc(对象的存活率、回收预计耗时、回收效果...)
而当进行回收时,会优先选取回收价值高的Region,从而减少垃圾回收的区域。
-
大对象存储
在以往分代模型中,当某个大对象进入内存时,如果整个新生代在垃圾清理之后依然无法使用,但是老年代却有足够的空间,此时该大对象会直接进入到老年代;
而在G1中并没有使用上述的方式存储过大的对象,而是大对象会被安排到了一个叫Humongous的Region中,在发生新生代或者老年代垃圾回收时,都会顺带清理Humongous的Region。
5.2 G1新生代垃圾回收
经过上面的文字分析,新生代的Region个数为所有Region个数的5%;这个数值其实是很小的,那么当新生代Region不够用的时候,JVM会划分更多的Region个数给新生代;
当新生代的Region个数占比所有Region个数超过 60%时,就会进行一次新生代的垃圾回收。
新生代垃圾回收会造成STW。
具体的垃圾回收算法同其它几个新生代垃圾回收器一样,新生代都使用复制算法。
先进行STW,将所有的用户线程到一个安全点
-
将Eden的对象进行分析,将存活对象拷0贝到To、然后清空Eden,然后将原本的To Region,改成From Region。
当某个对象生命周期达到一个阈值的时候会晋升到老年代。
5.3 G1老年代垃圾回收
老年代垃圾回收触发机制与参数-XX:InitaingHeapOccupancyPercent
有关。
但是需要注意的是:这一次的老年代回收,其实是一次混合垃圾回收,会同时清理新生代、老年代、Humongous。
与新生代回收算法一致,依然使用复制算法,但是垃圾回收的过程等同于老年代响应时间优先的CMS方式
流程分为:
- 初始标记,会发生STW
- 并发标记
- 最终标记,会发生STW
- 根据不同Region的回收价值,选用回收性价比最高的Region进行并发清理,而不像CMS不进行有选择性的进行回收,这就是为了控制STW的时间。
注:文章可能存在知识错误或错别字,欢迎指出