1.概述
java与c++相比,在内存的分配与回收方面更加具备“自动化”,似乎我们并不需要了解虚拟机GC与内存分配。然而当需要排查各种内存溢出,内存泄露的问题时,当垃圾收集成为系统优化的瓶颈时,我们必须了解JVM的“自动化”技术,以实现对其的监控与调节。
在上一篇博文(java内存区域)中介绍了java内存运行时区域的各个部分,其中虚拟机栈,程序计数器,本地方法栈是线程私有的,这几个区域在编译器其分配的内存空间大小基本上是确定的。然而方法区和java堆则不一样,随着线程的进行,方法的执行以及对象的创建等,其内存空间的大小是不断变化的,所以垃圾回收的作用范围主要是这些区域。
内存的回收主要包括三个方面:
- 确定哪些内存需要回收
- 确定什么时候回收
- 如何回收
2.确定哪些内存需要回收
我们可以肯定的是,我们需要回收的对象是那些不再使用的对象,在c++中我们通过析构函数释放对象占用的内存空间。而在java中,这一步是虚拟机帮我们完成的。那么在回收对象的内存空间之前,虚拟机需要判断哪些对象是无用的。下面介绍两种算法:
2.1 引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1。当计数器的值为0时就认为这个对象是无用的。
这种算法简单易懂,判定效率也很高,不需要进行复杂的计算,但是其很难解决对象之间相互引用的问题。比如下图:
objA引用B,objB引用A,这时就算我们不在使用A和B了,但由于A和B之间相互引用导致引用计数器的值不为0,通过引用计数算法无法将其回收。正是由于这种算法的弊端,所以主流的java虚拟机都没有选择这种算法来管理内存。
2.2 可达性分析算法
通过一系列的称为“GC ROOT”的对象作为起始点,从这些节点通过其指向其他对象的用用向下搜索,搜索走过的路径称为引用链,当一个对象到“GC ROOT”之间没有引用链就认为其是不可用的。也就是从root开始不可达的节点对象为无用对象
图中对象4,5,6不可达。
在java中可作为GC root的对象包括下面几种:
- 虚拟机栈(本地变量表),中引用的对象
- 方法区中静态类型引用的变量
- 方法区中常量引用的变量
- 本地方法栈中引用的对象
3.java的引用
在JDK1.2之后,java对引用的概念进行了扩充,将引用分为:强引用,软引用,弱引用,虚引用。
- 强引用。在代码中·new·出的对象就存在强引用,垃圾收集器永远不会回收掉存在强引用的对象。
- 软引用。用来描述一些还有用但并非必需的对象。对于软引用,当系统将要发生内存溢出异常之前,将会把这些对象列入回收范围,当软引用的对象回收之后如果还不够满足内存需求,才会抛出内存溢出异常。java提供SoftReference类来实现软引用。
- 弱引用。用来描述非必需的对象。被软引用关联的对象只能生存到下一次垃圾收集发生之前。也就是说在垃圾收集器回收的时候,软引用的对象在垃圾收集器的回收范围之内。java提供WeakReference类来实现软引用。
- 虚引用。一个对象存在虚引用不会对其生存时间产生影响。为一个对象设置虚引用的目的在于能在这个对象被收集器回收时收到一个系统通知。java提供PhantomReference实现虚引用。
4.什么时候回收
对象的回收至少要经历两次标记过程: 如果对象在进行可达性分析后,发生没有与gc root相连的引用链,那它将会被标记并进行第一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖该方法或者已经执行过,虚拟机将这两中情况记为“不需要执行。要进行finalize()的对象放入F-Queue队列中,由一个finalize线程去执行”。GC稍后将会对F-Queue中的对象进行第二次小规模的标记。除非对象在finalize()方法中拯救自己(创建了到gc root的引用链),否则它就真的会被回收。
5.垃圾收集算法
不同虚拟机的垃圾收集算法的实现各不相同,这里只介绍几种算法的主要思想。
5.1标记-清除算法
该算法分为标记
和清除
两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:
- 标记和清除的效率不高。
- 标记和清除之后会产生大量不连续的内存空间
5.2 复制算法
它将可用的内存按容量划分为大小相等的两块,每次只使用一块,当这一块用完的时候,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉
不足:将内存空间缩小为了原来的一半。
5.3.标记-整理算法
标记过程与标记-清除算法
一样,但后续的步骤不是直接对可回收对象进行清理,而是让所有存活的对象都想一端移动,然后直接清理掉端边界以外的内存。
5.4分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”(Grenerational Collection)算法.其将java堆分成新生代和老年代。新生代中选用复制算法,老年代中使用“标记-清除”或者“标记-整理”算法。
- 新生代:主要是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。
- 老年代:主要存放应用程序中生命周期长的内存对象,或者占用空间比较大的对象。