1、什么是垃圾回收机制?

  • 程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。

  • GC是不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行清除哪个对象,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用System.gc 方法来"建议"执行垃圾收集器,但是它是否执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。

2、如何判断对象可以被回收

1、引用计数法
  • 每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,计数器为0就代表该对象死亡,视之为垃圾

  • 优点:引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,

  • 缺点:它很难解决对象之间相互循环引用的问题(如下图)

alt

  • 主流的Java虚拟机里面都没有选用引用计数算法来管理内存
2、可达性分析算法
  • JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象,该方法是从GC Roots开始向下搜索,搜索所走过的路径为引用链。当一个对象到GC Roots没用任何引用链时,则证明此对象是不可用的,表示可以回收。

alt

  • 哪些可以作为 GC Root 的对象?

    虚拟机栈(栈帧中的本地变量表)中引用的对象

    方法区中类静态属性引用的对象

    方法区中常量引用的对象

    本地方法栈中 JNI(即一般说的Native方法)引用的对象

  • 可达性算法的优点:

    解决相互循环引用问题,是目前主流的虚拟机都采用的算法

3、五种引用
  • 强引用

    只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

  • 软引用(SoftReference)

    仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 可以配合引用队列来释放软引用自身

  • 弱引用(WeakReference)

    仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 可以配合引用队列来释放弱引用自身

  • 虚引用(PhantomReference)

    必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存

  • 终结器引用(FinalReference)

    无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。

3、垃圾回收算法

1、标记清除(Mark-Sweep)

执行过程:

当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。

  • 标记: Collector从引用根结点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。

  • 清除: Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

    alt

  • 优点:

    是可以解决循环引用的问题

    必要时才回收(内存不足时)

  • 缺点:

    效率不算高

    在进行GC的时候,需要停止整个应用程序,导致用户体验差

    这种方式清理出来的空闲内存是不连续的,产生内存碎片

2、标记–整理算法(Mark-Compact)
  • 执行过程:

    标记整理算法也称为标记压缩算法,是标记-清除算法的一个改进版。在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。

    alt

  • 优点:

    解决标记清除算法出现的内存碎片问题,

  • 缺点:

    从效率上来说,标记-整理算法要低于复制算法。

    移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。

    移动过程中,需要全程暂停用户应用程序。即:STW

3、复制算法(Copy)
  • 该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。

    alt

  • 优点:

    在存活对象不多的情况下,性能高,能解决内存碎片和标记整理算法中导致的引用更新问题。

  • 缺点::

    会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,复制算法的性能会变得很差。

4、分代回收算法(Generational Collection)
  • 原理:

    根据对象存活周期不同,分为三个年代:年青代、老年代、持久代。这是因为不同对象存活时间不一致,有些可能只使用一次,使用后就需要回收,而有些对象却会伴随整个程序的生命周期。分代有利于堆不同生命周期的对象进行管理,减少GC次数,提高运行效率。

alt 执行过程:

  • 新创建的对象首先分配在 eden 区
  • 新生代空间不足时,触发 minor gc ,eden 区 和 from 区存活的对象使用 copy 复制到 to 区中,存活的对象年龄加一,然后交换 from 区和 to 区
  • minor gc 会引发 stop the world,暂停其他线程,等垃圾回收结束后,恢复用户线程运行
  • 当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit)
  • 当老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full fc ,停止的时间更长!

4、垃圾回收器

1、相关 JVM 参数

alt

2、概念:
  • 垃圾收集器是垃圾回收算法(引用计数法、标记清楚法、标记整理法、复制算法)的具体实现,不同垃圾收集器、不同版本的JVM所提供的垃圾收集器可能会有很在差别。
3、相关概念:
  • 并行收集(Parallel):指多条垃圾收集的线程并行工作(不是真正的同时一起工作,而是在多个线程之间切换,只是宏观上看来是同时在工作),这个时候其它用户线程都处于暂停状态。
  • 并发收集(Concurrent):指用户线程和垃圾收集线程同时运行(但并不一定是并行的,可能会交替执行),在垃圾收集线程运行时,用户线程在另一个线程上继续运行,并发收集不会使其它的用户进程暂停。
  • 吞吐量(Throughput):即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99%
