JVM调优理论篇_二、常用垃圾回收器(JVM10种垃圾回收器)以及垃圾回收算法

JVM调优理论篇_二、常用垃圾回收器以及垃圾回收算法


前言

JVM调优,可以说是面试的一大重点,本文着重介绍垃圾收集器以及垃圾回收算法。会从种类、原理、特点、使用场景等方面全面介绍垃圾回收器、回收算法。希望能帮到更多的人更为深刻的去认识JVM。
本人水平有限,如有误导,欢迎斧正,一起学习,共同进步!


一、垃圾回收基础

1、什么场景下使用垃圾回收

内存比较苛刻的场景:想办法提高对象的回收效率,多回收掉一些对象,腾出更多的内存。
cpu使用率高的情况下:要尽可能高并发时回收的频率,让cpu更多的去执行你的业务而不是垃圾回收。

2、垃圾回收发生在哪个区域?

堆和方法区,主要在堆(堆主要存放创建的对象)。方法区(主要存放废弃的常量以及不需要使用的类)。因为栈、本地方法栈、程序计数器都是线程独享的

3、对象在什么情况下会被回收?(如何判断一个对象是否该被回收)

引用计数法:通过对象的引用计数器来判断对象是否被引用(java没用这个)。就是这个对象没引用一次,计数器就+1,为0的时候则是没人引用。但是有循环引用的问题,所以java没用这个。

可达性分析:以根对象(GC ROOTS)为起点向下搜索,走过的路径被称之为引用链,如果某个对象到根对象没用引用链相连时,就认为这个对象是不可达的,可以回收

GC Roots 包含哪些对象

GC Roots 包含哪些对象?
	a、虚拟机栈中引用的对象(栈帧中的本地变量表,也是局部变量)
	b、本地方法栈中JNI(Native方法)引用的对象
	c、方法区中类静态属性引用的对象
	d、方法区中常量引用的对象

Java的引用类型?

既然说到了引用链的引用,那java中有几种引用呢?java中有4种引用。

强引用(Strong Reference):
例如: Object obj = new Object(); 

只要强引用在,永远不会回收被引用的对象,即使是出现内存溢出,也不会回收这些对象。

软引用(Soft Reference):
例如:SoftReference<String> st = new SoftReference<>("hello");

软引用是用来描述一些有用但是非必须的对象。软引用的对象,只有内存不足的时候会被回收。利用这个特性,软引用比较适合实现一些缓存,网页缓存,图片缓存之类的。

弱引用(WeakReference):
例如:WeakReference<String> sr = new WeakReference<>('Hello');

弱引用也是描述非必须对象的。无论内存是否充足,都会回收被弱引用关联的对象。

虚引用(Phantom Reference):
例如:ReferenceQueue<String> queue = new ReferenceQueue();
	  PhantomReference<String> pr = new PhantomReference<>("Hello",queue);

不影响对象的生命周期,如果一个对象只有虚引用,那么他就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,必须和引用队列(ReferenceQueue)配合使用。当垃圾回收器准备回收一个对象时,如果发现他还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之相关的引用队列中,程序可以判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收,如果程序发现某个虚引用已经被加入了引用队列,那么就可以在所引用的对象的垃圾回收之前采取必要的行动。

一个对象即使不可达,也不一定会被回收(可以理解为,被判处死缓了,不是立即执行死刑,如果被改刑(由不可达变可达了)了,也可能会无罪释放)

4、垃圾回收的具体过程:

在这里插入图片描述

对象没有引用链和gc roots连接,就判断这个对象有无必要执行finalize(),无必要(对象未重写finalize或者虚拟机已经调用过finalize)的话,直接回收;有必要执行finalize方法的话,就把这个对象放进一个叫 f-queue 的队列,虚拟机自动创建低优先级线程去执行对象的finalize方法,执行完了,回收;如果对象在finalize方法的期间,重新建立的链接,有不可达变成了可达,就会从 f-queue 队列中移除,不回收了。
因此,finalize() 的建议:

