JVM垃圾回收原理解析
人气:0概述
Java运行时区域中,程序计数器,虚拟机栈,本地方法栈三个区域随着线程的而生,随线程而死,这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收问题。而Java堆和方法区则不一样,一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存可能也不一眼,我们只有在运行期,才能知道会创建的对象,这部分的内存分配和回收,是垃圾回收器所关注的。垃圾回收器需要完成三个问题:那些内存需要回收;什么时候回收以及如何回收。
那些垃圾需要回收
垃圾回收的基本思想是考察一个对象的可达性,即从根节点开始是否可以访问到这个对象,如果可以,则说明对象正在被使用,相反如果从根节点无法访问到这个对象,说明对象已经不再使用了,一般来说此对象就是需要被回收的。这个算法为根搜索算法。
可达性分析
但是实际中,一个不可达的对象有可能在某种条件下“复活”自己,那么对它的回收就是不合理的。为此给出一个对象可达性状态的定义,并规定了在什么状态下可以安全的回收对象。可达性对象包含了以下三种状态。
可达的:从根节点开始,按照引用节点,可以搜索到这个对象
可复活的:对象的所有引用都被释放,但是对象可能在finalize()方法中复活自己。
不可达的:对象的finalize()方法被调用,并且没有复活,那么就进入不可达状态。不可达的对象不可能会被“复活”,因为finalize()方法只能调用一次。
/** * * <p>Description: 1.对象被GC时,可以通过finalize拯救 2.finalize只被调用一次 </p> * @date 2019年8月25日 * @version 1.0 */ public class FinalizeTest { private static FinalizeTest currentObj; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize invoke"); //重新引用 currentObj = this; } public void alive() { System.out.println("live"); } public static void main(String[] args) throws InterruptedException { currentObj = new FinalizeTest(); currentObj = null; System.gc(); //finalize优先级地,先等待 Thread.sleep(500); if(currentObj == null) { System.out.println("dead"); }else { currentObj.alive(); } currentObj = null; System.gc(); //finalize优先级地,先等待 Thread.sleep(500); if(currentObj == null) { System.out.println("dead"); }else { currentObj.alive(); } } }
上面代码有一处一样的断码片段,但是得到的结果却并不相同,一次对象“拯救复活”成功,另一次失败,那么就可以被正常回收。
可以作为GC Roots包括下面几种:
- 虚拟机栈(栈帧中的本地表量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用(即一般Native的方法)的对象
四种引用类型
在JDK1.2之后对引用进行了扩充,分为强引用,软引用,弱引用,虚引用4种,这四种强度一次减弱。通过对引用的扩充,可以依据内存的使用来描述这样的对象:当内存足够,则保留内存中;如果内存空间进行垃圾回收后还是很紧张,则可以抛弃这类对象。很多系统的缓存功能符合这样的应用场景。
强引用
在Java中最常见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
软引用
软引用需要用SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
弱引用
弱引用需要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用
虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。 虚引用的主要作用是跟踪对象被垃圾回收的状态。
什时候回收
按HotSpot VM的serial GC的实现来看触发条件主要分为以下几种:
- young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
- full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。
HotSpot VM里其它非并发GC的触发条件复杂一些,不过大致的原理与上面说的其实一样。并发GC的触发条件就不太一样。以CMS GC为例,它主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen做并发收集。
如何回收
如何回收主要就涉及到垃圾回收的算法了。下面介绍几种垃圾回收算法的思想。
标记清除法(Mark-Sweep)
标记清除算法是现代垃圾回收算法的思想基础。它主要分为两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点,标记所有从根节点开始的可达队对象,因此未被标记的对象就是未被引用的垃圾对象。然后在清除阶段,清除所有的未被标记的对象。
标记清除算法的不足有:效率的问题和标记清除后产生的大量不连续的内存碎片。而内存碎片太多可能会导致在分配大对象时,无法找到连续的内存而不得不提前触发另外一次垃圾回收。
复制算法(Coping)
复制算法的核心思想是:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
如果系统中的待回收的对象很多,复制算法需要复制的存活对象就会相对较少,真正的垃圾回收时刻,复制算法的效率就会很高。而且对象是在垃圾回收过程中的,统一复制到新的内存空间,再清除原来使用的内存,因此可以确保回收后的内存空间是没有碎片的。但是另一方面,复制算法的代价是需要使用更多的内存空间。
复制算法比较适用于新生代。因为新生代垃圾对象通常多余存活对象,复制算法的效率会比较高。
标记整理算法(Mark Compact)
在老年代,大部分的对象都是存活对象。如果依然用复制算法,由于存活的对象多,复制的成本也将提高。因此基于老年代的垃圾回收特性,需要使用其他的算法。标记整理算法是一种老年代的回收算法。它在标记算法的基础上做了一些优化。和标记清除算法一样,它也是从更节点开始,但是并不是清除未标记的对象,而是将存活的对象压缩到内存的一边,之后清除边界外所有空间。这种方法避免了碎片的产生,又不需要过多的内存空间,因此性价比比较高。
标记整理法的最终效果等同于标记清除算法执行完成后,再进行一次内存碎片的整理,因此也可以把它称为标记清除整理(MarkSweepComact)。
分代算法(Generational Collecting)
分代算法是根据对象存活周期不同将内存化为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最合适的收集算法。新生代中的特点是对象朝生夕死,大约90%的新建对象会被回收,因此新生代适合复制算法。当一个对象经过几次回收后依然存活,对象就会被放入老年代的内存空间。在老年代中可以认为对象在一段时间内,甚至在程序的整个生命周期,是常驻内存的,可以对老年代使用标记清除和标记整理算法。
对于新生代和老年代来说,通常新生代的回收频率很高,但是每次回收的耗时都很短,而老年代回收的频率比较低,但是会消耗更多的时间。
分区算法(Region)
一般来说,相同条件下,堆空间越大,一次GC所需要的事件越长,从而产生的停顿也越长。为了更好的靠之停顿时间,将一块大的内存区域分割成多个大小形同的小区域,依据目标的停顿时间,每次回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。分区算法是将整个堆空间划分为连续的不同小区间。每个小区间独立使用,独立回收。
您可能感兴趣的文章:
加载全部内容