4、垃圾回收器---列表

alt

5、垃圾回收器---串行
  • 特点:单线程,堆内存较少,适合个人电脑

  • 语句

-XX:+UseSerialGC=serial + serialOld

alt

  • 安全点:

    让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象

    因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

  • Serial 收集器

    Serial收集器工作在新生代,是最基本的、发展历史最悠久的收集器

    特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!

  • Serial Old 收集器

    Serial Old 是 Serial 收集器的老年代版本 特点:同样是单线程收集器,采用标记-整理算法

  • ParNew 收集器

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

    特点:多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题

6、垃圾回收器---吞吐量优先
  • 特点:多线程,堆内存较大,多核 cpu,让单位时间内,STW 的时间最短

  • 语句:

-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio // 1/(1+radio)
-XX:MaxGCPauseMillis=ms // 200ms
-XX:ParallelGCThreads=n //来限制垃圾收集的线程数

alt

  • Parallel Scavenge 收集器

    与吞吐量关系密切,故也称为吞吐量优先收集器

    特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)

    该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)

  • GC自适应调节策略:

    Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。 当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。

  • Parallel Scavenge 收集器使用两个参数控制吞吐量:

    -XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms)

    -XX:GCTimeRatio=rario 设置吞吐量的大小

  • Parallel Old 收集器

    Parallel Scavenge 收集器的老年代版本

    特点:多线程,采用标记-整理算法(老年代没有幸存区)

7、垃圾回收器---响应时间优先
  • 特点:多线程,堆内存较大,多核cpu,尽可能让 STW 的单次时间最短

  • 语句:

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

alt

  • ParNew 收集器

    ParNew 收集器运行在新生代,是 Serial 收集器的多线程版本

    特点:多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题

  • CMS 收集器(Concurrent Mark Sweep)

    一种以获取最短回收停顿时间为目标的老年代收集器

    特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片 应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务

  • CMS 收集器的运行过程分为下列4步:

    初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。

    并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。

    重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题

    并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!

  • CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。

5、G1 收集器(Garbage First)

1、适用场景:

同时注重吞吐量和低延迟(响应时间)

超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域

整体上是标记-整理算法,两个区域之间是复制算法

2、相关参数:
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time

3、G1垃圾回收阶段:

alt

  • Young Collection:对新生代垃圾进行收集

  • Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。

  • Mixed Collection:会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。

4、Young Collection

alt

  • 新生代收集会产生 STW
  • E:eden,S:幸存区,O:老年代
  • 将堆空间划分连续几个不同小区间(region),每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间!
5、Young Collection + CM

alt

  • 在 Young GC 时会进行 GC Root 的初始化标记

  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的 JVM 参数决定 -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

6、Mixed Collection

alt

  • 会对 E S O 进行全面的回收

    最终标记会 STW

    拷贝存活会 STW

  • -XX:MaxGCPauseMills=xxms 用于指定最长的停顿时间!

  • 问:为什么有的老年代被拷贝了,有的没拷贝?

    因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)

7、Full GC
  • SerialGC

    新生代内存不足发生的垃圾收集 - minor gcSerialGC

    老年代内存不足发生的垃圾收集 - full gc

  • ParallelGC

    新生代内存不足发生的垃圾收集 - minor gc

    老年代内存不足发生的垃圾收集 - full gc

  • CMS

    新生代内存不足发生的垃圾收集 - minor gc

    老年代内存不足

  • G1

    新生代内存不足发生的垃圾收集 - minor gc

    老年代内存不足

  • G1 CMS 在老年代内存不足时(老年代所占内存超过阈值)

    如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理

    如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。

8、Young Collection 跨代引用
  • 新生代回收的跨代引用(老年代引用新生代)问题

alt

  • 卡表(Cardtable)

    为了便于寻找处于老年代中的GC Roots,老年代被划分为多个区域(card),一个区域(card)为512K

  • 脏卡:

    如果某一个区域(card)引用了新生代对象,则该区域被称为脏卡

  • Remembered Set

    Remembered Set 存在于E(新生代)中,用于保存新生代对象对应的脏卡

  • 在引用变更时通过 post-write barried + dirty card queue

  • concurrent refinement threads 更新 Remembered Set

