根底篇

一、了解JVM内存结构

Java 虚拟机界说了各种在程序履行期间运用的运转时数据区域。这些数据区域有一些是在Java虚拟机发动时创立的,并在Java虚拟机退出时毁掉,有一些数据区域是每个线程独有的,在线程创立时创立,在线程毁掉时毁掉,依据《Java虚拟机标准》的规则,Java虚拟机运转时所需求办理的数据区域首要如下图所示:

JVM关键知识点整理,从入门到提高到实践

程序计数器(线程私有)

程序计数器是一块十分小的内存区域,由于它仅仅用来记载记个数,可以看作是当时线程履行的字节码的行号指示器,分支、循环、跳转、反常处理、线程康复等根底功用都需求依靠这个计数器来完结。

由于JVM虚拟机的多线程是经过CPU时刻片轮转来完结的,所以就必定会发生某一个线程代码未履行完就被中止履行,那么当下次再履行取得时刻片履行时就需求这个记载的行号来告知线程应该从什么地方开端履行。

假如线程正在履行的是一个Java办法,这个计数器记载的是正在履行的虚拟机字节码指令的地址;假如正在履行的是本地(Native)办法,这个计数器值则应为空(Undefined)。此内存区域是唯 一一个在《Java虚拟机标准》中没有规则任何OutOfMemoryError情况的区域。

Java虚拟机栈(线程私有)

虚拟机栈是一种先进先出的数据结构,首要效果是用来寄存当时线程运转办法时所需求的内存空间,每个办法在被履行的时分,虚拟机都会创立一个栈帧用于存储局部变量表、操作数栈、回来地址、动态链接等信息,办法履行完毕后栈帧就会被移除虚拟机栈。

反常类型

这块区域会发生两种反常:StackOverflowError、OutOfMemoryError。

1. StackOverflowError

表明栈的深度超过虚拟机所答应的最大深度。

public class Test {
    static int count = 0;
    public static void main(String[] args) {
        try {
            A();
        } catch (Throwable e) {
            System.out.println(count);
            e.printStackTrace();
        }
    }
    private static void A() {
        count++;
        A();
    }
}

JVM关键知识点整理,从入门到提高到实践

这块区域默许巨细为1M,履行21053次超时栈的最大深度,当然你可以经过-Xss的办法进行批改,比方咱们改为-Xss2m表明巨细调整为2M。

JVM关键知识点整理,从入门到提高到实践

2. OutOfMemoryError

Java虚拟机的栈区域是支撑动态扩展的,那么当栈扩展无法恳求到满意的内存时,就会抛出OutOfMemoryError反常。

栈的动态扩展仅仅Java虚拟机标准中有支撑,但具体情况还要看具体的虚拟机开发商,比方咱们常用的HotSpot虚拟机的栈便是不可以动态扩展的。

本地办法栈(线程私有)

这块区域与虚拟机栈功用相似,虚拟机是办理Java中的办法,本地办法栈是办理由C言语完结的办法,也便是调用native的办法。(HotSpot直接把本地办法栈和虚拟机栈合二为一了。)

反常类型

相同会发生StackOverflowError、OutOfMemoryError。

办法区(线程同享)

办法区首要是用于存储已被虚拟机加载的类的信息、常量、静态变量等数据。

关于办法区和永久代

办法区是Java虚拟机标准所界说的空间,是一种标准,而HotSpot在JDK1.8之前,并没有严格依照Java虚拟机标准来规划,而是规划了一个名为永久代的部分,而且为了废物搜集器的分代规划又把永久代放入了堆空间以便办理。所以要注意不要把办法区和永久代搞混了,由于实践上关于其他虚拟机,比方J9来说是不存在永久代这个概念的。

当然HotSpot也意识到假如把永久代放在堆空间内,或许会呈现由于永久代的运用过多,导致堆空间内存溢出的问题,所以为了隔离这种影响,从JDK8开端,永久代也改名为元空间,并将其移出堆空间,转而是把这部分数据存储到了本地内存中。

反常类型

对办法区中的数据收回条件是十分严苛的,可是又不能彻底不收回,这一部分空间相同会呈现OutOfMemoryError反常。

永久代溢出:OutOfMemoryError: PermGen space

元空间溢出:OutOfMemoryError: Metaspace

堆(线程同享)

堆空间是最大的一块内存空间,首要效果便是用来寄存创立的方针,简直一切的方针都是寄存在堆空间的(当然有些特别的场景存在,比方:堆外分配、方针逃逸、栈上分配等一些为了进步功能的优化手法,了解即可),所以这也是咱们需求重点关注的一部分区域,咱们平常所谈论的废物收回,分代搜集也都是针对这一部分空间的方针处理。

反常类型

相同这一部分也会呈现OutOfMemoryError:Java heap space反常。

运转时常量池

运转时常量池是办法区的一部分,首要用来寄存编译期间生成的符合引证和字面量

反常类型

既然运转时常量池是办法区的一部分,天然遭到办法区内存的约束,当常量池无法再恳求到内存时会抛出OutOfMemoryError反常,具体反常类型依据不同的JDK版本来决议。

直接内存

这一部分区实践上并不是《Java虚拟机标准》中所界说的内存区域,可是由于在JDK1.4开端新加入了NIO类,使得Java经过native函数可以直接分配堆外内存,并经过方针引证对这块内存进行操作,防止数据在Java堆和native堆中的来回仿制,从而进步功能。

反常类型

这块区域不受限于Java堆的巨细约束,而是受限服务器本身的内存容量,所以也会呈现OutOfMemoryError反常。

二、关于废物收回

1. 怎么判别一个方针是废物

引证计数法

这是一种十分简略的办法,为每一个方针中添加一个计数器,当方针被引证时,计数器就加1,当引证被释放时,计数器就减1,终究假如计数器为0,则表明该方针没有任何引证联系了,即为废物方针。

这是一种完结简略,判定功率较高的办法,也有一些著名的运用事例,比方Python言语中就运用了这种办法,可是在JVM中并没有运用这种算法,由于它存在一个显着的问题:循环引证。

如下图所示,A引证B,B引证A,除此之外再无其他任何方针引证了这个两个方针,所以这两个方针应当为废物方针,但却由于计数器都不为0,所以不能被收回。

可达性剖析法

为了防止上述的问题,在JVM中选用的是另一种可达性剖析法来判别方针是否存活,这个算法的思维便是经过判别一系列被称为GCRoots的根方针,并作为起点,依据引证联系向下查找,查找过的路径称为引证链,假如某个方针到GCRoots方针没有任何一条引证链,则判别此方针为可收回方针。

如下图所示,D、G方针与GCRoots没有任何引证链联系所以为可收回方针。

JVM关键知识点整理,从入门到提高到实践

GCRoots的范围

  1. 在虚拟机栈(栈帧中的本地变量表)中引证的方针,比方各个线程被调用的办法仓库中运用到的参数、局部变量、暂时变量等。

  2. 在办法区中类静态特色引证的方针,比方Java类的引证类型静态变量。

  3. 在办法区中常量引证的方针,比方字符串常量池(String Table)里的引证。

  4. 在本地办法栈中JNI(即一般所说的Native办法)引证的方针。

  5. Java虚拟机内部的引证,如根本数据类型对应的Class方针,一些常驻的反常方针(比方 NullPointExcepiton、OutOfMemoryError)等,还有体系类加载器。

  6. 一切被同步锁(synchronized要害字)持有的方针。

  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

除此之外依据某些废物搜集器的选用,还有或许会存在暂时性的GCRoot方针,由于废物划代搜集的办法,比方扫描重生代方针的时分,还需求考虑被老时代中方针引证的情况,此刻老时代中的方针也可视为GCRoot方针。

2. 哪些区域需求废物收回

关于线程独享的内存区域来说,例如:程序计数器、Java虚拟机栈、本地办法栈这些,他们的生命周期都与线程相同,因而是不需求独自对其进行办理的。

真正需求进行GC办理的首要便是线程同享的堆(Heap)和办法区(Method area)这两块区域了。

3. 废物收回的算法

3.1 符号-铲除

咱们首要介绍符号铲除算法,由于这是一种最根底的废物收回算法,它的收回进程首要分为两个阶段:1、依据可达性剖析算法,符号每一个存活的方针,2、将没有被符号的方针作为废物方针进行收回。当然也可以反过来符号。

