JVM垃圾回收算法

JVM垃圾回收

一、哪些内存需要回收

  • 程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,不用垃圾收集器回收。
  • 回收对象主要针对堆内存空间。
  • 二、如何判断内存可回收

    垃圾收集器在对堆进行回收前,要判断堆中的对象哪些还存活,哪些可以回收,使用的方法有:

    引用计数法

    原理:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器加1,当引用失效时,计数器减1,当计数器值为0时表示该对象不再被使用。需要注意的是:主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

    可达性分析

    基本思路:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的。

    可达性分析算法判定对象是否可回收

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

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。
  • 判断对象是否被引用,java将引用分为:

    强引用(Strong Reference):类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

    软引用(Soft Reference):软引用是用来描述一些还有用但并非必需的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。

    弱引用(Weak Reference):弱引用也是用来描述非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前

    虚引用(Phantom Reference):为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

    三、常用的垃圾收集算法

    3.1 标记-清除算法(Mark-Sweep)——会有内存碎片

    这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。算法分为两个阶段:标记阶段和清除阶段

  • 标记阶段:标记出所有需要被回收的对象,采用从根集合(GC Roots)进行扫描。
  • 清除阶段:扫描整个空间中被标记的对象,进行回收。
  • 标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效。回收过程如下:

    标记-清除算法

    它的主要不足有两个:

  • 一个是效率问题,标记和清除两个过程的效率都不高;
  • 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
  • 3.2 标记-复制算法(Copying),简称复制算法-【新生代使用】

    将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。垃圾回收时,将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

    这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单。但是这种算法将内存缩小为了原来的一半,代价太高。

    复制算法

    新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间,而是将新生代分为一块较大的Eden空间和两块较小的Survivor空间。每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。

    HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1:1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。

    很显然,复制算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么复制算法的效率将会大大降低

    (1)为什么需要2个Survivor,不能只有1个吗?

    因为每次GC时的操作对象都是Eden和其中一块Survivor,把这两块的存活对象copy到s1区,清空Eden和S0,再将s1区copy到s0,保证每次GC都是针对Eden和一个survivor区。

    (2)为对象分配内存时,对象也会分配到Survivor区吗?

    在为对象分配内存时,对象除了会分配到Eden区,同时也会分配到Survivor中的一个区,因为Eden区满了的话,可以在其中一个Survivor区中分配空间。

    3.3 标记-整理(压缩)算法(Mark-compact)——【老年代使用】

    为了解决复制算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。由于进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。

    标记-整理算法

    3.4 分代收集算法 Generational Collection(分代收集)算法

    分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。

  • 新生代的特点是每次垃圾回收时都有大量的对象需要被回收,一般使用标记-复制算法。
  • 老年代的特点是每次垃圾收集时只有少量对象需要被回收,一般使用的是标记-整理算法。
  • 1 )新生代(Young Generation)的回收算法 (标记-复制算法)

  • 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)当Eden没有足够空间的时候就会发起一次Minor GC(将Eden和survivor0存活的对象存放到survivor1)。
  • 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
  • 2)年老代(Old Generation)的回收算法(标记-整理算法)

  • 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
  • 当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
  • 3) 持久代(Permanent Generation)(也就是方法区)的回收算法

    用于存放静态文件,如Java类、方法等。垃圾回收对持久代没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。

    方法区存储内容是否需要回收的判断与堆的判断方式不一样。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  • 在JDK1.8中堆内存已经移除了方法区,转而使用元空间替代方法区,元空间在直接内存中。

    3.5 垃圾回收分类

    GC类型

    GC算法

    触发条件

    Young GC

    (Minor GC)

    标记复制算法

    对新生代(Eden+1块Survivor区)进行回收,Minor GC 非常频繁

  • Eden区满时
  • 为对象分配Eden区,而Eden区不足时
  • Old GC

    (Major GC)

    标记-整理算法

    对老年代进行回收

  • 老年代空间不足、方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  • Major GC发生过程常常伴随一次Minor GC
  • Full GC

    (Minor GC+

    Major GC)

    对整个堆进行回收

  • 调用System.gc时,系统建议执行Full GC,但是不必然执行
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  • Full GC的速度一般会比 Minor GC慢10倍以上
  • Mixed GC

    收集整个新生代,和部分老年代

    只有G1有这个模式

    四、内存分配与回收策略

    对象优先在Eden分配

    当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。当Survivor空间不够用时,需要依赖老年代进行分配担保。

    大对象直接进入老年代

    所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

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

    当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。

    动态对象年龄判定

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

    空间分配担保

    Minor GC之前,需要老年代进行分配担保,虚拟机会先检查以下两项确保Minor GC可以执行。

  • 老年代最大可用连续空间是否大于发生GG时新生代所有对象总空间。
  • 计算每次晋升到老年代的平均大小,判断老年代可用空间是否大于平均晋升大小。
  • 如果条件不满足,会进行一次Full GC。

    参考书籍

    《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》

    声明:本站部分文章内容及图片转载于互联 、内容不代表本站观点,如有内容涉及侵权,请您立即联系本站处理,非常感谢!

    (0)
    上一篇 2022年8月2日
    下一篇 2022年8月2日

    相关推荐