JVM的垃圾回收算法、垃圾收集器和内存分配策略
怎样判断对象是否可回收
1、引用计数法
给对象添加一个引用计数器,每当一个地方引用,计数器加1;每当引用失效时,计数器的值就减1;当计数器为0时的对象就不可能被使用的。
- 当两个对象相互引用时,会导致无法回收这两个对象。
2、可达性分析法
同构根节点(GC Roots)为起点,向下查看引用链,如果对象不在任何一个根节点的引用链上,那么这个是不可达对象,可被回收。
如图:
- Java中可作为GC Roots的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中的JNI(即一般说的Native方法)引用对象。
引用的4种类型
在JDK1.2之后,对引用进行了扩展。将引用分为了强引用、软引用、弱引用、虚引用4种。4种引用强度依次逐渐减弱。
- 强引用常指直接同new 出来的对象,只要强引用还存在,垃圾收集器就不会回收掉被引用的对象。
- 软引用用来描述一些还有用,但是非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些类列进回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。SoftReference类来实现软引用
- 弱引用用来描述非必需的对象,强度比软引用更弱一些。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。WeakRefernce类来实现弱引用。
- 虚引用也称为幽灵引用或者幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置为虚引用关联的唯一目的就是能在这个对象被垃圾回收器回收时收到一个系统通知。PhantomRerence类引用。
Java对象如何被回收
在可达性分析算法中,不可达并不代表非死不可。对象会经历两次标记过程:在对象进行可达性分析时,如果对象不在GC Roots的引用链中,会进行第一次标记,并进行筛选,看此对象的finalize()方法是否有必要执行,当finalize()方法没有覆盖或者已经执行过一次,就不会执行(在执行此方法时如果重新再GC Roots的引用链上)。当对象没有必要执行时,就会将对象放到F-Queue队列中,虚拟机会建一个低优先级的线程区执行,这时队列会进行二次标记,如果重新有了引用,将进行标记移出队列。剩下等着回收吧。
回收方法区
方法区存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。
方法区的回收主要是废弃常量和无用类。
- 常量的回收比如字符串常量池中的对象没有引用将会回收
- 类的回收要满足一下条件:
- 该类的所有实例都已被回收。
- 该类的类加载器已被回收。
- 该类的Class对象没有在任何地方被应用,无法在任何地方通过反射访问该类的方法。
垃圾回收算法
-
标记-清除:
缺点标记和清除两个过程效率都不高,同时会产生空间碎片。 -
复制算法:
缺点是内存消耗大,需要有个备用内存区 -
标记-整理
在标记-清除上多了整理功能。 -
分代收集:
将内存分区,分成新生代,和老年代。新生代的对象朝生夕死采用复制算法(一个Edan,两个Survivor;8:1:1),老年代对象存活率高就采用标记-清除或者标记-整理。
垃圾收集器
有Serial、Serial Old、parNew 、Parallel Scavenge 、Parallel Old 、CMS(Concurrent Mark Sweep)、G1(Garbage-First)收集器。
这里主要说GMS和G1。
- GMS 是一种以获取最短回收停顿时间为目标的收集器。是基于标记-清除算法实现。主要包括4个步骤:
1. 初始标记。
2. 并发标记
3. 重新标记
4. 并发清理
* 在初始标记、重新标记时会“Stop The Word”。(停掉其他工作线程)
其中耗时的并发标记和并发清理和用户线程并发执行,减少停顿。
-
缺点:
- 对CPU资源敏感。像是CPU数量2个和4个造成的影响。
- 无法处理浮动垃圾。因为和用户线程并发执行,会产生新的垃圾。
- 会有空间碎片,因为是标记-清除算法。但给有参数配置是否整理和几次不压缩。
-
G1是服务端的垃圾收集器,说是当今前沿成果之一。
它将整个Java内存堆分为了多个大小相等的独立区域(Region),虽然还有新生代、老年代的概念,但是不再是物理隔离的了,都是一部分Region的集合。G1会跟踪各个Region里面的垃圾堆的价值(回收所获空间大小以及回收所需的时间),然后后台维护一个优先表,每次回收在有限的时间内,会有价值最大的,以获取最大的收集效率。
特点:
- 并行与并发:利用多核多CPU的条件来缩短“Stop The World”的时间。
- 分代收集:
- 空间整合:从整体上看是标记-整理清除算法
- 可预测的停顿时间:用户可以设置最大时间,回收器在时间内回收那些回收价值大的对象。
大致分为以下步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选标记
内存分配和回收策略
对象有限分配在Eden区,当Eden区没有足够内存时,虚拟机会发起一次Minor GC(新生代GC)。将存活的对象放入Survivor区(有两个和Eden比例8:1:1,1.8好像成了1:1:1),Survivor采用复制算法,每次存活下来的对象年龄加1,当到达一定值时,将放入老年代(默认15,可设置)。当对象无法加入Survivor时,将通过担保机制提前进入老年代(如数组)。还有一种情况,当Survivor中相同年龄的对象大小的总和超过Survivor空间的一半时,超过或等于此年龄的对象直接进入老年代。
在发生Minor GC之前,虚拟机会检查老年代最大可用内存空间是否大于新生代所有对象的总空间,如果条件成立,直接Minor GC 。如果不成立。会查看虚拟机是否设置是否允许担保失败。如果允许会检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均值大。如果大于,将进行Minor GC 。如果小于或者设置不允许冒险,将进行一次Full(老年代GC) GC。
- 内存溢出发生在Full GC 之后。
参考来源:《深入理解JAVA虚拟机》