收回之前内存情况

JVM关键知识点整理,从入门到提高到实践

收回之后内存情况

JVM关键知识点整理,从入门到提高到实践

优点:

  1. 整个收回进程只需求简略的设置一个符号位,相对而言体系资源耗费较少,速度较快。
  2. 整个收回进程不会移动存活的方针。
  3. 比较仿制算法,内存运用率高。

缺陷:

  1. 履行功率不稳定,假如符号的是存活方针,那么存活方针较多时就需求很多的符号和铲除,假如符号的是可收回方针,那么可收回方针较多时就需求很多的符号和铲除。
  2. 内存碎片问题,从上图中也可以看出,再一次收回完结后内存未运用空间看起来仍然很零碎,这样将导致大方针由于没有接连的内存空间而无法被分配。

3.2 符号-仿制

运用符号仿制算法当存活方针较少时的可以得到不错的收益,根本的算法思维是将内存分为两个巨细持平的区域,分配方针时每次只运用其间的一块区域,当这块区域用完时,就把还存活的方针仿制到另一块区域上,然后再把这块区域已运用的空间直接铲除。

收回之前内存情况

JVM关键知识点整理,从入门到提高到实践

收回之后内存情况

JVM关键知识点整理,从入门到提高到实践

优点:

  1. 假如大部分方针都是可收回的,那么只需求仿制少数存活的方针,功率较高。
  2. 不存在内存碎片的问题。
  3. 分配方针简略,只需移动堆顶指针,按顺序分配即可。

缺陷:

  1. 内存运用率较低,需求空出一半的内存空间用来确保容得下存活的方针。
  2. 存活方针在内存中的位置会发生改变,需求移动方针的引证地址。
  3. 相同只适宜存活方针较少的场景,假如存活方针较多就会仿制很多的存活方针。

3.3 符号-收拾

符号收拾算法一同处理了符号铲除的内存碎片问题和符号仿制的内存糟蹋的问题,比较符号铲除算法,符号收拾多个一步收拾阶段,即移动存活方针,让存活方针向堆的一端移动,然后再收拾掉其他的内存空间。

收回之前内存情况

JVM关键知识点整理,从入门到提高到实践

收回之后内存情况

JVM关键知识点整理,从入门到提高到实践

优点:

  1. 处理了内存碎片的问题。
  2. 处理仿制算法的内存糟蹋的问题。

缺陷:

  1. 相同假如存活方针较多,每次移动存活方针又会带来不小的开支。

三、方针分配战略

一般主动内存办理都需求处理的以下三个问题:

  1. 为新方针分配空间。
  2. 承认存活方针。
  3. 收回逝世方针所占用的空间。

其间第2个问题实践上要处理的便是怎么判别一个方针是废物的?这个在前面的文章中现已有介绍,第3个问题实践上便是废物收回的办法,这个在后边的文章中也会介绍,本节再来看看关于方针分配的问题是怎么处理的。

首要咱们仍然依据分代区分的思维,将堆空间分为重生代、老时代,其间重生代一般又被分为一个Eden区和两个Survivor区。

1. 方针优先在Eden区分配

大多数情况下,方针必定是优先分配在Eden区的,假如Eden区空间缺乏,就会触发一次重生代的收回(也可以叫做:Minor GC或YGC)。

TLAB

本地线程分配缓冲,内存分配实践上被依照不同的线程区分在不同的内存之间进行,每个线程在Eden区中中有一块独享的小区域,这样做的优点是可以削减同步处理带来的功能耗费。

可以运用-XX:TLABSize设置巨细。

2. 大方针直接进入老时代

大方针一般指的是那种需求占用接连的内存空间的方针,比方很大的一个数组方针。

为什么大方针不优先在Eden区分配?

首要咱们知道Eden区的方针都是默许被咱们假定为朝生夕死的方针,在Eden区中的方针默许需求经历15次废物收回(动态年纪)才会被放入老时代,所以假定这个大方针不是一个短寿鬼,那么咱们就需求在内存中来回仿制15次,这必定会下降废物收回的功率,所以干脆直接放入老时代,以防止大方针的频频仿制进程。

写代码时应该注意防止大方针的频频发生

了解这个分配原则后,咱们平常在写代码就应当尽量防止不必要的大方针发生,尤其是那种朝生夕死的大方针,由于这样的方针就会频频的进入老时代,而且假如老时代的接连内存空间缺乏,就会频频的触发FullGC,由于要为大方针收拾出接连的内存空间。

一同大方针必定需求耗费更多的内存仿制的开支。

运用-XX:PretenureSizeThreshold这个参数可以设置大方针的阈值,不过要注意这个参数只对Serial和ParNew两款重生代搜集器有用。

分配演示

public class Test {
    public static void main(String[] args) throws InterruptedException {
        byte[] bytes = new byte[1024*1024*1];//分配1M内存
        Thread.sleep(Integer.MAX_VALUE);//让程序休眠,调查内存情况
    }
}

设置JVM参数,JDK1.8环境

-Xms20m(堆的初始巨细)

-Xmx20m(堆的最大巨细)

-XX:NewSize=10m(重生代的初始巨细)

-XX:MaxNewSize=10m(重生代的最大巨细)

咱们可以经过jmap指令检查heap的分配情况

JVM关键知识点整理,从入门到提高到实践
Eden区一共运用了5M,4M大约来自JDK本身发动时所需加载的方针所占用的内存空间。

设置-XX:PretenureSizeThreshold=1024(单位为byte),废物搜集器为Serial,再看一下效果。

JVM关键知识点整理,从入门到提高到实践
这时分1M的byte数组就被直接分配到了老时代中了。

3. 长期存活的方针进入老时代

方针首要被分配到Eden区,当发生MinorGC后,假如方针仍然存活,那么就会被移动到Survivor区,此刻方针的年纪就会+1岁,当到达指定年纪后方针仍然存活,这样的方针就归于长期存活的方针,那么就会被放入老时代中,这样做的优点当然是为了削减方针在重生代中来回仿制带来的功能耗费。

运用-XX:MaxTenuringThreshold参数可以装备年纪的巨细,其间parallel默许为15,CMS默许为6。

示例演示

public class Test {
    public static void main(String[] args) {
        byte[] b1 = new byte[1024 * 256];
        byte[] b2 = new byte[1024 * 1024 * 1];
        byte[] b3 = new byte[1024 * 1024 * 2];
        byte[] b4 = new byte[1024 * 1024 * 2];
    }
}

当运用默许年纪时,发生MinorGC后,有一部分方针进入Survivor区。

