Java内存回收简介

前言

大家都知道,C++的内存管理是由开发人员自己掌握的,而Java则是自动完成的,那么它是怎么实现的呢?在前人们设计垃圾收集技术时,解决了下面三件事情:

1)那些内存需要回收?

2)什么时候回收?

3)如何回收?

哪些内存需要回收和什么时候回收

要确认那些对象可以回收,就需要确认该对象实例是否已经不再被使用了,那么常见的实现由哪些呢?

引用计数法

此方法的实现为:给每一个对象实例添加一个引用计数器,每当有一个地方引用它了,该计数器就加一。当对象引用计数器为零的时候,说明没有再引用它的地方了,所以该对象实例“死了”,可以被回收了。那么是不是这种算法就可以了呢?但是实际情况中,主流的一些Java虚拟机并没有采用此算法,原因是因为,当遇到循环引用的情况时,就不好处理了。

Java采用的算法:可达性分析算法

该算法通过一些列可以作为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,说明该对象已死(不过不一定就被回收,可以自救,但是实际情况中不会这么做)。在Java中,可以作为GC Roots对象的对象包括:

1)栈帧中的局部变量引用的对象

2)方法区静态变量引用的对象

3)方法区中常量引用的对象

4)本地方法栈中Native方法里引用的对象

可达性分析

延伸

上面讲的是堆中的垃圾回收,在Java中,除了堆中会有垃圾回收,还有其他内存区域有内存回收吗?是有的,那就是方法区,方法区中存放的是和类相关的一些信息,那么如果确定那些类可以被回收呢:

1)该类的所有实例都被回收,在堆中已经不存在该类的实例

2)加载该类的ClassLoader已经被回收

3)该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

如何回收

如何回收已经成为垃圾对象实例,也有很多不同的算法,每种算法有它的优势和劣势。

标记-清除算法

这种算法是最容易理解的,它就是通过上面可达性分析算法后,标记出已死的对象实例,然后将这些区域的内存进行回收。不过它有两个明显的不足:

1)标记和清除两个步骤的效率很低

2)由于被回收的区域可能是不连续的,所以当需要连续内存时,必须提前触发一次垃圾收集动作

标记-清除

复制算法

复制的具体实现原理就是将内存分为两块,每次只使用其中一块,当这一块快使用完了的时候,就将这块中还存活的对象复制到另一块中,然后清理掉前一块中的内存空间。这种算法用于商业虚拟机回收新生代,因为新生代的对象98%都是“朝生夕死”,就将这块区域分为一块大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当回收时,将较少存活对象复制到另一块Survivor中,清理掉之前使用的Eden和其中一块Survivor。

复制

标记-整理算法

标记-整理算法和标记-清除算法的前面步骤是一样的,只是在标记后,并不马上清理,而是让所有存货的对象向一端移动,然后清理掉另一端的边界以外的对象。这种算法和上面的标记-清除算法用于虚拟机中老年代的回收算法。

标记-整理

分代收集法

分代收集

分代收集法就是根据对象的存活周期将内存划分为两块:新生代和老年代,然后不同的区域采用不同的垃圾回收算法,如上面介绍到的:新生代采用复制算法,老年代采用标记-清除或者标记-整理算法。在实际情况中,当新生代中另一块Survivor区域无法存放下存活下来的对象时,这些对象将进入老年代,那么有哪些情况,新生代的对象会进入老年代呢?

1)大对象

2)每次Eden进行MinorGC后对象年龄加1进入survivor,对象年龄达到15时进入老年代

3)如果Survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于等于该年龄的对象就直接进入老年代

4)如果survivor空间不能容纳Eden中存活的对象。由于担保机制会进入老年代。如果survivor中的对象存活很多,担保失败,那么会进行一次Full GC

坚持原创分享,您的支持将鼓励我不断前行!