JVM原理与调优

一、虚拟机架构

JVM虚拟机架构图

1.1 类加载器(ClassLoader)

负责从文件系统或网络中加载Class信息, 常见的classloader有:

①启动类加载器(Bootstrap ClassLoader): 负责加载$JAVA_HOME\lib目录或被-Xbootclasspath参数所指定的路径的Class,无法被java程序直接引用。

②扩展类加载器(Extensions ClassLoader): 负责加载$JAVA_HOME\lib\ext目录或被-Djava.ext.dirs参数所指定的跑路的Class,该类加载器由sun.misc.Launcher$AppClassLoader实现,用户可以直接使用。

③系统类加载器(System ClassLoader)或叫应用程序类加载: 根据Java程序提供的类路径来加载Java类,在程序中一般都是由它来完成类加载的。该类加载器是由sun.misc.Launcher$AppClassLoader实现,通过ClassLoader.getSystemClassLoader()方法获取实例,用户可以直接使用,程序常用的类加载器有ClassLoaderURLClassLoader

④用户自定义类加载器(Custom ClassLoader):通过继承 java.lang.ClassLoader类的方式实现。

1.2 方法区(Method Area)

方法区又叫静态区或非堆(non-heap)。方法区主要存储信息有类型信息、域信息,方法信息,常量,静态变量。jdk<=1.7叫永久代(PermGen),>=1.8叫元空间(MetaSpace)。

①方法区与java堆一样,是各个线程共享的内存区域。

②方法区在jvm启动的时候被创建,并且它的实际的物理内存空间中和java堆区一样都是可以不连续的。

③方法区的大小,跟堆空间一样,可以选择固定大小或者扩展。

④方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误java.lang.OutOfMemoryError:Metaspace

⑤关闭jvm就会释放这个区域的内存。

⑥JDK8 彻底将永久代移除出 HotSpot JVM,将其原有的数据(数据类型、常量、字段信息等)迁移至 Java Heap 或 Native Heap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace),元空间本质和永久代类似,都是JVM规范的方法区的实现。元空间与永久代最的区别是元空间并不在虚拟机中,而是使用本地内存。Java8通过 -XX:MetaspaceSize-XX:MaxMetaspaceSize 配置调整方法区的内存大小。Java创建的Class实例是保存到java heap中。

⑦永久代又称为方法区或非堆内存, 仅在Full GC时才会被回收。

方法区结构:

方法区详细信息
类型信息1. 类型的全限定名 2. 超类的全限定名 3. 直接超接口的全限定名 4. 类型标志(该类是类类型还是接口类型) <br/>5. 类的访问描述符(public、private、default、abstract、final、static)
类型的常量池存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类型、字段、方法<br/>的符号引用。常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存中所有类型使用<br/>到的类型、字段、方法的符号引用,所以它也是动态连接(栈中对应的方法指向这个引用)的主要对象(在动态链接<br/>中起到核心作用)。
字段信息1. 字段修饰符(public、protected、private、default) 2. 字段的类型 3. 字段名称
方法信息1. 方法名 2.方法的返回类型(包括void)3. 方法参数的类型、数目以及顺序 4. 方法修饰符(public、private、<br/>protected、static、final、synchronized、native、abstract) 5. 针对非本地方法,还有些附加方法信息<br/>需要存储在方法区中(局部变量表大小和操作数栈大小、方法体字节码、异常表)
类变量(静态变量)指该类所有对象共享的变量,即使没有创建该对象实例,也可以访问的类变量。它们与类进行绑定
指向类加载器的引用JVM 必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,<br/>那么 JVM 会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。JVM 在动态链接的时候需要这个<br/>信息。当解析一个类型到另一个类型的引用的时候,JVM 需要保证这两个类型的类加载器是相同的。这对 JVM 区分名<br/>字空间的方式是至关重要的。
指向 Class 实例的引用JVM 为每个加载的类和接口都创建一个 java.lang.Class 实例(JDK6 存储在方法区,JDK6 之后存<br/>储在 Java 堆),这个对象存储了所有这个字节码内存块的相关信息,如平时使用的 this.getClass().getName() this.getClass().getDeclaredMethods() this.getClass().getDeclaredFields(),可以获取类的各种<br/>信息,都是通过这个 Class 引用获取。
方法表为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,JVM 的实现者还可<br/>以添加一些其他的数据结构,如方法表。JVM 对每个加载的非虚拟类的类型信息中都添加了一个方法表,方法表<br/>是一组对类实例方法的直接引用(包括从父类继承的方法)。JVM 可以通过方法表快速激活实例方法。(这里的方法表<br/>与 C++ 中的虚拟函数表一样。正像 Java 宣称没有指针了,其实 Java 里全是指针。更安全只是加了更完备的检<br/>查机制,但这都是以牺牲效率为代价的,Java 的设计者始终是把安全放在效率之上的,所有 Java 才更适合于网络开发)