[GC (Allocation Failure)
Desired survivor size 1048576 bytes, new threshold 2 (max 2)
[PSYoungGen: 6350K->1016K(9216K)] 6350K->4476K(19456K), 0.0015667 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 3230K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 27% used [0x00000000ff600000,0x00000000ff8299b8,0x00000000ffe00000)
  from space 1024K, 99% used [0x00000000ffe00000,0x00000000ffefe020,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 3460K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 33% used [0x00000000fec00000,0x00000000fef61010,0x00000000ff600000)
 Metaspace       used 3193K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 349K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0

当设置-XX:MaxTenuringThreshold=0后,发现Survivor区没有存活方针了。

[GC (Allocation Failure)
Desired survivor size 1048576 bytes, new threshold 0 (max 0)
[PSYoungGen: 6350K->0K(9216K)] 6350K->4417K(19456K), 0.0017623 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 2214K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 27% used [0x00000000ff600000,0x00000000ff829960,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 4417K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 43% used [0x00000000fec00000,0x00000000ff050420,0x00000000ff600000)
 Metaspace       used 3181K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0

4. 方针动态年纪判别

在HotSpot虚拟机规划中,并不是彻底要等方针年纪到达-XX:MaxTenuringThreshold设置的值今后才会被放入老时代,也有一种例如的情况,当Survivor空间中相同年纪一切方针巨细的总和大于Survivor空间的一半,年纪大于或等于该年纪的方针就可以直接进入老时代。

5. 栈上分配

简直一切的方针都是分配在堆内存中,可是还有一种比较特别的分配办法是分配在栈上,这是借助于逃逸剖析来辅佐完结的,逃逸剖析中指出假如方针的效果域不会逃出办法或许线程之外,也便是无法经过其他途径拜访到这个方针,那么就可以对这个方针采取一定程度的优化,这其间就包括了:栈上分配。

栈上分配的优点在于,方针可以随着栈的出栈进程被天然的毁掉,节省了堆中废物收回所耗费的功能。

方针分配大致的流程图

JVM关键知识点整理,从入门到提高到实践

四、方针的引证联系

在Java中引证类型别离有强引证(Strong Reference)、软引证(Soft Reference)、弱引证(Weak Reference)、虚引证(Phantom Reference)这4 种类型,对应的引证强度依次减弱。

1. 强引证

关于直接引证经过new要害字生成的方针便是强引证联系,此类引证有必要由废物搜集器判别承认没有任何引证联系后才干被收回。

2. 软引证

软引证是用来处理一些有用可是并非必需的方针,这些方针就可以用软引证相关,软引证相关的方针会在体系即将发生内存溢出OOM之前收回。

下面这段代码演示了在内存缺乏时,即将发生OOM,所以软引证方针被收回。

-Xms10m -Xmx10m,设置最大内存10M。

public class Test {
    public static void main(String[] args) {
        byte[] b = new byte[1024 * 1024 * 5];
        SoftReference softReference = new SoftReference(b);
        b = null;
        System.gc();
        System.out.println("b方针没有任何引证,手动调用gc,b方针被收回:" + b + " ,软引证方针:" + softReference.get());
        //由于现已存在5M方针,再分配6M必定不行分配,所以为了防止OOM,软引证方针被收回。
        byte[] b1 = new byte[1024 * 1024 * 6];
        System.out.println("b1方针,超过内存最大值,触发废物收回,b1方针:" + b1 + " ,软引证方针被收回:" + softReference.get());
    }
}

JVM关键知识点整理,从入门到提高到实践

3. 弱引证

弱引证和软件引证效果相似,只不过弱引证等级更低,在下一次废物收回时就会被收回,不管当时内存是否满意。

public class Test {
    public static void main(String[] args) {
        byte[] b = new byte[1024 * 1024 * 1];
        WeakReference weakReference = new WeakReference(b);
        b = null;
        System.out.println("弱引证方针在gc前:" + weakReference.get());
        System.gc();
        System.out.println("弱引证方针在gc后:" + weakReference.get());
    }
}

JVM关键知识点整理,从入门到提高到实践

4. 虚引证

对引证方针彻底没有影响,随时或许被收回,唯一意图是方针在收回时能收到一个告诉。

运用场景

软引证:一般都可以作为缓存运用,作为一种筛选战略,可防止OOM。

弱引证:也可作为缓存运用,可是会更快的被铲除,所以与软引证的缓存等级不相同,更适用于一些暂时、短期缓存。 另外还可参阅ThreadLocal中的一种运用场景,便是主动删去,防止由于key的内存走漏,key有一个强引证和一个弱引证,一旦强引证没了,就希望key可以主动收回,而不需求首要删去这个key,由于删去这个key或许比较麻烦,所以就可以经过弱引证完结。

虚引证:java直接内存就运用了虚引证的办法,堆中有一个方针,保存了堆外内存的引证,当这个方针被收回时,会收到告诉,则收回堆外内存。

进步篇

一、分代废物收回算法

前面在根底篇中提到了三种废物收回算法,实践上每种废物搜集器都有各自的优缺陷,没有一种算法可以习惯一切的场景,因而在JVM中并没有彻底的选用其间任意一种废物收回算法,而是依据不同的场景挑选适宜的算法。

分代搜集

为了满意挑选适宜的废物收回算法,JVM中选用了分代搜集的理论进行规划,它建立在如下两个分代假定之上:

  1. 弱分代假说:绝大多数方针都是朝生夕死的。
  2. 强分代假说:熬过越屡次废物搜集进程的方针就越难以消亡。

依据这两个假定的根底上,JVM对堆空间进行了区分,并依据方针的年纪(熬过屡次的方针)区分到不同的空间上,所以就区分出了:重生代、老时代

重生代

依据弱分代假说理论,大多数方针都是可收回的,所以可以选用符号仿制算法。

老时代

依据强分代假说理论,把在重生代经历过屡次收回都存活的方针,放入老时代,那么老时代中的方针大部分就都是不可收回的,所以可以选用符号铲除或许符号收拾算法,只需求少数的符号和铲除可收回方针即可。

一同重生代中的方针由于死的快,而老时代中的方针大多数都是难以消亡的,所以把这个两部分区域区分开来,就又能以不同收回频率去进行收回,老时代的收回频率往往要远低于重生代的收回频率。

对符号仿制算法的改进

现在咱们知道重生代可以选用符号仿制算法进行收回,而符号仿制算法的缺陷便是需求空出一半的内存空间,那么在JVM中实践上并没有这样做,IBM公司曾有一项专门研究对重生代朝生夕灭的特色做了更量化的诠释——重生代中有98%的方针是熬不过第一轮GC的。因而并不需求依照1:1的份额来区分重生代的内存空间。

依据这一项研究,HotSpot虚拟机就又把重生代区分为了3个部分:Eden、Survivor0、Survivor1,他们的份额默许为8:1:1,也便是说两个Survivor区选用彻底仿制的算法,这样一来仅仅糟蹋了重生代的10%的内存空间。

当然理论之下总有意外,假如存活的方针便是超过了10%怎么办?当然这时分一般就会依靠老时代来进行担保了。

符号铲除仍是符号收拾?

HotSpot中关注吞吐量的Parallel Scavenge搜集器是依据符号收拾算法的,而关注推迟的CMS搜集器则是依据符号铲除算法的。

为什么Parallel Scavenge挑选符号收拾,CMS挑选符号铲除?

符号铲除和符号收拾的首要区别就在于方针的移动收拾,假如移动则内存收回时会愈加杂乱,假如不移动则方针分配时会愈加杂乱,所以符号铲除算法在废物收回时速度更快、中止时刻更短,而符号收拾算法尽管中止时刻稍长,可是方针的分配和拜访则比较符号铲除愈加速,又由于分配和拜访的频率要远远高于废物收回的频率,所以从总比来看吞吐量是要高于符号铲除算法的。

CMS中还有一种特别的做法,便是一般情况下选用符号铲除算法,直到碎片程度太严峻的时分可以再选用符号收拾算法。

二、三色符号法

三色符号法是JVM中用来符号方针是否为废物的一种办法,首要是针对CMS、G1等废物搜集器运用的,这类搜集器都有一个废物收回线程与用户线程一同履行的并发进程,这是一般符号铲除算法不能支撑的。

三色符号法便是为了使废物扫描阶段可以与用户线程并发履行而发生的,由于传统的符号铲除算法,必需求暂停一切用户线程。

在传统的符号铲除算法下,只需两个情况位:0、1,比方0:表明未标识,1:表明方针可达,一次扫描完毕后,铲除一切情况为0的,然后再把情况为1的重置为0。

可是假如与用户线程一同履行就不能这样玩了,由于一旦一同履行就有或许在一条链未悉数扫描完的情况下,用户线程改动了这条链上的引证联系,比方现在有一条引证链:A—>B—>C—>D,当扫描到C时,A和B都被标识为1,此刻C到D的引证联系被删去,那么D方针就不能承认是否为废物方针,由于有或许D又被用户线程设置为其他方针的引证了,那么为了D不被误删,只能让D的标识也为1,可是假如D便是没有被其他方针引证了,那么D就逃过了这次废物搜集的进程,这就会形成很多的起浮废物。

当然必定也不能设置为0,由于0在未扫描之前尽管表明的是未符号方针,可是在扫描开端后就表明废物方针了。

所以上述问题很显着便是缺少了一个表明中间情况的进程,由于线程一同进行,所以引证链上的方针并不是简略的可达与不可达的联系,而是会有一个扫描进程中的情况,所以就呈现了三色符号法。

1. 三色符号法中的三色

白色

表明方针尚未被废物搜集器拜访过。明显在可达性剖析刚刚开端的阶段,一切的方针都是 白色的,若在剖析完毕的阶段,仍然是白色的方针,即代表不可达。

灰色

表明方针现已被废物搜集器拜访过,但这个方针上至少存在一个引证还没有被扫描过,也便是整个引证链还未悉数扫完。

黑色

表明方针现已被废物搜集器拜访过,且这个方针的一切引证都现已扫描过。黑色的方针代表现已扫描过,它是安全存活的,假如有其他方针引证指向了黑色方针,无须从头扫描一遍。黑色方针不或许直接(不经过灰色方针)指向某个白色方针。

初始阶段:悉数为白色

JVM关键知识点整理,从入门到提高到实践

A方针扫描完结后变为黑色,B方针正在扫描则标识为灰色,剩下的白色方针标识还未被扫描。

JVM关键知识点整理,从入门到提高到实践
终究依照可达性剖析算法一轮扫描下来结果如下
JVM关键知识点整理,从入门到提高到实践

终究白色方针E即为废物方针。

首要便是运用三个调集,别离来寄存三种色彩的方针,开端扫描时把被扫描的白色方针从白色调集中移动到灰色调集中,灰色方针扫描完结后,又被移动到黑色方针调集中,终究完结一切初始符号时识别到的GCRoot引证链路径后,余下的白色调集中的方针即为废物方针。

2. 三色符号的漏标问题

三色符号的思维十分简略,但仔细剖析一下就会发现其间的问题,假如把一个白色方针的引证设置到一个黑色的方针上,那么这个白色方针就会被错误的以为是一个废物方针,由于黑色方针表明的是这个方针现已完结了扫描且这个方针的一切引证都现已扫描过。

第一次符号时,联系如下:

JVM关键知识点整理,从入门到提高到实践
用户线程批改了引证联系如下:
JVM关键知识点整理,从入门到提高到实践
此刻接着扫描E方针,发现E方针之后没有引证联系了,把E方针设置为黑色,废物搜集器以为两条引证链上的方针悉数扫描完毕,可是F方针却被遗漏了。

Wilson于1994年在理论上证明了,当且仅当以下两个条件一同满意时,会发生方针消失的问题,即本来应该是黑色的方针被误标为白色:

  1. 赋值器刺进了一条或多条从黑色方针到白色方针的新引证。
  2. 赋值器删去了悉数从灰色方针到该白色方针的直接或直接引证。

3. 怎么处理漏标问题?

既然问题的发生需求一同满意上述两个条件,那么要处理就只需损坏其间一种即可,CMS和G1恰好别离运用其间一种条件来处理。

3.1. 增量更新(Incremental Update)

增量更新要损坏的是第一个条件,当黑色方针刺进新的指向白色方针的引证联系时,就将这个新刺进的引证记载下来,等并发扫描完毕之后,再将这些记载过的引证联系中的黑色方针为根,从头扫描一次。这可以简化理解为,黑色方针一旦新刺进了指向白色方针的引证之后,它就变回灰色方针了。

C方针被批改为灰色,那么就会沿着灰色方针持续扫描,终究会扫描到F方针。

JVM关键知识点整理,从入门到提高到实践

3.2. 原始快照(Snapshot At The Beginning, SATB)

原始快照要损坏的是第二个条件,当灰色方针要删去指向白色方针的引证联系时,就将这个要删去的引证记载下来,在并发扫描完毕之后,再将这些记载过的引证联系中的灰色方针为根,从头扫描一次。这也可以简化理解为,不管引证联系删去与否,都会依照刚刚开端扫描那一刻的方针图快照来进行查找。

第一、二两条链扫描完结后,多出了第三条引证链,从之前的灰方针E开端,指向F方针,这样F方针就不会被收拾掉了。

JVM关键知识点整理,从入门到提高到实践

运用这种办法会有一个问题,假定引证联系如下:

JVM关键知识点整理,从入门到提高到实践

之后引证联系被改动

JVM关键知识点整理,从入门到提高到实践

E到F的引证没有了,F也没有再被其他方针引证,可是由于E方针为灰色方针,所以为了防止漏标,E方针终究仍是会有一条到F的引证联系,这便是起浮废物问题,F方针会逃过本次的废物扫描,等候下次再被收拾,但这总比漏标要好的多,但这种情况仍是比较少的,由于只需在改动灰色方针时才需求记载。

三、废物收回器

一般咱们认知比较高的废物收回器都在下图中了,当然图上的最新收回器是G1,而在JDK11时发布的ZGC也是论题度很高的一款新式废物收回器,尽管有这么多种废物收回器,不过就当下来看,现在用的最多的还以parallel、cms、g1这三种为代表。

JVM关键知识点整理,从入门到提高到实践

接下来,咱们就别离来谈谈这三种废物收回器。

1. Parallel

首要是Parallel,见名知意,这一款可以并行履行的废物收回器,其首要的关注点在于确保体系的吞吐量,你或许会觉得这款废物收回器太老了,也不能做到并发收回,但它可是现在运用最多的JDK8中默许的废物收回器,而这一点我发现很多人都不知道(查查你们公司现在正在用的废物收回器是不是它),而且假如服务内存本身就比较小,那关于Parallel来说本身占用内存也是比较少的。

1.1. Parallel Scavenge、Parallel Old收回算法

Parallel Scavenge是针对重生代的废物收回器,而Parallel Old是针对老时代的废物收回器,关于重生代的收回算法,参阅前面相关的理论知识,应该挑选符号-仿制算法,而老时代,可以用符号-铲除或许符号-收拾,那寻求吞吐量的情况下,Parallel Old必定是挑选了符号-收拾。

吞吐量

这儿有必要说明一下什么是吞吐量?在废物收回中,吞吐量指的便是运转用户线程时刻占体系总运转时刻的比值。

举个比方:运转用户代码时刻为99分钟,废物搜集器进行废物收回运转了1分钟,那么吞吐量便是:99 / (1+99) = 99%

寻求高吞吐量可以最大程度的运用CPU资源完结运算的任务,这就比较适宜关注后台运算,而与用户交互较少的场景。

1.2. 两个要害参数:

-XX:MaxGCPauseMillis

设置最大GC暂停时刻的方针(以毫秒为单位)。这是一个软方针,而且JVM将尽最大的尽力来完结它。 默许情况下,没有最大暂停时刻值,这需求额定注意,他很有或许会形成较长期的GC暂停。 下面的示例显现怎么将最大方针暂停时刻设置为500ms: -XX:MaxGCPauseMillis = 500

当然你不能简略的以为这个值设置的越小越好,你要知道Parallel Scavenge是怎么做到操控中止时刻的?实践上便是简略的添加废物收回频率而已,也便是说你设置的中止时刻越短,废物收回的频率就会越频频,比方:本来30秒一次废物收回,一次中止2秒,现在由于设置的中止时刻为1秒,所以有必要10秒履行一次废物收回,尽管中止时刻短了,可是吞吐量也低了。

-XX:GCTimeRatio

这个参数的值则应当是一个大于0小于100的整数,也便是废物搜集时刻占总时刻的 比率,相当于吞吐量的倒数。比方把此参数设置为19,那答应的最大废物搜集时刻就占总时刻的5% (1/(1+19)),默许值为99,即答应最大1%(1/(1+99))的废物搜集时刻。

2. CMS

CMS(Concurrent Mark Sweep)是HotSpot虚拟机中第一款完结并发搜集的废物收回器,是为那些希望运用较短的废物搜集暂停时刻而且可以在运用程序运转时与废物搜集器同享处理器资源的运用程序而规划的,简略来说,CMS便是寻求最短中止时刻的废物搜集器。

CMS的热度一直都很高,也算是具有重要含义的一款废物收回器,不过惋惜的是,它并没有成为任何一版JDK中的默许废物收回器,我想应该也是由于它缺陷显着,后边又有了更超卓的G1的原因吧,尽管如此,CMS的规划理念仍是很值得咱们学习的,所以让咱们一同看看它究竟是怎么做到一同兼顾废物收回与方针发生的。

2.1 收回战略

CMS首要针对老时代进行废物收回,可以配合Serial或许ParNew重生代废物搜集器进行收回,而且从名字上包括Mark Sweep就可以看出CMS搜集器是依据符号-铲除算法完结的,相对之前的废物搜集器CMS整个收回进程要稍微杂乱一些,大致分为4步:

  • 初始符号(CMS initial mark)
  • 并发符号(CMS concurrent mark)
  • 从头符号(CMS remark)
  • 并发铲除(CMS concurrent sweep)
2.1.1. 初始符号(CMS initial mark)

首要初始符号,需求暂停用户线程,不过这一步仅仅符号GCRoots能直接相关到的方针,因而暂停时刻很短。

只符号GCRoots直接可达方针

JVM关键知识点整理,从入门到提高到实践

2.1.2. 并发符号

并发符号便是接着初始符号的根方针持续往下符号,这个阶段是最耗时的,可是好在是与用户线程并发履行的。

考虑一种情况,老时代方针被重生代方针引证,假如此刻只扫描老时代的GCRoots方针,A方针就会被遗漏,所以并发符号时实践上也会扫描重生代方针。

JVM关键知识点整理,从入门到提高到实践

2.1.3. 从头符号

从头符号阶段是为了批改并发符号期间,因用户程序持续运作而导致符号发生变动的那一部分方针的符号记载,这个阶段的中止时刻一般会比初始符号阶段稍长一些,但也远比并发符号阶段的时刻短。

2.1.4. 并发铲除

收拾删去掉符号阶段判别的现已逝世的方针,由于不需求移动存活方针,所以这个阶段也是可以与用户线程一同并发的。

并发预收拾阶段

实践上除了上述的首要流程之外,CMS还有一步并发预收拾阶段,这个阶段首要是发生在从头符号之前,此阶段作业与从头符号相似,意图首要是为了希望可以在从头符号前触发一次重生代的GC,这样就可以削减从头符号的中止时刻,此阶段首要符号重生代提升到老时代的方针,直接分配到老时代的方针,并发进程中引证发生批改的方针,默许情况下当eden区到达了2M,则会敞开并发预收拾阶段,当eden区运用到达50%时中止预收拾,或许预收拾阶段超过默许时刻5秒时也会中止预收拾,装备CMSScavengeBeforeRemark参数,也可强制使每次从头符号前都触发一次YGC,可是这样的做法,尽管削减了从头符号的任务,但假如刚好现已履行过一次YGC,从头符号又履行一次,也会形成STW时刻变长。

怎么处理并发符号时引证联系改动问题?

由于第二阶段废物符号是与用户线程并发履行的,那就有或许发生错误符号的问题,比方一个方针咱们刚刚符号完,结果用户线程又把其他方针引证到这个刚刚符号完的方针上。

如下图,当废物线程符号时,A的这条引证链走到B就现已走完了,可是假如之后用户线程让B方针又引证了C方针,那么C方针就会被漏标,终究会被作为废物方针被收拾掉,明显C方针是不能被收回的。

JVM关键知识点整理,从入门到提高到实践

为了处理这样的问题,CMS首要将老时代等份区分成了好多小块,这些小块的调集可以叫做card table(byte数组,数组中每一元素对应一个块),当某一个方针的引证发生改变时(只记载黑色方针引证发生改变),就改动这个方针地点的块的标识,比方符号为:脏card,这样咱们在终究符号时只需在遍历一次一切的脏card即可。

怎么承认重生代方针是否存活?

  1. GC可达性剖析
  2. 老时代引证重生代方针

GC可达性剖析不用多说,首要剖析一下老时代引证重生代方针的问题,方才剖析初始符号时就现已了解到,在分代搜集中仅仅扫描GCRoots必定是不行的,要承认老时代方针是否存活就有必要扫描一切重生代方针,所以方才介绍了CMS并发预收拾阶段便是为了来一次重生代的废物收回,这样重生代中大多数方针就被收回了。

现在问题是重生代要判别哪些方针被老时代引证了,老时代的方针的都是长期存活的,一次废物收回可没用,那就只能全量扫描老时代了?明显CMS不会这样做,这时分card table又派上用场了,当有重生代引证老时代方针时,只需求把老时代地点的card符号新增一个标识即可,就像上面符号为相同,这样重生代只需求扫描一切有相关标识的card即可。

card table是一个byte数组,一个byte有8个位,只需约定好每一位的含义就可以区分标识是方针在并发期间批改了,仍是老时代引证重生代方针!

2.2 CMS缺陷

  1. 由所以并发履行,所以会占用用户线程,CPU核心数小于4的服务器不引荐运用。
  2. 起浮废物问题,由于CMS是与用户线程并发履行的,所以并不能等候内存占用到达100%了再收回,jdk6今后默许是92%,就会敞开CMS废物收回,假如进程中发生Concurrent Mode Failure,则会切换成serial old进行收回。
  3. 废物碎片:CMS选用符号-铲除算法,因而会存在碎片问题,CMS默许情况下每一次FullGC都会进行一次紧缩收拾,经过参数可以装备UseCMSCompactAtFullCollection默许为true, CMSFullGCsBeforeCompaction便是表明装备每多少次CMS的FullGC履行一次紧缩,可是假如用户调用system.gc或许担保失利,那也会触发紧缩的FullGC。

2.3 CMS常见问题处理思路

并发模式失利和提升失利都会导致长期的中止,常见处理思路如下:

  1. 下降触发CMS GC的阈值。即参数 -XX:CMSInitiatingOccupancyFraction 的值,让CMS GC尽早履行,以确保有满意的空间。
  2. 添加CMS线程数,即参数 -XX:ConcGCThreads。
  3. 添加老时代空间。
  4. 让方针尽量在重生代收回,防止进入老时代。

一般CMS GC的进程依据符号铲除算法,不带紧缩动作,导致越来越多的内存碎片需求紧缩。 常见以下场景会触发内存碎片紧缩:

  1. 重生代Young GC呈现重生代提升担保失利(promotion failed))
  2. 程序主动履行System.gc()

可经过参数CMSFullGCsBeforeCompaction的值,设置多少次Full GC触发一次紧缩。

默许值为:0,代表每次进入Full GC都会触发紧缩,带紧缩动作的算法为单线程Serial Old算法,暂停时刻(STW)时刻十分长,需求尽或许削减紧缩时刻。

2.4 要害参数

-XX:CMSInitiatingOccupancyFraction

这个参数指的是一个百分比(0-100),表明当内存空间运用率到达百分之N时就开端履行废物收回,设置的过小,简略导致内存运用率低,设置过高,假如并发收回时,内存无法满意程序分配新方针的需求,就会呈现一次并发失利(Concurrent Mode Failure),冻结用户线程的履行,暂时启用Serial Old搜集器来从头进行老时代的废物搜集, 但这样中止时刻就很长了。

JDK5时这个值默许为68%,JDK6时,现已把默许值提升至92%,这个值要依据实践情况来设置。

-XX:ConcGCThreads

设置用于并发GC的线程数。缺省值取决于JVM可用的CPU数量。

-XX:CMSFullGCsBeforeCompaction

这个参数的效果是要求CMS搜集器在履行过若干次(数量 由参数值决议)不收拾空间的FullGC之后,下一次进入Full GC前会先进行碎片收拾(默许值为0,表明每次进入Full GC时都进行碎片收拾)。

3. Garbage-First (G1)

Garbage-First (G1)是一款十分具有特别含义废物搜集器的技术发展表现,由于比较G1之前的废物搜集器,G1初次打破了依据老时代或许重生代一整块内存进行搜集的规划思维,G1规划上仍然有分代的思维,可是在内存上不再进行分代上的物理区分,也便是在一块大的内存区域中,既有年青代也有老时代,G1适用于具有大内存的多核服务器,G1尽管与CMS相同都是寻求低中止时刻的废物搜集器,可是由于G1在规划上的突破,使其能在更大的内存空间收回时,保持优秀的废物收回功率,这是G1之前的一切废物搜集器所不能做到的。

3.1 G1中的分代规划

G1与其他的废物搜集器比较不再有物理上的区域区分,而是直接运用一整块内存空间,而且区分为多个巨细持平的独立区域(Region),每一个Region可以在逻辑上被区分为Eden区、Suvivor区、Old区、Humongous区,而且每一个类型的Region也没有固定的数量、巨细与地址。

Humongous区是G1中新增的区域,专门用来寄存大方针的,G1中界说一个方针假如超过Region巨细的50%就归于大方针。

每个Region的巨细可以经过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。

JVM关键知识点整理,从入门到提高到实践

每一个Region表明的含义是不固定的,Eden区或许会变成Old区,G1可以依据优化战略自行调整它们之间的份额,所以一般运用G1时,不需求手动装备重生代与老时代巨细。

3.2 可预测的中止时刻

在G1中运用参数-XX:MaxGCPauseMillis,可以操控最大的中止时刻,这仍然是一个软方针,但比较Parallel Scavenge设置而言,这要愈加可控一些,由于现在的内存现已被区分为许多小的Region区,G1一般可以在并发符号阶段完结之后,就能计算出每个Region区收回时的巨细,以此来评估出此次可优先收回的Region区域,当然假如你把这个值设置的太小,那么G1终究也只能牺牲每次收回的废物量而导致废物收回变得更频频,这反而下降了整体功能。

3.3 收回进程

3.3.1. Initial marking phase

JVM关键知识点整理,从入门到提高到实践

此阶段为GCRoot根节点符号,需求暂停用户线程,这个阶段是在MinorGC(年青代废物收回)阶段完结的。

3.3.2. Root region scanning phase

此阶段会对在初始符号阶段符号的Suvivor区域进行扫描,看看是否有对老时代方针的引证并进行符号,此阶段可以与服务一同运转,但一定会在下一个MinorGC(年青代废物收回)开端之前完结。

3.3.3. Concurrent marking phase

JVM关键知识点整理,从入门到提高到实践

此阶段开端对整个堆区域进行扫描,此阶段也是与服务一同运转,但或许会被MinorGC(年青代废物收回)中止。

3.3.4. Remark phase

JVM关键知识点整理,从入门到提高到实践

此阶段是STW阶段,会暂停用户线程,处理并发阶段时引证发生改变的一些方针。

3.3.5. Copying/Cleanup phase

JVM关键知识点整理,从入门到提高到实践

仿制/收拾阶段完结之后

JVM关键知识点整理,从入门到提高到实践

终究一个阶段仍然需求暂停用户线程,计算Region中的数据以及对RSet进行收拾,依据希望的中止方针进行相应处理挑选。

3.4 常见问题

3.4.1 CSet调集

CSet调集即每次选出来的待收回的调集,Young GC和Mix GC都将会向CSet调集中添加内容。

3.4.2 RSet调集

之前在介绍CMS时提到了一个问题怎么承认重生代方针是否存活?关于G1相同存在这个问题,便是那些在老时代的方针引证了重生代的方针,与CMS相同,G1也是把每一个Region区分为一些Card Table块,不同的是由于CMS的老时代只需一个,所以只需求保护一个对应的Card Table调集,而G1中的老时代会有很多个,这就需求保护很多个Card Table调集,所以G1在外面又加了一层调集,直接用来记载当时重生代被哪些老时代引证了,这个调集便是RSet,RSet可以理解为是一个Map调集,Key便是Region分区的开端地址,Value又是一个调集,调集中的元素便是这个Region分区中Card Table的脏下标。

3.4.3 起浮废物

关于起浮废物的问题,前面在介绍三色符号法时现已提到过了,由于G1选用的是SATB的办法来处理漏标的问题,因而会发生起浮废物的问题(具体解说看前面的三色符号法的介绍)。

3.4.4 Allocation (Evacuation) Failure

与CMS相同,由于都是与用户线程并行履行的,因而有或许会遇到用户线程发生废物的速度比废物收回器收回的速度要快,一旦遇到这样的情况,那就会发生一次Full GC。

3.4.5 Young GC

Young GC仍是针对Eden区和Suvivor区的收回,一次YGC后,存活下来的Eden区和Suvivor区的方针将被仿制到一块新的区域,并会放入CSet调集中,相同的经过屡次YGC后仍然存活的方针将会被移动到old区。

3.4.6 Mix GC

当完结并发符号后,G1就会进入混合废物搜集阶段,在此阶段G1会挑选将一些old区添加到即将搜集的Eden区和Suvivor区中,当挑选了满意多的old区域今后,G1就又会回到YGC的收回。

3.4.7 资源耗费

内存

比较G1之前的废物收回器,由于其特有的挑选收回办法,使得在大内存下G1仍然可以操控好收回时刻,不过也由于G1中每个Region都需求保护一份RSet调集,这就导致G1中的RSet或许会占整个堆容量的20%乃至更多的内存空间。

CPU

CMS和G1都有由于并发符号进程用户线程改动方针引证联系的问题,二者都需求进行Card Table的保护,CMS和G1中都经过写后屏障进行保护,不过G1中为了完结原始快照的算法还需求写前屏障来盯梢指针的改变情况,所以在用户程序运转进程中会发生由盯梢引证改变带来的额定负担。

3.4. 运用主张

  1. 一般情况运用G1时,不主张指定年青代的巨细或许调整其占比,由于这样会使希望GC暂停的设置失效。
  2. G1尽管可以指定暂停时刻的数值,但并不主张设置的太低,G1的吞吐量方针是90%的运用程序履行时刻和10%的废物收回时刻,设置过于急进的方针则会使废物收回变的十分频频,这将直接影响吞吐量。
  3. 关于混合废物收回的调整,请注意下面几个参数值的设置:-XX:G1MixedGCLiveThresholdPercent、-XX:G1MixedGCLiveThresholdPercent、-XX:G1HeapWastePercent、-XX:G1MixedGCCountTarget、-XX:G1OldCSetRegionThresholdPercent
  4. 关于大方针,不管是分配仍是收回都会带来一定的危害,主张依据实践情况调整G1HeapRegionSize的值来防止过多的方针被界说为大方针。

3.5. 要害参数

标题 1
-XX:MaxGCPauseMillis 希望的最大GC暂停时刻,默许为:200ms,G1的默许战略是希望在吞吐量与推迟之间保持平衡,所以假如你希望取得较高的吞吐量,那么可以经过削减GC暂停的频率来完结,而削减GC暂停频率的首要办法便是添加最大GC暂停时刻。
-XX:ParallelGCThreads 废物搜集暂停期间用于并行作业的最大线程数。默许依据运转JVM计算机的可用线程数决议,计算办法:当进程可用的CPU线程数小于等于8时,则直接运用该数,不然,将设置为:8 + (n - 8) * (5/8)
-XX:ConcGCThreads 用于并发作业的最大线程数,默许情况下,此值为:-XX:ParallelGCThreads除以4。
-XX:G1HeapRegionSize 默许会依据最大堆的巨细,依照区分出2048个region来计算出每个region的巨细,最大值为32M,用户可自界说的范围是1~512M,且有必要是2的幂。
-XX:G1NewSizePercent 重生代最小堆的百分比占比,默许为Java堆的5%。
-XX:G1MaxNewSizePercent 重生代最大堆的百分比占比,默许为Java堆的60%。
-XX:G1HeapWastePercent 为了更有用的进行废物收回,G1会从CSet中挑选释放一些对内存空间增益更大的region,其间有一项参阅便是可收回空间要大于XX:G1HeapWastePercent设置的值,默许为:5%,表明占当时堆空间的5%。
-XX:G1MixedGCCountTarget 在混合收回阶段,G1希望可以最大化的的进行收回,但一同还需求考虑XX:MaxGCPauseTimeMillis,因而一般会把一次大的混合收回,拆分为屡次,这个次数就由XX:G1MixedGCCountTarget决议,默许为:8次,这样就削减了每一次混合收回的暂停时刻,以到达XX:MaxGCPauseTimeMillis的方针值。
-XX:G1MixedGCLiveThresholdPercent 在混合收回阶段,会防止收回那些需求很多时刻来处理的region,那么怎么判定是否需求很多时刻来处理呢?那么在大多数情况下,占用率高的region就需求耗费更多的时刻来处理,XX:G1MixedGCLiveThresholdPercent便是设置的存活方针占用率的阈值,默许为:85%,也便是假如一个region中的存活方针占比到达此-XX:GCPauseTimeInterval= region的85%,那么就不会收回这个region。
-XX:G1ReservePercent 保留闲暇区域的百分比,默许为10%
-XX:G1OldCSetRegionThresholdPercent 设置混合废物收回周期中要搜集的old 区数量的上限。默许值为堆的10%
-XX:InitiatingHeapOccupancyPercent 设置触发符号周期的堆占用阈值,默许为占用整个堆的 45%

3.6 G1日志剖析


G1 Evacuation Pause
young(年青代收回):表明年青代运用空间满了。
mixed(年青代+老时代一同收回):表明老时代运用占用到了堆空间的-XX:InitiatingHeapOccupancyPercent设置的值。
G1 Humongous Allocation
大方针恳求都会触发一次GC。
[GC pause (G1 Evacuation Pause) (young), 0.0264657 secs]
   并行履行阶段
   GC发动了10个线程并行收回,耗时20.7ms
   [Parallel Time: 20.7 ms, GC Workers: 10]
      记载GC开端时刻
      [GC Worker Start (ms): Min: 99341.2, Avg: 99341.2, Max: 99341.3, Diff: 0.1]
      根扫描
      [Ext Root Scanning (ms): Min: 0.7, Avg: 1.3, Max: 5.1, Diff: 4.4, Sum: 13.1]
      更新RSet调集
      [Update RS (ms): Min: 0.0, Avg: 0.9, Max: 1.2, Diff: 1.2, Sum: 8.6]
         [Processed Buffers: Min: 0, Avg: 5.1, Max: 18, Diff: 18, Sum: 51]
      扫描RSet调集
      [Scan RS (ms): Min: 0.0, Avg: 1.6, Max: 2.0, Diff: 2.0, Sum: 16.4]
      Root方针对region引证的情况扫描
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.5, Diff: 0.5, Sum: 1.9]
      存活方针仿制并将存活方针从某个region区移动其他region区
      [Object Copy (ms): Min: 15.5, Avg: 16.6, Max: 16.8, Diff: 1.4, Sum: 165.8]
      GC终止
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 10]
      GC作业时,被其他JVM任务占用的时刻,本身和GC无关   
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.5]
      GC进程总耗时
      [GC Worker Total (ms): Min: 20.6, Avg: 20.6, Max: 20.7, Diff: 0.1, Sum: 206.3]
      GC完毕时刻
      [GC Worker End (ms): Min: 99361.8, Avg: 99361.8, Max: 99361.9, Diff: 0.0]
   串行履行阶段
   Root方针批改,比方region内的方针被移动了,那则要更新一下引证地址
   [Code Root Fixup: 0.1 ms]
   Root方针收拾
   [Code Root Purge: 0.1 ms]
   收拾card table中已扫描的标志
   [Clear CT: 0.2 ms]
   其他耗时总计
   [Other: 5.3 ms]
      挑选要收回的CSet调集
      [Choose CSet: 0.0 ms]
      软引证处理
      [Ref Proc: 4.2 ms]
      添加可以被收回的软引证
      [Ref Enq: 0.0 ms]
      软引证处理或许需求更新card table为脏
      [Redirty Cards: 0.2 ms]
      大方针计算(YGC阶段也会带着处理一点大方针)
      [Humongous Register: 0.0 ms]
      大方针收回耗时
      [Humongous Reclaim: 0.0 ms]
      CSet收回,并置位闲暇
      [Free CSet: 0.6 ms]
   各个区域的收回前后对比记载   
   [Eden: 492.0M(492.0M)->0.0B(500.0M) Survivors: 52.0M->39.0M Heap: 863.3M(1024.0M)->377.1M(1024.0M)]
 [Times: user=0.31 sys=0.00, real=0.03 secs]

