深入理解JVM——垃圾回收与内存分配机制详细讲解

所谓垃圾回收,也就是要回收已经“死了”的对象。

那我们如何判断哪些对象“存活”,哪些已经“死去”呢?

一、判断对象已死

1、引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。

但是在Java虚拟机里面没有选用引用计数算法来管理内存。

优点:实现简单,效率高。

缺点:很难解决对象之间相互循环引用的问题。

2、可达性分析算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

如图,object5、object6、object7 为可回收对象

深入理解JVM——垃圾回收与内存分配机制详细讲解_第1张图片

主流的Java虚拟机使用可达性分析算法

关于GC Roots:

在Java语言中,GC Roots包括以下几种:

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

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可 达,判定对象是否存活都和“引用”离不开关系。

引用分为四种:

  • 强引用:代码中普遍存在,垃圾收集器不会回收强引用的对象。比如new Object()
  • 软引用:有用但非必需,在系统将要发生内存溢出异常OOM之前,会把这些对象列入回收范围,进行回收。
  • 弱引用:非必需,无论当前内存是否足够,下次垃圾回收都会回收掉这些对象。
  • 虚引用:最弱的引用关系,是否有虚引用不对其生存时间构成影响。相当于什么时候回收都没问题,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

二、三种垃圾回收算法

1、标记-清除算法

最基础的收集算法——“标记-清除”(Mark-Sweep)算法。一般用于老年代

算法分为“标记”和“清除”两个阶段:

  1. 标记出需要回收的对象
  2. 清除被标记的对象

缺点:

  • 当有大量对象等待被回收,此时就需要大量的标记和清除操作,导致两个过程的效率随对象数量增长而降低,执行效率不稳定
  • 标记,清除后会产生大量不连续的内存碎片,导致空间碎片化问题

深入理解JVM——垃圾回收与内存分配机制详细讲解_第2张图片

2、标记-复制算法

为了解决效率问题,标记-复制算法出现了。他将内存分为两块,每次只使用其中一块,当一块内存用完了,就将还存活的对象复制到另一块上面,然后把已经使用过的内存空间一次性清理掉。

缺点:内存缩小为原来的一半。

深入理解JVM——垃圾回收与内存分配机制详细讲解_第3张图片

但是在新生代的内存划分中,研究表明,有98%的对象熬不过第一轮收集,因此没必要采用1:1的内存划分。针对这种情况,产生了半区分代策略。是把新生代分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍 然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空 间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1 。如果另外一块 Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直 接进入老年代,这对虚拟机来说就是安全的。

3、标记-整理算法

在Mark-Sweep算法的基础上做了改良,用于解决空间碎片化问题。标记-整理(Mark-Compact)算法在标记后不是简单做清除,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。一般用于老年代。

优点:解决了空间碎片化问题,为后续内存分配和访问提高效率

缺点:使内存回收的过程更加复杂。如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方必须全程暂停用户应用程序才能进行。

深入理解JVM——垃圾回收与内存分配机制详细讲解_第4张图片

三、安全点和安全区域

安全点

在做可达性分析时,需要保持分析期间整个系统不会发生变化,这就导致GC进行时必须停顿所有Java执行线程(Stop The World),即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也必须要停顿。

程序执行时并非在所有地方都能停下来开始GC,只有在到达安全点(Safepoint)时才能暂停。Safepoint 的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的,例如方法调用,循环跳转,异常跳转等。

如何在GC发生时让线程都跑到最近的安全点再停顿下来?

  • 抢先试中断:先把所有线程中断,发现不在安全点的线程恢复线程,让它跑到安全点。
  • 主动式中断:设置一个不可读的内存位置作为中断标志,标志与安全点重合,当线程执行到这个标志时自己中断挂起。

安全区域

安全区域(Safe Region)是指在一段代码片段中,引用关系不会发生变化。在这个区域的任何地方开始GC都是安全的。典型的安全区域比如线程处于Sleep状态或者Blocked状态。

在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region。当要发起GC时,就不用管标识为Safe Region状态的线程了。当线程要离开Safe Region时,要检查是否处于GC状态,如果是,就要继续等待,直到收到可以安全离开Safe Region的信号为止。

你可能感兴趣的:(JVM,jvm)