1.3 堆(Heap)

JVM内存划分为堆内存和非堆内存,堆内存分为新生代(Young Generation)、老年代(Old Generation),非堆内存永久代(方法区,Permanent Generation)。

①新生代Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。

②堆一般用来存放程序动态生成的实例对象和数组,垃圾回收器根据GC算法回溯这些数据。

③堆内存是对所有线程共享的。

④堆内存采用分代管理

⑤Java中采用的是根可达算法(GC Roots)来标记哪些是非垃圾

image

内存分代结构

​ 在为对象分配空间时,把一块确定大小的内存空间从堆中划分出来(如果经JIT优化编译后,对象可能被才分成标量类型从而编程了栈上分配)。新创建的对象分配到新生代(Eden)区上(当Eden区没有足够大小的连续空间来分配给新创建的对象时,JVM会触发一次Minor GC),如果JVM启动了本地线程分配缓冲(TLAB, -XX:-UseTLAB-XX:+UseTLAB),则对象将按线程优先分配在TLAB上(此区域任然位于新生代Eden区)。

关于TLAB(Thread Local Allocation Buffer)

在创建对象时可能会存在线程安全的问题, 划分给对象A的内存,在指针还没来得及修改位置,对象B又使用了原来分配内存的情况。通过TLAB可以解决该问题,TLAB为每个线程分配独立的内存空间,创建线程时会预先分配一小块内存(故称为"本地线程分配缓冲"),当在哪个线程中创建对象就在哪个线程分配的内存中进行分配内存,如果线程自身的TLAB用完了再去申请新的TLAB(这个时候再去进行指针的同步锁定,从而减少开销)

HeapSize不等于NewSize + OldSizeHeapSize会自动调整,各区所占堆内存比例:

Young Generation1/3
Eden(8/10)*(1/3)
From Survivor(S0)(1/10)*(1/3)
TO Survivor(S1)(1/10)*(1/3)
Old Generation2/3

例 如-Xms90m -Xmx90m -XX:+UseParNewGC
固定堆大小 90m
新 生 代1/3 * 90m = 30m
Eden (8/10)*(1/3*90) = 24m
S0 (1/10)*(1/3*90) = 3m
S1 (1/10)*(1/3*90) = 3m
老年代 2/3 * 90m = 60m

1.3.1 GC Roots(根可达算法)

根可达算法,从根开始查找,各根路径能到达的对象都是可用对象,否则就是垃圾。

什么是根呢?Which instances are roots?

  • JVM stack(线程栈变量):即main线程栈中的方法可以访问到的对象
  • native method stack(JNI指针):本地方法用到的本地变量
  • run-time constant pool(常量池):方法区中的常量
  • static references in method area(静态变量)

1.3.2 新生代与复制算法

​ 新生代中主要有Eden区、From Survivor(S0)和TO Survivor(S1)组成。新生代的垃圾回收是算法采用的是复制算法(Copying),复制算法的基本思想就是将内存分为3块(Eden区、S0区、S1区),S0和S1每次只使用其中一块,当GC发生时,根据根可达算法GC Roots)标记哪些在用的对象,除此之外都是垃圾。Eden区存活下来的对象会被复制到S1区,而SO区存活下来的对象根据其年龄决定去向,年龄达到阈值(阈值通过-XX:MaxTenuringThreshold参数来指定)会被移动到老年代,没有达到阈值的对象移动到S1区中(S1区的空间不足时会将所有对象移动到老年代),执行Eden区S0区内存清理(清理后Eden区和S0区是空的,是清理整块内存,无需以一个个的删,以确保空间的连续性,解决了空间碎片问题,同时速度也快),接下来会将S0和S1进行角色互换(GC后不管怎样S1区都是处于空白状态)。Minor GC将一直重复这个过程。