4、GC通用参数

标题
-Xmn 重生代巨细,一般主张为整个堆巨细的1/2~1/4之间,但假如运用G1废物搜集器,则一般主张不要设置
-Xms 该参数有两个效果,别离为:堆的最小值以及初始值,默许为物理内存的1/64
-Xmx 堆的最大值,默许为物理内存的1/4,关于大多数运用服务来说,-Xms,-Xmx应该设置为相同的
-XX:SurvivorRatio Eden区和Survivor区份额,默许是8,即表明eden区和两个Survivor区的份额为,8:1:1
-XX:+UseTLAB 运用TLAB分配,默许为敞开
XX:+DisableExplicitGC 禁用System.gc(),默许为禁用
-XX:+PrintGCDetails 打印GC详情信息,默许为不打印
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log 日志文件的输出路径
-XX:+HeapDumpOnOutOfMemoryError OOM时生成Dump文件
-XX:HeapDumpPath=/memory.hprof OOM文件生成地址

问题排查篇

一、排查东西

1. JVM自带东西

1.1 jmap

一般经过jmap可以生成堆的当时运用情况的快照,然后用它来剖析或许调优JVM内存运用。

JVM关键知识点整理,从入门到提高到实践

打印堆的直方图。关于每个Java类,将打印方针数,以字节为单位的内存巨细以及彻底限定的类名。JVM内部类称号以*前缀打印。假如指定了live子选项,则仅计算活动方针。

