【深入理解Java虚拟机】自动内存管理机制——垃圾回收机制

  Java与C++之间有一堵有内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。C/C++程序员既拥有每一个对象的所有权,同时也担负着每一个对象生命从开始到终结的维护责任,而Java最大的优势之一就是将内存控制的权力交给了Java虚拟机。在虚拟机自动内存管理机制的帮助下,程序员几乎不用担心内存泄漏和内存溢出的问题。


垃圾回收机制

  垃圾回收机制(GC)是java语言最独特的优势之一,但它并不是第一个实现GC的语言,第一门真正使用内存动态分配和垃圾收集技术的语言是1960年诞生的Lisp语言。

  经过半个世纪的发展,目前内存动态分配和内存自动回收技术已经相当成熟。要想深入了解垃圾回收机制,需要考虑三个问题:哪些内存需要回收,什么时候回收,如何回收

1. 垃圾回收区域(哪些内存需要回收)

  根据java虚拟机对内存区域的划分,我们知道程序计数器、虚拟机栈、本地方法栈三个区域属于线程私有,随线程而生,随线程而灭。方法的调用对应着栈帧在栈中从进栈到出栈的过程,而栈帧需要多少空间在编译期就可以确定,在方法结束或者线程结束时这几个区域自然可以跟着回收,不需要过多的考虑回收的问题。

  对于java堆和方法区,只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收是动态的,所以需要考虑垃圾回收的问题。

  因此,我们明确:垃圾回收机制所关注的区域主要就是堆空间和方法区

2. 对象存活判定算法(什么时候回收)

  垃圾回收机制主要关注的是堆空间(还有方法区)的内存回收,但是应该在什么时候回收才是恰当的,这涉及到回收时机的问题。我们知道,堆空间主要存放的就是实例对象,因此我们很自然地可以想到当所存放的对象已死(不会再使用)的时候就该回收对应的内存区域,当该对象还有可能使用时就不能回收。

  因此,回收时机的问题就转变为了如何判断一个对象是否存活(是否还会被引用)的问题。为此,虚拟机里实现了一些对象存活判定算法来判定对象的存活状态,主要包括引用计数和可达性分析算法

  先来介绍这里涉及到的引用的概念。所谓引用,指的是reference类型的数据中存储的数值代表的是另外一块内存的起始地址。更进一步,引用可以分为四种:(1)强引用。类似于Object obj=new Object()这一类,只要强引用还存在,永远不能回收所引用的对象。(2)软引用。用SoftReference类实现,用以描述还有用但是非必需的对象,如果空间足够,不会回收,如果将要发生溢出,会将这些对象回收,回收之后内存空间还是不够才会报溢出异常。(3)弱引用。用WeakReference类实现,用以描述非必需对象,这些对象只能存活到下一次垃圾收集之前,无论内存是否足够,在发生垃圾回收时都会回收弱引用所关联的对象。(4)虚引用(幽灵引用、幻影引用)。用PhantomReference类来实现,是一种最弱的引用关系,无法通过虚引用访问对象,唯一目的是当这个对象呗回收时收到一个通知。

  理解了引用的概念之后,重点记录引用计数和可达性分析这两种算法。

(1)引用计数算法

  引用计数是最简单的一种对象存活判定算法,其基本思想是:给每一个对象添加一个引用计数器,每当有一个引用指向该对象时,计数器值加1,当引用失效时,计数器值减1;当计数器值为0的时候,该对象就不可能再被使用

  引用计数思想简单,实现也很容易,效率也比较高。但遗憾的是,几乎没有虚拟机选用引用计数来管理内存,最主要的原因是它存在一个问题难以解决:对象之间相互循环引用

  如上图,objA有一个字段ref指向objB,objB有一个字段指向objA,当objA和objB都变为null时,这两个对象在代码中实际再也无法访问到,但是由于相互循环引用,他们的引用计数都不为0,无法回收。

(2)可达性分析算法

  由于引用计数的缺点,java虚拟机并没有使用引用计数,而是使用了可达性分析算法来判定对象是否存活。

  可达性分析算法的基本思想:通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下逐渐搜索,搜索所经过的路径称为引用链。当一个对象到所有的GC Roots没有任何引用链相连时,则说明该对象时不可达的,即判定为该对象已死。

