JVM调优和JVM调优实战总结
1、认识JVM
- java Virtual Machine(java虚拟机)是java程序实现跨平台的一个重要的工具。
JVM组成部分:
- 类加载系统:负责完成类的加载
- 运行时数据区:在运行java程序的时候会产生的各种数据会保存在运行时数据区
- 执行引擎:执行具体的指令
2、数据区
运行时数据区也就是JVM在运行时产生的数据存放的区域,这块区域就是JVM的内存区域,也称为JVM的内存模型JMM。
JMM分成了以下几个部分:
- 堆空间(线程共享):存放new出来的对象
- 元空间(线程共享):存放类元信息、类的模版、常量池、静态部分
- 线程栈(线程独享):方法的栈帧
- 本地方法区(线程独享):本地方法产生的数据
- 程序计数器(线程独享):配合执行引擎来执行指令
线程栈:执行一个方法就会在线程栈中创建一个栈帧。
栈帧包含如下四个内容:
- 局部变量表:存放方法中的局部变量
- 操作数栈:用来存放方法中要操作的数据
- 动态链接:存放方法名和方法内容的映射关系,通过方法名找到方法内容
- 方法出口:记录方法执行完后调用次方法的位置
3、对象的创建流程
类加载校验:
- 校验该类是否被加载。主要是检查常量池中是否存在该类的类元信息。如果没有,则需要进行加载。
分配内存:
为对象分配内存。具体分配策略如下:
- Bump the Pointer(指针碰撞):如果内存空间的分配是绝对规整的,则JVM记录当前剩余内存的指针,在已用内存分配。
- Free List(空闲列表):如果内存空间的分配不规整,那么JVM会维护一个可用内存空间的列表用于分配。
对象并发分配存在的问题:
- Compare And Swap:自旋分配,如果并发分配失败则重试分配之后的地址。
- Thread Local Allocation Buffer(TLAB):本地线程分配缓冲,JVM被每个线程分配一块空间,每个线程在自己的空间中创建对象。
设置初值:
- 根据数据类型,为对象空间赋初始化值。
设置对象头:
为对象设置对象头信息,对象头信息包含以下内容:类元信息、对象哈希码、对象年龄、锁状态标志等。
- 对象头中的Mark Work字段(32位)
- 对象头中的类型指针(Klass Pointer):类型指针用于指向元空间当前类的类元信息。比如调用类中的方法,通过类型指针找到元空间中的该类,再找到相应的方法。开启指针压缩后,类型指针只用4个字节存储,否则需要8个字节存储。
- 指针压缩:过大的对象地址,会占用更大的带宽和增加GC的压力。
执行init方法:
- 为对象中的属性赋值和执行构造方法
4、垃圾回收机制
4.1 对象成为垃圾的判断依据
在堆空间和元空间中,GC这条守护线程会对这些空间开展垃圾回收工作,GC如何判断这些空间的对象是否是垃圾,有两种算法:
引用计数法:
对象被引用,则计数器+1,如果计数器是0,那么对象将被判定为是垃圾,于是被回收,但是这种算法没有办法解决循环依赖的对象。因此JVM目前的主流厂商没有使用这种算法。
可达性分析算法:GC Roots根 gc roots根节点:在对象的引用中,会有这么几种对象的变量,来自于对象的变量:来自于线程栈中的局部变量表中的变量、静态变量、本地方法栈中的变量,这些变量都被称为gc roots根节点。 判断依据:gc在扫描堆空间中的某个节点时,向上遍历,看能不能遍历到gc roots根节点,如果不能,意味着这个对象是垃圾。
4.2 对象中的finalize方法
Object类中有一个finalize方法,也就是说任何一个对象都有finalize方法。这个方法是对象被回收之前的最后一根救命稻草。
- GC在垃圾对象回收之前,先标记垃圾对象,被标记的对象的finalize方法将被调用。
- 调用finalize方法如果对象被引用,那么第二次标记该对象,被标记的对象将移除出即将被回收的集合,继续存活
- 调用finalize方法如果对象没有被引用,那么将会被回收
- 注意,finalize方法只会被调用一次。
4.3对象的逃逸分析
对象的创建都是在碓空间中创建,但是会有个问题,方法中的未被外部访问的对象。
这种对象没有被外部访问,且在堆空间上频繁创建,当方法结束,需要被gc,浪费了性能。
所以在1.7之后,就会进行一次逃逸分析(默认开启),于是这样的对象直接在栈上创建,随着方法的出栈而被销毁,不需要进行gc。
在栈上分配内存的时候:会把聚合量替换成标量,来减少栈空间的开销,也为了防止栈上没有足够连续的空间直接存放对象。
- 标量:java中的基本数据类型(不可再分)
- 聚合量:引用数据类型
5、垃圾回收算法
5.1 标记清除算法、复制算法、标记整理算法
标记清除算法:
复制算法:
标记-整理算法:
5.2 分代收集算法
- 堆空间被分成了新生代(1/3)和老年代(2/3),新生代中被分成了eden(8/10)、survivor1(1/10)、survivor2(1/10)
- 对象的创建在eden,如果放不下则触发minor gc
- 对象经过一次minor gc后存放的对象会被放入到suvivor区,并且年龄+1
- survivor区执行的复制算法,当对象年龄到达15。进入老年代。
- 如果老年代放满,就会出发Full GC。
5.3 对象进入到老年代的条件
- 大对象直接进入到老年代:大对象可以通过参数设置大小,多大的对象被认为是大对象。-XX:PretenureSizeThreshold
- 大对象的年龄到达15岁时将进入到老年代,这个年龄可以通过这个参数设置:-XX:MaxTenuringThreshold
- 根据对象动态年龄判断,如果s区中的对象总和超过了s区中的50%,那么下一次做复制的时候,把年龄大于等于这次最大年龄的对象都一次性全部放入到老年代。
- 老年代空间分配担保机制:在minor gc时,检查老年代剩余可用空间是否大于年轻代里现有的所有对象(包含垃圾)。如果大于等于,则做minor gc。如果小于,看下是否配置了担保参数的配置:-XX:-HandlePromotionFailure,如果配置了,那么判断老年代剩余的空间是否小于历史每次minor gc后进入老年代的对象的平均大小。如果是,则直接full gc,减少一次minor gc。如果不是,执行minor gc。如果没有担保机制,直接full gc。
6、垃圾回收器
6.1 Serial收集器
- 单线程执行垃圾收集,收集过程中会有较长的STW(stop the world),在GC时工作线程不能工作。虽然STW较长,但简单、直接。新生代采用标记、整理算法。
6.2 Parallel收集器(-XX:+UseParallelGC,-XX:+UseParallelOldGC)
- 使用多线程进行GC,会充分利用cpu,但是依然会有stw,这是jdk8默认使用的新生代和老年代的垃圾收集器。充分利用CPU资源,吞吐量高。
- 新生代采用复制算法,老年代采用标记-整理算法。
6.3ParNew收集器(-XX:+UseParNewGC)
- 工作原理和Parallel收集器一样,都是使用多线程进行GC,但是区别在于ParNew收集器可以和CMS收集器配合工作。
- 主流的方案:ParNew收集器负责收集新生代。CMS收集器收集老年代。
6.4 CMS收集器(-XX:+UseConcMarkSweepGC)
目标:尽量减少stw时间,提升用户的体验,真正做到gc线程和用户线程几乎同时工作。CMS采用标记-清除算法。
- 初始标记:暂停所有的其他线程(STW),并记录gc roots直接能引用的对象。
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要STW,可以与垃圾收集线程一起并发运行。这个过程中,用户线程和GC线程并发,可能会有导致已经标记过的对象状态发生改变。
- 重新标记:为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的算法做重新标记。
- 并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。
- 并发重置:重置本次GC过程中的标记数据。
6.5 三色标记算法
在并发标记阶段,对象的状态可能发生改变,GC在进行可达性分析算法分析对象时,用三色来标识对象的状态。
- 黑色:这个对象及其所有引用都已被GC Roots遍历,黑色的对象不会被回收
- 灰色:这个对象及其部分的引用没有被GC Roots遍历。在重新标记时该对象如果是白色的话,那么将会被回收。
6.6 垃圾收集器组合方案
不同的垃圾收集器可以组合使用,在使用时选择适合当前业务场景的组合。
年轻代 | 老年代 | 备注 |
Serial | Serial Old | 简单、直接 |
Serial | CMS | |
ParNew | CMS | 推荐使用 |
ParNew | Serial Old | |
Parallel | Parallel Old | 吞吐量高、jdk默认使用的组合 |
Parallel | Serial Old |
7、JVM调优实战
7.1.JVM参数详解
- Xss:每个线程的栈大小。设置越小,说明一个线程栈里能分配的栈桢就越少,但是对JVM整体来说能开启的线程数会更多。
- -Xms:设置堆的初始可用大小,默认物理内存的1/64
- -Xmx:设置堆的最大可用大小,默认物理内存的1/4
- -Xmn:新生代大小
- -XX:NewRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
以下两个参数设置元空间大小建议值相同,且写死,防止在程序启动时因为需要元空间的空间不够而频繁full gc。
- -XX:MaxMetaspaceSize:最大元空间大小
- -XX:MetaspaceSize:元空间大小,默认是21M,达到该值后会触发Full GC,同时会按100%进行动态调整,为了减少大数据量占满元空间,频繁触发Full GC,建议在初始化时设置为跟MaxMetaspaceSize相同的值。
7.2.JVM调优实战
设置JVM参数:
-Xms3072M -Xmx3072M -Xss1M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8
调整JVM参数:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8
7.3.调优的关键点
- 设置元空间大小,最大值和初始化值相同
- 根据业务场景计算出每秒产生多少的对象。这些对象间隔多长时间会成为垃圾。
- 计算出堆中新生代eden、survivor所需要的大小:根据上一条每条产生的对象和多少时间成为垃圾来计算出,依据是尽量减少full gc。
7.4.结合垃圾收集器的调优策略
结合垃圾收集器:PraNew+CMS,对于CMS的垃圾收集器,还需要加上相关的配置:
- 对于一些年龄较大的bean,比如缓存对象、spring相关的容器对象,配置相关的对象,这些对象需要尽快的进入到老年代,因此需要配置:-XX:MaxTenuringThreshold=5
- 大对象直接进入到老年代:-XX:PretenureSizeThreshold=1M
- CMS垃圾收集器会有并发模式失败的风险(转换为使用serialOld垃圾收集器),如何避免这种风险:将full gc的触发点调低:-XX:CMSInitiatingOccupancyFraction=85(默认是92),相当于老年代使用率达到85%就触发full gc,于是还剩15%的空间允许在cms进行gc的过程中产生新的对象。
- CMS垃圾收集器收集完后会产生碎片,碎片需要整理,但不是每次收集完就整理,设置做了3次Full GC之后整理一次碎片:
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=3
PraNew+CMS的具体JVM参数配置:
java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=85 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=3 -jar debezium-service.jar
- 并行并发CMS垃圾回收器:+UseConcMarkSweepGC