JVM关键知识点整理,从入门到提高到实践

打印heap的运用情况,装备的参数信息,运用的废物搜集器等信息。

MaxHeapSize:最大堆空间

NewSize:重生代分配巨细

MaxNewSize:重生代最大分配巨细

OldSize:老时代分配巨细

NewRatio:重生代占整个堆空间的份额,2表明:重生代:老时代 = 1:2

SurvivorRatio:Survivor区占重生代空间的份额,8表明:Survivor:eden = 2:8

MetaspaceSize:元空间巨细

后半部分是heap的运用情况

JVM关键知识点整理,从入门到提高到实践

生成当时heap运用情况的快照,方便经过专业的内存剖析东西进行剖析。

JVM关键知识点整理,从入门到提高到实践

1.2 jstack

jstack指令首要用于生成虚拟机当时时刻线程快照信息,用于盯梢并调试虚拟机仓库信息,经过这个指令可以检测死锁、死循环、线程长期中止等问题。

指令格局:jstack [ options ] pid

死锁问题

死锁代码

public class DeadLock {
    public static Object one = new Object();
    public static Object two = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            //获取第一个锁,而且不会被其他线程抢占
            synchronized (one) {
                try {
                    System.out.println(Thread.currentThread().getName() + "取得one锁,等候two锁。");
                    //确保第二个线程此刻先获取到了第二个锁
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //恳求获取第二锁,而且任然持有第一个锁
                synchronized (two) {
                    System.out.println(Thread.currentThread().getName() + "取得two锁。");
                }
            }
        }).start();
        new Thread(() -> {
            synchronized (two) {
                try {
                    System.out.println(Thread.currentThread().getName() + "取得two锁,等候one锁。");
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (one) {
                    System.out.println(Thread.currentThread().getName() + "取得one锁。");
                }
            }
        }).start();
    }
}

