JVM GC之一找出不可达对象并回收
JAVA运行时数据区域
1、程序计数器:当前线程所执行的字节码的行号指示器。一个处理器只会执行一条线程中的指令,为了线程切换后能回复到正确的执行位置,所以每条线程都需要一个独立的计数器。各条线程之间互不影响,独立存储,属于‘线程私有’内存。
2、java虚拟机栈:描述的是JAVA方法执行的内存模型:每个方法执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法的被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。所以也是线程私有的。
3、本地方法栈:和java虚拟机栈发挥的作用类似,只不过Java虚拟机栈是为JAVA方法服务的,而本地方法栈是为Native方法服务。所以也是线程私有的。
4、JAVA堆:JAVA堆是被所有线程共享的区域。所有的对象实例及数组都要在堆上分配。
5、方法区:是各个线程共享的内存区域。主要存储被虚拟机加载的类信息,常量、静态变量、编译后的字节码数据等。有一个别名:非堆。
6、运行时常量池:方法区的一部分,当然也是线程共享的咯。除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放各种字面量和符合引用。
7、直接内存:并不是虚拟机运行时的数据区的一部分。是在NIO中基于通道和缓冲区的I/O方式,使用Native函数库直接分配堆外内存。避免了JAVA堆和Native堆中来回复制数据。和(操作系统中内存页的用户空间和系统空间的虚拟映象类似)
=======================================
垃圾收集器
1、程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此内存的分配和回收都具备确定性。因此不需要考虑回收问题,因为线程结束或者方法结束,内存自然就回收了。而Java堆和方法去不一样,一个接口中的多个实现类需要的内存不一样,一个方法中的多个分支也不一样,只有在程序处于运行时才能知道创建哪些内存,所以这部分的内存的分配和回收都是动态的。
2、引用计数算法
引用计数:每当被引用时引用计数加1,有引用断开时引用计数减1.当引用计数为0时表示该对象可以被回收。JVM并不是通过引用计数算法来进行回收的,主要原因是很难解决对象之间的相互循环引用的问题。
3、根搜索算法:GC Roots
JVM是通过根搜索算法判定对象算法存活的。算法的基本思路是:通过一系列的名为GC Roots (GC 根节点)的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径,当一个对象到GC Roots没有任何引用链相连(图论说:从GC Roots到这个对象不可达)时, 证明此对象是不可用的。
4、可以作为GC Roots的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈JNI中的引用的对象。
===========================================
对象Life or death
1、根搜索算法中不可达的对象也并非是‘非死不可’的,暂时是‘缓刑’阶段,要真正判断一个对象死亡要经历两次标记过程:如果对象在进行根搜索后发现对象不可达,那它将会进行被第一次标记并且进行筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机掉用过,这两种情况都视为‘没有必要执行’。
如果对象被认为有必要执行finalize()方法,那么这个方法会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行。这里的‘执行’也只是指虚拟机会触发这个方法,但并不承诺一定会执行。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC会对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()中重新与引用链上的任何一个对象建立了关联,就会被移出‘即将回收’集合,如果没有移出,那就真的离死亡不远了。
finalize()方法只会被系统自动调用一次。
============================================
回收方法区(HotSpot虚拟机中的永久代)
1、Java虚拟机规范中说过可以不要求虚拟机在方法区实行垃圾收集,在方法区进行垃圾回收的‘性价比’一般比较低。在堆中尤其是新生代中,常规的一次垃圾回收就可以回收70%--90%的空间,而永久代的垃圾收集效率远低于此。
2、永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
3、如果字符串‘abc’进入了常量池,但是当前系统中没有任何String对象引用‘abc’常量,也没有其他地方引用了这个字面量,如果此时发生内存回收,而且必要的话 这个‘abc’常量就会被回收掉。
4、判断无用的类必须同时满足三个条件:
该类的所有实例都已经被回收,即JAVA堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收
加载该类的Class对象没有任何地方引用,而且不能通过反射访问。
5、在大量使用反射、动态代理、CGLib及动态生成JSP和频繁自定义ClassLoader的场景需要虚拟机自动卸载,以防止永久代不会溢出。
===================================================
垃圾收集算法:标记--清楚算法、复制算法、标记-整理算法、分代收集算法
1、标记--清除算法
最基础的算法,分为标记和清除两个阶段:
标记:首先标记出需要清除的对象,标记的过程就是上面的对象Life or death。之所以说是基础的算法,是因为后续的算法都是基于这个算法改进的。
该算法有两个缺点:效率和空间问题(其实计算机最纠结的无外乎这两个地方:时间和空间问题)。标记和清除的效率都不高。空间问题:标记清除之后会产生大量的碎片,可能会导致,当程序在以后的运行过程中需要分配较大的对象时无法找到足够的连续内存而不得不提前触发另一次收集动作。
2、复制算法
复制算法可以解决算法1的效率问题。将可用内存按容量划分成大小相等的两块,每次只使用其中的一块。当这一块的内存用完时,就吧还活着的对象复制到另一块上面,然后再把已使用的内存一次性清楚干净。这样使得每次都是对其中的一块内存回收,内存分配时也不用考虑内存碎片问题,只要移动堆顶指针即可。但是代价是内存缩小为原来的一半。
现在的商业虚拟机都采用这中算法来回收新生代,因为IBM研究表明新生代的的对象98%都是朝生夕死的,所以不需要按照1:1的比例来划分空间。
而是把内存分成一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor空间。当回收时,把Eden和Survivor中还活着的对象一次性拷贝到Survivor空间中,最后清理掉Eden和Survivor空间。HotSpot默认Eden和Survivor空间的大小是8:1,这样可以保证每次可用内存是90%(80%+10),只有10%是浪费的。
当Survivor空间不足时就要依赖老年代进行分配担保(就是说 有大头(老年代)在后面,才放心只留出10%来存放Eden和Survivor中活着的对象,万一Survivor不足时还有老年代在后面撑腰)。
3、标记-整理算法
复制算法在对象存活率较高时就要执行交多的复制操作,效率会变低。重点是如果不想浪费掉50%的空间就需要有额外的空间进行担保,以应对被使用的内存中有100%存活的极端情况,所以老年代一般不能直接使用这一算法。因为老年代没有其他的内存可担保了。
根据老年代的特定,提出了 ‘标记--整理’算法。原理是:标记过程仍然与‘标记--清除’算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有活着的对象都向一端移动,然后直接清理边界以外的内存。
4、分代算法
当前商业虚拟机都采用这种算法回收。没有什么新的思想,只是根据对象的存活周期把内存划分成几块,一般是新生代和老生代。这样可以根据各个年代的特定采用合适的算法进行垃圾收集。
在新生代中,对象的存活周期较短,朝生夕死,采用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
在老生代中,对象的存活率较高,没有额外的空间对它进行分配担保,必须使用‘标记--清除’或‘标记--整理’算法进行回收。