Java基础学习——GC(垃圾收集)浅析
好好学习,天天向上的小德子又回来啦,最近在准备面试,时间有点紧张,所以断更了几天。(马爸爸说996是年轻人一种福报,那求给个机会啊,我愿意……哦不,我乐意996,现在就缺一个机会)本来打算玩几天再写博客的,但看了东西不写还是记不住,不复习是真不行,看来不仅人类的本质是复读机,学习的本质也是复读啊……
GC(Garbage Collection)译过来就是垃圾收集,面试中的超高频考点,本文介绍一些关于JVM中GC的重难点。
哪些对象需要回收(换句话说,怎么判段需要回收的对象)
一般有两种方法来解决这个问题:一是引用算术法,二是可达性分析算法。
1. 引用计数法
- 定义是这样的,给对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就加1,;当引用失效时,计数器值就减1;当计数器值为1时,对象就是不可能再被使用的。
- 优点:实现简单,效率很高。(微软的COM技术中就用到了它)
- 缺点:很难解决对象之间相互循环引用的问题。(这也是主流的Java虚拟机都没有使用它的原因)
2. 可达性分析算法
- 这个算法的基本思想就是通过一系列的“GC Roots”的对象作为起点,开始往下搜索,搜索走过的路径称为引用链,若一个对象没有任何引用链连接(用图论的话就是不可达),就会被判为不可用。如下图的Object 5、6、7是不可达的,会被判断为是可回收的对象。
在Java中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(即一般说的Native方法)引用的对象。
最近看到一道与这个相关的面试题,分享在下面:
问:什么情况会造成内存泄漏
答:在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:
首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;
其次,这些对象是无用的,即程序以后不会再使用这些对象。
如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
Java的四种引用
在Java中,引用分为4种:强引用、软引用、弱引用、虚引用,这4中引用强度依次逐渐减弱。
- 强引用:类似下面这样的引用,
Object obj = new Object();
只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:用来描述一些还有用但是却非必须的对象,关于这些对象,系统在发生内存溢出异常之前,将会把这些对象列进回收范围,并进行第二次回收。如果回收完内存还不够,则会抛出内存溢出异常。
- 弱引用:弱引用也是用来描述非必须的对象,但它的强度比软引用更若一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,这些对象都会被回收掉。
- 虚引用是最弱的一种关系,一个对象是否有虚引用,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。
**当系统被判定为不可存活时,仍可以通过finalize()方法进行自救,**但一个对象的finalize()只能被系统调用一次!
垃圾回收算法
1. 标记 - 清除算法
这是最简单、最基础的垃圾收集算法,后面的算法都是在此基础之上优化改进而来的。
- 两个步骤:标记和清除,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
- 缺点:
- 效率问题:标记和清除两个步骤效率都不高;
- 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太对可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集操作。
2. 复制算法
简单来说,就是把空间分成两块,每一次只用其中一块,当只一块内存用完之后,将还存活的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。这样会对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂问题。
- 优点:不存在产生大量空间碎片等问题。
- 缺点:会造成一定空间浪费。
3. 标记 - 整理算法
该算法是先进行一次标记过程,再将存活的对象都向一端移动,然后清理掉端边界以外的内存。
4. 分代收集算法
利用Java堆中分为新生代和老年代的这个思想,根据每个年代的特点采用最适当的收集算法。
- 新生代:复制算法;
- 老念代:标记 - 清理或者标记 - 整理。
垃圾收集器
上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。
1. Serial收集器
最悠久、最基本的垃圾收集器,单线程收集器。
特点:在它进行垃圾收集时,必须暂停其他所有的工作线程(“Stop the world”)。
优点:简单而高效,没有线程交互的开销。
2. ParNew收集器
ParNew收集器是Serial收集器的多线程版本,使用复制算法。
3. Parallel Scavenge收集器
新生代收集器,使用复制算法,也是并发多线程收集器。
Parallel Scavenge收集器的目的是达到一个可控制的吞吐量,所谓吞吐量就是CPU运行用户代码的时间与CPU总消耗时间的比值。
吞
吐
量
=
运
行
用
户
代
码
时
间
/
(
运
行
用
户
代
码
时
间
+
垃
圾
收
集
时
间
)
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
4. Serial Old收集器
Serial的老年代版本,单线程收集器,使用“标记 - 整理”算法。
5. Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和“标记 - 整理”算法,吞吐量优先收集器。
6. CMS收集器
CMS关注的点是尽可能缩短垃圾时用户线程的停顿时间,使用“标记 - 清除”算法,也成为“并发低停顿收集器”。
步骤:
- 初始标记;
- 并发标记;
- 重新标记;
- 并发清除。
缺点:
- 对CPU资源敏感,虽然不会导致用户线程停顿,但因为占用一部分线程资源二使应用程序变慢。
- 无法处理浮动垃圾(浮动垃圾指在标记之后产生的垃圾)。
- 使用标记 - 清除算法,容易产生大量空间碎片。
7. G1收集器
当今最先进的收集器。
特点:
- 并行与并发,充分利用CPU、多核环境下的硬件优势来缩短“Stop the world”的时间;
- 分代收集;
- 空间整合,整体基于“标记 - 整理”算法,局部基于复制算法,不会产生内存空间碎片;
- 可预测的停顿。
步骤:
- 初始标记;
- 并发标记;
- 最终标记;
- 筛选回收。
(本文中大量知识点都参考了《深入理解Java虚拟机》一书)