05.垃圾回收机制

1. 自动垃圾回收

1.1 c/c++内存管理

  • 在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏。我们称这种释放对象的过程叫做垃圾回收,而需要程序员编写代码进行回收的方式为手动回收
  • 内存泄漏指的是不再使用的对象再系统中未被回收,内存泄漏的积累可能会导致内存溢出。

1.2 java的内存管理

  • java中为了简化对象的释放,引入了自动的垃圾回收机制。通过垃圾回收器来对不在使用的对象完成自动的回收。垃圾回收器主要负责对堆上的内存进行回收。其他很多现代语言比如C#、Python、Go都拥有自己的垃圾回收器
  • 应用场景

2. 方法区的回收

  • 线程不共享部分(程序计数器、java虚拟机栈、本地方法栈),都是伴随着线程的创建而创建,线程的销毁而销毁。而方法的栈帧再执行完方法之后就会自动弹出栈并释放对应的内存
  • 方法区的回收
    • 方法区的回收的主要内容就是不在使用的类
    • 判定一个类可以卸载。需要同时满足三个条件
      • 此类的所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象
      • 加载该类的类加载器已经被回收
      • 该类对应的java.lang.Class对象没有在任何地方被引用

3. 堆回收

3.1 引用计数法和可达性分析法

  • 如何判断一个对象是否可以被回收?
    • Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在被使用,不允许回收

  • 只有无法通过引用获取到对象时,该对象才会被回收。图片中A的实例对象要回收,有两个引用要去除:
    • 栈中a1变量到对象的引用
    • B对象到A对象的引用

3.1.1 引用计数法

  • 引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1

  • 引用计数法的优点是实现简单,C++中的只能指针就采用了引用计数法,但是同时也存在缺点,主要有两点

    • 每次引用和取消引用都需要维护引用计数器,对系统性能有一定的影响

    • 存在循环引用的问题,所谓循环引用就是当A引用B,B同时引用A会出现对象无法回收的问题

3.1.2 可达性分析算法

  • Java使用的是可达性分析算法来判断对象是否可以被回收的。可达性分析对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系
  • 下图中A到B再到C和D,形成了一个引用链,可达性分析算法指的是如果从某个GC Root对象是可达的,对象就不可被回收

哪些对象是GC Root对象

  1. 线程Thread对象
  2. 系统类加载器加载的java.lang.Class对象
  3. 监视器对象,用来保存同步锁synchronized关键字持有的对象
  4. 本地方法调用时使用的全局对象

3.1.3 查看GC Root

  • 通过arthas和Eclipse Memory Analyzer工具可以查看GC Root,MAT工具是Eclipse推出的java堆内存检测工具,具体步骤如下
    • 使用arthas的heapdump命令将堆内存快照保存到本地磁盘中
    • 使用MAT工具打开堆内存快照文件
    • 选择GC Roots功能查看所有的GC Root

3.2 五种对象的引用

  • 可达性算法中描述的对象引用,即是GC Root对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收,除了强引用之外,Java中还设计了集中其他引用方式
    • 软引用
    • 弱引用
    • 虚引用
    • 终结器引用

3.2.1 软引用

  • 软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收
  • 在JDK1.2之后提供了SoftReference类来实现软引用,软引用常用于缓存中

  • 软引用的执行过程如下
    • 将对象使用软引用包装起来,new SoftReference(对象)
    • 内存不足是,虚拟机尝试进行垃圾回收
    • 如果垃圾回收仍然不能解决内存不足的问题,回收软引用对象
    • 如果软引用被回收后问题仍然没有解决,抛出异常

模拟:将jvm虚拟机最大内存-Xmx200M -Xms200M,创建软引用对象存放200M的对象查看最终结果

  • 由于当前线程对象GC Root没有引用指向byte对象,强引用被断开,只剩下软引用,当内存超过最大内存时,软引用被回收。
  • 软引用中的对象在内存不足时回收,SoftReference对象本身也需要被回收,如何知道哪些SoftReference对象需要回收呢?
    • SoftReference提供了一套队列机制
      • 软引用创建时,通过构造器传入引用队列
      • 在软引用中包含的对像被回收时,该软引用对象会被放入引用队列
      • 通过代码遍历引用队列,将SoftReference的强引用删除
  • 获取软引用回收对象

