Java和C++之间显著的一个区别就是对内存的管理。和C++把内存管理的权利赋予给开发人员的方式不一样,Java拥有一套自动的内存回收系统(Garbage Collection,GC)简称GC,能够无需开发人员干预而对再也不使用的内存进行回收管理。算法
垃圾回收技术(如下简称GC)是一套自动的内存管理机制。当计算机系统中的内存再也不使用的时候,把这些空闲的内存空间释放出来从新投入使用,这种内存资源管理的机制就称为垃圾回收。编程
其实GC并非Java的专利,GC的的发展历史远比Java来得久远的多。早在Lisp语言中,就有GC的功能,包括其余不少语言,如:Python(其实Python的历史也比Java早)也具备垃圾回收功能。安全
使用GC的好处,能够把这种容易犯错的行为让给计算机系统本身去管理,能够防止人为的错误。同时也把开发人员从内存管理的泥沼中解放出来。编程语言
虽然使用GC虽然有不少方便之处,可是若是不了解GC机制是如何运做的,那么当遇到问题的时候,咱们将会很被动。因此有必要学习下Java虚拟机中的GC机制,这样咱们才能够更好的利用这项技术。当遇到问题,好比内存泄露或内存溢出的时候,或者垃圾回收操做影响系统性能的时候,咱们能够快速的定位问题,解决问题。ide
接下来,咱们来看下JVM中的GC机制是怎么样的。函数
首先,咱们若是要进行垃圾回收,那么咱们必须先要识别出哪些是垃圾(被占用的无用内存资源)。性能
Java虚拟机将内存划分为多个区域,分别作不一样的用途。简单的将,JVM对内存划分为这几个内存区域:程序计数器、虚拟机栈、本地方法栈、Java堆和方法区。其中程序计数器、虚拟机栈和本地方法栈是随着线程的生命周期出生和死亡的,因此这三块区域的内存在程序执行过程当中是会有序的自动产生和回收的,咱们能够不用关心它们的回收问题。剩下的Java堆和方法区,它们是JVM中全部线程共享的区域。因为程序执行路径的不肯定性,这部分的内存分配和回收是动态进行的,GC主要关注这部分的内存的回收。 学习
对像实例是不是存活的,有两种算法能够用于肯定哪些实例是死亡的(它们占用的内存就是垃圾),那么些实例是存活的。第一种是引用计数算法:this
引用计数算法会对每一个对象添加一个引用计数器,每当一个对象在别的地方被引用的时候,它的引用计数器就会加1;当引用失效的时候,它的引用计数器就会减1。若是一个对象的引用计数变成了0,那么表示这个对象没有被任何其余对象引用,那么就能够认为这个对象是一个死亡的对象(它占用的内存就是垃圾),这个对象就能够被GC安全地回收而不会致使系统出现问题。spa
咱们能够发现,这种计数算法挺简单的。在C++中的智能指针,也是使用这种方式来跟踪对象引用的,来达到内存自动管理的。引用计数算法实现简单,并且判断高效,在大部分状况下是一个很好的垃圾标记算法。在Python中,就是采用这种方式来进行内存管理的。可是,这个算法存在一个明显的缺陷:若是两个对象之间有循环引用,那么这两个对象的引用计数将永远不会变成0,即便这两个对象没有被任何其余对象引用。
public class ReferenceCountTest { public Object ref = null; public static void main(String ...args) { ReferenceCountTest objA = new ReferenceCountTest(); ReferenceCountTest objB = new ReferenceCountTest(); // 循环引用 objA <--> objB objA.ref = objB; objB.ref = objA; // 去除外部对这两个对象引用 objA = null; objB = null; System.gc(); } }
上面的代码就演示了两个对象之间出现循环引用的状况。这个时候objA和objB的引用计数都是1,因为两个对象之间是循环引用的,因此它们的引用计数将一直是1,而即便这两个对象已经再也不被系统所使用到。
因为引用计数这种算法存在这种缺陷,因此就有了一种称为“可达性分析算法”的算法来标记垃圾对象。
经过可达性分析算法来判断对象存活,能够克服上面提到的循环引用的问题。在不少编程语言中都采用这种算法来判断对象是否存活。
这种算法的基本思路是,肯定出一系列的称为“GC Roots”的对象,以这些对象做为起始点,向下搜索全部可达的对象。搜索过程当中所走过的路径称为“引用链”。当一个对象没有被任何到“GC Roots”对象的“引用链”链接的时候,那么这个对象就是不可达的,这个对象就被认为是垃圾对象。
从上面的图中能够看出,object1~4这4个对象,对于GC Roots这个对象来讲都是可达的。而object5~7这三个对象,因为没有链接GC Roots的引用链,因此这三个对象时不可达的,被断定为垃圾对象,能够被GC回收。
在Java中,能够做为GC Roots的对象有如下几种:
当经过可达性分析算法断定为不可达的对象,咱们也不能判定这个对象就是须要被回收的。当咱们须要真正回收一个对象的时候,这个对象必须经历至少两次标记过程:
当经过可达性分析算法处理之后,这个对象没有和GC Roots相连的引用链,那么这个对象就会被第一次标记,并判断对象的finalize()方法(在Java的Object对象中,有一个finalize()方法,咱们建立的对象能够选择是否重写这个方法的实现)是否须要执行,若是对象的类没有覆盖这个finalize()方法或者finalize()已经被执行过了,那么就不须要再执行一次该方法了。
若是这个对象的finalize()方法须要被执行,那么这个对象会被放到一个称为F-Queue的队列中,这个队列会被由Java虚拟机自动建立的一个低优先级Finalizer线程去消费,去执行(虚拟机只是触发这个方法,可是不会等待方法调用返回。这么作是为了保证:若是方法执行过程当中出现阻塞,性能问题或者发生了死循环,Finalizer线程仍旧能够不受影响地消费队列,不影响垃圾回收的过程)队列中的对象的finalize()方法。
稍后,GC会对F-Queue队列中的对象进行第二次标记,若是在此次标记发生的时候,队列中的对象确实没有存活(没有和GC Roots之间有引用链),那么这个对象就肯定会被系统回收了。固然,若是在队列中的对象,在进行第二次标记的时候,忽然和GC Roots之间建立了引用链,那么这个对象就"救活"了本身,那么在第二次标记的时候,这个存活的对象就被移除出待回收的集合了。因此,经过这种两次标记的机制,咱们能够经过在finalize()方法中想办法让对象从新和GC Roots对象创建连接,那么这个对象就能够被救活了。
下面的代码,经过在finalize()方法中将this指针赋值给类的静态属性来"拯救"本身:
public class FinalizerTest { private static Object HOOK_REF; public static void main(String ...args) throws Exception { HOOK_REF = new FinalizerTest(); // 将null赋值给HOOK_REF,使得原先建立的对象变成可回收的对象 HOOK_REF = null; System.gc(); Thread.sleep(1000); if (HOOK_REF != null) { System.out.println("first gc, object is alive"); } else { System.out.println("first gc, object is dead"); } // 若是对象存活了,再次执行一次上面的代码 HOOK_REF = null; System.gc(); if (HOOK_REF != null) { System.out.println("second gc, object is alive"); } else { System.out.println("second gc, object is dead"); } } @Override protected void finalize() throws Throwable { super.finalize(); // 在这里将this赋值给静态变量,使对象能够从新和GC Roots对象建立引用链 HOOK_REF = this; System.out.println("execute in finalize()"); } }
#output:
execute in finalize()
first gc, object is alive
second gc, object is dead
能够看到,第一次执行System.gc()的时候,经过在方法finalize()中将this指针指向HOOK_REF来重建引用连接,使得本应该被回收的对象从新复活了。而对比一样的第二段代码,没有成功拯救的缘由是:finalize()方法只会被执行一次,因此当第二次将HOOK_REF赋值为null,释放对对象的引用的时候,因为finalize()方法已经被执行过一次了,因此无法再经过finalize()方法中的代码来拯救对象了,致使对象被回收。
上面咱们已经知道了怎么识别出能够回收的垃圾对象。如今,咱们须要考虑如何对这些垃圾进行有效的回收。垃圾收集的算法大体能够分为三类:
这三种算法,适用于不一样的回收需求和场景。下面,咱们来一一介绍下每一个回收算法的思想。
"标记-清除"算法是最基础的垃圾收集算法。标记-清除算法在执行的时候,分为两个阶段:分别是"标记"阶段和"清除"阶段。
在标记阶段,它会根据上面提到的可达性分析算法标记出哪些对象是能够被回收的,而后在清除阶段将这些垃圾对象清理掉。
算法思路很简单,可是这个算法存在一些缺陷:首先标记和清除这两个过程的效率不高,其次是,直接将标记的对象清除之后,会致使产生不少不连续的内存碎片,而太多不连续的碎片会致使后续分配大块内存的时候,没有连续的空间能够分配,这会致使不得再也不次触发垃圾回收操做,影响性能。
复制算法,顾名思义,和复制操做有关。该算法将内存区域划分为大小相等的两块内存区域,每次只是用其中的一块区域,另外一块区域闲置备用。当进行垃圾回收的时候,会将当前是用的那块内存上的存活的对象直接复制到另一块闲置的空闲内存上,而后将以前使用的那块内存上的对象所有清理干净。
这种处理方式的好处是,能够有效的处理在标记-清除算法中碰到的内存碎片的问题,实现简单,效率高。可是也有一个问题,因为每次只使用其中的一半内存,因此在运行时会浪费掉一半的内存空间用于复制,内存空间的使用率不高。
标记-整理算法,思路就是先进行垃圾内存的标记,这个和标记-清除算法中的标记阶段同样。当将标记出来的垃圾对象清除之后,为了不出现标记-清除算法中碰到的内存碎片问题,标记-整理算法会对内存区域进行整理。将当前的全部存活的对象移动到内存的一端,将一端的空闲内存整理出来,这样就能够获得一块连续的空闲内存空间了。
这样作,能够很方便地申请新的内存,只要移动内存指针就能够划出须要的内存区域以存放新的对象,能够在不浪费内存的状况下高效的分配内存,避免了在复制算法中浪费一部份内存的问题。
在现代虚拟机实现中,会将整块内存划分为多个区域。用"年龄"的概念来描述内存中的对象的存活时间,并将不一样年龄段的对象分类存放在不一样的内存区域。这样,就有了咱们平时据说的"年轻代"、"老年代"等术语。
顾名思义,"年轻代"中的对象通常都是刚出生的对象,而"老年代"中的对象,通常都是在程序运行阶段长时间存活的对象。将内存中的对象分代管理的好处是,能够按照不一样年龄代的对象的特色,使用合适的垃圾收集算法。
对于"年轻代"中的对象,因为其中的大部分对象的存活时间较短,不少对象都撑不过下一次垃圾收集,因此在年轻代中,通常都使用"复制算法"来实现垃圾收集器。
在上图中,咱们能够看到"Young Generation"标记的这块区域就是"年轻代"。在年轻代中,还细分了三块区域,分别是:"eden"、"S0"和"S1",其中"eden"是新对象出生的地方,而"S0"和"S1"就是咱们在复制算法中说到了那两块相等的内存区域,称为存活区(Survivor Space)。
这里用于复制的区域只是占用了整个年轻代的一部分,因为在新生代中的对象大部分的存活时间都很短,因此若是按照复制算法中的以1:1的方式来平分年轻代的话,会浪费不少内存空间。因此将年轻代划分为上图中所示的,一块较大的eden区和两块同等大小的survivor区,每次只使用eden区和其中的一块survivor区,当进行内存回收的时候,会将当前存活的对象一次性复制到另外一块空闲的survivor区上,而后将以前使用的eden区和survivor区清理干净,如今,年轻代可使用的内存就变成eden区和以前存放存活对象的那个survivor区了,S0和S1这两块区域是轮替使用的。
HotSpot虚拟机默认Eden区和其中一块Survivor区的占比是8:1,经过JVM参数"-XX:SurvivorRatio"控制这个比值。SurvivorRatio的值是一个整数,表示Eden区域是一块Survivor区域的大小的几倍,因此,若是SurvivorRatio的值是8,那么Eden区和其中Survivor区的占比就是8:1,那么总的年轻代的大小就是(Eden + S0 + S1) = (8 + 1 + 1) = 10,因此年轻代每次可使用的内存空间就是(Eden + S0) = (8 + 1) = 9,占了整个年轻代的 9 / 10 = 90%,而每次只浪费了10%的内存空间用于复制。
并非留出越少的空间用于复制操做越好,若是在进行垃圾收集的时候,出现大部分对象都存活的状况,那么空闲的那块很小的Survivor区域将不能存放这些存活的对象。当Survivor空间不够用的时候,若是知足条件,能够经过分配担保机制,向老年代申请内存以存放这些存活的对象。
对于老年代的对象,因为在这块区域中的对象和年轻代的对象相比较而言存活时间都很长,在这块区域中,通常经过"标记-清理算法"和"标记-整理算法"来实现垃圾收集机制。上图中的Tenured区域就是老年代所在的区域。而最后那块Permanent区域,称之为永久代,在这块区域中,主要是存放类对象的信息、常量等信息,这个区域也称为方法区。在Java 8中,移除了永久区,使用元空间(metaspace)代替了。
在这篇文章中,咱们首先介绍了采用最简单的引用计数法来跟踪垃圾对象和经过可达性分析算法来跟踪垃圾对象。而后,介绍了垃圾回收中用到的三种回收算法:标记-清除、复制、标记-整理,以及它们各自的优缺点。最后,咱们结合上面介绍的三种回收算法,介绍了现代JVM中采用的分代回收机制,以及不一样分代采用的回收算法。