[笔记]Java虚拟机垃圾回收的原理是“引用计数”吗?

来源:互联网 发布:淘宝有没有hm旗舰店 编辑:程序博客网 时间:2024/05/25 08:14

  迷茫时,能遇见一本好书,并且能够静下心把它读完,那真的是一件愉快和值得纪念的事。

  2017年2月7日-2017年3月16日,读完《深入理解Java虚拟机》第2版(周志明著)。接下来这几篇,我将会把印象深刻的几个知识点总结下来,权当用做日后复习。

  看这本书之前,每当提到虚拟机的垃圾回收,我能说的应该就是这么一句:是通过引用计数来实现的,当一个对象的引用计数为0时,虚拟机就会将之回收。

  在《深入理解Java虚拟机》这本书中,我才发现了真解: 引用计数法(Reference Counting),并没有被主流Java虚拟机用来管理内存,最主要的原因是它很难解决对象之间相互循环引用的问题。在主流的实现中,是通过可达性分析(Reachability Analysis)来判定对象是否存活的。

----------下面将进入正文----------

1.主流的虚拟机有三种:

Sun HotSpot、BEA JRocket、IBM J9。

其中HotSpot VM是目前使用范围最广的Java虚拟机。

2.垃圾收集(Garbage Collection,GC)的实现:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

下面将一一解答。

2.1.哪些内存需要回收

答案是:Java堆是垃圾收集器管理的主要区域,因此很多时候Java堆也被称作“GC堆”(Garbage Collected Heap)。Java虚拟机规范中不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的“性价比”一般比较低:在堆中,尤其在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而方法区的垃圾收集效率远低于此。

下面来看一下Java虚拟机内存区域的划分:


如图所示,Java虚拟机运行时数据区主要有5部分组成。其中,方法区和堆是所有线程共享的数据区,其他的三个是线程私有的区域。

Java堆:是Java虚拟机所管理的内存中最大的一块,被所有线程共享。在虚拟机启动时创建。用于存放对象实例,几乎所有的对象实例都在这里分配内存。

由于现代收集器基本都用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;新生代可以再细致分为:Eden空间、From Survivor空间、To Survivor空间。

方法区:与Java堆一样,也是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在HotSpot虚拟机上,很多人更愿意把方法区称为“永久代”(Permanent Generation)。方法区中的垃圾收集主要回收两部分内容:废弃常量和无用的类。

2.2.什么时候回收?

书中没有明确说。根据书中的例子,应该是在创建新对象时如果发现内存不足,则虚拟机会主动进行GC;

猜想应该还有一种触发方式,即收集器会设定一个阈值,当某个区域的内存达到或者超过这个阈值时,主动进行GC;

平时编程中我们常用的手动触发GC的方法是:在代码中调用System.gc()方法。

这里讲一下Minor GC和Full GC:

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多数都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC/Full GC):指发生在老年代的GC。出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge)收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

2.3.如何回收?

2.3.1.查找并标记

  • 问题1:标记的方法

答:使用可达性分析(Reachability Analysis)来判定对象是否存活。这个算法的基本思想是:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。这里引出了两个概念:

    • 什么是GC Roots对象:

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

·虚拟机栈(栈帧中的本地变量表)中引用的对象

·方法区中类静态属性引用的对象

·方法区中常量引用的对象

·本地方法栈中JNI(即一般说的Native方法)引用的对象

    • 什么是引用

在JDK1.2之前,引用的定义是:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存有一个引用。在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种。

  • 问题2:被标记的对象意味着死亡了吗?

答:答案是否的。要宣告一个对象死亡,至少要经历两次标记过程:步骤1.可达性分析后发现没有与GC Roots的引用链,则被第一次标记;步骤2.然后进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法;步骤3.如果有必要执行,则这个对象将会被放置在一个叫做F-Queue的队列中;步骤4.稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()方法中成功自救了,则将它移除出“即将回收”的集合,否则就要回收它。

这里补充两点:

1.判定finalize()方法没有必要执行的逻辑是:对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过。

2.对象在finalize()里自救的方法:将自己重新与引用链上的任何一个对象建立关联即可,譬如把自己赋值给某个类变量或者对象的成员变量。

  • 问题3:标记过程遇到的问题

答:在枚举根节点时GC必须停顿所有Java执行线程,Sun将这件事情称为“Stop The World”。缘由:可达性分析工作必须在一个能确保一致性的快照中进行--这里的“一致性”的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断发生变化的情况,这点不满足的话分析结果就无法得到保证。

由于目前的主流Java虚拟机使用的都是准确式GC,当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内的什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样GC扫描时就可以直接得知这些信息了。HotSpot并没有为每条指令都生成OopMap,前面已经提到,只在“特定的位置”记录了这些信息,这些位置就称为安全点,即程序执行时并非在所有位置都能停顿下来开始GC,只有到达安全点时才能暂停。

    • 安全点(SafePoint)

安全点的选定标准是:是否具有让程序长时间执行的特征。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生安全点。

虚拟机现在采用的都是主动式中断(Voluntary Suspension)来暂停线程从而响应GC事件。实现:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

    • 安全区(Safe Region)

安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域的任何地方开始GC都是安全的。在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,这样当在这段时间里JVM要发起GC时,就不用管这些线程了。当线程要离开Safe Region时,它要检查系统是否完成了根节点枚举,没完成的话它就必须等待直到收到可以安全离开Safe Region的信号为止。

2.3.2.垃圾收集算法

标记-清除算法(Mark-Sweep):最基础的收集算法,分为“标记”和“清除”两个阶段。主要不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,在后续要分配较大对象时,如果找不到足够的连续内存则会导致触发另一次垃圾收集动作。

复制算法(Copying):将内存按1:1比例进行分块,每次只用其中的一块。当这块内存用完了,就将还存活的对象复制到另外一块上面,然后再把该块内存上已使用过的内存空间一次清理掉。

HotSpot虚拟机将内存分为一块较大的Eden空间和两块较小的Survivor空间。默认Eden和Survivor的大小比例是8:1,也就是整个新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

标记-整理算法(Mark-Compact):标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法(Generational Collection):当前商业虚拟机的垃圾收集都采用这种算法,根据对象存活周期不同将内存划分为几块。一般是把Java堆分为新生代和老年代,在新生代中选用复制算法,在老年代中使用“标记-清理”或者“标记-整理”算法来进行回收。

2.3.3.常用垃圾收集器

垃圾收集器是内存回收的具体实现。下图中有7种作用于不同分代的收集器,两个收集器之间存在连线,就说明它们可以搭配使用。其中,Serial收集器是最基本、发展历史最悠久的收集器。G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一。 

0 0