如何判断对象可以被回收
为了确定哪些对象是垃圾,jvm为我们提供了一些算法去判定。常见的判断是否存活有两种方法:引用计数法和可达性分析。
引用计数法
为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数。当该个数为零,意味着没有人再使用这个对象,可以认为“对象死亡”。每当有一个地方去引用它时候,引用计数器就增加1。但是,这种方案存在严重的问题,就是无法检测“循环引用”:当两个对象互相引用,它俩的计数都不为零,因此永远不会被回收。而实际上对于开发者而言,这两个对象已经完全没有用处了。
缺点:如果两个对象互相引用,计数器都为1,即使他们都没有被使用,都不会被清理。这种方法不使用本java虚拟机中。
可达性分析算法
可达性分析基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。不能到达的则被可回收对象。
- Java虚拟机中的垃圾回收器采用可达性分析来探索所有的对象
- 扫描堆中的对象,判断是否能根据GC Root的引用链找到该对象,找不到则回收。
哪些对象可以作为GC Root?
- 虚拟机栈(帧栈中的本地变量表)中引用的对象。
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI 引用的对象。
MAT是eclipse出品的一个java内存的分析工具 ,可以用来找到可以作为GC Root的对象。
通过下面这个案例进行分析:
/**
* 演示GC Roots
*/
public class Demo2_2 {
public static void main(String[] args) throws InterruptedException, IOException {
List<Object> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
System.out.println(1);
System.in.read(); //停留等待键盘输入数据
list1 = null;
System.out.println(2);
System.in.read(); //停留等待键盘输入数据
System.out.println("end...");
}
}
运行代码,此时代码停留在第一步,使用 jps 命令查看当前进程ID:
使用 jmap -dump:format=b,live,file=文件地址.bin 线程ID 命令将当前内存情况转储成一个文件:
然后在运行窗口随便输入一个什么,让程序向下运行,然后再转储现在的内存情况。
接着使用MAT工具打开生成的两个文件:
在list=null后,GC Root中list对象被回收。
Java中的五种引用
无论使用何种方法判断对象是否存活,都和引用息息相关,引用有以下几种:
强引用 软引用 弱引用 虚引用 终结器引用
强引用
一般平常代码中大部分引用都是强引用,指向某一对象的所有强引用都断开,该对象才能被回收。
软、弱引用
软引用和弱引用都用来描述一些还有用,但是非必须的对象。弱引用比软引用更弱。
指向某一被软、弱引用的对象的所有强引用都断开,该对象可能被回收。
- 软引用:如果垃圾回收之后,内存依然不足,只被软引用的对象会被回收。
- 弱引用:只要发生垃圾回收,只被弱引用的对象就会被回收。也就是只能生存到下次垃圾回收之前。
- 对象回收后,软弱引用本身转移到引用队列中。
- 遍历引用队列,释放引用。
虚引用
- 虚引用的ByteBuffer,没有被强引用,被回收掉,分配的直接内存尚未回收
- 虚引用进入引用队列中,RefferenceHandler在队列中寻找到虚引用Cleaner
- 调用Unsafe.freeMemory()方法释放直接内存;
- 释放引用。
终结器引用
- 终结器对象引用的对象没有被强引用,在被回收前,终结器引用转移到引用队列,一个优先级较低的线程finallize在引用队列中寻找终结器引用;
- 并找到终结器引用的对象,调用finalize()方法;
- 下次垃圾回收时,回收该对象。
回收算法
标记清除算法
“标记-清除”算法是最基础的收集算法。算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(标记过程参见1.2可达性分析)。后续的收集算法都是基于这种思路并对其不足加以改进而已。
“标记-清除”算法的不足主要有两个:
- 效率问题:标记和清除这两个过程的效率都不高
- 空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
复制算法
复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。这样每次只需要对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可。
- 不会有内存碎片
- 需要占用双倍内存空间
标记整理算法
标记整理算法在标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。
- 速度慢
- 不会有内存碎片
流程图如下:
分代收集算法
不同对象的生命周期是不一样的,不同周期对象课采用不同垃圾回收算法,以提高效率。这种算法没什么特别的,无非是上面内容的结合罢了,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。
虚拟机中的共划分为三个代:新生代(Young Generation)、老年点(Old Generation)和持久代
(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系
不大。年轻代和老年代的划分是对垃圾收集影响比较大的。
新生代
HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden(伊甸园)区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
GC刚开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着,From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
老年代
当老年代空间不足,尝试Minor GC,空间仍不足,触发Full GC,触发 STW,时间相比于Minor GC会长的多,因为新生代和老年代回收算法不同,且老年代中对象多,回收较为复杂,采用标记-清理算法或者标记-整理算法。
Minor GC 和 Full GC的区别
新生代GC(Minor GC):Minor GC指发生在新生代的GC,因为新生代的Java对象大多都是朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快。当Eden空间不足以为对象分配内存时,会触发Minor GC。
老年代GC(Full GC/Major GC):Full GC指发生在老年代的GC,出现了Full GC一般会伴随着至少一次的Minor GC(老年代的对象大部分是Minor GC过程中从新生代进入老年代),比如:分配担保失败。Full GC的速度一般会比Minor GC慢10倍以上。当老年代内存不足或者显式调用System.gc()方法时,会触发Full GC。