a、在实际项目中,尽量避免使用 finalize() ,因为操作不当的话娿,很可能导致这个对象无法被回收,容易oom
b、finalize() 优先级低,何时被调用无法确定,因为什么时间gc不确定
c、建议使用  try...catch...finally 来替代 finalize();

二、垃圾回收算法

基础垃圾回收算法:标记清除、标记整理、复制
综合垃圾回收算法:分代收集算法、增量算法

1、标记清除算法

a、标记要清除的对象;
b、清理要回收的对象。
缺点:会存在内存碎片,

2、标记整理算法

a、标记要清除的对象;
b、把所有存活的对象压缩到内存的一端;
c、清理掉边界外的全部内存空间
优点:解决掉了内存碎片的问题

3、复制算法

把内存分为两块,每次只使用其中的一块,把正在使用的内存中的存活对象复制到未使用的内存中去,然后清除掉正在使用的内存的全部对象交换两个内存的角色,等待下次回收。

三种算法的优缺点对比

回收算法优点缺点
标记清除实现简单存在内存碎片,分配内存会受到影响(假设一个大对象,有可能遍历完全部的内存碎片,也没位置去存放)
标记整理无内存碎片整理存在开销(假设好多对象存活,需要整理到一端,是存在开销的)
复制算法性能好,无碎片内存利用率低,每次只能使用一半的空间

4、分代收集算法

把内存分为多个区域,不同的区域使用不用的回收算法回收对象。详解:分代收集算法:(各种商业虚拟机堆内存的垃圾收集算法几乎都是分代收集算法)
根据对象的存活周期,把内存分成多个区域,不同的区域使用不同的回收算法回收对象。

a、回收类型可以分为三类:

  • 新生代回收(Minor GC | Young GC)
  • 老年代回收(Major GC)
  • 清理整个堆(Full GC)

可以认为 Major GC 约等于 Minor GC ,因为每当重gc的时候,一般也会伴随着轻gc。

b、对象的分配过程(一般情况下,非绝对):

一个对象先放到伊甸园中去,然后第一次gc的时候,把这个对象放到存活区0,下次gc的时候,把伊甸园、存活区0的对象复制到存活区1,然后清空伊甸园和存活区0,再下次gc的时候,把伊甸园和存活区1的对象放到存活区0,然后清空伊甸园和存活区1,周而复始。对象经历过一次gc以后,岁数就+1,当达到阈值,默认是15,的时候,就会把这个对象放到老年代。老年代的清除算法是标记整理,新生代的算法是复制算法

c、对象分配的例外:

  • 新建的对象不一定会被分配到伊甸园,也可能直接分配到了老年代

     对象的大小超过 -XX:PretenureSizeThreshold ,就会直接分配到老年代。(这个参数默认是0,也就是说,默认都会进入到伊甸园)
     新生代空间不够(假设你的对象非常大,整个新生代都放不下,就会直接放到老年代,比如说一个超大的数组)
    
  • 对象也不一定要达到阈值才会进去老年代

     动态年龄:如果存活区空间所有相同年龄对象大小的总和大于存活区空间的一半,那么年龄大于等于该年龄的对象就会直接进老年代
    

d、不同区域触发垃圾回收的条件

minor gc(轻gc):

  • 伊甸园空间不足

major gc(重gc):

  • 老年代空间不足(可能是空间不足,也可能是内存碎片导致没有连续的空间)
  • 元空间不足
  • 要晋升到老年代的对象所占用的空间大于老年代剩余的空间
  • 显示的调用了 System.gc(); (System.gc方法的作用是建议垃圾回收器执行垃圾回收)

e、分代收集的好处:

  • 更有效的清除不在需要的对象(对于生命周期比较短的对象,在新生代的时候就会被回收掉了)
  • 提升了垃圾回收的效率(每次只扫描一个代的对象)

f、分代收集的算法的调优规则

  • 合理的设置存活区的大小,避免内存浪费
  • 让gc尽可能的发生在新生代,尽量减少full gc 的发生