9、Remark
  • 重新标记阶段:在垃圾回收时,收集器处理对象的过程中

  • 语句:


pre-write barrier + satb_mark_queue

alt

  • 黑色:已被处理,需要保留的

  • 灰色:正在处理中的

  • 白色:还未处理的

10、JDK 8u20 字符串去重
  • 语句:
-XX:+UseStringDeduplication
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
  • 优点:节省了大量内存

  • 缺点:新生代回收时间略微增加,导致略微多占用 CPU 过程

  • 将所有新分配的字符串(底层是 char[] )放入一个队列

  • 当新生代回收时,G1 并发检查是否有重复的字符串

  • 如果字符串的值一样,就让他们引用同一个字符串对象

  • 注意,其与 String.intern() 的区别

    String.intern() 关注的是字符串对象

    字符串去重关注的是 char[]

    在 JVM 内部,使用了不同的字符串标

11、JDK 8u40 并发标记类卸载
  • 语句:
-XX:+ClassUnloadingWithConcurrentMark 默认启用

  • 所有对象在经过并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类
12、JDK 8u60 回收巨型对象
  • 一个对象大于region的一半时,就称为巨型对象
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
13、JDK 9 并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成,否则退化为 FulGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值

    • 进行数据采样并动态调整

    • 总会添加一个安全的空挡空间

14、JDK 9 更高效的回收

6、垃圾回收调优

1、相关参数
  • 掌握 GC 相关的 VM 参数,会基本的空间调整,可以去JavaSE官网查看

  • 查看虚拟机参数命令(也可以在 VM options 中设置)

D:\JavaJDK1.8\bin\java  -XX:+PrintFlagsFinal -version | findstr "GC"
2、调优领域
  • 内存
  • 锁竞争
  • cpu 占用
  • io
3、确定目标
  • 要根据项目的具体需求,比如是追求低延迟还是高吞吐量? 来选择合适的GC
  • CMS G1 ZGC(低延迟)
  • ParallelGC(高吞吐量)
  • Zing
4、最快的 GC 是不发生 GC
  • 首先排除减少因为自身编写的代码而引发的内存问题

  • 查看 Full GC 前后的内存占用,考虑以下几个问题

    数据是不是太多?

    resultSet = statement.executeQuery(“select * from 大表 limit n”)

  • 数据表示是否太臃肿

    对象图

    对象大小 Integer 24 VS int 4

  • 是否存在内存泄漏

    static Map map …

    第三方缓存实现

5、新生代调优
  • 新生代的特点

    所有的 new 操作分配内存都是非常廉价的

    TLAB thread-lcoal allocation buffer

    死亡对象回收零代价

    大部分对象用过即死

    Minor GC 时间远小于 Full GC

  • 新生代内存越大越好么?

    不是,新生代内存太小:频繁触发 Minor GC ,会 STW ,会使得吞吐量下降

    新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且新生代内存变大之后,触发 Minor GC 清理新生代所花费的时间也会更长

  • 新生代内存设置为:容纳 [ 并发量*(请求-响应) ] 的数据为宜

  • 幸存区需要能够保存当前活跃对象+需要晋升的对象

  • 晋升阈值配置得当,让长时间存活的对象尽快晋升

-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution

6、老年代调优(以 CMS 为例):
  • CMS 的老年代内存越大越好
  • 先尝试不做调优,如果没有 Full GC 那么已经,否者先尝试调优新生代。
  • 观察发现 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent

7、案例
  • 案例1:Full GC 和 Minor GC 频繁

    考虑新生代内存设置过小

  • 案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)

    重新标记的内存范围是整个堆,在重新标记之前,先执行一次ygc,回收掉新生代无用的对象,并将有用的对象放入幸存区或晋升到老年代,这样再进行新生代的扫描时,只需要扫描幸存区的对象即可,一般幸存区非常小,这大大减少了扫描时间

  • 案例3:老年代充裕情况下,发生 Full GC(jdk1.7)

    永久代内存不足