《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

时间:2024-01-14 14:46:02

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了,而java堆和方法区则不一样,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

一、判断对象是否存活的方法

1.引用计数法(主流java虚拟机并没有采用这种方式)

所谓引用计数法,就是当引用对象时就+1,当引用失效时,计数器值就-1,任何时候当计数值为0的对象就是不可能在被使用了,为什么java虚拟机不用这个呢,因为这个无法解决对象互相循环引用问题。

例:对象A和对象B

A.instance=B;

B.instance=A;

除此之外就无对对象的操作,而此时无法用引用计数法来进行回收他们。

2.可达性分析算法(在主流的商用程序语言都是通过此来判断对象是否存活的)

算法思想:通过一系列的称为GC Roots对象作为起始点,从这些节点向下搜索,搜索所连接的路径称为引用链,当发现有对象与GC Roots对象无法通过引用链相连,则证明这些对象是不可用的,所以它们将被判定为是可回收的对象。

《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

注意:在Java中,可作为GC Roots对象包括下面几种:

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

二、再谈引用

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。

  1. 强引用:类似于Object obj =new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  2. 软引用:是描述一类还可用但非必需的对象,在内存还充足时保存,当将要发生内存溢出异常之前,将会对这些对象进行第二次回收,如果回收后还没有足够的内存,才会抛出内存溢出异常。
  3. 弱引用:非必需对象,在进行垃圾回收时,都会回收掉这些对象。
  4. 虚引用:最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例,虚引用唯一目的就是当对象被收集器回收时收到一个通知。

三、生存还是死亡

要真正宣告对象的死亡,要经过两次标记过程;如果对象在进行可达性分析后没有与GC Roots相连接的引用链,那么它将被第一次标记并且进行一次筛选,筛选原则时finalize()方法是否有必要被执行,如果有必要则进入F-Queue队列,再队列中finalize()方法是对象逃脱死亡的最后一次机会,稍后GC会对队列进行第二次标记,如果对象要在finalize()方法中拯救自己——只要重新与引用链上的任意对象关联即可,如果没有关联,则死亡,注意finalize()方法只会被执行一次触发一次。

四、回收方法区

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

判断一个常类是否是废弃常量比较简单,而判断一个类是否是废弃类则需要同时满足三个条件才能算是“无用的类”:

  1. 该类的所有实例都已经回收。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

五、垃圾收集算法

标记-清除算法

算法分为标记-清除两个阶段,首先标记出所有需要回收的对象,而在标记完成后统一回收所有被标记的对象。

不足:首先效率问题,效率都不高,其次容易造成内存碎片,当需要分配较大对象时,无法找到足够的连续内存而不得不触发另一次垃圾收集动作。

《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

复制算法

为了解决效率问题,复制算法是将内存空间分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象赋值到另一块上,在把这个内存空间清理一次。有点不需要考虑内存碎片问题,高效,缺点内存缩小到原来的一半,代价有点大。

《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率会贬低,有人提出标记-整理算法:标记过程与标记清除算法相同,但后续不是清除,而是将对象向某一边移动,然后清理出端边界以外的内存。

《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

分代收集算法

当前商业虚拟机都采用该方法,该方法一般根据对象存活周期的不同分成几块,java堆一般分成新生代和老年代,新生代朝生夕死,只存在少量的存活对象则采用复制算法,移动少量存活对象,而老年代则存活大部分对象,没有额外空间对它进行分配担保,则采用标记-清理算法或者标记-整理算法进行回收。

六、安全点

如果直接用GC Roots直接找引用链的话需要进行枚举,必然会消耗很多时间,所以采用OopMap的数据结构再特定的位置记录下那些地方存放着对象引用,而这些特定的位置称之为安全点,程序执行时并非再所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。

安全点停顿

如何在GC发生时,所有的线程都跑到最近的安全点上再停顿下来。两种方案:抢先式中断和主动式中断。

其中抢先式中断不需要线程的执行代码主动去配合,而是在GC发生时,中断所有线程,判断如果中断点不在安全点则恢复线程,让它跑到安全点中断,现在几乎没有虚拟机采用此方法暂停线程。

而主动式中断思想是建立标志位,当标志位为真时,就将自己挂起,标志位的设置与安全点是重合的。

安全区域

安全点看似已经解决进入GC的问题,但是当线程不执行的时候呢(处于Sleep状态),就无法响应JVM的中断请求,这种时候就需要安全区域(Safe Region)来解决。

安全区域是指在一段代码之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,我们也可以把Safe Region看做是被扩展了的Safepoint。

七、垃圾收集器

如果说收集方法是内存回收的方法论,而垃圾收集器就是内存回收的具体实现。

《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

如图所示展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们可以搭配使用。

Serial收集器

是最基本、发展历史最悠久的收集器,属于单线程收集器,当进行收集时必须暂停其他线程,这对很多应用来说是很难接受的。

《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本。

《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

Parallel Scavenge收集器

此收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点时尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

停顿时间越短就越适合用户交互程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Serial Old收集器

是Serial收集器的老年代版本。

《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法

《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

重点:CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。特别适用于服务的交互服务上。

是基于“标记-清除”算法实现的,整个过程分为4个步骤

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

CMS收集器缺点

CMS收集器对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感。

CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。

CMS是一款基于“标记-清除”算法实现的收集器,则会产生大量的空间碎片,将会给大对象分配带来很大麻烦。

G1收集器

是当今收集器技术发展的最前沿成果之一,是一款面向服务端应用的垃圾收集器。

具备以下特点:

  1. 并行与并发
  2. 分代收集
  3. 空间整合
  4. 可预测的停顿

G1收集器的运作大致可划分为以下几个步骤

初始标记

并发标记

最终标记

筛选回收

《深入理解Java虚拟机》之(二、垃圾收集器与内存分配策略)

八、内存分配与回收策略

对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的。

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说是一个坏消息。

长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生,哪些对象应放在老年代中。通过Minor GC来进行确定,建立一个对象年龄计数器,每进行一次Minor GC如果对象依然存在,则该对象的年龄增加一岁。当年龄达到某一个阈值时,就进入老年代中。

动态对象年龄判断

为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。