jvm基础学习

174 阅读25分钟

JVM总结要点:

jvm的要点,包括四大块,类的加载机制,javm内存的结构,gc算法-垃圾回收的要点,gc分析,怎么使用命令调优。类的加载机制的关键:类生命周期,类的加载器具体执行命令,双亲委派机制。内存结构包括,堆内存,方法区,栈。GC算法常用的种类:cms,G1,serial(单线程)parnew(多线程)等等。常用的调优命令:jps,jmap,jstst,jstack等等。常用的调优工具:jconsole,jvisualvm等。

第一块: 字节码 指令分为四大类: 1.栈操作的指令(对于栈本身对应的指令,其他三种对应编程语言) 2.程序流程控制指令 3. 对象操作指令,包括方法调用指令 4.算术运算及类型转换指令

简单介绍一下操作指令 -0 :aload _0

本地变量 用-load 指令 加载到栈, a前缀表示引用类型 a_store 把对象通过store 压到本地的变量表 字节码是给jvm运行的一条条指令

javap -c Hello.class Compiled from "Hello.java" public class Hello { public Hello(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class Hello 3: dup 4: invokespecial #3 // Method "":()V 7: astore_1 8: return } 简单介绍 0,1是字节的偏移量, 0代表一个字节,1-4代表3个字节 #1表示常量池标号1那个常量

jvm是一个基于栈的计算机, 每个线程都有一个属于自己的线程栈,用于存储栈帧 每一次方法调用,jvm都会自动创建一个栈帧,栈帧由操作数栈,局部变量数组,以及一个class引用组成 ,class引用指向当前方法在运行时常量池中对应的class。 字节码对基本类型的数据有对应的指令处理 Int long ,float ,double 虚拟机这层数据类型最小的是int, byte等类型会被转换为int

循环的字节码实现时候-for each for- I 循环的差别

方法调用的指令:

invoke static ,顾名思义调用某个累的静态方法 Invoke special 用来调用构造函数和同一个类里面的private方法,以及可见的超类方法 invoke virtual 如果是具体类型的目标对象,用于调用,公共,受保护,以及package级的私有方法 invoke interface 通过接口引用来调用方法时候,会用这个指令 invoke dynamic: JDK7新增 ,调用动态类型语言, 也是JDK8支持lambda表达式的基础指令

类的生命周期

  1. 加载loading:找class文件
  2. 验证 verification :验证格式,依赖
  3. 准备preparation : 静态字段,方法表
  4. 解析 resolution :符号解析为引用
  5. 初始化 initialization :构造器,静态变量赋值,静态代码块
  6. 使用 using
  7. 卸载unloading

类的加载时机:

前4个显式的,后面4个隐式的

  1. 当虚拟机启动时,初始化用户指定的主类,就是启动执行的main方法所在的类
  2. 当遇到用以新建目标类实例的new指令时候,初始化new的目标类,就是new一个类时候也会初始化
  3. 当遇到静态方法的指令时,初始化该静态方法所在的类。
  4. 当遇到访问静态字段的指令时,初始化该静态字段的所在的类
  5. 子类初始化时候会触发父类的初始化,
  6. 当一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,也会把这个接口初始化
  7. 使用反射api对某个类反射调用时,初始化这个类
  8. 当使用方法指针,methodhandle实例时,初始化该方法指针指向的方法所在的类

不会初始化的情况

  1. 通过子类引用父类的静态字段,只会触发父类的初始化,不会触发子类的初始化
  2. 定义对象数组,不会触发该类的初始化
  3. 常量在编译存入调用调用类的常量池中,本质上没有直接引用定义常量的类,不会触发初始化
  4. 通过类名获取的class的对象不会触发类的初始化
  5. 通过classloader默认的loadclass的方法,也不会触发初始化,只是加载

类的加载器

三类加载器

  1. 启动类加载器 BootstrapClassLoader
  2. 扩展类加载器 ExtClassloader
  3. 应用类加载器 AppClassLoader 加载器特点
  4. 双亲委托(父类委派 应用类加载器先查询扩展类是否加载过,扩展类先看启动类加载器是否加载过)
  5. 负责依赖(加载器会来加载当前加载类依赖的类)
  6. 缓存加载(所有的类只被加载一次,下次使用去缓存中取)

jvm内存模型

jvm的内存结构 每个线程都只能访问自己的线程栈,每个线程都不能访问别的线程的局部变量(不可见) 所有原生类型的局部变量都存储在线程栈中,因此对其他线程不可见 线程可以将一个原生变量值的副本传递给另一个线程,但是不能共享原生局部变量本身, 堆内存中包含了java代码中创建的所有对象,不管是哪个线程创建的,其中也涵盖了包装类型,比如,byte,integer,Long 不管是创建一个对象并将其赋值给局部变量还是赋值给另一个对象的成员变量,创建的对象都会保存在堆内存中

如果是原生数据类型的局部变量(int),那么它的内容就全部保留在线程栈上,如果是对象引用(包装类, object等),则栈中的局部变量槽位中保存着对象的引用地址,而实际的对象内容保存在堆中,对象的成员变量与对象本身一起存储在堆上,不管成员变量的类型是原生数值还是对象引用,类的静态变量和类定义都保存在堆中

总结: 方法中使用的原生数据类型,和对象引用地址在栈上存储,对象,对象成员和类定义,静态变量在堆上, 堆内存又称为共享堆,堆中的所有对象,可以被所有线程访问,只要他们能拿到对象的引用地址,如果一个线程,可以访问某个对象时,也就可以访问该对象的成员变量,如果两个线程同时调用某个对象的同一方法,则它们都可以访问到这个对象的成员变量,但每个线程的局部变量副本是独立的

Jvm内存的整体结构: 没启动一个线程,jvm就会在栈空间分配对应的线程栈 线程栈也叫java方法栈,如果使用了jni方法,则会分配一个单独的本地方法栈,native stack 线程执行过程中,一般会有多个方法组成调用栈,比如a调用b,b调用c,没执行到一个方法就会创建相应的栈帧 frame

jvm栈内存结构

栈帧是逻辑上的概念,具体的大小,在方法编写完以后,字节码生成后就基本可以确定 比如返回值需要一个空间存放,每个局部变量需要对应的地址空间,还有给指令使用的操作数栈,以及class指针(标识这个栈帧对应的是哪个类的方法,指向非堆里面的class对象)

jvm堆内存结构

堆内存是所有线程公用的内存空间,jvm将heap内存分为年轻代,和老年代两部分,年轻代还分为3个内存池,新生代和两个存活区,大部分gc算法有两个存活区 s0和s1,在我们可以观察到的任何一个时刻,s0和s1总有一个是空的,但一般较小,也没有浪费多少空间 non-heap非堆内存,本质上还是heap,只是一般不归gc管理,里面划分为3个内存池。metaspace -以前叫持久代,java8换名字叫 metaspace 元空间,包含常量池和方法区 ccs,。 compressed class space 存放class信息的存放,压缩的对象指针的地方,和元空间有交叉,code cache 存放jit编译器编译后的本地机器代码

小结:什么是jmm: jmm规范:JMM规范明确定义了不同的线程之间,通过哪些方式,在什么时候可以看见其他线程 保存到共享变量中的值;以及在必要时,如何对共享变量的访问进行同步。这样的好 处是屏蔽各种硬件平台和操作系统之间的内存访问差异,实现了Java并发程序真正的 跨平台。 1. 3. 能被多个线程共享使用的内存称为“ 共享内存 ”或“ 堆内存 ”。 4. 所有的对象(包括内部的实例成员变量),static变量,以及数组,都必须存放到堆 内存中。 局部变量,方法的形参/入参,异常处理语句的入参不允许在线程之间共享,所以 不受内存模型的影响。 5. 多个线程同时对一个变量访问时【读取/写入】,这时候只要有某个线程执行的是 写操作,那么这种现象就称之为“冲突”。 6. 可以被其他线程影响或感知的操作,称为线程间的交互行为, 可分为: 读取、 写入、同步操作、外部操作等等。 其中同步操作包括:对volatile变量的读写,对 管程(monitor)的锁定与解锁,线程的起始操作与结尾操作,线程启动和结束等 等。 外部操作则是指对线程执行环境之外的操作,比如停止其他线程等等。

JMM规范的是线程间的交互操作,而不管线程内部对局部变量进行的操作。

CPU会在合适的时机,按需要对将要进行的操作重新排序,但是有时候这 个重排机会导致我们的代码跟预期不一致。 怎么办呢?JMM引入了内存屏障机制。 内存屏障可分为 读屏障 和 写屏障 ,用于控制可见性。 常见的 内存屏障 包括: 1 #LoadLoad 2 #StoreStore 3 #LoadStore 4 #StoreLoad 这些屏障的主要目的,是用来短暂屏蔽CPU的指令重排序功能。 和CPU约定好,看见 这些指令时,就要保证这个指令前后的相应操作不会被打乱。 比如看见 #LoadLoad , 那么屏障前面的Load指令就一定要先执行完,才能执行 屏障后面的Load指令。 比如我要先把a值写到A字段中,然后再将b值写到B字段对应的内存地址。如果 要严格保障这个顺序,那么就可以在这两个Store指令之间加入一个 #StoreStore 屏障。 遇到 #LoadStore 屏障时, CPU自废武功,短暂屏蔽掉指令重排序功能。 #StoreLoad 屏障, 能确保屏障之前执行的所有store操作,都对其他处理器可 见; 在屏障后面执行的load指令, 都能取得到最新的值。换句话说, 有效阻止屏障 之前的store指令,与屏障之后的load指令乱序 、即使是多核心处理器,在执行这 些操作时的顺序也是一致的。 代价最高的是 #StoreLoad 屏障, 它同时具有其他几类屏障的效果,可以用来代替另 外三种内存屏障。 如何理解呢? 就是只要有一个CPU内核收到这类指令,就会做一些操作,同时发出一条广播, 给某 个内存地址打个标记,其他CPU内核与自己的缓存交互时,就知道这个缓存不是最新 的,需要从主内存重新进行加载处理。

jvm启动参数

1.系统属性参数--主要环境变量(可以用命令传递变量) 2.运行模式参数

3堆内存设置参数 4gc设置参数 5分析诊断参数 6java agent参数

.运行模式参数

-server 特点是启动慢,效率高, 适用生产环境, 64位的jdk默认这个模式 -client :jdk7之前在x86机器上默认是client 启动速度快, -xint 解释模式,解释执行的所有字节码,会低很多 -xcomp,jvm会把所有字节码都编译本地,从而很快,但是需要预热 -xmixed 混合模式,jvm自己决定解释模式和编译模式混用,jvm默认是这个模式,我们也可以用java -version看到mixed mode

堆内存设置参数

-Xmx, 指定最大堆内存。 如 -Xmx4g。这只是限制了 Heap 部分的最大值为 4g。这个内存不包括栈内存,也不包括堆外使用的内存。 -Xms, 指定堆内存空间的初始大小。 如 -Xms4g。 而且指定的内存大小,并 不是操作系统实际分配的初始值,而是GC先规划好,用到才分配。 专用服务 器上需要保持 –Xms 和 –Xmx 一致,否则应用刚启动可能就有好几个 FullGC。 当两者配置不一致时,堆内存扩容可能会导致性能抖动。 -Xmn, 等价于 -XX:NewSize,使用 G1 垃圾收集器 不应该 设置该选项,在其 他的某些业务场景下可以设置。官方建议设置为 -Xmx 的 1/2 ~ ¼。 -XX:MaxPermSize=size, 这是 JDK1.7 之前使用的。Java8 默认允许的 Meta空间无限大,此参数无效。 -XX:MaxMetaspaceSize=size, Java8 默认不限制 Meta 空间,一般不允许 设置该选项。 -XX:MaxDirectMemorySize=size,系统可以使用的最大堆外内存,这个参 数跟 -Dsun.nio.MaxDirectMemorySize 效果相同。 -Xss, 设置每个线程栈的字节数,影响栈的深度。 例如 -Xss1m 指定线程栈为 1MB,与-XX:ThreadStackSize=1m 等价。

一般-xmx 7-80%主内存,留2-30的余量

-XX:+UseG1GC:使用 G1 垃圾回收器 -XX:+UseConcMarkSweepGC:使用 CMS 垃圾回收器 -XX:+UseSerialGC:使用串行垃圾回收器 -XX:+UseParallelGC:使用并行垃圾回收器 // Java 11+ -XX:+UnlockExperimentalVMOptions -XX:+UseZGC // Java 12+ -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC Jvm启动参数-分析诊断: -XX:+-HeapDumpOnOutOfMemoryError 选项,当 OutOfMemoryError 产生,即内存溢出(堆内存或持久代) 时,自动 Dump 堆内存。 示例用法: java -XX:+HeapDumpOnOutOfMemoryError -Xmx256m ConsumeHeap -XX:HeapDumpPath 选项,与 HeapDumpOnOutOfMemoryError 搭配使用,指定内存溢出时 Dump 文件的 目录。 如果没有指定则默认为启动 Java 程序的工作目录。 示例用法: java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/ ConsumeHeap 自动 Dump 的 hprof 文件会存储到 /usr/local/ 目录下。 -XX:OnError 选项,发生致命错误时(fatal error)执行的脚本。 例如, 写一个脚本来记录出错时间, 执行一些命令,或者 curl 一下某个在线报警的 url。 示例用法:java -XX:OnError="gdb - %p" MyApp 可以发现有一个 %p 的格式化字符串,表示进程 PID。 -XX:OnOutOfMemoryError 选项,抛出 OutOfMemoryError 错误时执行的脚本。 -XX:ErrorFile=filename 选项,致命错误的日志文件名,绝对路径或者相对路径。 -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=1506,远程调试。

jdk内置命令行工具

Jps 查看所有运行中的进程-仅限于当前账户的权限下,也不可查看不同java版本下面的进程 jstat -gcutil pid 1000 1000 查看jvm当前信息的语句 常用选项就 3 个: -heap 打印堆内存(/内存池)的配置和 使用信息。 -histo 看哪些类占用的空间最多, 直方图。 -dump:format=b,file=xxxx.hprof Dump 堆内存。 演示: jmap -heap pid jmap -histo pid jmap -dump:format=b,file=3826.hprof 3826

Jdk图形化工具- jconsole 打开命令行工具直接输入jconsole就可以打开

Jvisualm 需要下载才能运行

Gc的背景和一般原理: 为什么会有gc,内存的资源本质上拥有稀缺性

Gc算法:

引用计数法:所有资源存在引用计数,初始为0,当有对象引用时候+1;当对象不再引用减一,gc时候查看有没有被使用,没有被使用就回收 问题:两个资源之间互相之间引用的难以回收

标记清除算法 marking 标记, 遍历所有的可达对象,在本地内存中分门别类记下 Sweeping (清除)这一步保证了,不可达对象所占用的内存,在之后进行内存分配可以用 并行gc和cms的基本原理 优势:可以处理循环依赖,只扫描部分对象 除了清除,还要做压缩,整理清除以后导致的内存碎片化 怎么才能标记和清除上百万对象呢? 答案就是STW ,让整个jvm所有线程全部暂停 ,除了垃圾回收器以外全部都停止。

对象和分代假设: 分代假设:大部分新生对象很快就没用了,存活很长时间的对象,更可能存活更长时间

内存池划分: 不同类型对象不同区域,不同策略处理

对象分配在新生代的eden区 标记阶段eden存活的对象就会复制在存活区 注意:为什么是复制,不是移动? 因为两个s0,s1的区域里面复制就可以直接吧新生代和s0中直接放弃 两个存活区 from to互换角色,对象存活到一定的周期,会提升到老年代:一般是15次,有一个参数去控制

老年代默认都是存活对象,采用移动方式, 1.标记所有gc root可达的对象 2.删除所有不可达对象 3.整理老年代空间中的内容,方法是将所有的存活对象复制,从老年代空间开始的地方依次存放

gc root的对象: 1当前正在执行的方法里面的局部变量和输入参数 2.活动线程, 3所有类的静态字段 4.jni引用 这阶段暂停的时间,和堆内存的大小,对象的总数没有直接关系,而是存活对象的数量来决定。增加堆内存的大小并不会影响标记的时间

三个算法: 标记清除算法 标记复制算法:主要在年轻代 标记清除-整理算法 :主要是清除以后产生的内存碎片的整理

串行gc

对年轻代使用标记复制:对老年代使用标记-清除-整理算法 两者都是单线程的垃圾回收器 不能进行并行处理,所以都会触发全线暂停,停止所有的应用线程, 因此这种gc算法不能充分利用多核cpu,不管有多少cpu内核,jvm在垃圾回收时候只能使用单个核心,只适合几百mb的内存

##并行gc -jdk 678默认的gc 对年轻代和老年代回收都会触发stw事件 对年轻代使用标记复制:对老年代使用标记-清除-整理算法 -xxparallelGcthreads 来指定gc回收的线程数,默认就是cpu核心数 并行垃圾回收器适用于多核服务器,因为对系统资源的有效使用,能大大更高的吞吐量

  • 在gc期间,所有cpu都会并行处理垃圾,总暂停时间更短
  • 在两次gc周期的间隔期,没有gc线程运行,不会额外消耗任何系统资源

##cms gc 最大可能性的并发的标记清除算法 -xx +userConcMarkSweepGC 对年轻代使用标记复制:对老年代使用标记-清除算法 gms gc的设计目标是避免在老年代垃圾收集时候出现长时间的卡断, 1,不对老年代进行整理,而是使用空闲列表来管理内存空间的回收 2,在mark-and-sweep标记-清除的阶段大部分工作和应用线程一起并发执行 也就是说,没有明显的应用线程暂停,默认情况下。cms使用了1/4的核心数进行处理,但是值得注意的是,它仍然还是和应用线程争抢cpu资源的 如果服务器是多核cpu,并且调优的主要目标是降低stw的时间,那么就可以考虑用cms, 进行老年代并发回收时,可能伴随多次的年轻代的minor gc

cmsgc 六个阶段

  1. inital mark 初始标记 这个阶段伴随着stw暂停,初始标记的目标是标记所有的根对象,包括根对象直接引用的对象,以及年轻代中所有存活对象所引用的对象(老年代单独回收)
  2. concurrent mark 并发标记 :在这个阶段,cmsgc遍历老年代,标记所有的存活对象,从前一阶段initalmark找到的根对象开始算起,并发标记就是与应用程序同时运行,不用暂停
  3. concurrent preclean 并发预清理:这阶段同样是和应用线程并发执行,前一阶段的引用关系可能已经发生了改变如果在并发标记过程中引用关系发生了变化,JVM 会通过“Card(卡片)”的方式将发生了改变的区域标记为“脏”区,这就是所谓的 卡片标记(Card Marking)。
  4. final mark 最终标记 :最终标记阶段是此次 GC 事件中的第二次(也是最后一次)STW停顿。本阶段的目标是完成老年代中所有存活对象的标记。因为之前的预清理阶段是并发执行的,有可能 GC 线程跟不上应用程序的修改速度。所以需要一次 STW 暂停来处理各种复杂的情况。通常 CMS 会尝试在年轻代尽可能空的情况下执行 Final Remark阶段,以免连续触发多次 STW 事件。
  5. concurrent sweep 并发清除 :此阶段与应用程序并发执行,不需要 STW 停顿。JVM 在此阶段删除不再使用的对象,并回收他们占用的内存空间。
  6. concurrent reset 并发重置 这阶段和应用程序并发执行,重置cms算法内部的数据,为下次循环做准备

CMS 垃圾收集器在减少停顿时间上做了很多复杂而有用的 工作,用于垃圾回收的并发线程执行的同时,并不需要暂停 应用线程。 当然,CMS 也有一些缺点,其中最大的问题就 是老年代内存碎片问题(因为不压缩),在某些情况下 GC 会造成不可预测的暂停时间,特别是堆内存较大的情况下。

G1 gc

G1 的全称就是 garbage -first 意为垃圾优先,哪里一块的垃圾多就优先清理这部分, G1 gc 最主要的设计目标就是将stw暂停的时间和分布,变成可预期并且可以配置的, g1 gc堆不再分为年轻代和老年代,而是划分为多个(2048)个小块 region 每个小块可能一会被定义为eden区,一会是survivor区或者old区

这样划分以后,使得g1不必要每次回收整个堆空间,而是增量的处理一部分的内存快,称为此次gc的回收集。 每次gc暂停都会收集所有年轻代的内存快,但是只包含部分的老年代的内存块 G1的另一项创新是,在并发阶段估算每个小块存活对象的总数, 构建回收集合的原则就是,垃圾最多的小块会被优先收集,这也是g1的名称来源

G1 处理过成: 1、年轻代模式转移暂停(Evacuation Pause) G1 GC 会通过前面一段时间的运行情况来不断的调整自己的回收策略和行为,以此来比较稳定地控制暂 停时间。在应用程序刚启动时,G1 还没有采集到什么足够的信息,这时候就处于初始的 fully-young 模式。当年轻代空间用满后,应用线程会被暂停,年轻代内存块中的存活对象被拷贝到存活区。如果还 没有存活区,则任意选择一部分空闲的内存块作为存活区。 拷贝的过程称为转移(Evacuation),这和前面介绍的其他年轻代收集器是一样的工作原理。

2、并发标记(Concurrent Marking) 同时我们也可以看到,G1 GC 的很多概念建立在 CMS 的基础上,所以下面的内容需要对 CMS 有一定的理解。 G1 并发标记的过程与 CMS 基本上是一样的。G1 的并发标记通过 Snapshot-At-The-Beginning(起始快 照)的方式,在标记阶段开始时记下所有的存活对象。即使在标记的同时又有一些变成了垃圾。通过对象的存 活信息,可以构建出每个小堆块的存活状态,以便回收集能高效地进行选择。 这些信息在接下来的阶段会用来执行老年代区域的垃圾收集。 有两种情况是可以完全并发执行的: 一、如果在标记阶段确定某个小堆块中没有存活对象,只包含垃圾; 二、在 STW 转移暂停期间,同时包含垃圾和存活对象的老年代小堆块。 当堆内存的总体使用比例达到一定数值,就会触发并发标记。这个默认比例是 45%,但也可以通过 JVM参数 InitiatingHeapOccupancyPercent 来设置。和 CMS 一样,G1 的并发标记也是由多个阶段组成,其中一些阶 段是完全并发的,还有一些阶段则会暂停应用线程。

阶段 1: Initial Mark(初始标记) 此阶段标记所有从 GC 根对象直接可达的对象。 阶段 2: Root Region Scan(Root区扫描) 此阶段标记所有从 "根区域" 可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域。 阶段 3: Concurrent Mark(并发标记) 此阶段和 CMS 的并发标记阶段非常类似:只遍历对象图,并在一个特殊的位图中标记能访问到的对象。 阶段 4: Remark(再次标记) 和 CMS 类似,这是一次 STW 停顿(因为不是并发的阶段),以完成标记过程。 G1 收集器会短暂地停止应用线程, 停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标记开始时未被标记的存活对象。 阶段 5: Cleanup(清理) 最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升GC 的效率,维护并发标记的内部状态。 所有不包含存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的: 例如空堆区的回收,还有大部分的存活率计算。此阶段也需要一个短暂的 STW 暂停。

3、转移暂停: 混合模式(Evacuation Pause (mixed)) 并发标记完成之后,G1将执行一次混合收集(mixed collection),就是不只清理年轻代,还将一部 分老年代区域也加入到回收集中。混合模式的转移暂停不一定紧跟并发标记阶段。有很多规则和历史数 据会影响混合模式的启动时机。比如,假若在老年代中可以并发地腾出很多的小堆块,就没有必要启动 混合模式。 因此,在并发标记与混合转移暂停之间,很可能会存在多次 young 模式的转移暂停。 具体添加到回收集的老年代小堆块的大小及顺序,也是基于许多规则来判定的。其中包括指定的软实时 性能指标,存活性,以及在并发标记期间收集的 GC 效率等数据,外加一些可配置的 JVM 选项。混合收 集的过程,很大程度上和前面的 fully-young gc 是一样的。

gc退化

特别需要注意的是,某些情况下 G1 触发了 Full GC,这时 G1 会退化使用 Serial 收集器来完成垃圾的清理工作,它仅仅使用单 线程来完成 GC 工作,GC 暂停时间将达到秒级别的。

1.并发模式失败 G1 启动标记周期,但在 Mix GC 之前,老年代就被填满,这时候 G1 会放弃标记周期。 解决办法:增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads 等)。

2.晋升失败 没有足够的内存供存活对象或晋升对象使用,由此触发了 Full GC(to-space exhausted/to-space overflow)。 解决办法: a) 增加 –XX:G1ReservePercent 选项的值(并相应增加总的堆大小)增加预留内存量。 b) 通过减少 –XX:InitiatingHeapOccupancyPercent 提前启动标记周期。 c) 也可以通过增加 –XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

3.巨型对象分配失败 当巨型对象找不到合适的空间进行分配时,就会启动 Full GC,来释放空间。 解决办法:增加内存或者增大 -XX:G1HeapRegionSize