本文参考于《深入理解Java虚拟机》
1. 对象已死?
垃圾收集器在对堆进行回收
前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)
了。
1.1 常见的判断方法
JVM判断对象是否已死的方式:引用计数法、可达性分析法。
(1)、引入计数法
1. 引入计数法思想
在对象中添加一个引用计数器
,每当有一个地方引用它
时,计数器值就加一
;当引用失效
时,计数器值就减一
;任何时刻计数器为零的对象就是不可能再被使用的。
2. 存在的问题
在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题
。
图示解析
A对象和B对象进行了
相互循环引用
。而
导致垃圾收集器无法对它们进行回收。
(2)、可达性分析法
1. 可达性分析法思想
这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集
,从这些节点开始
,根据引用关系向下搜索
,搜索过程所走过的路径称为 “引用链”(Reference Chain),如果某个对象到GCRoots间没有任何引用链相连
,或者用图论的话来说就是从GC Roots到这个对象不可达
时,则证明此对象是不可能再被使用的。
2. 可达性分析法图示说明
对象object 5、object 6、object 7虽然互有关联
,但是
它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。
3. 固定可作为GC Roots的对象
- 在
虚拟机栈(栈帧中的本地变量表)中引用的对象
,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。 - ·在方法区中类
静态属性引用的对象
,譬如Java类的引用类型静态变量
。 - ·在方法区中
常量引用的对象
,譬如字符串常量池(String Table)里的引用。 - ·在本地方法栈中
JNI(即通常所说的Native方法)引用的对象
。 - ·Java虚拟机内部的引用,如
基本数据类型对应的Class对象
,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
。 - ·所有
被同步锁(synchronized关键字)持有的对象
。
1.2 再谈引用
在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用
4种,这4种引用强度依次逐渐减弱。
(1)、强引用
强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系
。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。当内存空间不足的时候,JVM宁愿抛出OOM错误,也不会对强引用对象进行回收来解决内存不足的问题
。
(2)、软引用
1. 软引用所引用的对象的说明
软引用是用来描述一些还有用,但非必须的对象
。只被软引用关联着的对象,在`系统将要发生内
存溢出异常前,会把这些对象列进回收范围之中进行第二次回收(其实就是当**内存空间还是足够**的时候,
JVM虚拟机不会对软引用对象进行回收;当**内存空间不足**的时候,
JVM虚拟机就会对软引用对象进行回收`)。
2. 软引用本身的说明
如果软引用所引用的对象被垃圾回收,软引用本身是不会被清理掉的,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中
。
3. 特别注意
在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生
。
(3)、弱引用
1. 弱引用所引用的对象的说明
弱引用也是用来描述那些非必须对象
,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作
,无论当前内存是否足够
,都会回收掉只被弱引用关联的对象
。
2. 弱引用本身的说明
弱引用可以和一个引用队列联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中
。
(4)、虚引用
1. 虚引用所引用的对象的说明
虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系
。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。如果一个对象被虚引用所引用
,那么它就和没有任何引用一样
,在任何时候都可能被垃圾收集器回收
。
2. 虚引用本身的说明
为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知
。
3. 虚引用和软引用、弱引用的区别
就是虚引用必须和其相关的引用队列所关联
。当一个虚引用所引用的对象要被垃圾收集器回收时,JVM虚拟机会将其虚引用加入其所关联的引用队列。所以,我们通过判断引用队列中是否加入了虚引用来了解相关的虚引用对象是否将要被垃圾收集器所回收
。那么,我们就可以在该虚引用对象所被回收之前做出相应的操作。
1.3 不可达对象并非“非死不可”
1. 非死不可的情况
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡
,至少要经历两次标记过程
:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链
,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法
,或者finalize()方法已经被虚拟机调用过
,那么虚拟机将这两种情况都视为“没有必要执行”,那么该不可达对象就是非死不可
了。
2. 可以挽救的情形
被判定为需要执行finalize()方法的对象将会被放在一个队列中进行第二次标记,如果此时这个对象被引用链上的任何一个对象所关联的话
,那么该对象就逃过一劫,否则会真的被回收。
1.4 回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量
和不再使用的类型
。
(1)、如何判断一个常量时废弃常量?
1. 判断依据
假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”
,换句话说,已经没有任何字符串对象引用常量池中的“java”常量
,且虚拟机中也没有其他地方引用这个字面量
。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。
(2)、如何判断一个类是无用的类?
1. 判断依据
该类所有的实例都已经被回收
,也就是Java堆中不存在该类及其任何派生子类的实例。加载该类的类加载器已经被回收
,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
。
2. 特别说明
Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收
。
2. 垃圾收集算法
2.1 标记 -- 清除
1. 算法思想
算法分为 “标记”和“清除” 两个阶段:首先标记出所有需要回收的对象
,在标记完成后,统一回收掉所有被标记的对象
,也可以反过来,标记存活的对象
,统一回收所有未被标记的对象
。标记过程就是对象是否属于垃圾的判定过程,这在前面讲述垃圾判断算法时其实已经介绍过了。
2. 主要缺点
- 第一个是执行效率不稳定,如果Java堆中包含大量对象,而且
其中大部分是需要被回收的
,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低
。 - 第二个是内存空间的碎片化问题,
标记、清除之后会产生大量不连续的内存碎片
,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.2 标记 -- 复制
1. 算法思想
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,它将可用内存按容量划分为大小相等的两块
,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面
,然后再把已使用过的内存空间一次清理掉
。适用于对象存活较少的情形。
2. 详解标记 -- 复制过程
- 对
FROM区域中的存活对象
进行标记
- 将FROM区域的存活对象复制到TO区域
清除FROM区域中的剩余的需要回收对象
交换FROM和TO区域的位置
3. 优点
- 对于
多数对象都是可回收
的情况,算法需要复制的就是占少数的存活对象
。 - 而且每次都是针对整个半区进行内存回收,
分配内存时也就不用考虑有空间碎片的复杂情况
,只要移动堆顶指针,按顺序分配即可。
4. 缺点
如果内存中多数对象都是存活的
,这种算法将会产生大量的内存间复制的开销。- 这种复制回收算法的代价是将可用内存缩小为了原来的一半,
空间浪费未免太多了一点
。
2.3 标记 -- 整理
1. 算法思想
针对老年代对象的存亡特征提出了另外一种有针对性的 “标记-整理”(Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样
,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
。
2. 优点
可以有效的避免内存碎片的产生
。
3. 缺点
对象的整理移动需要消耗一定的时间,所以效率较低
。
2.4 分代收集算法
1. 算法思想
这种算法只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代
。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,所以新生代一般采用标记 -- 复制算法进行垃圾收集。而老年代的对象存活时间较长
,而且没有额外的空间对它进行分配担保
,所以一般使用标记 -- 清除算法和标记 -- 整理算法。
2. 详解分代收集过程
补充概念:
Stop The World:暂停其他用户的线程,等待垃圾回收结束后,用户线程才可以恢复执行。
- 当我们创建一个新对象时,
新对象默认分配在Eden区域
。
- 当我们
新创建的对象在Eden区域无法分配到足够的内存空间
时,这时会触发一次垃圾回收(Minor GC),同时还会触发Stop The World。此时,JVM会将Eden区域和FROM区域的存活对象复制到TO区域
,然后将它们的寿命加一
。
- 然后
将Eden区域和FROM区域中不需要存活的对象进行清除
。
- 再
交换FROM和TO
- 然后Eden区域的内存空闲出来很多后,现在可以继续将Eden区域的内存分配给相应的新对象。如果内存不够分配,将
再次触发第二次Minor GC
,流程相同,相应的存活的对象寿命需要加一
。当幸存区中的对象寿命超过了默认的15
(说明该对象频繁被使用,且价值比较高)时,该对象就会被放入老年代。
- 如果
新生代和老年代的内存都满了
,那么就会触发一次从新生代到老年代的全面的清理(Full GC)。先会触发一次Minor GC,然后触发一次Full GC
。对新生代和老年代中不需要存活的对象进行回收。
3. 小结
新创建的对象首先会被分配在Eden区域
。- 新生代空间不足时,触发Minor GC,
Eden和 FROM幸存区需要存活的对象会被复制到TO幸存区
中,存活的对象寿命+1,并且交换FROM和TO。 - Minor GC会引发 Stop The World:
暂停其他用户的线程
,等待垃圾回收结束后,用户线程才可以恢复执行。 - 当对象寿命超过阈值(说明该对象频繁被使用且价值较高)时,会晋升至老年代,默认的最大寿命是
15
。 - 如果
新生代、老年代中的内存都满了
,就会先触发Minor GC,再触发Full GC,对新生代和老年代中不需要存活的对象进行回收
。