![](https://img2018.cnblogs.com/blog/1608161/201904/1608161-20190408140340351-384107365.png)

  实际上这相当于图论的知识,从一些对象出发一定可以访问到其他所有的对象,如果没有任何路径可达,那么说明这些对象无法访问到,就应该被回收。

  在java语言中,可作为GC Roots的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(一般说的Native方法)引用的对象

(3)对象的自我拯救

  java使用可达性分析算法来判定哪些对象可以回收,但是并不是说可达性分析算法中不可到达的对象都“非死不可”,这涉及到垃圾回收的另一个机制——对象的自我拯救

  要真正回收一个对象,要经历两次标记过程第一次是在可达性分析算法中发现没有和GC Roots相连的引用链,被判定为不可达;第二次是判定此对象有没有必要执行finalize方法,对象可以在finalize方法中通过重新和引用链上的其他对象进行关联来拯救自己。

  所谓的是否有必要执行指得是:对象是否覆盖了finalize方法;该对象的finalize方法是否已经被调用过。当对象没有覆盖或者已经执行过finalize方法时,认为没有必要执行,则回收该对象。

  总结起来,就是finalize方法为对象提供了一次可以逃脱死亡命运的机会。但是,需要注意的是:机会只有一次,因为任何一个对象的finalize方法只可能会被系统调用一次。

(4)方法区的回收

  我们之前说过,方法区也称为永久代,这块区域也可以不实现垃圾回收,回收的性价比也很低,因此虚拟机往往把这块区域回收与否设置为参数,可以通过不同的参数来指定。

  那么要实现这块区域的回收,需要主要考虑的是两部分内容:废弃的常量和无用的类

  判定一个常量是不是无用常量比较简单,以字符串为例,比如常量池中有一个字符串“abc”,当没有任何String类型的对象引用“abc”这个常量时,就可以对其进行清理。

  判定一个类是否为无用的类比较复杂,需要考虑以下三个方面:

  • Java堆中不存在该类的任何实例对象
  • 加载该类的ClassLoader已经被回收
  • 该类对应的Class对象没有在任何地方引用,无法在任何地方通过反射访问该类的方法。

3. 垃圾收集算法(如何回收)

  在确定了哪些对象需要回收之后,就需要采用一定的算法对可回收的那部分内存进行清理,各个平台的虚拟机操作内存的方法有所不同,垃圾清理的算法实现也各不相同。

(1)标记-清除算法

  标记-清除算法是最基础的算法,顾名思义,此算法分为标记和清除两个阶段。标记阶段正如前面所介绍的那样,通过可达性分析和是否有必要执行finalize方法来标记出所有需要回收的对象,标记完成后直接回收可以回收的空间。

![](https://img2018.cnblogs.com/blog/1608161/201904/1608161-20190408140400455-2135959073.png)

  然而,这种算法有两个很明显的不足之处:一是效率问题,标记和清除两个过程的效率都不高。二是空间问题,也就是内存碎片化问题,清除之后产生大量不连续的内存碎片,可能会导致以后需要分配较大空间时,无法找到足够的连续内存。

(2)复制算法

  为了解决效率问题,提出了“复制算法”。复制算法的基本思想是:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完,将还存活的对象复制到另外一块上面,然后把使用过的那一块空间一次性清理掉。这样,每次回收都是对整个半区进行回收,不会出现碎片问题,实现简单,运行高效,但代价是将可用内存缩小为一半。

![](https://img2018.cnblogs.com/blog/1608161/201904/1608161-20190408140410865-209509919.jpg)

  现在的虚拟机大都采用复制算法来回收新生代。有研究表明,新生代中的对象98%是“朝生夕死”的,所以不需要按1:1来划分内存,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间,当回收时,将Eden和Survivor空间中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才使用的Survivor空间。HotSpot默认Eden:Survivor=8:1,这样只会浪费10%的空间。

  当然,这里有一个问题就是:无法保证每次回收存活的对象不会超过10%,这种情况下,需要依赖老年代的分配担保机制来进行,将大对象直接分配到老年代。

  因此,总结起来,复制算法的不足之处在于:当对象存活率较高时会进行大量的复制操作,使得效率变低;另外,它会浪费一部分的内存空间,还需要额外的空间来进行分配担保。

(3)标记-整理算法

  老年代由于对象存活率较高,因此一般不采用复制算法,针对老年代的特点,有人提出了另外一种“标记-整理”算法。

  “标记-整理”算法基本思想是:类似于“标记-清除”,所不同的是,在回收时不是直接清除,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

![](https://img2018.cnblogs.com/blog/1608161/201904/1608161-20190408140421479-1807306949.png)

(4)分代收集算法

  当前的商业虚拟机大都采用分代收集算法,将堆内存分为新生代和老年代,根据各个年代不同特点采用适当的算法。新生代对象存活率低,选用复制算法,而老年代对象存活率高,选用“标记-清除”或者“标记-整理”算法

4. 内存分配与回收策略

  Java技术体系中的自动内存管理机制归结起来就是完成两个任务的自动化:内存动态分配和垃圾自动回收。垃圾自动回收再解决了哪些内存需要回收、什么时候回收、如何回收这三个问题后,已经有了较为清晰的理解。

  对于内存动态分配需要理解以下几点:

  • 对象优先在新生代的Eden区分配
  • 大对象直接进入老年代(需要大量连续空间的对象)
  • 长期存活的对象将进入老年代(每个对象有一个年龄计数器,被移动到Survivor空间的存活对象年龄为1,每熬过一次GC,年龄增加一岁,增加到一定程度(默认15岁)后进入老年代)
  • 动态对象年龄判定(Survivor空间中相同年龄所有对象大小总和超过Survivor空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代)
  • 空间分配担保(老年代为Survivor空间无法容纳的对象进行分配担保,首先需要保证自己有足够的空间,虚拟机会检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小(相当于一个经验值),如果不满足,就先进行一次Full GC来腾出更多的空间)
博学 审问 慎思 明辨 笃行
原文地址:https://www.cnblogs.com/gzshan/p/10670134.html