一、GC的作用
进行内存管理
C语言中的内存,申请内存之后需要手动释放;一旦忘记释放,就会发生内存泄漏!
而Java语言中,申请内存后会由GC来释放内存空间,无需手动释放
GC虽然代替了手动释放的操作,但是它也有局限性:
- 需要消耗更多的资源;
- 没有手动释放那么及时;
- STW(Stop The World)会影响程序的执行效率
二、GC主要回收哪些内存
(1)堆:主要回收堆中的内存
(2)方法区:需要回收
(3)栈(包括本地方法栈和JVM虚拟机栈):不需要回收,栈上的内存什么时候释放是明确的(线程结束,栈上的内存也就被释放了;对应的某个栈帧销毁[某个方法执行完毕],也会导致对应的局部变量被释放)
(4)程序计数器:不需要被回收
GC回收内存的基本单位:对象
GC回收对象的基本思路
(1)标记:判断当前对象的生死,对象不再被使用为死,则需要回收,反之不需要被回收;
标记的方法:
- 引用计数法
记录当前这个对象是否有引用指向,有则引用计数加1,如果当前这个对象的引用指向了其他新的对象,则引用计数减1,当引用计数为0的时候,我们认为这个对象需要被回收!
缺点:无法解决循环引用问题
下面用一段伪代码来演示一下循环引用问题:
class Test{
Test t = null;
}
Test a = new Test();
Test b = new Test();
a.t = b;
b.t = a;
a = null;
b = null;
我们发现,在上述代码中已经没有办法使用对象a和对象b了,但是它们的引用计数不为1.想使用对象a,就得找到对象a的引用,但是对象a的引用又在对象b当中。想使用对象b,就得找到对象b 的引用,但是对象b的引用又在对象a当中。
- 可达性分析:
代码中的对象具有一定的关联关系,这样错综复杂的关系,构成了一个"有向图"。可达性分析也就是遍历这个对象关系的“有向图”。如果某个对象可以被遍历到,那么它就是可达的(非垃圾),那么就是不可达的(是垃圾)
那么可达性分析从哪里开始呢?
a)针对每个线程的每个栈帧的局部变量表(线程有很多,每个线程栈帧也有很多,每个栈帧也会有很多个变量);
b)常量池中引用的对象;
c)方法区中静态变量引用的对象;
因为遍历的起点不止一个,而是很多个起点,因此把这些起点也称之为GCRoot
- 回收方法区对象的规则:
a)该类的所有实例已经被回收;
b)加载类的ClassLoader也已经被回收了;
c)该类对象没有在代码中使用了
同时具备以上三个条件,就认为该类对象是可以被回收的
回收的方法:
- 标记-清除【适合老年代】
通过上面的图,我们可以发现,两个空闲区被其他的对象分隔开了。一旦需要一个比较大的空间,就会申请失败。
标记-清除法的优缺点:
优点:简单高效
缺点:会出现内存碎片
- 标记-复制【适合新生代】
优点:解决了内存碎片问题,保证回收之后不会存在碎片(回收后使用的对象之间是连续的,空余内存之间也是连续的)
缺点:需要一块额外的空间,如果生存的对象较多就比较难低效
- 标记-整理【适合老年代】
优点:没有内存碎片问题,也不需要额外的空间
缺点:类似于顺序表的删除操作,效率不是很高
三、分代回收
按照对象的年龄,将堆内存分为:新生代(伊甸区和生存区)、老年代
对象的年龄不是直接使用时间来记录,而是使用对象活过GC轮次来记录(GC是按照一定周期来运行)
一个对象的一生:
(1)对象诞生于新生代的伊甸区。新产生的对象的内存就是新生代中的内存
(2)第一轮GC扫描伊甸区之后,就会把大量的对象回收掉。少数没有被回收的对象,就会通过标记-复制算法进入到生存区
(3)少数进入生存区的对象,再次被GC扫描(对这些对象进行可达性分析)。如果发现该对象已经不可达,也就被销毁了。没有被销毁的对象,再次通过标记-复制算法,把它拷贝到另一个生存区。
(4)对象在两个生存区中经过若干次拷贝,如果还没有被回收,那么就说明这些个对象存活时间比较久,就拷贝到老年代
(5)老年代的对象也是要经过GC扫描的。由于老年代的对象生存时间比较长。因此扫描周期要比新生代的周期要长
相关术语:
- Partical GC:只进行一部分内存区域的GC
- Full GC:针对整个内存区域进行GC
- Minor GC:针对新生代内存的GC,执行频繁,速度较快
- Major GC:针对老年代的GC,没那么频繁。速度较慢,通常由Minor GC 触发
四、垃圾回收器
垃圾回收器做的两件事情:标记(可达性分析)+回收(标记清除,标记复制,标记整理)
- Serial收集器(给新生代使用,串行回收)【存在STW】
采用复制算法,单线程进行标记和回收
- ParNew收集器(新生代收集器,多线程GC)
采用复制算法,多线程进行标记和回收
- Parallel scavenge收集器(新生代收集器,并行GC)
设计初衷是为了缩短STW时间,以牺牲吞吐量和新生代空间作为代价。
相当于承诺用户,在一定时间内就会完成一次GC。
- Serial Old收集器(老年代收集器,串行GC)
- Parallel old收集器(老年代收集器,并行GC)
使用多线程完成标记整理,效率更高,消耗的CPU资源更多
- CMS垃圾回收器(老年代收集器,并行GC,采用多线程标记清除算法)
a)初始标记【STW】
只是把和GCRoot相关的对象标记出来,涉及STW
b)并发标记
执行整个标记遍历的过程(从GCRoot开始,把能访问的对象都遍历)
不需要暂停用户线程
消耗的时间相对比较久,但是可以和用户线程并发
注意:当进行并发标记的时候,当用户线程也在执行,可能导致某个对象,刚刚标记的时候不是垃圾,代码执行后,就成了垃圾
c)重新标记(CMS remark)【STW】
修正误差
d)并发清除
多线程的方式将刚刚的垃圾对象都清除释放掉,可以和应用程序并发执行
优点:能够让STW时间尽量短
缺点:有内存碎片; GC操作和应用程序并发进行,消耗CPU资源多;
- G1回收器(Java11开始默认使用)
既可以回收新生代,也可以回收老年代
每个矩形称为一个region
E表示伊甸区
S表示生存区
T表示老年代
H表示存放大对象的区域
以region为单位进行回收,回收粒度更精细
针对新生区的region同样适用复制算法
针对老年代的回收类似于CMS
a)初始标记【STW】:只去找和GRoot直接相连的对象
b)并发标记:和应用程序并发执行,进行可达性分析,遍历所有对象。如果发现某个老年代region中已经没有存活对象,就直接回收
c)最终标记:修正第二步产生的误差
d)筛选回收:挑选出对象存活率低的region进行回收
五、总结
到此这篇关于Java基础之垃圾回收机制详解的文章就介绍到这了,更多相关Java垃圾回收机制内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!