3.2.2 弱引用

  • 弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接会被回收

  • 在JDK1.2版本之后提供了WeakReference类来实现弱引用,弱引用主要是在ThreadLocal中使用

  • 弱引用对象本身也可以使用引用队列进行回收

3.2.3 虚引用

  • 虚引用与终结器引用在开发中是不会被使用的
  • 虚引用也叫幽灵引用/欢迎引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现

3.2.4 终结器引用

  • 终结器引用指的是在对象需要被回收时,对象将会被放置在Finalizer类中的引用队列中,并在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该对象才真正的被回收。在这个过程中可以在finlaize方法中再将自身对象使用强引用关联上,但是不建议这样做,如果耗时过长会影响其他对象的回收

3.3 垃圾回收算法

3.3.1 核心思想

  • Java是如何实现垃圾回收的呢?简单来说,垃圾回收要做的有两件事

    • 找到内存中存活的对象

    • 释放不再存货的对象的内存,使得程序能再次利用这部分空间

3.3.2 垃圾回收算法的分类

  • Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用

上述图片因为垃圾回收导致任务执行时间过长。

垃圾回收算法的评价标准

  • 吞吐量:吞吐量指的是CPU用于执行用户代码的时间与CPU总执行时间的比值,即吞吐量=执行用户代码的时间/(执行用户代码的时间+GC时间)。吞吐量数值越高,垃圾回收效率越高
  • 最大暂停时间:指的是所有在垃圾回收过程中STW时间最大值。比如下图中,黄色部分的STW就是最大的暂停时间。显而易见上面的图片比下面的图拥有更少的最大暂停时间。最大暂停时间越短,用户使用系统时受到的影响越短

  • 堆使用效率:不同垃圾回收算法,堆堆内存的使用方式时不同的。比如标记清除算法,可以使用完整的堆内存。而复制算法会将堆内存一分为二。每次只能使用一般内存。从堆使用效率上来说,标记清除算法要优于复制算法
  • 一般来说堆内存越大,最大暂停时间就越大,以及最大暂停时间不可兼得。
  • 不同的垃圾回收算法,适用于不同的场景。
3.3.2.1 标记-清除算法
  • 核心思想:
    • 标记阶段:将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象
    • 清除阶段:从内存中删除没有被标记的,也就是非存活的对象
  • 优点:
    • 实现简单,只需要在第一阶段给每个对象维护一个标志位,第二个阶段删除对象即可
  • 缺点
    • 内存碎片化问题:由于内存是连续的。所以在对象被删除后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很可能这些内存单元的大小过小无法进行分配

3.3.2.2 复制算法
  • 核心思想:
    • 准备两块空间 From空间和To空间,每次在对象分配阶段,只能使用其中一块空间 (From空间)
    • 在垃圾回收GC阶段,将From中存活对象复制到To空间
    • 将两块空间的From和To 名字互换
  • 优点:
    • 吞吐量高
    • 不会发生碎片化
  • 缺点:
    • 内存使用效率低
3.3.2.3 标记-整理算法
  • 核心思想:也叫标记压缩算法,是对标记-清除算法中容易产生内存碎片问题的一种解决方案
    • 标记阶段:将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象
    • 整理阶段:将存活对象移动到堆的另一端,清理掉存活对象的内存空间

  • 优点:
    • 内存使用率高
    • 不会发生碎片化
  • 缺点:
    • 整理阶段效率不高
3.3.2.4 分代GC
  • 现代的优秀垃圾回收算法,会将上述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)
  • 分代垃圾回收将整个内存区域划分为 年轻代 和 老年代 。

  • 查看分代之后的内存情况
    • JDK8中,添加jvm参数 -XX:+UseSerialGC参数使用分代回收的垃圾回收器,运行程序
    • 在arthas中使用memory查看命令参数,显示出三个区域的内存情况
  • JVM参数

参数名

参数含义

示例

-Xms

堆内存初始大小total,必须是1024倍数且大于1MB

-Xms1024

-Xms1g

-Xmx

堆最大大小max,必须是1024的倍数且大于2MB

-Xmx1M

-Xmx81920k

-Xmn

新生代大小

-Xmn256m

-XX:SurvivorRatio

