众说周知,Java与C++同为支持面向对象的语言,但他们对内存的管理方式却有很大的不同。C++开发者往往需要手动调用内存分配函数对内存进行分配,并在使用完这块内存之后,手动进行释放。而Java开发者一般来说不需要关心内存是如何进行分配的,只需要将精力放在业务开发上。究其原因,是因为Java拥有垃圾回收机制,旨在自动化的对内存进行管理。笔者在这里并不想就这两门语言的内存管理机制进行比较,两者的内存管理方式各有优劣。这里,笔者想介绍一下Java的内存回收机制,以及Java的垃圾收集器。
在详细介绍Java的垃圾回收机制之前,首先我们需要明确3三件事情,即:
在前面我们介绍过了Java内存区域,在里面有简单的涉及到一点儿内存回收的知识,强调了对方法区和Java堆的内存回收的必要。那么其他的内存需要进行内存回收吗?准确来说,对所有Java内存区域都需要进行内存回收。只不过,程序计数器、虚拟机栈、本地方法栈这3个区域随线程而生,随线程而灭,线程销毁的时候,与之对应的这三个区域的内存就会被回收,整个过程都是确定的行为,其所需的内存基本上在类结构确定下来时就已知了。而方法区和Java堆不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存可能不一样,这部分内存的大小只能在运行时才能知道,对这部分内存的分配与回收是动态的过程,是不确定的。因此,垃圾回收器主要是关注这部分的内存。
在上一个问题中,我们明白了Java的垃圾回收主要是针对Java堆和方法区进行回收,因此,针对这两个区域,来讨论什么时候进行内存回收。
判断对象是否需要回收的方式一般有两种:引用计数算法和可达性分析算法。
引用计数算法,故名思义,它主要根据对象被引用的情况来判断对象是否需要回收。如果一个对象没有被任何一个对象引用的话,则判定这个对象需要被回收。具体来说,就是在一个对象实例被创建的时候,对这个对象实例被引用的情况进行计数,每次对象被引用,就将引用计数器加1,当引用结束的时候,将引用计数器减1,直到引用计数器归零,就进行回收。
引用计数法是一个很简单的算法,易于实现,效率也高。但是却有一个弊端,当出现两个对象互相引用的时候,引用计数器永远无法归零,那么就永远无法被回收,所以主流的Java虚拟机都没有选择这个算法。目前来说,主流的实现方式是可达性分析算法。
可达性分析算法是通过可达性分析来判定对象是否需要被回收的。这个算法的基本思想路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
无论是引用计数算法还是可达性分析算法,其算法都与“引用”有关。在JDK1.2之后,Java将引用分为了强引用、软引用、弱引用和虚引用4种,这4种引用强度依次减弱:
方法区也被称为永久代,很多人认为永久代是不需要被回收的,Java虚拟机规范也没有明确要求永久代需要被回收,但就实际情况而言,对永久代的回收是一个必须的过程,尽管永久代的回收效率相对来说很低。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
回收废弃常量的过程和回收Java堆中的对象非常类似。当常量池中的常量没有被引用的时候,该常量就被判定为废弃常量,可以进行回收。
判定一个常量是否是“废弃常量”比较简单,判定一个类是否是“无用的类”就比较困难了。类需要同时满足下面3个条件才能算是“无用的类”:
虚拟机可以对满足上述3个条件的无用类进行回收,但是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
垃圾收集算法,主要有以下几种:标记-清除算法、复制算法、标记-整理算法。
标记-清除算法是最基础的收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。之所以说它是最基础的收集算法,是因为其他算法都是基于其上对其不足之处的改进。它的主要不足有两个:一个效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法是用来解决标记-清除算法的效率问题,它将可用内存按容量划分为大小相等的两块,每次只能使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的空间一次清理到。这样使得每次都是对这个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。只是这种算法的代价是将内存缩小为原来的一半,未免有点高。
现在的商业虚拟机都采用这种收集算法来回收新生代,因为实际上98%的对象都是“朝生夕死”,所以根本不需要按照1:1的比例来划分内存空间。因此,在内存划分的时候,将内存划分一块Eden区域和两块Survivor区域,Eden区域和Survivor区域的比例默认是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被浪费,这是可以接受的。
标记-整理算法是为了解决复制算法在对象存活率较高时就要进行较多的复制操作,效率变低以及大量空间浪费,所以在老年代一般不能直接选用这种算法。标记-整理算法的标记过程与标记-清除算法一样,但并不马上对内存进行回收,而是先让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存,也就是把存活的对象进行了整理,减少了内存碎片。
分代收集算法说白了就是根据对象存活周期的不同将内存划分为几块,比如说老年代和新生代,然后根据不同区域的特点采用不同的收集算法。一般对新生代采用复制算法,对老年代采用标记-清理算法或者标记-整理算法。