要点:

  • 刚创建的对象一般会分配到Eden区
  • 新生代的对象只会保存在Eden区和S0区
  • S1区空间主要用于Minor GC的复制算法交换数据,在Minor GC执行完成后S1区依然是空白状态
  • S0区的对象是通过Eden区过来的,Eden区的对象第一次执行Minor GC会将对象保存到S0区,S0区对象每执行一次Minor GC年龄都会增1,达到一定阈值会移动到老年代(会发起一次Full GC)
  • 复制算法不会产生内存碎片
  • 采用S0和S1两个Survivor(幸存区)的最大作用就是解决碎片化问题

运作图:

1.3.3 老年代

老年代的对象通常是通过Survivor区过来的,可能经过了很多次的GC才熬到了老年代,所以在老年代中的对象是没那么容易回收的。通常老年代的Full GC没有新生代Minor GC那么频繁,而且做一次Full GC所需时间要比Minor GC要长,所以老年代区空间在JVM分代结构中通常占比为三分之二,尽量避免内存不足进行频繁的Full GC。

1.3.4 Minor GC

Minor GC(Young GC)工作在新生代(新生代发生的GC叫做Minor GC,JVM中所有回收器都支持Minor GC),主要对新生代的Eden区进行清理(Survivor区满了不会触发),java绝大多数对象都是朝生夕死(研究表面新生代中的内存回收率在98%左右),所以Minor GC执行非常的频繁,执行速度也很快。在Minor GC之前会先判断老年代的连续空间是否大于新生代对象的总空间:

  • 如果大于,直接执行Minor GC。
  • 如果小于,先判断JVM参数-XX:HandlePromotionFailure(是否允许担保失败, 不过在jdk1.6 update 24之后HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,只要老年代的连续空间大于新生代对象的总大小或者历次晋升到老年代的对象的平均大小就进行Minor GC),如果HandlePromotionFailure=true继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小:

    • 如果大于,进行一次Minor GC,但是执行这次Minor GC有可能会失败,是否后会进行一次Full GC。
    • 如果小于或HandlePromotionFailure=false,直接执行Full GC。

执行完成一次Minor GC后存活下来的对象会分配到某一个Survivor区,如果Survivor不足以安置存活下来的对象,JVM会通过"空间分配担保机制"提前转移这些对象到老年代去, 如果老年代连续空间不足会触发异常Full GC。

1.3.5 Major GC

Major GC(Old GC)工作在老年代,老年代内存满了是会执行Major GC,速度比Minor GC慢10倍;在JVM中,只有CMS垃圾回收器支持单独回收老年代,其他都不支持单独回收老年代。

1.3.6 Full GC

Full GC是对老年代、新生代和方法区的垃圾进行回收,速度慢,工作线程的暂停时间长,触发Full GC的依据:

  • 老年代空间不足
  • 方法区空间不足
  • 直接调用System.gc(),但调用方法后不一定会执行垃圾回收
  • 执行Minor GC时,由Eden区和S0区向S1区复制时,对象所需空间大于S1可用内存,则把该对象转存到老年代,且此时老年代可用空间不足以安置该对象

1.3.7 GC Algorithms

常见的垃圾回收清除算法有Mark-Sweep(标记清除算法)Copying(复制算法)Mark-Compact(标记压缩算法)

1.Mark-Sweep(标记清除算法)

标记清除算法主要分为标记清除。当堆中可用内存空间被耗尽时,停止程序整个(Stop The World),然后执行两次扫描,第一次标记出有用的对象,第二次清除没标记的对象。

优点:

  • 非移动式的算法,无需考虑对象引用调整

缺点:

  • 效率低(要遍历两次)
  • 容易产生碎片(连续空间不足时容易引起频繁GC)
  • 在GC时,要停止整个程序,用户体验差(可能会出现程序卡顿)

2.Copying(复制算法)

将内存一份为二(实际运作是分成三份,见1.3.1 新生代与复制算法章节),将有用对象复制到另外一个(S1)区域, 然后清除原来区域的对象,只需扫描一次,适用于存活对象少,垃圾对象多的前提下,这种情况一般发生在新生代。

优点:

  • 无需标记清除,实现简单,执行效率高
  • 直接将可用对象复制到另外一个区域,在清除原区域内存,确保空间连续,不会产生内存碎片