伊甸园区和幸存者区比例,默认是8,即1g内存,伊甸园区800M、S0和S1分别为100M

-XX:SurvivorRatio=4

-XX:+PrintGCDetails

verbose:GC

打印GC日志

-

  • 算法核心
    • 分代回收时,创建出来的对象,首先会放入Eden伊甸园区
    • Young GC
      • 随着对象在伊甸园区越来越多,如果伊甸园区满,新创建的对象无法放入,就会触发年轻代GC,称为Minor GC或者Young GC。
      • Young GC会为对象记录他的年龄,初始年龄为0,每次GC都会加1 。Young GC采用复制算法
      • GC时会把伊甸园区与from中需要回收的对象回收,把没有回收的对象放入To区
      • 接下来S0会变成To区,S1变成From区。当伊甸园区满时再往里放入对象,依然会发生Minor GC
      • 随着Young GC的进行,当对象的年龄达到阈值(最大15,默认值与垃圾回收器有关),对象就会被晋升为老年代
    • 当老年代中空间不足,无法放入新的对象时,首先尝试Minor GC,如果Minor GC后内存空间依旧不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收
    • 如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代的时候,就会抛出异常OutOfMemory

案例:下面的程序为什么会出现OutOfMemory

3.4 垃圾回收器

  • 为什么分代GC算法要把堆分成年轻代和老年代
    • 系统中的大部分对象,就是创建出来之后很快就不再使用可以被回收,比如用户获取的订单数据,订单数据返回给用户后就可以被释放
    • 老年代中会存放长期存活的对象,比如Spring中的大部分bean对象,在程序启动之后就不会被回收了
    • 在虚拟机的默认设置中,新生代的大小要远小于老年代大小

  • 分代GC算法将堆分成年轻代和老年代的主要原因有
    • 可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率
    • 新生代和老年代使用不同的垃圾回收算法,新生代一般采用复制算法,老年代可以选择标记清除和标记整理算法,由程序员来灵活选择
    • 分代设计中允许只回收新生代,如果满足对象分配的要求就不需要对整个堆进行回收,STW的时间就会减少

3.4.1 垃圾回收器的种类

  • 垃圾回收器时垃圾回收算法的具体实现
  • 由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用,具体关系如下

3.4.1.1 Serial+Serial Old
  • Serial
    • Serial是一种单线程回收年轻代的垃圾回收器
    • 回收年代:年轻代
    • 回收算法:复制算法
    • 优点:
      • 单CPU情况下吞吐量非常出色
    • 缺点:
      • 多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间等待
    • 适用场景
      • java编写的客户端程序或者硬件配置有限的场景

  • Serial Old
    • Serial Old 是一种单线程串行回收老年代的垃圾回收器
    • 回收年代:老年代
    • 回收算法:标记整理算法
    • 优点:
      • 单CPU下吞吐量非常出色
    • 缺点:
      • 多CPU情况下不如其他垃圾回收器,堆内存过大的话会让用户线程长时间等待
    • 适用场景:
      • 与Serial年轻代垃圾回收器搭配使用
  • 如何开启Serial +Serial Old 垃圾回收器
    • -XX:+UseSerialGC
    • 新生代、老年代都会使用单线程垃圾回收器
3.4.1.2 ParNew+Cms
  • ParNew
    • ParNew垃圾回收器本质上是对Serial在多CPU下进行优化,使用多线程进行年轻代垃圾回收
    • 回收年代:年轻代
    • 回收算法:复制算法
    • 优点
      • 多CPU处理器下停顿时间较短
    • 缺点
      • 吞吐量和停顿时间不如G1,所以在JDK9之后不建议使用
    • 适用场景:在JDK8及之前的版本中,与CMS老年代垃圾回收器搭配使用
    • 如何开启ParNew
      • -XX:+UseParNewGC
      • 新生代使用ParNew,老年代使用串行垃圾回收器

  • CMS
    • CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同步执行,减少了用户线程的等待时间
    • 回收年代:老年代
    • 回收算法:标记清除算法
    • 优点:
      • 系统由于垃圾回收时间暂停的时间较短,用户体验好
    • 缺点
      • 内存碎片化问题:CMS采用了标记清除算法,在垃圾回收后会出现大量的内存碎片,CMS会在Full GC时进行碎片的清理,这样会导致用户线程暂停,可以使用参数-XX:CMSFullGCsBefore Compaction=N参数(默认是0) ,调整N次Full GC之后再整理
      • 退化问题:如果老年代出现内存不足,无法分配对象,CMS就会退化成Serial Old单线程回收老年代,会造成用户线程长时间等待。
      • 浮动垃圾问题:在并发清理过程中,可能会产生“浮动垃圾”,不能做到完全的垃圾回收。
    • 适用场景
      • 大型的互联网系统中用户请求数据量大,频率高的场景,比如订单接口、商品接口等
    • 如何开启CMS
      • -XX:+UseConcMarkSweepGC
    • 执行步骤
      • 初始标记:用极短的时间标记出GC Roots能直接关联到的对象
      • 并发标记:标记所有用户,用户线程不需要暂停
      • 重新标记:由于并发标记线程阶段有些对象发生了变化,存在错标、漏标等情况,需要重新标记
      • 并发清理:清理死亡的对象,用户线程不需要暂停