JVM关键知识点整理,从入门到提高到实践

死循环问题

死循环问题,一般咱们在linux平台运用top指令,找到CPU占用率高的进程,然后再找进程里面CPU占用率的线程,拿到线程ID,经过jstack指令打印后,查找相应的线程ID即可(jstack线程ID显现的是16进制,top里找到的是10进制,需求转换一下)。

一段死循环代码

public class EndlessLoop {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
            }
        });
        t.setName("endless loop thread");
        t.start();
    }
}

先检查top指令,找到占用CPU较多的进程(是咱们的java进程),进程ID:7971

JVM关键知识点整理,从入门到提高到实践

top -p 7971,再按H,找到最耗CPU的线程ID,7981

JVM关键知识点整理,从入门到提高到实践

jstack 7971,找到7981这个线程,7981转换成16进制便是0x1f2d,这样就找到了具体的线程了,而且假如你依照标准给线程起了称号,比方咱们这儿叫:endless loop thread,这样咱们就能很快的定位到具体的代码位置了。

JVM关键知识点整理,从入门到提高到实践

1.3 jstat

jstat首要是用来监控虚拟机各种运转情况信息的一种东西,经过jstat指令首要可以用来调查虚拟机在运转时废物搜集情况,以及类加载和编译情况。

指令格局为:jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]

