目录
垃圾回收
概述
垃圾回收机制
垃圾回收标记阶段
Finaliztion机制
垃圾回收阶段算法
System.gc()与STW
垃圾回收器
CMS
G1回收算法
查看 JVM 垃圾回收器设置垃圾回收器
垃圾回收
概述
垃圾回收功能是java中支持的功能,在其他的语言中也会存在自动的垃圾回收的功能(例如,python,C#,Ruby)等,垃圾回收机制主要解决的问题是,什么是垃圾,何时收集垃圾,如何收集垃圾这三个问题
什么是垃圾?
在java运行的过程中没有任何引用指向的对象就会被标记为垃圾
如果垃圾在内存中存放过多就会产生内存溢出的问题
为什么要进行垃圾回收?
为了防止垃圾过多将内存占满,出现内存溢出的问题,需要进行内存的清理
整理碎片化的内存空间,高效的使用内存
内存溢出与内存泄漏
内存溢出:在java中,如果内存空间不足,且GC也无法清理出更多的空间时就会产生内存溢出的问题
内存泄漏:在java中针对那些生命周期很长对象,长期占用着内存的这种现象就叫内存泄漏,比较多指已经不使用的对象却无法进行垃圾回收
例如:单例模式创建的对象,生命周期与应用程序的生命周期是一样长的
一些提供close()方法的资源未关闭,像数据库连接对象,IO流对象,网络连接对象
垃圾回收机制
自动的内存管理
优点:自动化的内存管理,降低内存溢出与内存泄漏的风险,可以减少程序员的压力,使程序员精力放在业务上
缺点:降低了程序员对内存管理的能力,出现内存相关的问题时更不易排查
垃圾回收标记阶段
标记阶段的目的是为了找出垃圾回收的对象
只有被标记为垃圾的对象才会被垃圾回收器进行回收
什么样的对象会被判定为垃圾对象呢?简单来说就是没有被引用的对象
判断的两种对象是否是垃圾对象有两种方式:引用计数算法与可达性分析算法
引用性分析算法
原理:引用性分析算法的原理是在对象中存储一个引用的计数属性,如果该对象被引用则更新这个属性的值,如果该属性的值为0则被标记为垃圾对象
优点:垃圾对象容易识别,判定的效率高,回收没有延迟性
缺点:需要维护一个属性增加时空的开销,无法解决循环引用的问题(有两个以上的对象互相引用,又没有其他对象引用这个循环就会出现这个问题),出现内存泄漏的问题
可达性分析算法
原理:会根据GCRoot(根对象)来自上向下的找到被引用的对象,根据被引用的对象再找到以该对象为节点,这个对象的引用对象,这样一个搜索的链条叫做引用链(Reference Chain),没有被引用链所关联的对象就会被标记为垃圾回收算法
优点:能够解决的循环引用的问题,防止内存泄漏的问题出现
缺点:回收的过程中需要有一个搜索的过程,造成时间上的开销
能够作为GCRoot的对象:
1.虚拟机栈内引用的对象
2.方法区内引用的对象
3.被synchronize引用的对象
4.虚拟机内部引用的对象,(例如基本类型的Class类对象,异常类对象,系统类加载器)
目前现代JVM使用的都是可达性分析算法
Finaliztion机制
在一个对象被标记为垃圾对象之后,不会立马对对象进行销毁,对象还会有一个机会进行复活或者进行最后的逻辑处理,在销毁前会调用一个回调方法finalize()
finalize()方法继承的自Object类,在Object类中的方法体中是空的,finalize()方法只允许调用一次
finalize()方法可以使对象复活或者完成最后的逻辑处理
当对象再次被标记为垃圾对象的时候就会被直接清除
对象的三种状态
可触及的:被引用链关联着的对象,未被标记为垃圾
可复活的:被标记为垃圾对象,但是重写了finalize()方法还未调用过
不可触及的:调用过finalize方法后再次被标记为垃圾对象的
对象的生死状态
具体流程:
如果对象被标记为一个垃圾对象,则系统开始进行筛选
如果该对象没有重写finalize()方法则JVM将其视为不可触及的,如果对象重写了finalize()方法并且还没有被调用过,则JVM会将其判定为可复活的,并将其加入一个队列中,然后由一个虚拟机自动创建的、低优先级的 Finalizer 线程触发其finalize()方法执行。
如果对象在进行finalize()方法的调用之后会没有被销毁掉,则在第二次被标记之后会直接被判定为不可触及的状态
线程自救的例子
public class CanReliveObj {
static Object obj ;
@Overrideprotected void finalize() throws Throwable {System.out.println("线程自救");obj = this;}
public static void main(String[] args) {
CanReliveObj canReliveObj = new CanReliveObj();canReliveObj = null;//从引用链中断开System.gc();//触发Full GCSystem.out.println("第一次触发Full GC");try {Thread.sleep(2000);//当前线程休眠,使Finalizer线程执行if (obj == null){System.out.println("obj is dead");}else{System.out.println("obj is alive ");}} catch (InterruptedException e) {e.printStackTrace();}
obj = null;//再次从引用链中断开System.gc();//触发Full GCSystem.out.println("第二次触发Full GC");if (obj == null){System.out.println("obj is dead");}else{System.out.println("obj is alive ");}}
}
垃圾回收阶段算法
垃圾回收标记阶段之后就是一个垃圾回收阶段的工作了要将垃圾对象清除出去
在JVM中常见的垃圾回收算法有三种:
标记--复制算法
标记--清除算法
标记--压缩算法
标记--复制算法
标记复制算法会使用两个相同的区域,将正在使用的区域中的对象被标记完成了之后会将存活下来的对象全部复制到另一个未使用的区域中去,原区域的对象就被清除了
优点:清除之后在内存空间是连续,空间使用率高
缺点:需要使用双倍的空间,如果对象较多则效率较低,时空的开销都比较大
应用场景:对象不多,且垃圾回收比较频繁的情况
标记--清除算法
标记清除算法并不是真正的清除掉对象,而是维护一个新的集合,将被标记为垃圾的对象的引用都记录下来,在下次为新对象分配内存区域的时候通过集合内的引用覆盖原来的内存区域
优点:实现简单,是一个基础的垃圾回收算法
缺点:效率相对来说较低,并且清除之后空间不连续,空间碎片较多
标记--压缩算法
标记压缩算法在对象被标记后,会将存活下来的对象移动到内存区域的另外一端并且整理好,就相当于标记清除算法之后再移动一次,但是被标记为垃圾的对象会被清除
优点:内存空间连续,重新分配内存时只需要给JVM一个起始地址即可,减少了系统开销
缺点:效率相对于复制算法是偏低的
小结
三个算法的比较:
算法 | 速率 | 空间开销 | 是否移动 |
---|---|---|---|
标记复制 | 最快 | 两倍(不堆积空间碎片) | 是 |
标记清除 | 中等 | 少(堆积空间碎片) | 否 |
标记压缩 | 最慢 | 少(不堆积空间碎片) | 是 |
System.gc()与STW
System.gc()是系统提供的主动触发Full GC的方式,提供这个方法目的是为了给操作者调用垃圾回收方法
System.gc会有一个免责的声明,调用该方法后不会立即执行该算法
STW(Stop The Word)
在GC回收算法进行的过程中会导致所有用户线程进行暂停,
为什么需要暂停所有的线程?
1.在垃圾的标记阶段与部分的垃圾回收阶段算法都需要获取数据的一致性快照
2.一致性快照是指的是截取某个时间点上的对象信息与其之间的引用关系
3.保证一致性的目的是为了确保在垃圾回收的过程中对象之间的引用不会再变化
4.越优秀的GC回收算法的效率越高,并且暂停的时间越短
垃圾回收器
垃圾回收器主要是是垃圾回收算法的具体实现,在不同JVM中默认使用的垃圾回收算法也不相同
可以对不同的使用场景来选择使用不同的垃圾回收器
垃圾回收器的分类
根据线程的数量:
单线程:(Serial)
只有一个线程进行工作,工作时其用户线程都会暂停,适用于较小的系统进行使用
多线程:(Parallel)
提供多个线程进行工作,同样暂停其他用户线程,适用于较大的系统,在多核CPU的处理下效率更高
根据工作模式:
独占式:暂停其他的所有的用户线程
并发式:与其他的用户线程并发执行
根据区域划分:
年轻代:只进行年轻代的垃圾回收
老年代:只进行老年代的垃圾回收
GC的性能指标
吞吐量:用户代码的执行时间与总时间的占比
垃圾收集时间:垃圾收集花费的时间在总时间中的占比
暂停时间:暂停时间的长短
内存占用:java堆空间所占用的内存空间大小
对象的回收时间:对象标记到回收所经历的时间
CMS
Concurrent Mark Sweep并发标记清除算法
开发的目标就是追求的低停顿
这是一个并发执行的算法,可以与用户线程一起执行
垃圾回收过程
1.初始标记:暂停其他所有线程,只有一条线程标记所有的对象
2.并发标记:与用户线程一起执行,寻找新出现的垃圾对象
3.重写标记:暂停其他用户线程,使用多条线程,将刚才找到新出现的垃圾对象标记出来
4.并发清除:与用户线程一起执行,只用一条线程将刚才标记的对象全部清除掉,过程比较耗时
优点:能够实现并发的垃圾收集
缺点:基于标记清除算法,会造成内存碎片
并发导致吞吐量降低
无法清除浮动垃圾
三色标记算法
由于并发收集器中需要使用到并发标记,会造成并发执行中引用链的变化问题,三色标记算法就是是在并发的垃圾收集器中用到的算法,用于标记阶段将的垃圾对象寻找出来
原理:将对象标记为三种颜色
黑色: 已经被标记过的对象且该对象的下属对象也完成了标记
灰色: 已经被标记过的对象但是其下属对象还未被标记
白色:未被标记的对象,表示不可触及的
工作流程:
算法先对GCRoot的根对象标记为黑色,并且将其下属对象标记为灰色
算法对灰色对象进行遍历,将灰色对象标记为黑色,接着将其下属对象标记为灰色
重复上个步骤,直到所有对象变成黑色与白色,白色即代表未垃圾对象
存在的问题:
漏标
当在遍历灰色对象时,其上属引用断开了,但还是将其标记为了黑色,导致了漏标等到下次GC回收的时候才能清理掉
错标
在遍历灰色对象时,黑色对象重新引用了一个的白色对象,但黑对象不会再被处理,此时被引用的白色对象还是会被作为垃圾对象处理掉
如何解决这两种错误
这两种错误只会在满足这两种情况下时才会出现
1.灰色指向白色的引用被断开
2.黑色指向白色的引用被建立
解决方式1:原始快照
如果在标记的过程中有灰色的对象断开了与白色对象的引用(开始遍历灰色对象时),则将这个灰色对象存储起来,等到标记结束后再对灰色对象重新标记一遍
解决方式2:增量更新
如果在标记的过程中有黑色的对象新引用了白色的对象,则将这个黑色对象存储起来,标记结束后再以这个黑色对象为根重新向下标记一遍
GC回收算法虽然实现了并发收集,但是从来没有被JDK正式当过默认的收集器
G1回收算法
在已经有了这么多的垃圾回收算法的情况下,面对越来越大的业务量以及越来越多的用户,传统的操作STW的垃圾回收器无法满足现代JVM的需求,所以对着垃圾回收器进行了不断的优化,Garbage-First就是目前比价新的垃圾回收算法
java给G1回收算法的目标是尽可能的提高吞吐量,并且保证STW是可控的
G1回收算法是针对所有的分区进行处理的
原理:
G1回收器会将得内存区域分割为比较多的小块区域(Region),根据每个区域的垃圾量进行排序
以垃圾优先的思想优先对垃圾较多的区域进行并发的清理
工作流程:
1.初始标记:暂停其他所有线程,只有一条线程标记所有的对象
2.并发标记:与用户线程一起执行,寻找新出现的垃圾对象
3.重写标记:暂停其他用户线程,使用多条线程,将刚才找到新出现的垃圾对象标记出来
4.筛选回收:为了提高效率暂停了用户线程进行空间的清理,根据不同的Region的回收成本与价值进行排序以尽量少的STW来清除空间
适用场景:多核的内存空间大的系统,且程序占用的内存较大
查看 JVM 垃圾回收器设置垃圾回收器
打印默认垃圾回收器 -XX:+PrintCommandLineFlags -version
JDK 8 默认的垃圾回收器
年轻代使用 Parallel Scavenge GC 老年代使用 Parallel Old GC
打印垃圾回收详细信息
-XX:+PrintGCDetails -version
设置默认垃圾回收器
Serial 回收器 -XX:+UseSerialGC 年轻代使用 Serial GC, 老年代使用 Serial Old GC
ParNew 回收器 -XX:+UseParNewGC 年轻代使用 ParNew GC,不影响老年代。
CMS 回收器 -XX:+UseConcMarkSweepGC 老年代使用 CMS GC。
G1 回收器
-XX:+UseG1GC 手动指定使用 G1 收集器执行内存回收任务。 -XX:G1HeapRegionSize 设置每个 Region 的大小。