缺点:

  • 浪费空间
  • 移动复制对象需要调整对象的引用
  • 对于G1这种分拆成大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,有一定内存占用和时间开销成本

3.Mark-Compact(标记压缩算法)

标记压缩算法分为标记整理,首先对需要回收的对象进行标记清除,之后再进行碎片整理,可以理解成标记-清除-压缩(Mark-Sweep-Compact)算法。该算法适用于内存中大量的对象存活情况下,比如在老年代中,会有很多顽固对象(长时间回收不掉),此时采用标记压缩算法会比较合适的(如果老年代采用复制算法,存活对象多,复制成本高)。

优点:

  • 不会产生碎片

缺点:

  • 效率低(三种算法中最低)
  • 移动对象需要调整对象的引用
  • 移动过程中,需要全程暂停用户应用程序(STW)

1.4 垃圾收集器

从JDK1-JDK15,虚拟机提供了多种垃圾收集器,每种垃圾收集器都有自己的优缺点,不存在完美完美的垃圾收集器,我们可以根绝优缺点和业务实际应用需求来选择用哪种。上图中如果两个收集器之间存在连线,表示它们可以搭配使用。

注意CMS与Serial Old的连线关系是别备选关系,如果CMS出现Concurrent Model Failure会执行Serial Old备选。

垃圾收集器概述表:

垃圾收集器类型作用域使用算法STW特点适用场景
Serial串行回收新生代复制算法响应速度优先适用于单核 CPU环境下的 Client模式
Serial Old串行回收老年代标记-压缩算法响应速度优先适用于单核 CPU环境下的 Client模式
ParNew并行回收新生代复制算法响应速度优先多核 CPU环境中 Server模式下与 CMS配合使用
Parallel Scavenge并行回收新生代复制算法吞吐量优先适用于后台运算, 而交互少的场景
Parallel Old并行回收老年代标记-压缩算法吞吐量优先适用于后台运算, 而交互少的场景
CMS(Concurrent Mark-Sweep)并发回收老年代标记-清除算法部分STW响应速度优先适用于B/S业务, 也就是交互多的场景
G1(Garbage-First)并发,并行回收新生代&老年代复制算法&标记-压缩算法部分STW响应速度优先面向服务端的应用

1.4.1 概念介绍

1.Stop-the-world(STW)

​ 它是指JVM由于要执行GC而停止了应用程序的执行,并且这种情形会在任何一种GC算法中发生。当Stop-the-world发生时,除了GC以外的线程均处于等待的状态,直到GC任务完成。实际上,很多GC优化都是通过减少Stop-the-world的时间来提高程序的性能。

2.安全点(Safe-point)

​ 程序执行时并非在所有地方都能停顿下来开始GC,只有在某些特定的位置才可以,这些特定的位置被称为安全点(Safe-point)。在使用GC roots分析可达性时,引用关系不会发生改变的点就是安全点,常用的安全点有:方法调用循环跳转异常跳转

3.JVM 运行模式

  • Client:启动快,进入稳定期后运行速度不如Server快。
  • Server:启动慢,进入稳定期后运行速度优于Client. Server模式采用的是重量级的虚拟机,对程序会进行更多的优化。

查看JVM运行模式命令:java -version

4.并行回收

指多条垃圾回收线程并行工作,但此时用户线程仍处于等待状态。

5.并发回收

指用户线程与垃圾回收线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾回收程序运行在另一个CPU上。

6.吞吐量

即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾回收时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

1.4.2 Serial

Serial又名DefNewCopy

Serial运行过程:

特点:

  • 新生代垃圾回收器
  • 串行单线程
  • 采用复制算法(copying)
  • STW机制
  • 适用于Client运行模式,也是Client模式下默认的垃圾收集器

优点:

  • 没有线程交互开销

缺点:

  • 执行过程中会暂停整个应用程序(STW)
  • 单线程,效率不高(但与其他单线程收集器相比,相对简单,相对高效)

配置参数:

  • -XX:+UseSerialGC:使用Serial垃圾收集器

1.4.3 ParNew

ParNew收集器其实就是Serial收集器的多线程并行版本,ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以限制垃圾线程数。ParNew适用于Server运行模式,也是许多运行在Server模式的首选新生代收集器。ParNew追求GC时降低用户线程的停顿时间,适合交互式应用,良好的反应速度提升用户体验。