3.4.1.3 ps+po
  • Parallel Scavenge时JDK8默认的垃圾回收器,多线程并发回收,关注的是系统的吞吐量,具备自动调整堆内存大小的特点
  • 回收年代:年轻代
  • 回收算法:复制算法
  • 优点:
    • 提高吞吐量,并且手动可控,为了提高吞吐量,虚拟机会动态调整堆的参数
  • 缺点:
    • 不能保证单次的停顿时间
  • 适用场景
    • 后台任务,不需要与用户交互,并且容易产生大量的对象。比如:大数据处理,大文件导出

  • Parallel Old
    • 主要是为了Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集
    • 回收年代:老年代
    • 回收算法:标记整理算法
    • 优点:
      • 并发收集,在多核CPU下效率较高
    • 缺点:
      • 暂停时间较长
    • 适用场景:
      • 与PS配套使用
    • 如何开启:
      • -XX:UseParallelGC或者-XX:UseParallelOldGC 可以使用ps+po的
  • 注意:
    • ps+po Oracle建议在使用这个组合时,不要设置堆内存的最大值,垃圾回收器会根据最大暂停时间和吞吐量自动调整堆内存大小
      • 最大暂停时间:-XX:MaxGCPauseMillis=n 设置每次垃圾回收时的最大停顿毫秒数
      • 吞吐量:-XX:GCTimeRatio=n 设置吞吐量为n(用户线程执行时间=n/n+1)
      • 自动调整内存大小:-XX:UseAdaptiveSizePolicy设置可以让垃圾回收器根据吞吐量和最大停顿的毫秒数自动调整堆内存大小
3.4.1.4 G1垃圾回收器

G1垃圾回收器出现之前的垃圾回收器,内存一般是连续的 如下图

  • G1的整个堆会被划分成很多个大小相等的区域,称之为区Region,区域不要求是连续的,分为Eden、Survivor、old区。Region的大小通过堆空间大小除以2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32m指定,其中32m指定region的大小为32m,Region size必须是2的指数幂,取值范围从1~32m

  • G1垃圾回收方式
    • 年轻代回收
      • 年轻代回收,回收Eden区和Servivor区中不再用的对象,会导致STW,G1中可以通过参数-XX:MaxGCPauseMillis=n(默认200)设置每次垃圾回收的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间
        • 新创建的对象会直接存放在Eden,当G1判断年轻代不足(max默认60%),无法分配对象时需要回收时会执行Young GC
        • 标记处Eden和Servivor区域中的存活对象
        • 根据配置的最大暂停时间选择某些区域将存活的对象复制到新的Servivor区中(年龄+1),清空这些区域
          • G1在进行年轻代回收时,会记录每个Eden和Serivor区的平均耗时,以作为下次回收时的参考依据。这样可以根据最大暂停时间计算出本次回收时最多能回收多少个Region区域了,比如-XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次最多回收5个Region
        • 后期连续的进行Young GC,与上述操作相同,只不过Servivor区中对象会被搬运到另一区域
        • 当某个区域的对象年龄达到阈值(默认15),将会被放入老年代
        • 注意:如果某些对象的大小超过了Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存时4G 每个Region区2m,当一个对象超过1M时,会被直接放入老年代当中,如果对象过大会横跨多个Region
    • 混合回收
      • 多次回收之后,会出现很多老年代,此时总堆的占有率达到阈值时(-XX:InitiatingHeapOccupancyPercent 默认45%)会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法完成
      • 混合回收被分为:初始标记、并发标记、最终标记、并发清理
      • G1对老年代的清理会选择存活度最低的区域来进行回收,这样可以保存回收效率最高,这也是G1名称的由来

  • G1垃圾回收器的老年代回收采用复制算法,这样不会产生内存碎片

  • Full GC阶段
    • 如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法,此时会导致用户线程暂停。所以尽量表征应该用的堆内存有一定多余的空间
  • G1垃圾回收器
    • 回收年代:年轻代和老年代
    • 回收算法:复制算法
    • 优点:
      • 对比较大的堆内存空间回收较快,不会产生内存碎片,并发标记的satb算法效率高
    • 缺点
      • 在jdk8之前还不够成熟,建议jdk9之后使用
    • 适用场景
      • jdk8最新版本,jdk9之后建议默认使用
    • 如何开启G1
      • -XX:+UseG1GC 打开G1的开关,jdk9之后默认不需要打开
      • -XX:MaxGCPauseMillis=n 最大暂停时间