下面咱们经过几个演示事例,看一下具体的运用办法,环境声明:JDK1.8,运用-XX:+UseSerialGC废物收回器

常用的指令:jstat -gc pid interval[ms] count

JVM关键知识点整理,从入门到提高到实践

每一列含义解说

S0C:Survivor0区容量

S1C:Survivor1区容量

S0U:Survivor0区已运用容量

S1U:Survivor1区已运用容量

EC:Eden区容量

EU:Eden已运用容量

OC:老时代容量

OU:老时代已运用容量

MC:元空间容量

MU:元空间已运用容量

CCSC:紧缩类容量

CCSU:紧缩类已运用容量

YGC:重生代废物收回次数

YGCT:重生代废物收回耗时

FGC:FullGC发生次数

FGCT:FullGC耗时

GCT:总GC耗时

有时分还可以运用:jstat -gcutil pid interval[ms] count,检查运用份额。

JVM关键知识点整理,从入门到提高到实践

1.4 jvisualvm

假如你的服务器现已敞开了远程JMX,你可以经过jvisualvm东西查询。

JVM关键知识点整理,从入门到提高到实践

2. Arthas

阿里开源的一款线上监控诊断产品,简略好用,官网材料也很具体,本文就不多赘述了。

3. Eclipse MAT(Memory Analyzer Tooling)

