Java的GC

作者: lionel880 | 来源:发表于2020-01-26 13:26 被阅读0次

前言

GC(Garbage Collection),内存的动态分配与内存回收技术的重要组成部分。

  • 从整体上而言,我们要理解一下3个问题
    • 哪些内存需要被回收
    • 什么时候回收
    • 如何回收

一、哪些内存需要被回收

  • 程序计数器、虚拟机栈、本地方法栈
    关于GC,我们不能只考虑具体的方法,要整体上明白GC的作用是实现内存管理,结合Java虚拟机内存管理知识,我们知道虚拟机栈,本地方法栈,程序计数器都是随线程的,大小都是在一开始就确定的,所以这几个区域的分配和回收都是确认的。他们在线程或方法开始的时候创建,在线程或方法结束的时候销毁

  • 堆和方法区
    这部分在事先我们是不知道的,我们不知道真正运行时会进入哪个分支,要创建哪些对象,所以这部分的是不确定的,它是分配和回收也是动态的,是GC需要关注的

在堆和方法区内如何判断对象要被回收?--即如何判断对象死亡了

  1. 引用计数法
    引用时加1,容易理解,当问题是难以解决互相循环引用的问题
  2. 可达性分析---目前的方法
    我们将一些量称之为root,从root的引用,不断延伸,引用链到最后节点,如果对象不在引用链上,则会被标记,它不可达
    • GC roots有哪些:虚拟机栈中引用的对象(意味着有线程正在引用)、 方法区中类的静态属性引用的对象(因为这个是属于类的,一开始就创建好的被引用)、方法去中常量池引用的对象(final的那些,为了速度快)、本地方法栈中JNI(即一般说的Native方法)引用的对象(线程正在引用)
  • Tip,这里的引用后来又扩展除了强引用、软引用、弱引用、虚引用

  • 回收方法区
    刚讲的是对象的回收,其实更进一步,虚拟机也可以对方法区进行回收,如某个常量“abc”是否没有被引用了等。这里判断回收的方法和之前不一样

    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
    • 加载该类的ClassLoader已经被回收。
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该
      类的方法

目前很多框架大量运用反射、动态代理、CGLib等,自定义的ClassLoader都需要有这类卸载的功能,保证永久带不溢出

二、如何回收--各种垃圾收集算法

在收集的实现上,主要有一下几种

  1. 标记--清除 Mark-Sweep
    这个是最基础的思路,标记哪些对象要被回收,然后回收。其他的都在对其的改进思路
    • 缺点 1.容易产生大量的不连续的内存碎片,导致大对象无法使用,需要再一次清理
    • 缺点 2.效率问题,和复制算法相比,效率低
  2. 复制算法
    为了提升效率,有了复制算法。将一块内存分为2块,每次使用其中一块,一块用完时,清理,将存活的移到另一块,再把这一块清空。
    优点 1.简单、高校
    缺点 1.内存可用缩小了一半
    2.对象存活率较高时,效率很低
  • tip 在实践中,由于新生代对象死亡很快,在现代的算法里,是将年轻代的内存划分为一块较大的Eden区和2块较小的Survivor区。默认8:1:1.每次用一个Eden和Survivor。复制时,把它们都复制到另一个Survivor中,将Eden和Survivor清理
  1. 标记-整理
    从复制算法的优缺点可以看出,复制算法适合于年轻代,存活率低的场景。为了改进复制算法,提出了标记--整理算法
    先标记不可达的对象,但后续不是清理,而是将存活的对象进行整理,移动,排整齐之后,清理掉边界以外的内存


    image.png

优点:1.适合于老年代,这种对象存活率较高的内存区域
2.可以解决复制算法内存使用率低的问题

分代收集算法 Generation Collection
这个本身并没有新的算法,而仅仅是将内存划分为年代,根据不同年代的特点,选用不同的GC方法,以提升效率

总体来说:1. 年轻代对象朝生暮死,适合用复制算法
2. 年老代存活率高,且没有额外的空间,适合使用标记-清除或标记-整理算法

三、HotSpot虚拟机gc算法

我们都知道虚拟机有很多的gc策略,什么CMS,ParNew等等,但是虚拟机底层是如何执行这些策略的呢?
这就要涉及到最基本的几个概念

  • 1.枚举跟节点
    我们知道,现在对象回收都是采用可达性分析,而可达性分析的基础就是枚举跟节点。,枚举的特点是什么,是枚举时的东西是固定的,它不可以变化,否则无法完成枚举,因此所有的gc枚举跟节点必然会stop the world,这是难以避免的。
    我们能做的,只是让这个过程尽可能得快,由于目前的主流Java虚拟机使用的都是准确式GC,虚拟机可以直接从对象中获得引用存放的信息,因此整个过程是很快的

    1. 安全点 Safe Point
      我们并不是在线程的任何时刻都可以进行枚举跟节点,只是在“特定的位置”才行,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停
      目前主流的都是轮询式,gc时,虚拟机设置一个标志位,其他线程轮询到这个标志位,进行安全点暂停,等待gc
      tip:如何选择安全点?
      是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、 循环跳转、 异常跳转等,所以具有
      这些功能的指令才会产生Safepoint
    1. 安全区域 Safe Region
      是安全点的扩展,如一些线程block或sleep状态,不会去轮询状态位,这时候状态相当于不会变化,这个区域就当做是安全区域

四、具体的垃圾回收算法

重点了解CMS,实际使用,他的特点
我们所说的CMS实际上是一款老年代的收集器,一般采用Paraller New作为配合

Paraller New就是Serial的升级版,新生代GC时,多线程去GC,在多核下比Serial效果好

CMS(Concurrent Mark Sweep)
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,有些则目的是最大吞吐量。
从名字中可以看出,这是一个基于并发标记-清除策略的算法


image.png

初始标记-并发标记-重新标记-清除。
CMS是通过并发来缩短gc停顿时间,但再初始标记阶段,仍然要暂停所有线程,这无法避免

CMS的几个缺点
1.并发必然会对CPU敏感,默认采用25%的核进行gc回收,当核比较少时,对性能影响较大

  1. 浮动垃圾
    多次标记的过程,因为清理时,用户线程才在运营,因此需要预留一部分给用户线程用,如现在默认设计92%,那么如果预留的空间不够怎么办,虚拟机会使用Serial Old算法(单线程标记-整理),从而使得整体停顿时间上升
    3.标记-清除
    导致碎片多,需要额外触发full-gc,或者进行整理,可以配置再多少次full gc后进行整理,而内存整理是无法并发的,因此停顿时间变长

五、内存分配和回收

了解了理论知识,我们来看一个完整的过程

1.对象优先在Eden区进行分配,我们知道年轻代采用复制算法,有2个survivor区用于复制。当Eden区不够时,触发young gc,young gc就是指发生再新生代的gc,特点是频繁,但比较快

  1. 大对象直接进入年老代
    这是一个优化措施,避免触发复制,少一次young gc

  2. 年龄大的对象进入年老代
    虚拟机为每个对象设置了Age属性,挺过一次young gc,就加1,达到一定阈值就进入年老代,或同代的对象超过survivor也能进入年老代,这是一个优化

4.空间分配担保
我们知道young gc采用复制算法,可能一个survior不够,怎么办,必然会导致一部分对象进入老年代。那么就需要老年代进行空间担保,这有担保策略,根据历史平均值。如果最终老年代空间不够,则会导致full gc

GC日志的典型举例

youngGC日志


image.png

FullGC日志


image.png

相关文章

网友评论

      本文标题:Java的GC

      本文链接:https://www.haomeiwen.com/subject/pscezctx.html