降低停顿时间的两种方式:
1.在多CPU环境中使用多条GC线程,从而垃圾收集的时间减少,从而用户线程停顿的时间也减少;
2.实现GC线程与用户线程并发执行。所谓并发,就是用户线程与GC线程交替执行,从而每次停顿的时间会减少,用户感受到的停顿感降低,但线程之间不断切换意味着需要额外的开销,从而垃圾收集和用户线程的总时间将会延长。

ParNew运行过程:

特点:

  • 新生代垃圾收集器
  • 并行多线程
  • 采用复制算法(copying)
  • STW机制
  • 适用于Server运行模式

优点:

  • 多线程,效率高

缺点:

  • 执行过程中会暂停整个应用程序(STW)

配置参数:

  • -XX:ParallelGCThreads:限制垃圾收集的线程数
  • -XX:+UseParNewGC:使用ParNew垃圾收集器

1.4.4 Parallel Scavenge

Parallel Scavenge又名PS ScavengePSYoungGen,是一种追求CPU吞吐量的收集器,故也称为吞吐量优先收集器,该收集器的目标是达到一个可控制的吞吐量,支持GC自适应调节策略(与ParNew收集器最大不同之处)。

吞吐量:指用户线程运行时间占CPU总时间的比例。
CPU总时间包括 : 用户线程运行时间 + GC线程运行的时间。
因此,吞吐量越高表示用户线程运行时间越长,从而用户线程能够被快速处理完。(高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。)

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

Parallel Scavenge运行过程:

特点:

  • 新生代垃圾收集器
  • 并行多线程
  • 采用复制算法(copying)
  • 适用于Server运行模式
  • STW机制
  • 支持GC自适应调节策略
  • 一种追求CPU吞吐量的收集器
  • JDK 1.8 Server模式下默认新生代收集器

优点:

  • 多线程,效率高
  • 高吞吐量
  • 自适应调节参数

缺点:

  • 执行过程中会暂停整个应用程序(STW)

配置参数:

  • -XX:GCTimeRadio:直接设置吞吐量大小,GC时间占总时间比率.相当于是吞吐量的倒数。
  • -XX:MaxGCPauseMillis:设置最大GC停顿时间,Parallel Scavenge会根据这个值的大小确定新生代的大小。如果这个值越小,新生代就会越小,从而收集器就能以较短的时间进行一次收集;但新生代变小后,收集的频率就会提高,吞吐量也降下来了,因此要合理控制这个值。
  • -XX:+UseAdaptiveSizePolicy:通过命令就能开启GC自适应的调节策略(区别于ParNew)。我们只要设置最大堆(-Xmx)和MaxGCPauseMillis或GCTimeRadio,收集器会自动调整新生代的大小、Eden和Survior的比例、对象进入老年代的年龄,以最大程度上接近我们设置的MaxGCPauseMillis或GCTimeRadio。
  • -XX:+UseParallelGC:使用Parallel Scavenge垃圾收集器
  • -XX:+UseAdaptiveSizePolicy:打开之后,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量, 这种调节方式称为GC自适应的调节策略(GC Ergonomics)

1.4.5 Serial Old

Serial Old又名MarkSweepCompactMSC,是Serial收集器的老年代版本。主要适用于Client运行模式,也可以在Server模式下运行

Server模式下主要用途:
1.在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
2.作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。

Serial Old运行过程:

特点:

  • 老年代垃圾收集器
  • 串行单线程
  • 采用标记压缩算法(Mark-Compact)
  • STW机制
  • 主要适用于Client运行模式,也可以在Server模式下运行

优点:

  • 没有线程交互开销

缺点:

  • 执行过程中会暂停整个应用程序(STW)
  • 单线程,效率不高

配置参数:

  • -XX:+UseSerialOldGC:使用Serial Old垃圾收集器

1.4.6 Parallel Old

Parallel Old又名PS MarkSweep,是Parallel Scavenge收集器的老年代版本,在注重吞吐量的场景下,可以采用 Parallel Scavenge + Parallel Old 的组合,也是Java8中默认的组合方式。采用并行多线程+标记压缩算法(Mark-Compact),在执行垃圾收集的某个阶段会暂停整个应用程序(Stop the world)。

Parallel Old运行过程:

特点:

  • 老年代垃圾收集器
  • 并行多线程
  • 采用标记压缩算法(Mark-Compact)
  • 适用于Server运行模式
  • STW机制
  • JDK1.6及之后用来Parallel Old来代替老年代的Serial Old收集器
  • Java8默认采用Parallel Scavenge+Parallel Old组合方式

优点:

  • 多线程,效率高
  • 高吞吐量
  • 自适应调节参数

缺点:

  • 执行过程中会暂停整个应用程序(STW)

配置参数:

  • -XX:+UseParallelOldGC:指定使用Parallel Old收集器;
  • -XX:GCTimeRatio:设置并行垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
  • -XX:MaxGCPauseMillis:设置并行收集最大暂停时间

1.4.7 CMS

CMS全称Concurrent Mark Sweep,又名ConcurrentMarkSweep,是一种以获取最短收集停顿时间为目标的收集器,对于大多数应用应用程序来说快速响应比吞吐量更为重要。CMS运行期间可能失败,这时虚拟机将启动后备预案: 临时启用Serial Old(MarkSweepCompact)收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了

CMS运行过程:

  • 初始标记:标记GC Roots能直接关联的对象,时间短,速度快,需要STW
  • 并发标记:遍历之前标记到的关联对象,继续向下标记所有存活节点,和用户线程一起工作,时间较长,无需STW。
  • 重新标记:为修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间介于初始标记与并发标记之间,通常不会很长,需要STW
  • 并发清理:直接清除非存活对象,和用户线程一起工作,清理之后,将该线程占用的CPU切换给用户线程,无需STW。

特点:

  • 老年代垃圾收集器
  • 并发多线程
  • 采用标记清除算法(Mark-Sweep)
  • 适用于Server运行模式
  • 部分阶段STW
  • 追求响应速度,适合用于处理很多的交互任务场景
  • 用户线程和GC线程之间不停地切换会有额外的开销,垃圾收集总时间较长

优点:

  • 收集停顿时间短
  • 并发多线程,效率高,响应速度快

缺点:

  • 对CPU资源敏感
  • 无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生
  • 采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC
  • 用户线程和GC线程不断来回切换,消耗一定的时间成本

1.浮动垃圾(Floating Garbage)是指在之前判断该对象不是垃圾,由于用户线程同时也是在运行过程中的,所以会导致判断不准确,可能在判断完成之后在清除之前这个对像已经变成了垃圾对象,所以有可能本该此垃圾被收集但是没有被回收,只能等待下一次GC再将该对象回收,所以这种对像就是浮动垃圾。

2.Concurrent mode failed的产生是由于CMS收集年老代的速度太慢,导致年老代在CMS完成前就被沾满,引起full gc,避免这个现象的产生就是调小-XX:CMSInitiatingOccupancyFraction参数的值,让CMS更早更频繁的触发,降低年老代被沾满的可能。

配置参数:

  • -XX:+UseConcMarkSweepGC:使用CMS垃圾回收器

1.4.8 G1

G1(Garbage-First)是一种面向服务器应用程序的垃圾收集器,适用于多核处理器大容量内存的服务器,做到短时间停顿的同时达到一个高的吞吐量。G1按固定大小把内存划分为很多小区域(region),在逻辑上,某些小区域构成Eden,某些构成Survivor,某些构成老年代,这些小区域物理上是不相连的,并且构成新生代和老年代的区域可以动态改变。

Humongous是特殊的Old类型,专门放置大型对象

Remembered Set:

一个对象和它内部所引用的对象可能不在同一个Region中,那么当垃圾收集时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?
当然不是,每个Region都有一个Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,从而在进行可达性分析时,只要在GC Roots中再加上Remembered Set即可防止对所有堆内存的遍历。

新生代:

在新生代中,当Eden区空间占满后,会触发新生代GC,将Eden和Survivor中存活的对象拷贝到Survivor,或直接晋升到Old Region中。Young GC的执行是多线程并发的,期间会停顿所有的用户线程(STW)

老年代:

当堆空间的占用率达到一定阈值(配置参数-XX:InitiatingHeapOccupancyPercent)后会触发Old GC。

G1运行过程:

  • 初始标记:标记GC Roots能直接关联的对象,需要STW
  • 并发标记:遍历之前标记到的关联节点,继续向下标记所有存活节点(在此期间所有变化引用关系的对象,都会被记录在Remember Set Logs中)
  • 最终标记:标记出在并发标记期间新产生的垃圾,需要STW
  • 筛选回收:根据用户指定的期望回收时间回收价值较大的对象,需要STW