5、增量算法

每次只收集一小片区域的内存空间的垃圾(假设你的内存非常大,如果一次收集全部的垃圾,那么耗费的时间就会很长,就可能造成系统长时间的停顿,可以用这个算法,每次只收集一小部分的内存空间)

三、垃圾回收器

专业术语

STW(Stop The World):简写stw,也叫全局停顿,java代码停止运行,native代码继续执行,但是不能与jvm进行交互。造成stw的原因:多半由垃圾回收导致;也可能有dump线程,死锁检查,dump堆导致等。危害:服务停止、没有响应;主从切换、危害生产环境等(所以要尽可能的缩短stop the world的时间)
并行收集&并发收集

  • 并行收集:多个垃圾收集线程并行工作,但是在垃圾收集的过程中,用户线程(你的业务线程)是处于等待的状态的。

  • 并发收集:指用户线程和垃圾收集线程同事工作

     并行:吃饭吃到一半,电话来了,你一边打电话一边吃饭,叫支持并行
     并发:吃饭吃到一半,电话来了,你停下来接电话,接完电话以后继续吃饭,叫支持并发(不一定是同事进行的)
    

吞吐量:cpu用于运行用户代码的时间与cpu总消耗时间的比值。运行用户代码的时间/(运行用户代码的时间+垃圾收集的时间)。比如说:一个jvm,总共运行了100分钟,垃圾收集占了1分钟,那么它的吞吐量就是 99/99+1 = 99%。

总体介绍

从下图中可以看到,
新生代中的收集器有:Serial、ParNew、Parallel Scavenge
老年代的收集器有:CMS、Serial Old、Parallel Old
G1 收集器既可以在新生代中,又可以在老年代中。

在这里插入图片描述

新生代垃圾收集器

1、Serial收集器

是最基本的、发展历史最悠久的收集器。使用的是复制算法

1.1、执行过程

多个用户线程并发的执行,当到某个点的时候,全部的用户线程都暂停,然后垃圾回收线程去执行,直到执行完了垃圾回收以后,其他线程再并发执行。
在这里插入图片描述

1.2、特点:
  • 单线程
  • 简单、高效。简单是说单线程,当然比较简单了,没多线程复杂;高效是和其他收集器的,单个线程相对比的。由于它是单线程的所以没有和其他线程的交互的开销。所以和其他垃圾收集器相比,单线程的话,工作效率会更高效一些
  • 收集过程全程 Stop The World。在垃圾收集的过程中,工作线程全程暂停
1.3、适用场景:
  • 客户端程序。应用以 -client 模式运行时,默认就是Serial。 java -client -jar XXX.jar
  • 单核机器。比如说 一些嵌入式,低性能的机器上

2、ParNew收集器

ParNew可以任务是Serial收集器的多线程版本,除了使用多线程以外,其他的和Serial收集器一样,包括:JVM参数、Stop Thw World的表现、垃圾收集算法都一样

2.1、执行过程

在这里插入图片描述

2.2、特点:

多线程。可使用 -XX:ParallelGCThreads 设置垃圾收集线程数(一般设置成cpu的核心数就行)

2.3、适用场景:

主要和cms收集器配合使用

3、Parallel Scavenge收集器

也叫吞吐量优先收集器,采用的也是复制算法,也是并行的多线程收集器,这点和ParNew类似

3.1、执行过程

在这里插入图片描述

3.2、特点
  • 达到一个可控制的吞吐量
    -XX:MaxGCPauseMillis : 控制最大的垃圾收集停顿时间(尽力)
    -XX:GCTimeRatio :设置吞吐量的大小,取值为0-100,系统花费不超1/1+n的时间用于垃圾收集
  • 自适应GC策略,可使用 -XX:+UseAdptiveSizePolicy 打开
    开启了自适应策略以后,无需手动的设置新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)等参数;虚拟机会自动根据系统的运行状况收集性能监控信息,动态的调整这些参数,从而达到最优的停顿时间以及最高的吞吐量
