Java的技术体系包括
- 支持Java程序运行的虚拟机(JVM)
- 提供接口支持的Java API
- Java 编程语言
- 第三方Java框架(如Spring等)
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。
我们知道,Java虚拟机运行时的内存分为:程序计数器,虚拟机栈,本地方法栈,堆,方法区几个部分。其中程序计数器,虚拟机栈,本地方法栈3个区域与线程相关,生命周期与线程同步。栈中的栈帧,随着方法的进入和退出而进行着入栈出栈的工作,每个方法需要分配多少内存在编译期间也已经确定下来,所以当方法结束时,内存也跟随着回收。因此这几个区域不需要考虑内存回收的问题,他们会随着线程结束或者方法结束而有条不紊的进行内存的释放。
而堆和方法区则不同,由于只有在程序运行期间才能知道会创建哪些对象,同时每个对象的大小,生命周期等也不尽相同,所以导致这部分内存的分配和回收都是动态的,因此垃圾收集所关注的重点区域就是这部分内存。我们所说的垃圾回收算法和内存分配策略绝大多数都是围绕堆内存展开。
如何判断一个对象需要被垃圾回收
可达性分析算法
Java主流实现中,都是通过可达性分析算法来判断对象是否需要被回收。算法的基本思路是,通过一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots之间没有任何引用链的时候,则证明此对象可以被回收。
其中GC Roots包括如下几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的变量
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
引用类型
Java中有值类型和引用类型两种。其中引用类型被定义为:如果reference类型的数据中存储的数值代表另一块内存的起始地址,就称这块内存代表着一个引用。在JDK1.2以后,Java对引用的概念进行了扩充,分为强引用,软引用,弱引用,虚引用。
- 强引用,new 出来的引用,默认的引用方式。如果有强引用存在,对象不会被回收
- 软引用,SoftReference,在虚拟机内存不足的时候,会回收掉这些对象
- 弱引用,WeakReference,被弱引用关联的对象,只能存活到下一次垃圾回收之前。换句话说,如果一个对象只有弱引用,那么当垃圾回收扫描到这个对象时,它将被回收
- 虚引用,PhantomReference
方法区的内存回收
在虚拟机规范中指出,方法区可以不要求实现垃圾回收。而且方法区的回收效率一般都比较低。在堆中尤其是新生代中,常规的一次垃圾回收,一般可以回收掉70%~95%的空间。但实际上很多虚拟机都实现了在方法区的内存回收,因为在需要大量使用反射,动态代理,CGLib等ByteCode框架,等频繁自定义ClassLoader的场景,是需要具备类卸载的功能,以保证方法区不会溢出。
方法区的内存回收主要分为两部分,废弃常量和无用的类。
- 回收废弃常量与回收Java堆中的对象类似,即没有任何对象引用常量池中的常量时,被回收。该常量可以包括字符串常量,类引用等。
- 无用的类,判定条件需同时满足以下3个条件
- 该类的所有实例都已经被回收。也就是堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
虚拟机可以对满足以上3点的类进行回收,但是否真的回收,可以通过虚拟机提供的参数进行控制。
垃圾回收算法
Java虚拟机中主流的垃圾回收算法分为以下几种
-
标记清除算法。算法分为标记、清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。这中算法是所有算法的基础,后续算法均是对其不足进行改造而得到的。它的不足主要体现在两方面
- 效率问题,标记和清除的效率都不高
- 空间问题,标记清除之后会产生大量的不连续内存碎片,这就导致在需要分配较大内存空间的时候,如果此时没有连续的足够大的内存空间,则需要提前进行下一次垃圾回收行为,这会造成资源的浪费和效率的降低。
-
复制算法。为了解决效率问题而产生。它将可用内存分为两部分,每次只使用其中的一块,当这一块内存用完之后,就将还存活的对象复制到另一块上面,然后再把刚才已使用的一块内存空间一次性清理掉即可。高效体现在两个方面
- 每次只对其中一块内存进行回收
- 内存分配时不需要考虑碎片问题,只需要移动堆顶指针按顺序分配即可。极大的提升了效率。
这种算法被用来实现在虚拟机的新生代中。并且分为三部分,Eden占80%,两块Survivor各占10%。每次使用Eden和其中的一块Survivor,当回收时,将Eden和Survivor中还存活的对象一次性复制到另一款Survivor中。最后清理掉Eden和刚才用过的Survivor空间。
-
标记整理算法,主要用于老年代的算法。它首先标记处所有需要回收的对象,然后让所有
存活的对象都向一端移动,再直接清理掉端边界以外的内存。
-
分代收集算法,根据对象的存活周期不同,将内存划分为几块。一般是将堆分为新生代和老年代。这样可以根据各个年代的特点采用最适当的收集算法。
- 在新生代中,每次垃圾回收都会有大量对象死去,少量存活,因此可以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
- 在老年代中,因为对象存活率高,没有额外空间进行分配担保,可以采用标记整理算法进行回收
在虚拟机通过可达性分析算法寻找需要回收的对象的时候,首先就是需要找到GC Roots,那么如何高效实现对GC Roots的查找(即如何发起内存回收),是虚拟机在具体实现过程中需要首先解决的问题,以HosSpot虚拟机为例,通过OopMap-安全点-安全区域的一套机制来快速准确的完成对GC Roots的枚举。
网友评论