特点:

  • 并发多线程
  • 按固定大小划分很多个内存区域块(region)
  • 速度快,卡顿时间短(整体性能不错)
  • 不会产生内存空间碎片
  • 除了并发标记阶段外都需要STW
  • 逻辑上划分Eden区、Survivor区和老年代区,而各小区域块内存不相连
  • 新生代和老年代内存区域可动态改变
  • 整堆回收跨越年代垃圾收集器
  • jdk 9之后默认的垃圾收集器
  • 在GC时S0/S1并不会交换角色
  • 在指定的时间内,扫描部分最有价值的region(而不是扫描整个堆内存)
  • 和CMS相比因为有STW,所以不会长生浮动垃圾

优点:

  • 采用并发多线程
  • 卡顿时间短,用户体验好
  • 不会产生内存碎片
  • 不会产生浮动垃圾

适合使用G1垃圾收集的特征:

  • Full GC持续时间太长或太频繁
  • 对象的创建速率和存活率变动很大
  • 应用不希望停顿时间长(长于0.5s甚至1s)

配置参数:

  • -XX:NewRatio: 配置新生代和老年代的比例,默认2:1
  • -XX:SurvivorRatio: 配置EdenSurvivor的比例, 默认8:2
  • -XX:InitiatingHeapOccupancyPercent:配置堆内存占有率阈值,达到阈值触发Old GC,默认值45
  • -XX:+UseG1GC:使用G1垃圾收集器

1.5 垃圾收集器常见组合

组合没有孰优孰劣,不同惨景使用不同的组合。

新年代老年代命令备注
SerialSerial Old-XX:+UseSerialGC
SerialCMS-XX:+UseConcMarkSweepGC -XX:-UseParNewGC当 CMS收集Concurrent Model Failure时会执行备选 Serial Old GC。在Java8中不推荐使用,并在Java9后完全弃用
ParNewSerial Old-XX:+UseParNewGC在Java8中不推荐使用,并在Java9后完全弃用
ParNewCMS-XX:+UseConcMarkSweepGC -XX:+UseParNewGC当 CMS收集Concurrent Model Failure时会执行备选 Serial Old GC
Parallel ScavengeSerial Old-XX:+UseParallelGC -XX:+UseSerialOldGC
Parallel ScavengeParallel Old-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy(或-XX:+UseAdaptiveSizePolicy)
G1G1-XX:+UseG1GC

命令等同对照表:

单独命令等同命令
-XX:+UseParallelGC-XX:+UseParallelGC -XX:+UseParallelOldGC
-XX:+UseParallelOldGC-XX:+UseParallelGC -XX:+UseParallelOldGC
-Xincgc (Java8弃用,Java9中已移除)-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:+UseConcMarkSweepGC-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
windows无选项Java 9:-XX:+UseG1GC,Java 9之前:-XX:+UseSerialGC,JVM在系统中默认设置查看
Unix无选项Java 9:-XX:+UseG1GC,Java 9之前:-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy,JVM在系统中默认设置查看
-XX:+AggressiveHeap-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy等

1.6 收集器使用验证

1.测试环境

操作系统:macOS Big Sur 11.6

查看虚拟机默认信息:java -XX:+PrintCommandLineFlags -version

-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_271"
Java(TM) SE Runtime Environment (build 1.8.0_271-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.271-b09, mixed mode)

2.测试程序

import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;

public class TestGC {
    public static void main(String[] args) {
        List<GarbageCollectorMXBean> gcList = ManagementFactory.getGarbageCollectorMXBeans();
        if (gcList == null || gcList.size() < 2) {
            return;
        }

        for (int i = 0; i < gcList.size(); i++) {
            GarbageCollectorMXBean bean = gcList.get(i);
            String generation = i == 0 ? "新生代" : "老年代";
            String gcName = bean.getName();
            System.out.println(generation +"==>" + gcName);
        }
    }
}

3.测试

  • java TestGC

运行结果:

新生代==>PS Scavenge
老年代==>PS MarkSweep

PS Scavenge是:Parallel Scavenge

PS MarkSweep是:Parallel Old

  • java -XX:+UseSerialGC

运行结果:

