概述
Java和C/C++,虽然同为老牌且热门的语言,但它们最大的差别之一就是GC了。C/C++需要自己管理内存,而Java有垃圾自动收集回收机制,简直不要太爽。(然而,如果你搞一些骚操作,还是有可能会导致内存泄露)。
Java垃圾回收机制是在JVM实现的,那么JVM是怎么进行GC的呢。
JVM进行垃圾回收可以概括成两个阶段。
1. 判断对象是否可回收
要进行GC,就要判断哪些对象是不会引用的,从而将它们进行释放处理。
2. 回收算法
对象是否可回收已经确定,要考虑使用什么算法来高效的进行回收。
当然,JVM为了快速的GC,还将内存分为多个区域:Eden区(俗称新生代),Old区(俗称老年代)
在回收时,也根据新生代和老年代采用了不同的方法
标记算法
要进行回收,就要判断对象是否可回收。
1. 引用计数法
原理是为每一个对象做计数处理,当该对象被引用时,计数+1。如果某个对象的引用计数为0,则说明该对象可以被回收。
听起来蛮不错的,但是引用计数法有一个致命的缺点:无法解决相互引用的问题
什么是互相引用呢,这里用一段代码来解释解释。
public class A{
B b;
}
public class B{
A a;
}
public void test(){
A a = new A();
B b = new B();
a.b = b;
b.a = a;
}
上述代码中,对象a,b互相引用对方,它们的计数肯定不为0,事实上a,b都是可回收对象,如此一来a,b将永远不会被回收,导致内存泄露。
2. 可达性分析算法
根据一些根对象(GC Roots)
来遍历查找对象间引用关系,当遍历完成后对象还未被引用,则认为是可回收的对象。
JVM可作为 GC Roots
对象如下:
- 虚拟机栈中的引用的对象
- 方法区中类的静态对象
- 方法区中常量引用对象
- 本地方法栈引用的对象
回收算法
已确定了对象是否可回收,接下来进行回收操作。JVM回收算法有以下几种
- 标记-清除
- 复制算法
- 标记-整理
1. 标记-清除算法
在一块大内存中,对对象做标记,然后回收不再使用的对象区域。但是这样做会导致内存碎片化,内存被分为七零八碎时,大对象的分配可能会因为内存不足,触发FullGC。
已对对象进行标记
清除之后
2. 复制算法
先标记对象,在回收的时候清除掉可回收对象,然后整理还存活的对象。
这样虽然解决了碎片问题,但是这样做效率很低。
标记处理
回收并复制对象
3. 标记-整理法
标记整理法在一开始就对内存分为两块,进行内存分配时,分配在其中一块,当进行垃圾回收时,将还存活的对象复制到另一块,先前一块清空。(这个就是JVM年轻代使用的算法)
标记对象
整理并清除不再使用的对象
4. JVM中使用的垃圾回收算法
在JVM中,将堆内存分为 年轻代
和老年代
(如果是使用G1垃圾回收器的话,就没有分代这个概念)。不同的内存有不同的特性和回收算法。
年轻代
新对象都会在这个区域分配(大对象就不是了),这里存放的一般都是生命周期比较短的对象,也是垃圾回收的主要区域。
年轻代主要使用的是标记-整理法
,因为对象分配的多,回收的也快,所以要采用高效的收集算法。
当年轻代的对象进行了多次后还存活(默认为15次),该对象将会被移到老年代。
老年代
老年代存储的都是能长久存活的对象,所以一般不会对老年代进行回收处理。
老年代因为不会被频繁回收,一般采用的是标记-清理算法
,当然标记-整理
也有可能被使用
JVM中的垃圾收集器
HotSpot虚拟机根据年轻代
和老年代
分别使用不同的垃圾回收器,具体如下图所示:
上图的连线代表新生代和老年代可以搭配用的组合
1. 年轻代收集器:Serial收集器
单线程回收器,在低版本的JDK中用到。它在进行垃圾回收时要停止其他所有线程,直到垃圾收集结束。这种收集方法就是大名鼎鼎的Stop The World
。在低版本的Go语言回收垃圾也是会将其他工作线程停掉,然后回收。
这种垃圾回收器非常慢,对系统而言并不友好,现在基本不会用到。
2. 年轻代收集器:ParNew收集器
其实就是 Serial
的多线程版本,用多个线程进行垃圾回收,在回收的时候也会暂停其他工作线程,它的收集算法、对象分配规则、回收策略都和Serial
一样
3. 年轻代收集器:Parallel Scavenge收集器
Parallel Scavenge
也是使用复制算法的收集器,也是多线程收集器,这个与ParNew
类似,但它的关注点不一样。
Parallel Scavenge
关注的是吞吐量,目标是达到一个可控制的吞吐量。吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾回收时间)
吞吐量越大,说明能够更快的响应用户的请求,达到良好的交互效果。
4. 老年代收集器:Serial Old收集器
Serial Old
是Serial
老年代版,使用单线程处理器,在进行垃圾回收时也会暂停其他工作线程,直到回收结束。
5. 老年代收集器:CMS收集器
CMS是以最短回收停顿为目标的垃圾收集器,也是目前使用最广的老年代垃圾收集器。(在JDK8中Hotspot默认的老年代收集器,JDK11就改为G1了)。
CMS 采用的是 标记-清理
算法,一般清理一遍过后,内存会有些许碎片,如果碎片过多或者老年代空间不够用,则会进行内存整理。
CMS收集的过程可以归纳为5个:
- 初始标记
暂停其他线程
,对所有对象进行第一次标记
- 并行标记
- 与业务线程一起
并发执行
,对对象进行第二次标记
- 与业务线程一起
- 重新标记
暂停其他线程
,修正在并发标记时标记错误的对象
- 并发清除
- 与业务线程一起
并发执行
,采用标记-清理
算法
- 与业务线程一起
- 重置状态,等待下一次执行
可以看到,CMS在初始标记和重新标记时,才会暂停业务线程,第二次标记和清除时都是并发执行的,这样整个垃圾回收的停顿时间就被大大减少
CMS虽然是一款优秀的垃圾回收器,但还有一些缺点
- 会占用一些的CPU,毕竟要并发标记和并发清除。默认的线程数为
(CPU数量+3)/4
- 不会清除浮动的垃圾。因为CMS在回收的时候是并发执行的,此时业务线程新产生的垃圾是无法回收的。
- 会导致内存碎片化
6. 新时代的收集器:G1收集器
G1是一款当今最前沿的垃圾收集器,目的是低延迟、高吞吐量、可以预测停顿时间。
G1在空间分配上与其他的收集器不一样,并且能够利用多个CPU来缩短线程停顿时间。
总结
垃圾回收一般会有两个步骤
- 标记对象是否可回收
- 清除
标记和清除都有不同的算法,各有优劣。
JVM中有多种垃圾回收器, 各有优劣,可以满足不同的业务需求