Java的JVM GC(Garbage Collection)垃圾回收原理机制及算法
Java GC(Garbage Collection)垃圾回收机制,Java VM中,存在自动内存管理和垃圾清理机制。GC机制对JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,防止出现内存泄露和溢出问题。Java中不能显式分配和注销内存。有些开发者把对象设置为null或者调用System.gc()显式清理内存。设置为null至少没什么坏处,但是调用System.gc()会一定程度上影响系统性能。Java开发人员通常无须直接在程序代码中清理内存,而是由垃圾回收器自动寻找不必要的垃圾对象,并且清理掉它们。
Java GC主要做三件事:
(a)哪些内存需要GC?
(b)何时需要执行GC?
(c)以何策略执行GC?
Java中什么哪些内存需要GC回收?
JVM会分配一个运行时内存空间。包括5大部分:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。其中程序计数器、虚拟机栈、本地方法栈是每个线程私有内存空间,随线程而生,随线程而亡。这3个区域内存分配和回收都是确定的,无需考虑内存回收的问题。
但方法区和堆就不同了,一个接口的多个实现类需要的内存可能不一样,只有在程序运行期间才会知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC主要关注的是这部分内存。GC主要进行回收的内存是JVM中的方法区和堆,涉及到多线程(指堆)、多个对该对象不同类型的引用(指方法区),才会涉及GC的回收。
小结:Java GC针对的是JVM中堆和方法区。
Java GC机制启动之前,需要确定堆内存中哪些对象是存活的,一般有两种方法:引用计数法和可达性分析法。
引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。引用计数法实现简单,判定高效,但不能解决对象之间相互引用的问题。
可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。通过称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索路径称为 “引用链(Reference Chain)”,以下对象可作为GC Roots:
(a)本地变量表中引用的对象
(b)方法区中静态变量引用的对象
(c)方法区中常量引用的对象
(d)Native方法引用的对象
当一个对象到 GC Roots 没有任何引用链时,意味着该对象可以被回收。
小结:Java GC垃圾回收机制,回收的是已死的Java对象(引用无法可达)。
Java GC垃圾回收算法
(一)标记 -清除算法
“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。之所以说它是最基础的内存回收算法,是因为后续的算法都是基于这种思路、并对其缺点进行改进而得到的。
主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存时候,不得不提前触发另一次垃圾收集动作。
(二)复制算法
“复制”(Copying)内存回收算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。
(三)Java GC的分代垃圾回收机制
GC分代回收算法
GC分代的假设:绝大部分对象的生命周期都非常短暂,存活时间短。
“分代回收”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
GC垃圾回收器会在下面两种情况下启动:
(a)大多数对象会很快变得不可达。
(b)只有很少的由老对象(创建时间较长的对象)指向新生对象的引用。
为强化这一假设,Java虚拟机在物理上划分为两个逻辑内存代——新生代(Young Generation)和老年代(Old Generation)。新生代(Young Generation): 新生代空间用来保存那些第一次被创建的Java对象,分为三个空间:
(a)一个伊甸园空间(Eden )
(b)两个幸存者空间(Survivor )
一共有三个空间,其中包含两个幸存者空间。每个空间的执行顺序如下:
(a)绝大多数刚刚被创建的对象会存放在伊甸园空间。
(b)在伊甸园空间执行了第一次GC之后,存活的对象被移动到其中一个幸存者空间。
(c)此后,在伊甸园空间执行GC之后,存活的对象会被堆积在同一个幸存者空间。
(d)当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。之后会清空已经饱和的那个幸存者空间。
(e)在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。
在新生代中,使用“停止-复制”算法进行内存清理。绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可到达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程称为“Minor GC” 。
老年代(Old Generation): 对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-整理算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。
小结:Java内存分配和回收机制是:分代分配,分代回收。新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。老年代中,其存活率较高、没有额外空间对它进行分配担保,就应该使用“标记-整理”或“标记-清理”算法进行回收。
Java GC优化永远是最后一项任务。因为Java GC将“Stop the World”(串行GC暂时中断程序执行)。Stop-the-world会在任何一种GC算法中发生。Stop-the-world意味着 JVM 因为要执行GC而停止了应用程序的执行。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态,直到GC任务完成。GC优化很多时候就是指减少Stop-the-world发生的时间。GC优化的根本原因,垃圾收集器清除Java创建的对象,GC执行的次数,即需要被垃圾收集器清理的对象个数,与创建对象的数量成正比,因此,应该减少创建对象的数量。
GC优化两个目的:
(a)将转移到老年代的对象数量降到最少。对象被创建在伊甸园空间,而后转化到幸存者空间,最终剩余的对象被送到老年代。某些比较大的对象会在被创建在伊甸园空间后,直接转移到老年代空间。老年代空间上的GC处理会比新生代花费更多时间。因此,减少被移到老年代对象的数据可以显著地减少Full GC的频率。减少被移到老年代空间的对象数量,可能被误解为将对象留在新生代。但是,这是不可能的。取而代之,你可以调整新生代空间的大小。
(b)减少Full GC的执行时间。Full GC执行时间比Minor GC要长很多。