3.3、适用场景:

比较注重吞吐量的场景

老年代垃圾收集器

1、Serial Old 收集器

serial收集器的老年代版本,标记整理算法

1.1、执行过程

在这里插入图片描述

1.2适用场景:
  • 可以和 Serial、ParNew、Parallel Scavenge 这三个老年代的垃圾回收器配合使用
  • CMS 收集器出现故障的时候,会用 Serial Old 最为后备使用

2、Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本,标记整理算法

2.1、执行过程

在的插入图片描述

2.2、特点

只能和 Parallel Scavenge配合使用,

2.3、使用场景

关注吞吐量的场景

3、CMS 收集器:(Concurrent Mark Sweep)

并发收集器、标记清除算法

3.1、执行过程
3.1.1、初始标记(initial mark)

用来标记 GC Root 能直接关联到的对象,这个过程会 stop the world,不过由于是标记gc Root直接关联的对象,stop的时间还是比较短的。(因为直接关联的对象比较少)

3.1.2、并发标记(concurrent mark)

并发标记阶段,能找到gc root对象的全部的关联对象;此时是用户线程和gc线程同时执行,无 stop the world

3.1.3、并发预处理(concurrent-preclean 不一定会被执行)

重新标记那些在并发标记阶段,引用被更新的对象(比如说,晋升到老年代的对象,或者原先就在老年代的对象,),从而减少后面重新标记阶段的工作量。并发执行,无 stop the world;可使用 XX:-CMSPrecleaningEnabled关闭并发预处理阶段,默认打开。

3.1.4、并发可终止的预处理阶段(concurrent-abortable-preclean,不一定会被执行)

和并发预处理做的事一样(重新标记那些在并发标记阶段,引用被更新的对象),并发执行,无stop the world阶段。当伊甸园使用量大于CMSScheduleRemarkEdenSizeThreadshold 的阈值时,才会执行该阶段(否则直接跳过)
为啥已经有了并发预处理阶段,还要有并发可终止的预处理阶段呢
并发可终止的预处理阶段的作用:允许我们能控制预清理阶段的结束时机,比如扫描多长时间(CMSMaxAbortablePreCleanTime 默认是5秒)或者伊甸园区使用占比达到一定的阈值(CMSScheduleRemarkEdenPenetration,默认50%)就结束本阶段。

3.1.5、重新标记阶段(remark)

修正并发标记期间,因为用户线程的继续运行,导致标记发生变动的那些对象的标记。一般来说,重新标记所花费的时间会比最初标记阶段长一些,但比并发标记的时间短。存在 stop the world
为啥已经有了并发标记的阶段,还需要有重新标记的阶段呢
因为并发标记的阶段是并发执行的,在标记的同时,用户线程可能会修改已经标记的状态,这样就可能会导致,已经失效的对象变的有效(可以容忍)或者有效的对象变的失效(不能容忍)

3.1.6、并发清理/并发清除阶段(concurrent sweep)

基于标记结果,清除掉前面标记出来的垃圾。并发执行,无 stop the world
为何不是并发整理而是并发清除呢
因为并发清理是要并发进行的,如果整理的话,还需要移动对象的位置。并发状态下,多个线程执行业务,多个线程执行gc,gc的同时还需要移动位置,移动的同时,还需要保证业务代码不出问题,难度太大。

3.1.7、并发重置(concurrent reset)

清理本次CMS GC 的上下文信息,为下一次的gc做准备。
在这里插入图片描述

3.2、CMS收集器的优点

stop the world的时间比较短(只有初始阶段、重新标记阶段存在stop the world)。大多数过程是并发执行的