3.4.2 使用垃圾回收器

3.4.2.1 垃圾回收器组合的选择
  • dk8及之前

    • ParNew+CMS(关注暂停时间),PS+PO(关注吞吐量),G1(jdk8之前不建议使用,堆较大并且关注暂停时间)

  • jdk9之后:
    • G1(默认)
  • jdk9及之后

    • 由于G1日趋成熟,JDK默认的垃圾回收器已经修改为G1,所以强烈建议在生产环境上使用G1垃圾回收器

4. 总结

4.1 java中有那几块内存需要进行垃圾回收

  1. 程序计数器:线程不共享,随着线程的创建而创建,随着线程的回收而回收。

  2. java虚拟机栈:线程不共享,随着线程的创建而创建,随着线程的回收而回收。

  3. 本地方法栈:线程不共享,随着线程的创建而创建,随着线程的回收而回收。

  4. 方法区:一般不需要回收,在一些特定的技术中 通过回收类加载器的方式去回收

  5. 堆:进行垃圾回收的主要内存部分,由垃圾回收器进行回收

4.2 常见的引用类型

  1. 强引用,最常见的引用方式,由可达性分析来判定
  2. 弱引用,(SoftReference)对象没有强引用下,在内存不足时,首先回收弱引用
  3. 软引用,(WeakReference)对象没有强引用下,在进行垃圾回收时,会直接进行回收
  4. 虚引用,通过虚引用知道了对象被回收
  5. 终结器用用,对象回收时可以自救,不建议使用。

4.3 常见的垃圾回收算法有哪几种

  1. 标记清除算法:标记之后再清除,容易产生内存随便
  2. 复制算法:从一块区域复制到另一块区域,堆内存使用率下降
  3. 标记整理算法:标记之后将存活的对象放到堆的另一边,在进行清除,对象移动耗时高,效率低
  4. 分代算法:将对象分为年轻代、老年代,年轻代分为(Eden、S0、S1区) ,年轻代和老年代执行不同的垃圾回收算法

4.4 常见的垃圾回收器有哪些

  1. Serial +Serial Old
    1. Serial主要回收年轻代内存垃圾回收,采用复制算法,单线程回收,
    2. Serial Old 主要回收老年代内存垃圾回收,采用标记整理算法
  2. ParNew+CMS
    1. ParNew 主要用于年轻代内存垃圾回收,采用复制算法,多线程回收,暂停用户线程
    2. CMS 主要用于老年代内存垃圾回收,采用标记清除算法,主要包括初始标记,并发标记,重新标记,并发清理
  3. PS+PO(JDK8默认垃圾回收器)
    1. PS 主要是用于年轻代内存垃圾回收,采用复制算法,多线程垃圾回收,可以自动调节堆大小与用户线程暂停时间
    2. PO 主要是用于老年代内存垃圾回收,采用标记清除算法,多线程垃圾回收。
  4. G1垃圾回收器
    1. G1垃圾回收器采用Region分区的方式将内存分为Eden、Servivor、Old三部分,其中堆内存被分为2048个Region
  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值