一款JAVA内存剖析东西,可以对dump出来的hprof文件进行剖析,相同具体运用办法可以拜见官网。

4. gceasy

gceasy是一款用于剖析GC日志的东西,可以对gc日志进行剖析,并剖析出问题,给出引荐的处理方案,拜见官网

二、常见问题

1. CPU过高

常见的CPU运用率较高问题一般有:很多循环嵌套处理逻辑、疯狂开线程、频频FullGC、杂乱算法等,遇到CPU过高的问题,可直接经过jstack抓取运用率高的线程进行检查,具体办法前面介绍jstack时现已介绍过了,这儿就不再赘述了。

2. 内存过高

前面几款东西介绍完之后,关于内存过高的原因也就好剖析了,经过jmap或arthas都可以检查内存运用情况,一同也都可以直接dump内存情况,然后经过Eclipse MAT进行剖析。

小事例

一、TLAB分配、栈上分配功能测试

TLAB分配

TLAB:本地线程分配缓冲,内存分配实践上被依照不同的线程区分在不同的内存之间进行,每个线程在Eden区中中有一块独享的小区域,这样做的优点是可以削减同步处理带来的功能耗费。

下面的小事例中发动了100个线程,假如没有TLAB优化,那么发动的线程越多,方针分配时的同步处理就越耗时

首要装备如下参数发动,-XX:-UseTLAB、-XX:-DoEscapeAnalysis,表明封闭TLAB分配,封闭逃逸剖析,确保方针只能在堆上分配。

public class TestAlloc {
    class User {
    }
    void alloc() {
        new User();
    }
    public static void main(String[] args) throws InterruptedException {
        TestAlloc t = new TestAlloc();
        CountDownLatch countDownLatch = new CountDownLatch(100);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10_0000; j++) {
                    t.alloc();
                }
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

履行耗时大约1秒。

JVM关键知识点整理,从入门到提高到实践

批改装备参数发动,-XX:+UseTLAB、-XX:-DoEscapeAnalysis,表明敞开TLAB分配,封闭逃逸剖析,确保方针只能在堆上分配。

履行耗时大约100毫秒,功能差距大约10倍。

JVM关键知识点整理,从入门到提高到实践

栈上分配

这是借助于逃逸剖析来完结的,逃逸剖析中指出假如方针的效果域不会逃出办法或许线程之外,也便是无法经过其他途径拜访到这个方针,那么就可以对这个方针采取一定程度的优化,比方:将方针直接分配到栈上,方针可以随着栈的出栈进程被天然的毁掉,既省去了堆上分配的耗费,也节省了堆中废物收回所耗费的功能。

仍是相同的事例,alloc办法中new出来的User方针,效果域只在该办法中,所以可以经过逃逸剖析的结果,完结栈上分配。方针优先栈上分配,所以TLAB是否敞开不影响,运用默许装备就行。

首要装备如下参数发动,-XX:-DoEscapeAnalysis,封闭逃逸剖析。

public class TestAlloc {
    class User {
    }
    void alloc() {
        new User();
    }
    public static void main(String[] args) {
        TestAlloc t = new TestAlloc();
        long start = System.currentTimeMillis();
        for (int j = 0; j < 1_0000_0000; j++) {
            t.alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

履行耗时大约300毫秒。

JVM关键知识点整理,从入门到提高到实践

当装备敞开逃逸剖析时 -XX:+DoEscapeAnalysis,履行耗时只需10毫秒左右。

JVM关键知识点整理,从入门到提高到实践

二、运用ParallelGC频频呈现FGC

运用的废物搜集器:ParallelGC

服务参数:Non-default VM flags: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=null -XX:InitialHeapSize=526385152 -XX:MaxHeapSize=8392802304 -XX:MaxNewSize=2797600768 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=1572864 -XX:OldSize=524812288 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC

初始化内存巨细:500M,最大内存:8G。

服务器运转一段时刻后,可以看出FGC次数频频且耗时长,104次FGC,耗时到达258秒,均匀每次FGC持续时刻2秒多。

JVM关键知识点整理,从入门到提高到实践

检查一下实时内存运用情况,eden区2G多,老时代5G多,很显着老时代常规情况下有5G多的方针就很不正常,初步判别便是很多的方针被过早的放入到了老时代。

JVM关键知识点整理,从入门到提高到实践

问题剖析

运用ParallelGC要特别注意AdaptiveSizePolicy参数的问题,仍是上面那张图,看看Eden和Survivor区的分配占比,显着不是8:1:1了,这便是由于AdaptiveSizePolicy动态调整的原因。

AdaptiveSizePolicy 有三个方针:

Pause goal:运用到达预期的 GC 暂停时刻。

Throughput goal:运用到达预期的吞吐量,即运用正常运转时刻 / (正常运转时刻 + GC 耗时)。

Minimum footprint:尽或许小的内存占用量。

所以为了到达希望的方针,Survivor区被调整的很小,导致重生代的方针被很多的移到了老时代了。
又由于每次FGC后老时代空间被动态调整的问题,导致老时代空间越来越大,一同也就意味着一次FGC的时刻将会变得越来越长。

问题处理

-XX:SurvivorRatio,指定份额Eden区和Survivor的份额,不要让AdaptiveSizePolicy动态调整。

合理操控老时代巨细,关于ParallelGC这样的废物搜集器,老时代空间越大,一次FGC的中止时刻就越长。

操控重生代巨细,重生代一般可以恰当调大一些,让那些朝生夕死的方针可以悉数在重生代被收回。

堆内存究竟设置多大适宜?这个一般要依据线上的实践运用情况来决议,其实假如不存在内存走漏问题,则只需从每次gc的后存活方针的巨细,就能大致估算出实践所需求的内存空间(GC日志的重要性),为了用来应对体系峰值时的业务量激增导致发生的方针也激增的场景,再做一些恰当的冗余即可。

废物搜集器之所以要分代便是为了可以快速的把一些朝生夕死的方针给处理掉,假如Survivor小到描述虚设,就失去了分代搜集的含义,由于每次Eden区的方针只需能熬过一次YGC就会被放到老时代(Survivor区太小不行放),实践上或许在第2次YGC时就可以收回了,关于ParallelGC这样的废物搜集器,方针一旦进入老时代就只能等候内存100%后触发FGC才会被收回了。

三、内存运用率过高

布景

JVM关键知识点整理,从入门到提高到实践
之前遇到过一次线上OOM问题,经排查剖析发现是由于有一个接口运用JPA办法查询数据库,而且一次性回来的数据量过多导致(大约200W条数据),不过关于问题接口数据量尽管较多,但回来的数据只需一个未7个字符长度的String类型字段,所以200W条大约也就十几M,理论上还不至于形成OOM。

问题剖析

一般遇到这样的情况,就需求用到一些内存剖析东西来进行检查了,所以咱们dump出堆内存信息,运用Eclipse MAT东西进行剖析。

JVM关键知识点整理,从入门到提高到实践

经过堆内存剖析可以看出,运用JPA查询时原方针会被进行各种包装,而且被包装后占用空间剧增,终究到达1G多。

JVM关键知识点整理,从入门到提高到实践

四、线程过多导致CPU运用率过高

依照前面介绍过的办法,线上遇到CPU运用率过高的问题,最直接的办法便是运用jstack进行剖析。

JVM关键知识点整理,从入门到提高到实践

线程数量反常

JVM关键知识点整理,从入门到提高到实践

导出jstack文件进一步剖析,承认都是线程池中的线程

JVM关键知识点整理,从入门到提高到实践

终究直接找到线程池构建的代码,发现构建时最大线程数设置错了,设置成了1000,所以改掉之后即可康复正常。