3.3、CMS收集器的缺点
  • 对cpu资源比较敏感(多线程同时执行嘛),并发阶段可能会导致应用吞吐量的降低。无法处理浮动垃圾,(浮动垃圾是说,一边进行垃圾回收一遍进行业务代码,业务代码执行的时候新产生的垃圾。浮动垃圾只能是下次gc的时候在处理了)
  • 需要预留足够的空间。不能等到老年代几乎满了才开始收集。因为cms执行的过程中,业务代码也在执行,若是业务代码执行的时候,发现预留的空间不够了,则会抛出 Concurrent Mode Failure 异常,此时会用 Serial Old 收集器做后备,只不过一旦用了Serial Old做垃圾回收的话,就会导致stop the world的时间变长。(cms出现故障的时候,就会使用serial old作为后备)可使用 CMSInitiatingOccupancyFraction 设置老年代占比达到多少就触发垃圾收集,默认是68%
  • 内存碎片问题。标记清除算法导致产生内存碎片
    UseCMSCompactAtFullCollection:在完成Full gc以后是否进行内存碎片的整理,默认是开启的
    CMSFullGCsBeforCompaction:进行几次Full gc以后就进行一次内存碎片整理,默认是0

很多情况下,minor gc和full gc是同一件事,但是对于cms收集器来说,minor gc 和full gc不是一回事,cms是作用在老年代的垃圾回收,并不是full gc(full gc是清理整个堆)

3.4、CMS收集器适用场景:

希望系统停顿时间短,响应速度快的场景,比如各种服务器应用程序

3.5、CMS收集器总结

1、初始标记
2、并发标记
3、并发预处理(不一定会有)
4、并发可终止的预处理(不一定会有)
5、重新标记
6、并发清除
7、并发重置

4、G1收集器(Garbge First)

  • 面向服务端应用的垃圾收集器,既可以新生代,又可以老年代。
  • 使用的是复制算法,不存在内存碎片
  • 带来了革命性的变化:堆内存布局的变化
4.1、总体介绍

G1把整个Java堆划分为了若干个大小相等的区域。每个区域叫一个region,region的大小可以通过 -XX:G1HeapRegionSize 指定Region的大小。region的取值大小为1M-32M,应为2的N次幂。G1还对每个格子进行了分类,一共有4类:Eden、Survivor、Old、Humongous(事实上,G1会把Humongous当做老年代的一部分看待)。只不过,G1中,同一个代里面的对象,可能是不连续的。比如说,Eden园里面的对象,被划分到了图中的5个地方,这5个Eden region就是不连续的。Humongous对象是用来存大对象的。某个对象的大小超过了region的一半,就认为是大对象,然后存到humongous region里面去。如果某个对象超级大,一个region都放不下,就会分配到多个连续的Humongous region里面去。(假设我们指定了region的大小是16M,只要对象的大小超过8M,就会分配到某个region里面去,然后把这个region标记为Humongousregion,然后有个对象是30M,就会分配两个连续的region去存储这个大的对象,并且会把这俩region都标记为Humongous)
在这里插入图片描述
G1的设计思想
内存分块(region)
跟踪每个Region里面垃圾堆积的价值大小(比如说,我回收这个region能回收20M的空间,回收那个region能回收12M的空间,那么前者的价值就大)
根据region的价值的大小,构建一个优先列表,根据允许收集的时间,优先回收价值高的region (这里应用了增量回收 的设计思想)

4.2、G1的设计思想

内存分块(region);跟踪每个Region里面垃圾堆积的价值大小(比如说,我回收这个region能回收20M的空间,回收那个region能回收12M的空间,那么前者的价值就大);根据region的价值的大小,构建一个优先列表,根据允许收集的时间,优先回收价值高的region (这里应用了增量回收 的设计思想)。

4.3、G1收集器-执行过程

在这里插入图片描述

4.4、G1收集器-垃圾收集机制

Young GC(回收新生代)
Mixed GC(回收新生代以及老年代的一部分)
Full GC(用Serial Old去回收垃圾)

4.4.1、G1回收机制_Yong GC

1、所有的 Eden region 都满了的时候,就会触发Young GC
2、伊甸园里面的对象会转移到Servivor Region里面去
3、原先的Servivior Region中的对象会转移到新的Servivor Region中,或者晋升到 Old region 中去
4、空闲的 Region 会被放入空闲列表中,等待下次被使用