新生代==>Copy
老年代==>MarkSweepCompact

Copy是:Serial

MarkSweepCompact是:Serial Old

  • java -XX:+UseG1GC TestGC

运行结果:

新生代==>G1 Young Generation
老年代==>G1 Old Generation

G1 Young Generation和G1 Old Generation均是G1

  • java -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy TestGC

或者:java Java-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:-UseAdaptiveSizePolicy TestGC

运行结果:

新生代==>PS Scavenge
老年代==>PS MarkSweep

PS Scavenge是:Parallel Scavenge

PS MarkSweep是:Parallel Old

  • java -XX:+UseParNewGC TestGC

运行结果:

Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release
新生代==>ParNew
老年代==>MarkSweepCompact

该组合已过期,在未来版本将会移除。

ParNew是:ParNew

MarkSweepCompact是:Serial old

  • java -XX:+UseConcMarkSweepGC -XX:+UseParNewGC TestGC

运行结果:

新生代==>ParNew
老年代==>ConcurrentMarkSweep

ParNew是:ParNew

ConcurrentMarkSweep是:CMS

  • java -XX:+UseConcMarkSweepGC -XX:-UseParNewGC TestGC

运行结果:

Java HotSpot(TM) 64-Bit Server VM warning: Using the DefNew young collector with the CMS collector is deprecated and will likely be removed in a future release
新生代==>Copy
老年代==>ConcurrentMarkSweep

该组合已过期,在未来版本将会移除。

Copy是:Serial

ConcurrentMarkSweep是:CMS

  • java -Xincgc TestGC

运行结果:

Java HotSpot(TM) 64-Bit Server VM warning: Using incremental CMS is deprecated and will likely be removed in a future release
新生代==>ParNew
老年代==>ConcurrentMarkSweep

不推荐使用,可能会在未来版本中删除。

ParNew是:ParNew

ConcurrentMarkSweep是:CMS

1.7 JVM其他常用命令

  • 查看JVM默认收集器

java -XX:+PrintCommandLineFlags -version

  • 查看JVM当前堆情况

jmap -heap pid

pid为进程ID

  • 打印GC简要信息

-verbose:gc -XX:+printGC

  • 堆配置
参数描述
-Xmx10m设置堆内存可被分配的最大上限,通常为操作系统可用内存的1/4
-Xms10m设置堆内存初始内存分配大小,通常为操作系统可用内存的1/64
-Xmn10m设置新生代大小,对-XX:newSize和-XX:MaxnewSize同时设置(也就是-XX:newSize = -XX:MaxnewSize = -Xmn)
-XX:NewSize=1m设置新生代初始内存的大小,应该小于-Xms的值
-XX:MaxnewSize=2m设置新生代可被分配的内存的最大上限;应该小于-Xmx的值
-XX:NewRatio=4设置新生代和年老代的比值,如果值为4表示新生代:老年代=1:4,也就是年轻代占堆的1/5
-XX:SurvivorRatio=3年轻代中Eden区与两个Survivor区的比值。如果值为3,Eden:Survivor=3:2,S0和S1各占1/5

开发过程中,有时候会将-Xms 与-Xmx两个参数的配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。

  • 方法区配置
参数描述
-XX:PermSize设置方法区初始内存分配大小,一般物理内存的1/64
-XX:MaxPermSize设置方法区分配的内存的最大上限,一般物理内存的1/4
  • 其他参数
参数描述
-XX:+PrintGCDetails打印GC详情
-Xloggc:log/gc.log指定GC日志输出文件
-XX:+PrintHeapAtGC每次GC前后都打印堆信息
-XX:+TraceClassLoading追踪类加载
-XX:+HeapDumpOnOutOfMemoryErrorOOM时导出堆到文件,配合-XX:+HeapDumpPath一起使用
-XX:HeapDumpPath=log/gc.dump导出OOM的路径
-XX:OnOutOfMemoryError=printstack.sh在OOM时,执行一个脚本,可以在OOM时,发送邮件,甚至是重启程序。
-Xss1M设置每个线程栈空间的大小。java 1.5后1M
-XX:ThreadStackSize和-Xss类似

二、JVM调优


参考

JVM方法区详解
JVM 系列 - 内存区域 - 方法区(六)
堆内存的分代管理
Jvm垃圾回收器

如果觉得我的文章对你有用,请随意赞赏