4.4.2、G1回收机制_Mixed GC
4.4.2.1、G1回收机制_Mixed GC的步骤

(是G1的最巧妙的地方,也是G1的设计思想体现的地方)
1、老年代大小占整个堆的百分之达到一定的阈值(可用 -XX:InitiatingHeapOccupancyPercent 指定,默认是45%)就触发
2、Mixed GC 会回收所有的 Young Region ,同时回收部分 Old Region。注意,这里是部分

4.4.2.2、Mixed GC的回收过程:

1、初始标记(Initial Marking)
标记 GC Root 能直接关联的对象,和CMS类似
存在 Stop the World
2、并发标记(Concurrent Marking)
同 CMS 的并发标记
并发执行,无 Stop the Wordl
3、最终标记(Final Marking)
修正在并发标记期间引起的变动
存在 Stop the World
4、筛选回收(Live Data Counting and Evacuation)
对各个 Region 的回收价值和成本进行排序
根据用户所期望的停顿时间(MaxGCPauseMills) 来制定回收计划,并选择一些 Region 回收

选择一系列的Region来构建一个回收集(可以想象是一个set)
把决定要回收的对象region中存活的对象复制到空的region中去(复制算法)
删除掉需要回收的region -> 无内存碎片
存在stop the world
4.4.3、G1回收机制_Full GC

复制对象内存不够,或者无法分配足够的内存(比如巨型对象没有足够的连续分区分配时),会触发 Full GC。 Full GC模式下,使用 Serial Old 模式(会长时间的 Stop the World)

4.4.4、G1优化的原则

尽量减少Full GC 的发生。尽量是Young GC 或者 Mixed GC

4.4.5、G1减少Full GC 的思路?
  • 增加预留内存(增加 -XX:G1ReservePercent,默认为堆的10%,内存大了,内存不够导致full gc的可能性就小了)
  • 更早的回收垃圾(减少 -XX:InitiatingHeapOccupancyPercent,老年代达到该值就触发 Mixed GC,默认为45%,更早的触发了Mixed gc,那么full gc的可能性就小了)
  • 增加并发阶段所使用的线程数(增大 -XX:ConcGCThreads,这样就会有更多的垃圾回收线程去工作,当然了,这样会影响应用的吞吐量)
4.4.6、G1收集器的特点

1、可以作用在整个堆
2、可控的停顿 MaxGCPauseMillis=200
3、无内存碎片

4.4.7、G1收集器的适用场景:
  • 占用内存较大的应用(6G以上)
  • 替换CMS垃圾收集器(G1设计的初衷就是为了代替CMS收集器,也就是说,CMS适用的场景,可以用G1来替代)
4.4.8、G1收集器总结

1、内存是一个个的 Region 了 不在是新生代、老年代了
2、包含了 Young GC、 Mixed GC、Full GC
3、Mixed GC的步骤和CMS有类似之处(初始标记、并发标记等)。但也有很多差异(CMS是标记清除算法会有内存碎片,G1是复制算法,无内存碎片)

4.4.9、项目中使用G1 or CMS
  • 对于JDK 8:都可以用
    如果你的机器内存 <= 6G。建议使用CMS;如果你的机器内存 > 6G,考虑使用G1(业内有人测试,G1在机器内存小的情况下并不理想,内存临界值是6-8)
  • 如果 jdk > 1.8: 用G1
    因为 CMS 从jdk9就已经废弃了。

5、其他垃圾收集器

Shenandoah
ZGC
Epsilon

这三在目前最新的jdk14中依然是试验状态,还未发布,所以暂时不做介绍。

四、总结

JVM调优的重要性不言而喻。如果我们不熟悉JVM的内存结构,工作原理,那么我们调优起来可以说是一头雾水了。随着技能水平的提高,我们必要是要熟悉JVM的内存结构的,而说起调优便不得不说垃圾回收。因此熟悉和掌握垃圾回收对开发人员来说还是非常有必要的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值