JVM
2021-10-08 07:25:38 4 举报
AI智能生成
JVM完整整理
作者其他创作
大纲/内容
JVM类加载机制
代码中用到这个类的时候会去加载一个类
(1)new
(2)包含main()方法的主类,必须立马初始化
(3)如果初始化一个类的时候,发现他的父类还没有初始化,那么必须先初始化他的父类。
一个类从加载到使用,一般会经历下面的这个过程
加载、验证、准备、解析、初始化、使用、卸载
验证:根据java虚拟机规范来校验加载进来的.class文件中的内容是否符合规范。校验成功后续才能交给JVM来运行
准备:写好的类有一些变量。class文件内容刚被加载到内存之后,会进行验证,确定字节码内容是规范的。然后给这个类分配一定的内存空间,给他里面的类变量(static修饰的变量)分配内存空间,来一个默认的初始值。
解析:把符号引用替换为直接引用的过程。
初始(核心阶段):准备阶段会给类分配好内存空间,类变量给一个默认的初始值。初始化阶段会正式执行我们的类初始化的代码。例如:
准备阶段会给flushInterval分配内存空间,同时flushInterval = 0,初始化会执行configuration.getInt("replica.flush.interval");
类加载器和双亲委派原则
(1)启动类加载器
Bootstrap ClassLoader,主要负责加载我们在机器上安装的java目录下的核心类(java安装目录下的lib)
(2)扩展类加载器
Extension ClassLoader,加载lib\ext目录
(3)应用程序加载器
Application ClassLoader,负责去加载“ClassPath”环境变量所制定的路径中的类
(4)自定义类加载器
根据自己的需求加载自己的类
双亲委派机制
(1)JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器。
(2)应用程序类加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载,如果父类加载器在自己负责的范围内没找到这个类,那么就会下推权利给自己的子类加载器。先让父类去加载,不行的话再由儿子加载,可以避免多层级的加载器结构重复加载某些类。
JVM中的有哪些内存区域
(1)存放类的方法区
这个方法区是JDK1.8之前的版本里用来代表JVM中的一块区域。主要存放:class文件加载进来的类、类似常量池的东西。但是JDK1.8以后,这块区域的名字改了,叫做Metaspace(元数据空间),用来存放自己写的各种类相关的信息
(2)执行代码指令用的程序计数器
我们写好的java代码会被编译成字节码,对应各种字节码指令。当jvm加载类信息到内存之后,实际就会使用自己的字节码执行引擎,去执行我们写的代码编译出来的代码指令。
程序计数器就是用来记录当前执行的字节码指令的位置的,也是记录目前执行到了那一条字节码指令。
程序计数器就是用来记录当前执行的字节码指令的位置的,也是记录目前执行到了那一条字节码指令。
(3)java虚拟机栈
(1)方法中有具备变量
(2)jvm使用java虚拟机栈来保存每个类的局部变量
(3)每个线程都有自己的Java虚拟机栈
(4)线程执行一个方法,就会对该方法调用创建对应的一个栈帧。栈帧包含局部变量表、操作数栈、动态链接、方法出口等
(5)方法执行完毕,就会从Java虚拟机中出栈
(4)Java堆内存
(1)存放我们在代码中创建的各种对象
示例
堆内存
全流程
(1)JVM进程启动,加载kafka类到内存,开始执行kafka中的main方法。main线程关联一个程序计数器,执行到哪一行就会记录这里。
(2)main线程执行main方法的时候,会在main线程关联的Java虚拟机栈里,压入一个main方法的栈帧。
(3)发现需要创建一个ReplicaManager类的实例对象。此时会加载ReplicaManager类到内存里来。
(4)创建一个ReplicaManager的对象实例对象在Java堆内存里。并且在main方法的栈帧的局部变量表引入一个replicaManager变量,让他引用ReplicaManager对象在Java堆内存中的地址。
(5)main线程开始执行ReplicaMnaager对象中的方法,会一次把自己执行到的方法对应的栈帧压入自己的java虚拟机栈
垃圾回收机制
(1)JVM本身是有垃圾回收机制的,他是一个后台自动运行的线程,只要启动一个JVM进程就会自带这么一个垃圾回收的后台线程。,这个线程会在后台不断检查JVM堆内存中的各个实例对象。
(2)如果某个实例对象没有任何一个方法的局部变量指向他,也没有任何一个类的静态变量,包括常量扥地方在指向他,那么这个垃圾回收线程,就会把这个没人指向的“RelicaManager”实例对象个回收掉,从内存里清楚掉,让他不在占用任何内存资源。
(3)这些不在被人指向的对象实例,即JVM中的“垃圾”就会定期的被后台垃圾回收线程清理掉,不断释放内存资源。
JVM分代模型
(1)JVM内存的分代模型:年轻代、老年代、永久代。
(2)大部分对象都是存活周期极短的。一旦没人引用实例对象了,就会被JVM的垃圾回收线程给回收掉,释放内存空间。少数对象是长期存活的。不会被轻易的垃圾回收。
(3)为什么要分成年轻代和老年代?
与垃圾回收有关,年轻代的特点是创建之后很快就会被回收,所以需要一种垃圾回收算法。老年代的对象特点是需要长期存在,所以需要另外一种垃圾回收算法,所以需要分成两个区域来放不同的对象。
(4)永久代:JVM里的永久代其实就是我们之前说的方法区。主要存放一些类信息。
新生代垃圾回收
新生代对象快满了,就会触发垃圾回收
那些变量引用的对象不能被回收
JVM使用可达性分析算法来判定那些对象时可以被回收的。
可达性算法
分析一下有谁在引用他,然后一层层往上去判断,看是否有GC Roots
示例
示例1
如果发生垃圾回收,回去分析ReplicaManager对象的可达性,发现他被人引用,而且是局部变量“replicaManager”引用。
JVM规范中,局部变量就是可以作为GCRoots
示例2
发现这个ReplicaManager对象被Kafka类的静态变量“replicaManager”给引用了
JVM规范中静态变量也作为一种GC Roots
只要对象被方法的局部变量、类的静态变量给引用了,就不会回收他们
引用类型
强引用
一个变量引用一个对象
此时垃圾回收绝对不会去回收这个对象
软引用
public static SoftReference<ReplicaManager> replicaManager = new SoftReference<ReplicaManager>(new ReplicaManager);
此时replicaManage对ReplicaManager的引用就是软引用
正常情况下不会回收软引用对象
如果垃圾回收后发现内存空间还是不够存放新的对象,还是会被回收
弱引用
和没引用类似
垃圾回收会被回收掉
虚引用
finalize()方法
如果对象要被垃圾回收了,如果这个对象重写了finalize方法会对它进行调用
查看是否把自己这个实例对象给了某个GC Roots变量,比如说代码中给了RepilicaManager类的静态变量,此时重新被GC Roots变量引用了自己,就不用被垃圾回收
垃圾回收算法及优劣
复制算法
新生代的垃圾回收算法
直接标记清理可能会造成内存碎片
将存活的对象转移到另外一块空白的内存内,保证没有内存碎片
缺点
内存的使用效率比较低,因为一块内存分成了多块
复制算法的优化
(1)Eden区和(2)survivor区
Eden区占80%内存空间,Servivor各占10%
确保90%的内存被使用到
流程
(1)刚开始对象分配在Eden区内
(2)如果过Eden区快满了,此时就会触发垃圾回收
(3)将Eden区中的存活对象一次性转移到一块空着的Survivor区
(4)清空Eden区
(5)新对象再次分配到Eden区
(6)再次Minor GC,将Eden区和上一次Minor GC存活对象的Survivor区内的存活对象转移到另外一块Survivor区去。
标记整理算法
老年代垃圾回收算法
流程
(1)标记出来老年代当前存活的独享
(2)让这些存活对象在内存里进行移动,让存活对象紧凑到一起,避免垃圾回收后出现过多内存碎片
(3)一次性把垃圾对象都回收掉
老年代垃圾回收算法的速度知道是新生代垃圾回收算法的10倍
Stop the World
进行垃圾回收是有专门的垃圾回收线程的,使用自己的垃圾回收算法,对执行的内存区域进行垃圾回收
垃圾回收的时候,JVM会在后台直接进入“Stop the World”状态,代码不能运行。一旦垃圾回收完毕,就可以继续恢复我写的Java系统的工作线程运行了。
老年代垃圾回收
新生对象进入老年代的场景
躲过15次GC之后进入老年代
XX:MaxTenuringThreshold
动态对象年龄判断
当前放对象的Survivor区域里,一批对象的总代笑大于这块Survivor区域的内存大小的50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代
逻辑如下:年龄1 + 年龄2 + 年龄n的多个年龄对象超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代。
大对象直接进入老年代
-XX:PretenureSizeThreshold
如果创建一个大于这个大小的对象,直接把这个大对象放到老年代里去,不会经历新生代
大对象频繁复制,耗费时间
Minor GC后的对象太多无法放入Survivor区
会将这些对象直接转移到老年代
老年代空间分配担保规则
问题:如果老年代也不够存放这些对象
(1)在执行任何一次MinorGC之前,JVM会先检查一下老年代的可用内存空间是否大于新生代所有对象的总大小,
极端情况下,新生代所有对象都存活
(2)如果失败,查看参数-XX:HandlePromotionFailure是否设置,如果设置,判断老年代对象是大于之前每一次MinorGC进入老年代对象的平均大小。
如果失败,直接触发一次Full GC,然后在执行Minor GC
如果成功
(1)Minor GC之后,剩余的存活对象的大小,小于Surivor区的大小,那么存活独享进入Survivor区域
(2)Minor GC过后,剩余的存活对象的大小大于Survivor区域的大小,但是小于老年代可用内存大小的,此时直接进入老年代即可
(3)Minor GC之后,剩余的存活独享大于Survivor区域的大小,也大于老年代可用内存的大小,发生“Handle Promotion Failure”的情况,这个时候就会触发一次“Full GC”
如果Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,此时就会到时OOM内存溢出。
Full GC的时机
(1)Minor GC之前,判断是否需要Full GC
老年代可用的连续空间 < 新生代历次YoungGC后升入老年代的对象总和的平均大小
(2)Minor GC之后,发现剩余对象太多需要放入老年代但是老年代空间不足
(3)老年代内存使用率超过了92%,也要直接触发Old GC,可以通过参数调整
示例
背景
一个系统不停从其他MySQL数据库以及其他数据源中提取大量的数据,加载到自己的JVM内存里来进行计算处理
500次/m 数据提取和计算的任务
分布式部署,每台机器大概每分钟100次数据提取和计算的任务
每次大概提起1w条左右的数据到内存里来计算,平均每次计算大概需要耗费10s时间
机器配置
4核8G,JVM内存4G,新生代和老年代分配1.5G
多久会填满新生代的内存空间
一条数据1kb
1万条数据10MB
新生代空间为:8:1:1 ,Eden = 1.2GB
1次数据计算10MB,1分钟100次,为1G,基本上就满了
触发Minor GC的时候会有多少对象进入老年代
两秒三个请求,30MB,一个请求要十秒处理,所以最后为有两百多的数据存活
一次MInor GC回收掉1GB对象,然后200MB的对象存活
因为Survivor的大小为100MB,所以此时直接进入老年代了
老年代什么时候被填满
一分钟一个轮回,进入200MB
1500 / 200 = 7
7次Minor GC之后,老年代使用了1400MB,剩余100MB
老年代什么时候Full GC
第八分钟技术是,执行Minor GC
执行之前检查,发现100MB比每次Minor GC后进入200MB的对象要小,此时Full GC
然后将新的Minor GC后的数据放入老年代
优化方案
痛点
每次Minor GC之后,后会直接进入老年代
增加新生代内存占比,2G分给新生代,Survivor此时大小为200MB
每次Minor GC依旧是200MB,可以进入Sruvivor区域
下次Minor GC时,依旧是200MB
垃圾回收器
年轻代垃圾回收器ParNew
垃圾回收时,系统程序所有的工作线程全部停掉,就一个垃圾线程在运行。此时4核CPU的资源没有充分利用,理论上4核可以支持4个垃圾回收线程并行执行,可以提升4倍性能
ParNew垃圾回收期就是多线程垃圾回收机制。Serial是单线程的,但是回收算法是一样的。
线上系统指定使用ParNew垃圾回收器
-XX:+UseParNewGC
加入这个选项,就会使用ParNew垃圾回收器
ParNew垃圾回收器的默认线程数量
默认跟随线上机器核数自动调整,4核 = 4个线程
如果想手动调节
-XX:ParalleGCThreads
部署的系统有服务器模式和客户端模式
客户端模式下,如果时单核CPU,然后开了多个垃圾回收线程,反而会导致频繁的上下文切换
老年代垃圾回收器CMS
老年代垃圾回收期一般使用CMS,采用标记清理算法。会产生内存碎片
CMS工作原理
如果停止一切工作线程,然后慢慢去执行标记-清理算法,会导致系统卡死时间过长,很多响应无法处理。
CMS垃圾回收期采取的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理
实现
代码片段
初始标记
Stop the World,然后标记出来所有GC Roots直接引用的对象
类静态变量和方法局部变量,ReplicaFetcher就不会管
虽然Stop the World,但是影响不大,因为他的速度很快,仅仅标记了GC Roots直接引用的那些对象
并发标记
系统可以随意创建各种新对象,继续运行。
垃圾回收线程会尽可能对已有的对象进行GC Roots追踪
对类似ReplicaFetcher之类的全部老年代里的对象,去看他被谁引用了。
发现他被ReplicaManager对象的实例变量引用,去产看ReplicaManager被谁引用了发现是Kafka
判断ReplicaFetcher对象被GC Roots间接引用,无需回收
可能会创建出新的对象,部分对象也可能会成为垃圾
他是最耗时的,但是因为是与系统程序并发,所有也不会对系统造成影响
重新标记
Stop the World,重新标记在第二阶段里新创建的一些对象,还有已有对象失去引用成为垃圾的情况。
速度快,因为是对第二阶段总变动过的少数对象进行标记
并发清理
系统随意运行,清理掉之前标记的垃圾对象
耗时,因为进行了对象的清理,但是是并发的
CMS默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4
Concurrent Mode Failure问题
CMS并发清理时,只回收了之前标记好的垃圾对象,但是因为这个阶段系统一直运行,部分对象可能进入老年代,然后没人引用变成垃圾对象。这种垃圾对象是“浮动垃圾”,这些对象是不会本次回收的
为了保证CMS垃圾回收期间,能让其他对象进入老年代,所以会预留空间。设置老年代占用多少比例的时候触发CMS垃圾回收
-XX:CMSInitiatingOccupancyFaction
JDK1.6默认92%
如果CMS垃圾回收期间,要放入老年代的对象大于可用空间,发生Concurrent Mode Failure.
此时自动用Serial Old垃圾回收器替代CMS,直接Stop the world,然后GC Roots追踪标记处全部垃圾翠香,不允许新的对象产生
内存碎片问题
设置在Full GC之后要再次进行Stop the World,停止工作线程,然后进行碎片整理,将存活对象移动到一起。
-XX:+UseCMSCompactAtFullCollection 默认打开
执行一定次数Full GC之后再执行一次内存碎片整理工作,默认0,即每次Full GC都会进行一次内存整理
-XX:CMSFullGCsBeforeCompaction
每日上亿请求量的电商系统JVM参数优化
案例背景
每日上亿请求量的电商系统
一个用户平均访问20次来计算,大致有500w日活用户
大部分是浏览,10%下单,大约50w订单
这些订单集中在每天4个小时的高峰期,平均每秒几十个订单
大促销场景,10分钟50w订单,每秒1000单
3台机器,每台机器每秒抗300个订单请求(4核8G机器)
下单请求比较耗时,每秒处理100~300个下单请求
优化思路
(1)预估系统的内存使用模型
(2)合理优化新生代、老年代、Eden和Survivor各个区域的内存大小
(3)尽量优化参数,避免新生代的对象进入老年代,让对象在新生代里被回收掉
内存使用模型估算
(1)每个订单1kb
(2)300个订单有300kb内存开销
(3)加上其他订单条目对象、库存、促销、优惠券等业务对象,放大10-20倍
(4)同时有订单相关的其他操作,比如订单查询等,扩大10倍
(5)每秒大概300kb*20*10=60MB内存开销
(6)1s过后就会成为垃圾对象,因为300个订单处理完成了,对象失去引用
年轻代垃圾回收参数优化
默认分配
一般4核8G会给JVM4G,其他留给操作系统
堆内存3G,新生代1.5G,老年代1.5G
每个Java虚拟机栈有1M,那么JVM如果有几百个线程大概会有几百M
永久代256M
-Xms3072 -Xmn1536M -Xss1M -XX-PermSize=256M -XX:MaxPermSize=256M -XX:handlePromotionFailure(1.6后废弃)
1s 300 订单60MB,1.5G新生代大概需要25s就会占满,然后进行Minor GC
判断老年代可用空间大小和历次MinorGC进入老年代对象的平均大小
能回收掉99%的对象,除了最近1s的订单,大概剩余100MB
-XX:SurViVorRatio默认值8,Survivor是150M的内存,所以20s就会MinorGC,然后存活对象放入S1,下次Minor GC,存活对象还是100MB放入S2
优化
问题:因为每次新生代垃圾回收在100MB左右,可能会突破150MB,会导致对象进入老年代。同时因为100MB大于Survivor区空间的50%,也可能导致对象进入老年代。
调整新生代与老年代的大小
原因:普通的业务系统,大部分对象都是短生命周期的,不应该频繁进入老年代,所以缩小老年代空间
新生代调整为2G,老年代1G,Eden 1.6,Survivor200MB
-Xms3072 -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8
能保证每次Minor GC后对象都留在Survivor里,不进入老年代
问题:新生代对象躲过多少次垃圾回收后进入老年代
默认15次
结合系统的运行模型分析:如果躲过15次已经过去几分钟了,这么久没回收,说明是需要存活的核心业务逻辑组件,应该尽早让他进入老年代
设定为5次
-XX:MaxTenuringThreshold=5
问题:多大的对象直接进入老年代
结合系统是否有大对象判断,此处设置1MB
-XX:PretenureSizeThreshold=1M
指定垃圾回收器
-XX:+UseParNewGc -XX:+UseConcMarkSweepGc
新JVM参数
-Xms3072M -Xmx3072 -Xmn2048M -Xss1M -XX:permSize=256M -XX:MaxPermSize=256M -XX:Survivor=8 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M
老年代垃圾回收参数优化
当前背景下对象进入老年代的时机
(1)-XX:MaxTenuringThreshold=5 躲过5次MinorGC
一般 @Service @Controller 之类的注解会一直使用,但是不会太多 ,一个系统大概几十MB
(2)超过1MB的大对象,直接进入老年代
(3)Minor GC之后存活的对象超过200MB,或者是超过Survivor的50%,会有部分对象进入老年代
基本上很多次Minor GC后会有一批对象进入老年代,200MB左右
Full GC触发条件
(1)没有打开 “-XX:HandlePromotionFailure”选项,结果老年代可用内存最多1G,新生代对象总大小最大1.8G,此时Minor GC前检查,发现老年代可用内存小于新生代总对象大小,导致每次Minor GC之前都Full GC(1.6后已经废弃)
(2)每次Minor GC之前,检查老年代可用内存空间小于历次Minor GC后升入老年代的平均对象大小
(3)某次Minor GC后要升入老年代的对象有几百M,但是老年代的可用空间不足了
(4)设置了“-XX:CMSInitiatingOccupancyFaction”参数,假设设定值为92%,前面条件都没满足,然后触发了此条件,就会Full GC
大促销期间可能1h1次,平时几个小时1次
Concurrent Mode Failure
假设1h后,老年代有900MB的对象,此时剩余可用100MB,此时触发Full GC。如果系统程序还在不停的创建对象,导致200MB进入老年代,此时触发Concurrent Mode Failure。导致系统进入Stop the World,CMS切换为Serial Old
因为概率极小,暂时不考虑。
CMS垃圾回收之后进行内存碎片整理的频率
因为Full GC的频率并不高,所以保持默认每次FUll GC之后后执行一次内存碎片
JVM参数
G1垃圾回收器
ParNew + CMS的痛点
Stop the World
所谓的优化,都是减少Stop the World
工作原理
特点
(1)G1垃圾回收器可以同时回收新生代和老年代的对象
(2)将Java堆内存拆分为多个大小相等的Region
(3)逻辑上有新生代和老年代
(4)可以设置一个垃圾回收的预期停顿时间
设定1h内G1垃圾回收导致的Stop the World时间不超过1分钟
可以尽量减少Minor GC和Full GC带来的系统停顿,避免影响系统处理请求
如何实现垃圾回收导致的系统停顿可控
G1会追踪每个Region里的回收价值
有多少垃圾,如果回收这个Region需要耗费的时间
尽量把垃圾回收对系统造成的影响控制在指定的时间范围内,在有限的时间尽量回收可能多得垃圾对象
Region
Region可能数据新生代也可能属于老年代
一开始谁都不属于,然后被分配给新生代,然后触发了垃圾回收。下次同一个Region又被分配了老年代,存放老年代对象
新生代和老年代各自的内存区域是不停的变动的,由G1自动控制
设置G1对应的内存大小
-XX:UseG1GC:指定使用G1垃圾回收期,此时自动用堆大小除以2048,手动设置:-XX:G1HeapRegionSize
刚开始,默认新生代堆内存的占比是5%,可以通过-XX:G1NewSizePercent手动设置
在系统运行中,JVM会不停的给新生代增加更多的Region,但是新生代占比不会超过60%,可以通过-XX:G1MaxNewSizePercent设置
一旦Region进行了垃圾回收,此时新生代的Region数量还会减少,这些都是动态的
依然有新生代和老年代,Eden区和Survivor区
新生代回收
一旦新生代达到了设定的占据堆内存的最大大小60%,比如都有1200个Region了,里面的Eden可能占据了1000个Region,每个Survivor是100个Region,而且Eden区还占满了对象,此时会触发新生代GC,用之前的复制算法。
区别
G1是可以设定目标GC停顿时间的,也就是G1执行GC时最多可以让系统停顿多长时间。
-XX:MaxGCPauseMills
进入老年代的时机
(1)达到一定年龄
-XX:MaxTenuringThreshold
(2)动态年龄判定规则,Minor GC后存活对象超过Survivor的50%
大对象Region
G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。如果对象太多大,会横跨多个Region来存放
判定规则
一个对象超过了一个Region大小的50%
在G1里,新生代和老年代的eRegion是不停的变化的,一次垃圾回收后,就有很多Region都空了,此时这些不属于新生代了,可以用来存放大对象。
回收时机
在新生代、老年代回收的时候,会顺带带着大对象Region一起回收
新生代 + 老年代的混合垃圾回收
-XX:InitiatingHeapOccupancyPercent,默认值45%
当老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代 + 老年代一起回收的混合回收阶段
例如:堆内存2048个Region,如果老年代占据了近1000个Region时,就会触发混合回收
G1垃圾回收的过程
(1)触发一个“初始标记”的操作,这个过程需要进入Stop the World,仅仅只是标记一下GC Roots直接能引用的对象,速度很快
(2)并发标记阶段,会允许系统程序运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象。耗时,因为要追踪所有存活对象
(3)最终标记阶段,进入Stop the World,系统程序禁止运行,会根据并发标记阶段记录的那些对象修改,最终标记一下有哪些存活对象那个,那些垃圾对象。
(4)混合回收阶段,会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率,将GC导致的停顿时间控制在我们指定的范围内
常用参数
-XX:G1MixedGCCountTarget
一次混合回收的过程中,最后一个阶段执行几次混合回收,默认8次。先停止系统,混合回收一些,再恢复系统运行,接着再禁止系统运行,混合回收,反复8次。
假设一次混合回收预期要回收掉一共160Region,那么此时第一次混合回收,会回收掉一些Region,比如20个Region,然后系统恢复一会,再混合回收。
反复回收的意义
可以尽可能让系统不要停顿时间过长,可以在多次回收的间隙也运行一下
-XX:G1HeapWastePercent
默认值5%
混合回收的时候,对Region回收都是基于复制算法进行的,都是要把回收回收的Region里存活的对选哪个放入其他Region,然后这个Region中的垃圾对象全部清理掉。这样的话回收过程中就会不断空出来新的Region,一旦达到了堆内存的5%,此时就会立即停止混合回收。
G1整体是基于复制算法进行Region垃圾回收的,不存在垃圾碎片问题
-XX:G1MixedGCLiveThresholdPercent
默认值85%
确定要回收的Region时候,必须是存活对象低于85%的RegionCIA可以进行回收
减少拷贝成本
回收失败
(1)如果进行Mixed回收的时候,无论是年轻代还是老年代都给予复制算法进行回收。
(2)如果拷贝的过程中发现没有空闲Reegion可以承载自己的存活对象了,就会触发一次失败,此时会立即切换为停止系统程序,然后采用单线程进行标记、清理、压缩整理,空出来一批Region,但是过程缓慢。
百万级用户的在线教育平台,基于G1垃圾回收期优化性能
案例背景
(1)一个百万级注册用户的在线教育平台,注册用户几百万,日活用户规模大概在几十万
(2)业务流程:浏览 -> 报课
(3)高频行为:上课
(4)高峰期:晚上八九点、周末。几十万的日活基本上都在这个时间
(5)高频功能:大量的游戏互动环节,会承载用户高频率、大量的互动点击
系统分析
活跃数:假设3h有60w活跃用户,平均每个用户会使用1h左右来上课,大概1h有20w活跃用户同时学习
互动数:假设每分钟互动一次,一小时60次互动,那么20w用户在1h内会进行1200w次互动操作,平均每秒大概3000次左右互动。
服务器配置:一般系统的核心服务需要部署5台4核8G的机器来抗住
请求产生的对象:一次互动请求大致会连带创建几个对象,占据几kb,假设为5KB,那么1秒600次就会占用3MB左右内存
G1垃圾回收器的默认内存
内存分配
堆内存:4G
新生代:默认初始值5%,最大60%
Java线程的栈内存:1MB
元数据区域:256M
-Xms4096M -Xmx4096M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseG1GC
此时堆内存4GB,Region = 4GB / 2048,每个Region大小是2MB,新生代占5%就是1000个Regoin,200MB空间
GC停顿时间
-XX:MaxGCPauseMills,默认值200毫秒
触发新生代GC的时机
G1会根据我们设定的GC停顿时间给新生代不停分配更多Region,然后到一定程度就会触发新生代GC,保证新生代GC的时候导致的系统停顿时间在预设范围内
(1)假设G1回收300个Region(600MB内存)大致需要200ms
(2)此时过了1分钟,占了100个Region,大概200M,发现距离设定值很远,接着分配Region
(3)三分钟后,占用了300个Region,大概600M,此时就会触发一次新生代GC
新生代GC优化
(1)应该给整个JVM的堆区域足够的内存
(2)合理设置-XX:MaxGCPauseMills,避免频繁GC和挺短过长
Mixed GC优化
老年代在堆内存占比45%就会触发
尽量避免对象过快的进入老年代,避免触发mixed GC
JVM GC导致系统卡顿
新生代
在Minor GC的时候,都必须停止系统程序的运行
MinorGC对系统的影响
(1)一般来说不大,新生代采用的复制算法效率极高,因为新生代里存活的对象很少,只要迅速标记少量存活对象,移动到Survivor,然后回收掉全部垃圾对象即可。速度很快。
(2)一次新生代gc一般只需要几毫秒,几十毫秒。对用户来说是无感知的。
(3)大内存下的频繁gc导致系统停顿几秒
如果系统部署在大内存的机器上,例如32核64G的机器,此时分配给系统的有几十个G,新生代Eden可能有30~40G的内存。此时如果系统负载非常高,每秒几万的访问请求。可能导致Eden区的几十G内存频繁塞满触发垃圾回收。每次垃圾回收需要几秒。
使用G1垃圾回收器解决大内存机器的新生代GC过慢问题
针对G1垃圾回收器,可以设置一个期望的每次GC的停顿时间,比如20ms。G1可以完美解决大内存垃圾回收时间过长的问题。
老年代
对象进入老年代的时机
(1)默认新生代经过15次垃圾回收后进入
(2)动态年龄判断
(1)一次Minor GC之后发现Survivor区域的几个年龄的对象加起来超过了Survivor区域的50%.
(2)年龄1 + 年龄2 + 年龄3 > 50%,那么年龄3以上的对象会进入老年代
(3)Minor GC过后,存活对象过多,无法放入Survivor,直接进入老年代
如果新生代的Survivor过小,会导致第二个和第三个条件频繁发生,然后大量对象快速进入老年代,导致频繁的老年代gc
老年代GC对系统的影响
(1)通常很耗费时间
(1)无论是CMS垃圾回收器还是G1垃圾回收器。因为都要经历几个阶段(初始标记、并发标记、重新标记、并发清理、碎片整理)
(2)一般老年代gc是新生代gc的10倍以上,每次停顿时间几秒钟
JVM性能优化是指系统因为内存分配、参数设置不合理导致对象频繁进入老年代,然后频繁触发老年代gc,导致系统频繁的每隔几分钟就卡死几秒钟。
名词解释
Minor GC / Young GC
新生代gc
Old GC
老年代gc,Old GC更加精准
Full GC
指针对新生代、老年代、永久代的全体内存空间的垃圾回收。
Major GC
不确定,有的认为是Old GC,有的认为是Full GC
Mixed GC
G1中的独有概念。在G1中,一旦老年代占据内存的45%,,就出发Mixed GC,对年轻代、老年代进行回收
代码模拟GC场景
模拟频繁Young GC的场景
JVM参数
-XX:NewSize=5242880 -XX:MaxNewSize=5242880 最初新生代和最大新生代5M
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 初始堆内存和最大堆内存10M
-XX:SurvivorRatio=8 Eden占比分配8:1:1
-XX:PretenureSizeThreshold=10485760 大对象10M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC 年轻代使用ParNew垃圾回收期 老年代使用CMS垃圾回收器
-XX:+PrintGCDetails 打印详细的gc日志
-XX:+PrintGCTimeStamps 答应每次GC发生的时间
-Xloggc:gc.log 设置将gc日志写入一个磁盘文件
代码实战
代码段
代码运行
(1)第一行代码一运行,在JVM的Eden区放入一个1MB的对象,同时在main线程的虚拟机栈中会压入一个main()方法的栈帧,main()栈帧内部有一个array1变量,这个变量指向堆内存Eden区的那个1MB的数组
(2)第二行代码运行,在堆内存创建第二个数组,并且让局部变量指向第二个数组,然后第一个数组没人引用了,成为垃圾对象
(3)第三行代码这行代码在堆内存的Eden区内创建了第三个数据,让array1指向了第三个数据,前面两个数组变成了垃圾对象
(4)第四行代码,array1 = null,让这个变量都不指向,前面三个数组全部变成垃圾对象。
(5)第五行代码,byte[] array2 = new byte[2*1024*1024]。此时分配一个2MB大小的数组,尝试放入Eden区
由于Eden总共就4MB的大小,前面是哪个数组占用了3MB,所以第五行代码会触发Young GC
GC日志
(1)根据JVM参数的配置,可以找到一个GC日志文件
(2)0.093: [GC (Allocation Failure) 0.093: [ParNew: 3724K->512K(4608K), 0.0012319 secs] 3724K->1654K(9728K), 0.0014396 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
解析
0.093:系统运行多少秒后发生了本次GC
GC(Allocation Failure):GC原因,对象分配失败
[ParNew: 3724K->512K(4608K), 0.0012319 secs]
触发的是ParNew垃圾回收器
3724K->512K(4608K)
年轻代可用空间是4608KB(Eden(4M) + 1个Survivor(0.5M))
对年轻代执行了一次GC,GC之前使用了3724KB,GC之后只有512KB对象是存活的
0.0012319 secs:本次GC耗费的时机
3724K->1654K(9728K), 0.0014396 secs
这是整个Java堆内存的情况
9728K:整个堆内存可用空间9728K(9.5M)
GC之前整个Java堆内存使用了3724K
GC之后Java堆内存使用了1654K
[Times: user=0.00 sys=0.00, real=0.00 secs]
本次GC消耗的时间,由于这里最小单位是小数点后两位,所以都是0.00
GC执行过程
(1)ParNew: 3724K->512K(4608K), 0.0012319 secs
代码中我们值放了3个1MB的数据,一共是3MB,也就是3072KB,这里却是3724KB
(1)数组本身是1MB,但是为了存储这个数据,JVM内置还会附带一些其他细腻,所以实际内存是大于1MB的
(2)除了自己创建的对象意外,可能还有一些看不见的对象在Eden区
经过垃圾回收后,创建的三个数组被回收
(2)ParNew: 3724K->512K(4608K), 0.0012319 secs
gc回收之后,从3724K降低到了512KB的内存使用,从Eden区转移到了Survivor From区,另一个Survivor To区
(3)下面的GC日志
par new generation total 4608K,used 2601K
ParNew垃圾回收器负责的年轻代总共有4608K可用内存,目前使用了3746K
为什么会使用了3746K:下面通过代码又分配了2M的内存 +之前存活的对象 + 额外的一些数据
eden... from... to ....
eden 、Survivor from、Survivor to的内存大小,使用占
concurrent..
Concurrent Mark-Sweep垃圾回收器(CMS),管理的老年代内存空间一共是5M,此时使用了1161K
Metaspace
Metaspace元数据空间和Class空间,存放一些类型信息、常量池之类的东西,此时他们的总容量、使用内存等
模拟对象进入老年代的场景
模拟动态年龄判定规则
JVM参数
代码解析
代码段1
代码运行
(1)系统先创建了3个2MB的数组,最后将局部变量array1设置为了null
(2)此时会在Eden区创建一个128KB的数组同时由array2变量来引用
(3)此时希望在Eden区再次分配一个2MB的数组,因为Eden区已经有了3个2MB的数组和1个128KB的数组,大小超过6MB了,Eden才8MB,所以此时会触发Young GC
GC日志分析
(1)ParNew: 7941K->789K(9216K)
(1)GC之前年轻代占用了7941KB的内存,这里大概是6MB的3个数组 + 128KB的数组 + 几百KB的位置对象
(2)Young GC过后,剩余的存活对象大概是789KB。除了未知对象,还需要加上128KB的数组。
(2)
(1)Form Survivor区域被占据了77%的内存,大概780KB左右,这是存活的对象
(2)Eden区域占据了27%的空间,大概是2MB左右,这个byte[] array3 = new byte[2* 1024 *1024]这行代码咋igc过后分配在eden内的数组
(3)此时他的年龄为1,Survivor区域总大小是1MB,此时存活对象已经有700多KB了,超过了50%
代码段2
代码运行
(1)经过一次YoungGC后,继续向下执行
(2)分配3个2MB数组,然后再分配一个128KB的数组,让array3变为空
(3)将次分配一个2MB的数组,此时会触发YoungGC
GC日志分析
(1)0.092: [GC (Allocation Failure) 0.092: [ParNew: 7975K->797K(9216K), 0.0006206 secs] 7975K->797K(19456K), 0.0007868 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
第一次的GC日志,上方分析过
(2)0.094: [GC (Allocation Failure) 0.094: [ParNew: 7163K->372K(9216K), 0.0026497 secs] 7163K->1129K(19456K), 0.0027220 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
(1)触发第二次Young GC,此时GC有还有372K的对象,但是array2本来就存活(128K) + 其他存活的对象(五六百K),此时不在了
(2)GC后Eden区的4个数组肯定被回收了,然后发现Survivor区域的对象都是存活的,而且超过了50%,而且年龄都是1岁。根据动态年龄判定规则:年龄1 + 年龄2 + 年龄n的独享 > Survivor的50%,年龄n以上的对象进入老年代。这里的年龄都是1,所以年龄大于等于1的直接进入老年代。
(3)通过一下日志确认这一点: concurrent mark-sweep generation total 10240K, used 757K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
CMS管理的老年代,此时使用的空间是757K,证明Survivor里的对象触发了动态年龄判定规则,进入了老年代
(3) eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee226e0, 0x00000000ff400000)
(1)说明Eden区当前有一个2MB的数组
(4) from space 1024K, 36% used [0x00000000ff400000, 0x00000000ff45d200, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
(1)from Survivor占用了300多k
模拟YoungGC后对象过大进入老年代规则
代码
(1)分配3个2MB的数组,然后最后让array1变量指向第三个2MB数组
(2)创建一个128K的数组,但是让他指向了null。Eden默认存在一定大小的位置对象
(3)接着执行array3的代码,想要分配一个2MB的数组,会触发Young GC
JVM参数
GC日志
(1)ParNew:8148K -> 662K(9216K)
(1)本次GC过后,年轻代里就剩下660KB的对象
(2)但是我们有引用一个2MB的数组存活,且存活对象大于From Survivor区大小
(3)并没有直接将这些存活对象全部放入老年代
(2)
(1)Eden区放入了一个新的2MB的数组(array3)
(3)
(1)From Survivor区有600多K的对象,就是未知对象
(2)所以并没有让2MB的数组和600KB的位置对象都进入老年代,而是把600KB的未知对象放入From Survivor
(4)
(1)有2MB的数组在老年代中
总结
Young GC过后存活对象放不下Survivor区域,部分对象会进入老年代,部分对象停留在Survivor中
模拟老年代GC
代码
(1)
直接分配了一个4MB的大对象,由于参数设定大对象为3MB,所以直接进入老年代。接着不对对象进行引用
(2)
连续分配4个数组,3个2MB,1个128KB,全部进入Eden
(3)
(1)此时执行这行代码,Eden已经放不下了,所以触发了Young GC
(2)大对象,直接放入老年代中,但是老年已经有了一个4M数组,放不下3个2MB + 1个128KB
GC日志
(1)ParNew (promotion failed): 8140K->8963K(9216K), 0.0025680 secs
Eden原来有8140的对象,但是无法回收,因为都被引用了
(2)CMS: 8194K->6906K(10240K), 0.0023044 secs
(1)执行了CMS垃圾回收器的Full GC(老年代Old GC + Young GC + 元数据GC)
(2)在CMS Full GC之前已经触发了Young GC,此时执行了Old GC
(3)CMS流程
(1)在Young GC之后,先将2个2MB的数组放入了老年代
(2)此时还要放1个2MB数组和1个128KB数组,触发CMS的Full GC
(3)回收掉4MB的数组,因为已经无人引用了
(4)放入剩余的数组,所以从8194K->6906K
总结
年轻代存活太多对象放不下老年代就会触发CMS的Full GC
GC分析
常用工具及命令
jstat
常用命令
(1)jstat -gc PID
查看一个Java进程的内存和GC情况
SOC:From Survivor区大小
S1C:To Survivor区大小
S0U:From Survivor区当前使用的内存大小
S1U:To Survivor区当前使用的内存大小
EC:Eden区的大小
EU:Eden区当前使用的内存大小
OC:老年代的大小
OU:老年代当前使用的内存大小
MC:方法区(永久代、元数据区)的大小
MU:方法区(永久代、元数据区)的当前使用的内存大小
YGC:系统运行迄今为止的Young GC次数
YGCT:Young GC的耗时
FGC:系统运行迄今为止的Full GC次数
FGCT:Full GC的耗时
GCT:所有GC的总耗时
(2)jstat -gccapacity PID
堆内存分析
(3)jstat -gcnew PID
年轻代GC分析,这里的TT和MTT可以看到对象在年轻代存活的年龄和存活的最大年龄
(4)jstat -gcnewcapacity PID
年轻代内存分析
(5)jstat -gcoldcapacity PID
老年代内存分析
(6)jstat -gcold PID
老年代GC分析
(7)jstat -gcmetacapacity PID
元数据区内存分析
命令实战
(1)获取每秒钟在年轻代的Eden区分配多少对象
jstat -gc PID 1000 10
每隔1s更新最新的一行jstat,一直执行10次
(2)Young GC的触发频率和每次耗时
(1)通过知道Eden区的大小和每秒Eden的增长大小,就能得到Young GC的频率
(2)jstat可以获取一共发生了多少次Young GC和总耗时,就能得到每次耗时了
(3)Young GC后有多少对象是存活和进入老年代的
(1)每次Young GC后有多少对象会存活,可以大致推测出来。之前有计算出多久发生一次Young GC,此时执行jstat -gc -PID 18000 10(每3分钟一次,执行10次)观察YoungGC后Eden、Surivor、老年代的变化
(4)Full GC的触发时机和耗时
知道了老年代的增长速率,那么就能知道Full GC的触发时机
jmap
jmap -heamp PID
会打印堆内存相关的一些参数设置以及当前堆内存里的一些基本各个区域的情况
jmap -dump:live,format=b,file=dump.hprof PID
将当前目录生成一个dump.hrpof文件,生成一个内存快照供后续分析
jhat
jhat可以分析堆快照,内置了web服务,可以通过浏览器图形化分析
jhat -port 7000 dump.hprof
打开了一个服务,然后可以通过7000端口访问
从测试到上线:如何分析JVM运行状况及合理优化
优化思路
尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量不让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC和JVM性能的影响
(1)开发好系统后进行预估性优化
(1)自行估算系统每秒大概的请求
(2)每个请求创建多少个对象
(3)占用多少内存
(4)机器应该选用什么样的配置
(5)年轻代应该给多少内存
(6)Young GC触发的频率
(7)对象进入老年代的频率
(8)老年代应该给多少内存
(9)Full GC触发的频率
(2)系统压测时的JVM优化
本地单元测试 -> 系统集成测试 -> 测试环境的功测试 -> 预发布环境的压力测试
在压测环境下的系统优化好JVM参数以后,观察Young GC和Full GC频率都很低,此时才可以部署系统上线
(3)对线上系统进行JVM监控
(1)每天在高峰期和低峰期使用jstat、jmap、jhat等工具进行查看优化
(2)使用一些部署的监控系统:Zabbix、OpenFaIcon、Ganglia
总结:对线上运行的系统,要不然用命令行工具手动监控,发现问题优化,要不然就依托公司的监控系统进行自动监控,可视化查看日常系统的运行状态
OOM
概括:系统JVM内存不够,但还在分配对象,导致内存放不下,造成溢出,此时JVM发生OutOfMemory
后果:一旦发生,系统停止运行,甚至JVM进程直接崩溃
Metaspace区域(类信息)
何时触发内存溢出
(1)两个参数来设置Metaspace区域的大小
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=512m
(2)一旦JVm不停加载类,加载了很多的类,
(3)Metaspace区域放满了,此时会触发Full GC,Full GC尝试回收Metaspace区域中的类
(4)以下类可以被回收
(1)这个类的类加载器先要被回收
(2)类的所有对象实例都要被回收等
(5)一旦回收不了多少类,此时还在拼命加载类键入Metaspace,就发生了内存溢出,系统崩溃
总结
(1)系统的Metaspace区域过小
(2)cglib等动态生成的一些类,代码中没有控制好,导致类过多,造成内存溢出
模拟JVM Metaspace内存溢出的场景
动态生成的类:通过程序动态生成的类
代码
(1)定义一个类代表汽车,有一个run方法,标识启动汽车,让汽车行驶
(2)通过Enhancer动态生成类,给Enhancer一个父类Car,然后在调用run方法时,先被MethodInterceptor拦截,判断如果是run方法,先做安全检查
(3)安全检查后,通过methodProxy.invokeSuper(o, objects)调用父类Car的run方法
(4)类似于如下代码
限制Metaspace大小
JVM配置
代码设置,加入计数器
结果
第273次时,Metaspace内存溢出
解决方案
JVM参数
GC日志分析
(1)0.531: [GC (Allocation Failure) 0.532: [ParNew: 67712K->2271K(76160K), 0.0018929 secs] 67712K->2271K(245504K), 0.0021394 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
(1)第一次GC,本身是一个Allocation Failure的问题,也就是说在Eden分配对象时发现Eden内存不足,于是触发了一次Young GC,对象对应的是Enhancer enhancer = new Enhancer(),Car car = (Car)enhancer.create()因为在循环创建,所以必然会塞满
(2)在默认内存分片下,年轻代可用空间是76M,这里面还包含了Survivor区域的大小
(2)0.819: [Full GC (Metadata GC Threshold) 0.819: [CMS: 0K->2705K(169344K), 0.0254210 secs] 20962K->2705K(245504K), [Metaspace: 9402K->9402K(1058816K)], 0.0255527 secs] [Times: user=0.00 sys=0.02, real=0.03 secs]
(1)Metadata GC Threshold
这是一次Full GC,因为Metaspace区域满了,所以触发Full GC
(2)20962K->2705K(245504K)
指堆内存(年轻代 + 老年代)一共是245M左右,20M左右被使用;Full GC比如会带着一次Young GC,因此这次Full GC其实是执行了ygc,所以回收了很多对象,剩下来2271K,这个大概就是JVM的一些内置对象了
(3)[CMS: 0K->2705K(169344K), 0.0254210 secs]
这些对象直接放入了老年代。Full GC带着CmS进行了老年代的Old GC,结果本来就是0KB,然后从年轻代转移来了2705K的对象
(4)[Metaspace: 9402K->9402K(1058816K)]
Metaspace已经使用了9M内存,接近10M了,所以Full GC,但是GC后对象全部存活
(3)0.845: [Full GC (Last ditch collection) 0.845: [CMS: 2705K->1657K(169344K), 0.0118531 secs] 2705K->1657K(245632K), [Metaspace: 9402K->9402K(1058816K)], 0.0119546 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
(1)Last ditch collection
因为之前Metaspace回收了一次,但是没有类可以回收,所以新的类无法放入Metaspace,所以最后再试一次Full GC
(2)Metaspace: 9402K->9402K(1058816K)]
还是没有回收掉任何类
(4)
JVM直接退出了,退出的时候打印出了当前内存的一个情况,年轻代和老年代基本上没占用,但是Metaspace溢出了
控制台日志
说明OutOfMemoryError,原因是Metasapce区域满了导致的
内存快照分析
(1) 实例最多的就是AppClassLoader,是CGLib之类的东西在动态生成类的时候搞出来的
(2)有一大堆自己的Demo1中动态生成出来的Car$EnhancerByCGLIB
直接对Enhancer做一个缓存,只有一个,不要无限制去生成类就可以了
线程的虚拟机栈(栈帧、局部变量)
何时造成内存溢出
(1)一个线程调用多个方法的入栈和出栈
每次方法调用的栈帧都是要占用内存的。虚拟机栈大小是固定的,比如1M,每次这个线程调用一个方法,都会将本次方法调用的栈帧压入虚拟机栈,这个栈帧是有方法的局部变量的,也是会占用内存的
如果不停的让线程调用方法,不停的往栈放入栈帧,此时就会消耗完线程栈内存,最终导致内存溢出的情况
一般来说,避免需限制的方法递归,就可以避免内存的溢出
模拟JVM 栈内存溢出的场景
代码
JVM参数
结果
解决方案
(1)由于栈内存溢出和堆内存是没有关系的,因为他本质是一个线程的栈中压入过多方法调用的栈帧导致栈内存不足。所以GC日志是没有用的,GC日志主要用来分析堆内存和Metasapce区域,内存快照瑶瑶分析内存占用,也是对于堆内存和Metaspace的。
(2)JVM参数
(3)定位异常
(1) 控制台异常
(2)只要把所有的异常都写入了本地日志文件,那么可以去日志里定位一下异常信息,然后根据异常进行解决
堆内存空间(创建的对象)
何时出发内存溢出
(1)平时系统不停的创建对象到Eden,Eden满了就出发Young GC,然后进入S区
(2)高并发场景下导致ygc后存活对象太多,此时只能进入老年代了,并出发了Full GC
(3)在Old GC过后,依然存活了很多对象,不足以存放ygc后存活的对象,造成内存溢出
总结:有限的内存中放了过多的对象,而且大多数都是存活的,此时几十GC过后还是大部分存活,不能存放更多对象了,引发内存溢出。如要场景如下
(1)系统承载高并发请求,因为请求量过大,导致大量对象都是存活的,所以要继续放入新的对象是在是不行了。
(2)系统有内存泄漏问题,莫名其妙弄了很多对象,结果对象都是存活的,没有及时取消对他们的引用,导致GC无法回收
模拟JVM 堆内存溢出的场景
代码
JVM参数
结果
解决方案
JVM参数
(1)通过日志查看为什么崩溃
显示是堆内存溢出
(2)不用进行GC日志分析,因为一般堆内存溢出有大量的GC日志。
(3)使用MAT分析内存快照
(1)分析下方信息
(1)The thread java.lang.Thread @ 0xfffb2790 main keeps local variables with total size 949,192 (59.38%) bytes.
main线程通过局部变量引用了949192个对象
(2)The memory is accumulated in one instance of "java.lang.Object[]" loaded by "<system class loader>".
内存都被一个实例对象占用了,就是java.lang.Object[]
(2)Details
一大堆java.lang.Object
(3)查找是怎么创建出来的 See stacktrace
很明显,在HeapDemo1.main()方法中,一直在调用ArrayList的add方法,然后找到代码,进行优化即可
原理图
对线上系统的OOM异常进行监控和报警
使用监控平台(Zabbix、OpenFalcon)就可以接入系统异常的一些监控和报警,一旦系统出现了OOM就发送报警给开发人员。一般来说有一下几个层面的监控
(1)机器(CPU、磁盘、内存、网络)的负载情况
(2)JVM的GC频率和内存使用率
(3)系统自身的业务指标
(4)系统的异常报错
监控体系的建议
(1)CPU监控,一旦负载过高,比如长期90%就需要报警
(2)内存监控,内存的使用率,如果内存长时间使用率超过了一定的法治,比如90%,那么就需要报警
(3)JVM的Full GC,频繁Full GC问题需要报警
(4)系统业务指标的监控
比如创建订单,1分钟超过一定数量报警,避免刷单
(5)系统try catch中的异常报错,必须报警
(6)本地磁盘的使用量和剩余空间,磁盘空间满了,系统无法运行
(7)机器上的网络负载,通过网络IO读写了多少数据,耗时等
如果没有这种监控和报警体系,发现OOM问题主要依靠两个
(1)线上系统因为OOM挂掉
(2)检查系统的线上日志,一般来说,系统异常的时候,会有日志框架写入本地日志文件。
JVM内存溢出的时候自动dump内存快照
假设发生了OOM,必然说明系统中某个区域的对象太多了,塞满了那个区域,而且一定是无法回收掉那些对象,最终才会导致内存溢出的,所以我们首先要知道是什么对象导致的OOM,就需要一份JVM发生OOM时的dump内存快照
JVM本身是在发生OOM之前都会尽可能去进行GC腾出来一些空间的,GC后依然没有空间,放不下对象才会触发内存溢出
在JVM中加入一下参数,就可以让他在OOM时dump一份内存快照
-XX:+HeapDumpOnOutOfMemoryError
在OOM的时候自动dump内存快照出来
-XX:HeapDumpPath=/usr/local/app/oom
将dump内存快照放到指定地方
实战案例
GC
每日百万交易支付系统如何设计JVM堆内存大小
支付的核心流程
压力
JVM内存里每天会频繁的创建和销毁100万个订单
支付系统每秒钟需要处理多少笔支付订单
(1) 一般用户交易都会发生在每天的高峰期,比如中午或者晚上
(2)100万平均分配到几个小时中,大概每秒100笔订单
(3)支付系统部署三台机器,每台机器实际上每秒大概处理30笔订单
每个支付订单处理要耗时多久
(1)用户发起一次请求,支付需要在JVM中创建一个支付订单对象,填充进去数据,然后写入数据库,还会处理一些其他事情,估计为1s
(2)每台机器1s接收30笔支付订单的请求,在JVM的新生代里创建了30个支付订单的对象,等待写库等处理
每个支付订单大概需要多大的内存空间
直接根据支付订单类中对的实例变量的类型来计算
Integer 4byte
Long 8byte
预算一个对象占据500字节
(3)1s后,30个支付订单处理完毕,订单对象引用被回收
(4)重复以上动作
每秒发起的支付请求对内存的占用
系统运行分析
(1)不停在新生代放入30个支付订单,然后新生代里的互相会持续的积累和增加
完整的支付系统内存占用预估
真正支付系统肯定会每秒创建大量其他的对象,结合这个访问压力以及对象的内存占用占据,可以大致估算整个支付系统每秒大致会占据的内存空间。
计算结果扩大10~20倍,每秒创建出来的被栈内存的局部变量引用的对象大致占据的内存空间为几百kb~1MB
下一秒继续新的请求创建1M对象在新生代,然后变成垃圾
(2)一段时间后,可能有几十万对象了,占据几百MB,新生代空间快满了
(3)触发Minor GC
支付系统的JVM堆内存如何设置
(1)减少频繁的MinorGC
(2)设定:使用4核8G的机器来部署支付系统
JVM进行至少可以给4G以上内存
新生代至少分派到2G内存空间
(3)1M/S 差不多近半个小时才会让新生代Minor GC
-Xms和Xmx 3G,整个堆内存3G空间,-Xmn设置为2G,新生代2G内存空间
横向扩展可以缓解压力
反面示例:
环境
1台2核4G的虚拟机
-Xms = 1G; -Xmx = 1G
-Xmn = 500M
sum = 100万交易;100笔交易/s;500byte/订单对象;每秒产生50kb/s
全局场景扩大10~20倍 50kb * 20 = 1M
促销场景下,访问量增加10倍,每秒对内存的占用增加到10M甚至几十M
由于每秒过来1000笔交易,1s处理不完,需要几十秒
此时新生代里积压了很多的数据,快要满了。但是少数请求处理的特别慢,因为压力太大,性能下降。
GC
(1)此时分配对象,触发Minor GC回收新生代,但是少数几十M的对象还在
(2)多次之后,这些对象被转移到老年代去
(3)老年代的对象被引用完后成为了垃圾对象,最终触发老年代的垃圾回收,极大的影响了系统的性能
每秒10w并发的BI系统频繁发生YoungGC
项目背景
(1)一个平台,有数十万甚至上百万的商家在平台上做生意,使用这个平台系统会产生大量的数据,然后基于这些数据需要为商家提供一些数据报表。BI(Business Intelligence)商业智能
(2)对这些经营数据依托各种大数据计算平台,比如Hadoop、Spark、Flink等技术进行海量数据的计算,计算出各种数据报表
(3)将计算好的各种数据分析表放入一些存储中,比如说MySQL、Elastcisearch、HBase
(4)基于MySQL、HBase、Elasticsearch中存储的数据报表,基于Java开发一个BI系统,基于各种条件对存储好的数据进行复杂的筛选和分析。
部署环境
(1)4核8G的服务器
(2)新生代分配1.5G左右,Eden区大约1G左右的空间
技术痛点:实时自动刷新报表 + 大数据量报表
(1)当使用系统的商家越来越多时,达到几万时,前端页面自动每隔几秒就发送一个请求到后台刷新一下数据,假设同意时间有几千个商家
(2)基本上BI系统部署的每台机器每秒的请求会达到几百个,我们假设是500个请求
(3)每个请求会加载出来一张报表需要的大量数据,同时还需要对这些数据进行内存中的现场计算加工
(4)根据测算,每个请求需要加载出来100kb数据进行计算,每秒500个请求就需要50M
Young GC
每秒50MB的数据,大概需要200s就会迅速填满Eden区,然后触发一次Young GC
对于1G左右的Eden进行Young GC速度很快,几十ms就可以搞定了
提升机器配置:运行大内存机器
(1)后期随着商家越来越多,并发压力增加,高峰期有10w每秒的并发压力,如果单纯依靠集群可能需要上百台机器。
(2)此时会选择提升机器配置。将部署的机器全面提升到16核32G的高配置机器上去。每台机器可以抗每秒几千请求,此时只需要二三十机器就可以了
(3)此时新生代至少分配到20G的大内存,Eden区会占据16G以上
(4)每秒大概会加载内存中几百MB的数据,大概几十秒,就会填满Eden
(5)Young GC。大内存下速度会很慢,此时导致系统卡顿几百毫秒或者1秒。导致很多请求积压,影响前端请求超时
G1优化大内存机器的Young GC性能
对GC设置一个预期的GC停顿时间,比如100ms,让G1保障每个Young GC的时候最多停顿100ms,避免影响中断用户的使用
代码模拟
JVM参数
堆内存设置为200MB,年轻代100MB,Eden80MB,每块Survivor10M,老年代100MB
示例程序
(1)先休眠30s,方便我们找到程序的PID
(2)loadData方法,循环50次,模拟每秒50个请求
(3)每次请求分配一个100KB的数组,模拟每次请求会从数据存储中加载出来100KB的数据,然后休眠1s,模拟这一切都是在1s内发生的
(3)模拟系统安好每秒50个请求,每个请求加载100KB数据的方式不停的运行
GC日志
完整日志
Eden
新生代每次新增5MB,大概16s后触发一次Young GC,GC存活对象大约5MB
Young GC
一次Young GC大概1毫秒,速度很快
每日百亿数据量的实时分析引擎频繁发生Full GC
项目背景
(1)数据计算系统,日处理数据量在上亿的规模
(2)不停的从MySQL数据库以及其他数据源里提取大量的数据加载到自己的JVM内存里来进行计算处理
(3)每分钟大概需要执行500次数据提取和计算的任务
(4)分布式运行的系统。所以生产环境部署了多台机器,每台机器大概每分钟负责执行100次数据提取和计算的任务
(5)每次会提取1w左右的数据到内存进行计算,平均每次计算大概花费10s
(6)每台机器4核8G,JVM4G,新生代和老年代分别1.5G
GC
Young GC
(1)每条数据20个字段,1KB,每次计算任务的1w条数据对应了10MB大小
(2)大概一分钟过后,Eden(8:1:1 1.2G)就满了
(3)此时需要执行Young GC,进行检查
(1)老年代的可用空间是否大于新生代全部对象。满足直接Young GC
(4)由于每个计算任务需要计算10s,假设此时有20个计算任务攻击200MB在计算,不能被回收。但是由于Survivor只有100MB,所以会直接进入老年代
Full GC
(1)每分钟会有200MB的数据进入老年代
(2)2分钟过后,老年代空间剩余1.1G,此时Young GC时进行检查
(1)老年代的可用空间是否大于新生代全部对象。不满足
(2)查看参数-XX:-headlePromotionFailure参数是否被打开了,一般会打开。判断老年代可用空间是否大于历次Young GC过后进入老年代的对象的平均大小。满足
(3)在7次Young GC过后,有1.4G对象进入老年代,老年代空间剩余不到100MB
(4)第8次YoungGC时进行检查,发现老年代空间小于之前每次进入老年代空间的大小,直接触发Full GC
(5)然后执行Young GC,200MB对象进入老年代。循环,七八分钟一次Full GC
JVM优化
系统问题
每次Young GC后会有一批数据进入老年代
解决方案
增加新生代内存比例,2G给新生代,1G给老年代。此时Survivor区大概是200MB,正好可以放下。然后下次Young GC时,又可以存放新的200MB存活对象
工作负载扩大10倍
分析
(1)此时每秒会加载100MB的数据进入内存
(2)十多秒就会塞满Eden,触发YoungGC
(3)由于一批数据处理需要10s以上才能计算完,所以导致有1G对象无法回收
(4)因为老年代空间也只有1G,所以最多在下次Young GC就会触发Full GC
解决方案
使用大内存机器来优化
计算类的系统,非常吃内存,所以更换成每台机器16核32G的高配置机器,此时Eden扩大10倍,比如16GB
按照每秒100MB的数据到内存计算,需要2分钟才会触发一次Young GC,所以每次YoungGc的时候存活对象大概就几百M
每个Survivor分配了2G内存,所以每次Young GC过后的对象不会放入老年代
不需要使用G1减少Young GC的停顿时间。因为后台自动计算系统,不面向用户,所以停顿对系统没有影响
代码模拟
JVM参数
示例代码
每秒执行一次loadData方法,他分配4个10MB的数组,但是都会成为垃圾对象,然后又分配data1和data2两个10MB的存活对象,此时Eden区已经占用了六七十MB空间了,接着data3变量一次执行两个10MB数组,触发Young GC,1s一次
GC日志
(1)程序运行后1s内就发生了Young GC
(2)GC后S1U有1218KB的存活对象,应该是那些位置对象
(3)可以看到OU中多出来了20MB对象,YoungGC后存活20MB对象,放不下进入了老年代
(4)每次YoungGC后老年代持续增长
(5)到达70MB后发生了一次Full GC,老年代变成了20MB
(6)Young特别耗时,因为Full GC由Young GC触发,所以要等Full GC结束,Young GC才能把存活对象放入老年代。
JVM优化
堆内存扩大到300MB,新生代扩大到200MB,Survivor比例调整为2:1:1,100:50:500
新GC日志分析
(1)每秒YoungGC过后会有10MB的存活对象进入Survivor,但是每个Survivor区都是50MB,可以轻松容纳,而且不超过50%的阈值。最终就1M的对象在老年代
(2)只有Young GC,没有Full GC,而且10次Young GC才50毫秒
每秒十万QPS的社交APP如何优化GC性能提升三倍
案例背景
(1)每秒几十万QPS(Query Per Second,每秒钟的查询数量)的社交APP
(2)因为这个社交APP的日用用户涨的很快,所以导致他的高峰期QPS很快就飙升到了10w,导致这个系统在高峰期的时候,年轻代的Eden区会迅速的填满,并且频繁触发Young GC
(3)YoungGC的时候,实际上有很多请求是没处理完毕的,因为每秒请求量太多了。导致有很多对象是需要存活下来的,但是Survivor区却放不下。导致大量对象快速进入老年代
(4)老年代频繁触发GC
优化线上系统JVM参数
核心优化点:增加机器,经理让每台机器承载更少的并发请求,减轻压力;同时给年轻代的Survivor区域更大的内存空间,让每次Young GC后的存活兑现停留在Survivor中,别进入老年代。
关键参数
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=5
5次Full GC后触发一次Compaction操作(压缩操作,避免内存碎片),
在5次Full GC的过程中,每一次Full GC之后都会产生大量的内存碎片,提高了Full GC的频率
优化方案
(1)使用jstat分析各个机器上的jvm的运行情况,判断Young GC后存活对象有多少,然后增加Survivor区的内存,避免对象快速进入老年代
(2)优化后,依然会有对象慢慢进入老年代,毕竟系统负载很高,所以第二个参数的优化针对CMS内存碎片问题
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
每次Full GC后都整理一下内存碎片
垂直电商APP后台系统对Full GC进行深度优化
业务背景
(1)一个垂直电商App,专门做某个细分领域的。
(2)百万注册量,日活几十万,日APP的整体请求量小几千万级别,高峰期QPS每秒几百。
(3)高峰期Full GC每小时会发生好几次。Full GC一般正常情况下,都是以天为单位发生的,比如每天一次,或者是几天一次
公司级别的JVM参数模板
作用:基本保证JVM性能别太差
参数设置
-Xms4096M
-Xmx4096M
-Xmn3072M
-Xss1M
-XX:PermSize=256M
-XX:MaxPermSize=256
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFractoin=92
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSParallellnitialmarkEnabled
-XX:+CMSScavengeBeforeRemark
优化每次Full GC的性能
-XX:+CMSParallellnitialMarkEnabled
在CMS垃圾回收器的初始标记阶段开启多线程并发执行
-XX:+CMSScavengeBeforeRemark
在CMS的重新标记阶段之前,先尽量执行一次Young GC。
重新标记也会Stop the World,如果在执行前能先回收掉一些对象,那么可以在重新标记阶段少扫描一些对象,减少耗时。
不合理设置JVM参数导致频繁Full GC
问题产生
设置了一个奇怪的JVM参数,导致线上频率的Full GC报警
GC日志
(1)这频繁的Full GC,实际上是JDK1.8以后的Metadata元空间数据区导致的,元空间一般是放一些加载到JVM里去的类。
(2)查看Metaspace内存占用情况
监控系统展示的Metaspace内存区域占用的波动曲线图。在不断的增加然后Full GC循环
日志分析结果及处理
(1)在系统运行过程中,不停的有新的类被加载到Metaspace区域里,然后不停的把Metaspace区域占满,接着触发一次Full GC回收掉Metaspace区域中的部分类。整个过程循环
(2)判断什么类不停的被加载
JVM添加参数:-XX:TraceClassLoading -XX:TraceClasUnloading
追踪类加载和类卸载的情况
(3)日志
看到在tomcat的catalina.out日志文件中,输出了这一堆日志
(4)频繁加载奇怪类的原因
这种类大概是使用Java中的反射时加载的,代码如下
如果代码中大量使用了类似于上方的反射东西,那么JVM就会动态去生成一些类放入Metaspace区域里
上面这种类,他们的Class对象都是SoftReference软引用。SoftReference对象回收公式:
clock - timestamp <=freespace * SoftRefLRUPolicyMSPerMB
clock - timestamp :一个软引用对象多久没有被访问过
freesapce :代表JVM中的空闲内存空间
SoftRefLRUPolicyMSPerMB:代表每1MB空闲内存可以允许SoftReference对象存活多久。
正常情况下,软引用对象不会被回收,但也不会快速增长
SoftRefLRUPolicyMSPerMB被设置成为了0,导致所有软引用对象,刚创建出来可能就被Young GC给回收掉。但是马上JVM会继续生成这种类,导致Metaspace区域被放满,触发Full GC,回收掉很多类,循环。
(5)解决方案
在大量反射代码场景下,-XX:SoftRefLRUPolicyMSPerMB这只大一点,1000,2000,3000,5000,确保如引用的一些类的Class对象不要被随便回收
线上系统每天数十次Full GC导致频繁卡死的优化
未优化前JVM性能分析
JVM性能表现如下
机器配置:2核4G
JVM堆内存大小:2G
系统运行时间6天
系统运行6天内发生Full GC次数和耗时:250次,70多秒
系统运行6天内发生Young GC次数和耗时:2.6w次,1400秒
综合分析
每天会发生40多次Full GC,平均每小时两次,每次Full GC300毫秒
每天4000多次Young GC,每分钟会发生3次,每次Young GC在50毫秒左右
JVM参数
-Xms1536M
-Xmx1536M
-Xmn512M
-Xss256K
-XX:SurvivorRatio=5
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSInitialtingOccupancyFraction=68
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSInitatingOccupancyOnly
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
根据线上系统的GC情况倒退运行内存模型
(1)每分钟发生3次Young GC,说明平均20s就会让Eden区满,也就是差生300多MB独享,平均每秒产生15~20MB的对象
(2)每小时2次Full GC,所以每30分钟触发一次Full GC,根据-XX:CMSInitiatingOccupancyFraction=68设置,应该在老年代有600多MB时触发一次Full GC,所以系统运行30分钟就导致老年代有600多MB的对象触发了Full GC。
(3)一次Young GC耗时50毫秒
(4)一次Full GC耗时300多毫秒
老年代为什么存在那么多对象
(1)使用jstat观察JVM实际运行的情况,通过观察,每次Young GC过后升入老年代的对象很少。
(2)观察到现象,不知道为什么,突然有几百MB对象会占据在老年代。分析后是大对象
解决方案
(1)定位系统的大对象
(1)使用jmap工具导出一份dump内存快照
(2)通过内存快照的分析,直接定位处理那个大对象
(3)分析一下在代码的那个地方,例如select * from tal
(2)优化
(1)代码上,解决代码中的bug,例如sql查询拼接where调节。避免大对象的出现
(2)年轻代明显过小,Survivor区域空间不够,因为每次Young GC过后存活对象在几十MB左右,如果Survivor就70MB很容易触发动态年龄判定。
-Xms1536M
-Xmx1536M
-Xmn1024M
-Xss256K
-XX:SurvivorRato=5
-XX:PermSize=256M
-XX:MaxPermSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=92
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSInitiatingOccupancyOnly
-XX:PrintGCDetails
-XX:+printGCTimeStamps
-XX:+PrintHeapAtGC
基本上每分钟发生1次Young GC,一次几十毫秒,十天一次Full GC,一次几百毫秒
电商大促活动下,严重Full GC导致系统直接卡死的优化实战
使用jstat观察系统运行情况
每秒都在执行Full GC,每次都耗时几百毫秒
JVM各个内存区域的使用了基本上没有什么问题,年轻代增长的不快,老年代才占用了10%,永久代也只使用了20%
System.gc()
每次执行都会指挥JVM去尝试执行一次Full GC,连带年轻代、老年代、永久代都进行回收。
-XX:+DisableExplicitGC
禁止显示执行GC,不准通过代码触发GC
一次线上大促营销活动导致的内存泄漏和Full GC优化
故障场景
需要给所有用户发短信、邮件、APP push消息,吸引用户登录APP来参与,数据库、缓存和机器的资源都是足够的,但是CPU使用率飙升,导致系统几乎陷入卡死的状态。
初步排查
线上系统的机器CPU负载过高的两个常见的场景
(1)自己在系统里创建了大量的线程,这些线程同时并发运行,而且工作负载都很重,过多的线程同时并发运行就会导致机器CPU负载过高
(2)机器上运行的JVM在执行频繁的Full GC,Full GC是非常消耗CPU资源的,他是一个非常重负载的过程
一旦JVM有频繁的Full GC,会带来一个明显的感觉,一个是系统可能会是不是卡死,因此Full GC会带来一定的Stop the World问题,一个是机器的CPU负载很高。
(1)使用排除法进行排查
(1)查看JVM Full GC的频率,如果频率过高,就是Full GC引起的CPU负载过高
通过排查发现,每分钟一次Full GC,一次耗时至少几百毫秒
(2)排查频繁Full GC的问题
一般频繁Full GC的三个场景
(1)内存分配不合理,导致对象频繁进入老年代,进而引发频繁的Full GC
(2)存在内存泄漏等问题,就是内存里驻留了大量的对象塞满了老年代,导致稍微有一些对象进入老年代就会触发Full GC
(3)永久代类太多,触发了Full GC
使用jstat分析线上系统的情况
(1)不存在内存分批额不合理,对象频繁进入老年代的问题
(2)永久代内存使用正常
(3)发现老年代驻留了大量的对象,几乎开塞满了,所以一旦年轻代有一些对象进入老年代就出发了Full GC,而且还回收不掉大量的对象
(3)导出一份内存快照
jmap -dump:formt=b,file=文件名 [PID]
(4)使用MAT工具进行分析
(1)Open a Heap Dump打开一个内存快照
(2)Leak Suspects,内存泄漏分析,找到系统创建的占用了过大的对象
(3)排查系统的代码问题,为什么创建这么多,而且回收不掉
问题
进过排查,发现是系统做了一个JVM本地的缓存,将很多数据都加载到内存里去缓存起来,然后提供查询服务的时候走本地内存,但是由于没有限制本地环境的大小,也没有使用LRU算法淘汰内存的数据,到时数据越来越多,造成内存泄漏
解决
使用类似EHCache之类放的缓存框架,可以固定缓存多少个对象,同时定期淘汰删除掉一些不怎么样访问的环境,便于新数据进入缓存
百万级数据处理导致的频繁Full GC问题优化
事故场景
线上系统进行一次版本升级,结果升级后系统对应的前端页面无法访问了。通过监控报警平台,发现线上系统所在机器的CPU负载非常高,持续走高,导致机器宕机
CPU负载高原因分析
通过监控和jstat分析,发现Full GC非常频繁,基本上两分钟一次Full GC,而且每次Full GC都非常耗时,长达10s
Full GC频繁的原因分析
(1)这个系统主要是用来进行大量数据处理后然后提供数据给用户看的,所以当时给JVM的堆分配了20G的内存,10G给年轻代,10G给老年代
(2)由于年轻代过大,Eden大概1分钟左右会被塞满,然后触发一次Young GC,而且Young GC过后直接有几个GB的对象直接进入老年代
(3)说明系统运行的时候产生了大量的对象,而且处理的极慢,在1分钟过后Young GC以后还有很多对象存活,导致直接进入老年代
(4)平均两分钟就会触发一次Full GC,而且由于老年代内存量很大,所以GC时间长达10s
优化
(1)由于每次Young GC后有几个G的大量对象存活,所以这明显是代码层面的问题了,必须要去优化代码,避免代码层面加载过多的数据到内存里去处理。
(2)通过MAT定位代码
在线程中执行“String.split”
(3)分析String.split()为什么造成内存泄漏
当时线上系统使用的JDK1.7,此时对“Hello World Ha Ha”进行拆分,他给每个切分出的字符串都创建一个新的数组,比如Hello就对应一个全新的数组“H,e,l,l,o”。在1.6时,因为字符串底层是字符数组,是通过映射标识的,例如Hello (0-4的偏移量)
在当时系统加载大量的数据,一次性几十万条数据,然后进行字符串切割,每个字符串切割为N个字符串,导致字符串数量暴增几倍到几十倍,从而导致频繁的Young GC和Full GC
(4)代码优化
对String.sprint这个代码逻辑可用可不用直接去除
示例
代码
jps查询PID
导出内存快照
jmap -dump:live,format=b,file=dump.hprof PID
MAt分析内存快照
(1)Open a Heap Dump
(2)Leak Suspects Report
(3)Problem Suspect 1
(4)Details
可以看到一个数据,和他占用的内存,以及他内部的每个元素的实例对象
(5)See stacktrace
线程执行代码堆栈的调用链
核心思路:开启多线程并发处理大量的数据,尽量提升数据处理完毕的速度,这样到触发Young GC的时候避免过多的对象存活下来
OOM
一个超大数据量处理系统是如何不堪重负OOM的
项目背景
(1)一个非常复杂的PB级数据计算系统。需要不停的从数据存储中加载大量的数据到内存里来进行复杂的计算 。每次少则几十万条,多则上百万条数据,内存负载压力非常大。
(2)在系统将数据计算完之后,需要将计算好的数据推送给另外一个系统,两个系统之间的数据推送和交互使用Kafka实现。
(3)考虑到数据计算系统要推送计算结果到Kafka去,必须设计一个针对Kafka的故障高可用机制
问题
在Kafka故障时,将一次计算的数据留存在内存里,不停的重试,知道Kafka恢复才可以。导致内存数据越来越多,每次Eden塞满之后,大量存活队形进入老年代且无法回收,造成内存溢出。
流程图
误写代码导致OOM
(1)无限循环调用
背景
设计了一个链路监控机制,会在比较核心的链路节点写一些重要的日志到Elasticsearch几区里去,事后基于ELK进行核心链路日志的一些分析。如果写日志发生了异常,必须将这个链路节点的异常也写入ES集群。
问题
伪代码如下
一旦es集群异常,就会导致无线循环调用log()方法,导致栈内存溢出
(2)没有缓存的动态代理
背景
(1)使用动态代理机制,在系统运行的时候,怎地已有的某个类,生成一个动态代理,然后对类的一些方法调用做一些额外的处理
(2)当时使用的自研的分布式事务框架,需要对已有类动态代理,然后实现分布式事务一些复杂底层机制
问题
伪代码如下
在系统负载很高的时候,导致一下创建了一大堆的类,塞满了Metasapce区域无法回收,导致OOM
解决方案
这个类只要生成一次就好了,下次直接使用这个动态生成的类创建一个对象就可以了
每秒仅仅上百请求的系统为什么会因为OOM而崩溃
案例背景
每秒仅有100+请求的系统却频繁的因为OOM而崩溃
分析
(1)发现了系统OOM,查看对应的日志
信息如下
http-nio-8080-exec-1089
Tomcat的工作线程
java.lang.OutOfMemoryError:Java heap sapce
堆内存溢出
结论
tomcat的工作线程在处理请求的时候需要在堆内存里分配对象,但是发现堆内存塞满了,而且无法回收,导致堆内存溢出
(2)使用MAT对内存快照进行分析
(1)发现占据内存最大的是大量的“byte[]”数组,一大堆的byte[]数组就占据了8G左右的内存空间。而线上机器只给了Tomcat的JVM堆内存分配8G左右
(2)分析byte[]数组
byte[]数组如图,每个数组大致10MB左右,大致有800个这样的数组
查看数组是被谁引用的,大致发现了是Tomcat的类引用的:org.apache.tomcat.util.thread.TashThread,所以是tocmat自己的线程类创建了大量的byte数组
发现Tomcat的工作线程大致有400个左右,也就是说每个Tomcat有两个byte数组,然后400个工作线程同时处理请求时,导致了内存溢出
(3)通过系统监控发现每秒请求不是400,是100,所以不可能是Tocmat的400个线程都处于工作状态,那就只能是一个请求需要处理4s导致的,在4s里会瞬间有400个请求同时在处理
(4)为什么Tocmat工作线程在处理一个请求的时候会创建2个10MB的数组
在Tocmat的配置文件中有如下配置:max-http-header-size:10000000,
每秒100个请求,每个请求处理4s,导致4s内有400个请求被400个线程处理,每个线程根据配置创建2个数组,每个数组10MB,占满了8G
(5)为什么一个请求需要4s
(1)查看日志,发现处理OOM以外,还有大量的服务请求调用超时异常
(2)查看系统的RPC调用超时的配置发现RPC调用超时时间设置为4s
下游服务器故障,网络请求都是失败的,此时按照4s超时时间卡主4s后才异常,导致4s内积压了400个请求,造成内存溢出
优化
降低超时时间
tomcat底层原理
tomcat工作流程
(1)一般写的系统是部署在Tomcat中的
(2)tomcat会监听一个端口,一般8080,然后系统的功能就可以运行起来了
(3)web系统部署在tomcat中运行原理图
(4)一旦tomcat监听8080端口收到了这个请求,就会把这个请求交个Spring Web MVC之类的框架去处理,这类框架一般底层都封装了Servlet/Filter之类的组件,用这类组件去处理请求
(5)Servlet根据 请求路径找到代码中用来处理这个请求的Controller组件
Tomcat底层原理
(1)Tocmat自己就是一个JVM进程,我们写好的类被Tomcat加载到内存里去,然后由Tomcat来执行我们的类
(2)Tomcat有自己的工作线程,少则一两百个,多则三四百个,从8080端口上收到的请求都会均匀的分配给这些工作线程去进行处理
(3)工作线程收到请求后,负责调用Spring MVC框架的代码,Spring MVC框架负责调用我们写好的代码,比如Controller类之类的
原理图
日志查看注意两点
(1)溢出类型是什么(堆内存溢出、栈内存溢出、Metaspace内存溢出)
(2)哪个线程运行代码的时候内存溢出了
系统上线,必须要有的参数
-XX:+HeapDumpOnOutOfMenmoryError
内存溢出时导出来一份内存快照
Jetty服务器的NIO机制导致堆外内存溢出
案例背景
使用Jetty作为Web服务器的时候,在某个非常罕见的场景下发生的一次堆外内存溢出的场景
案例发生
(1)收到线上的一个报警:某台机器部署的一个服务突然之间不可以访问了。然后登录机器看日志,发现如下信息
(2)通过日志发现有OOM异常,但是是Direct buffer memory区域,而且下面还有一大堆的jetty相关的方法调用栈
分析
(1)这次OOM是Jetty这个Web服务器在使用堆外内存的时候导致的
(2)如果创建了很多DirectByteBuffer对象,占用了大量的堆外内存,而且没有GC线程来回收,就会发生内存溢出
(3)创建大量DirectByteBuffer的可能性
(1)系统承载超高并发,负载压力很高,瞬间大量请求过来,创建过多的DirectByteBuffer,导致内存溢出
(4)使用jstat等工具观察线上系统的实际运行情况,同时根据日志看一下请求的处理耗时,综合性的分析
(1)查看各个接口的调用耗时,这个系统并发量不高,但是每个请求处理比较耗时,平均每个请求处理耗时1s
(2)通过jstat发现,随着系统不停的被调用会一直创建各种对象,包括jetty本身不停的创建DirectByteBuffer对象去申请堆外内存空间,接着到年轻代Eden区满,触发Young GC。
(3)由于在垃圾回收的一瞬间,可能有的请求还没处理完毕,此时就会有不少DirectByteBuffer对象处于存活状态,回收不了,转入Survivor区域,由于当时内存分配的不合理,导致年轻代只有一两百M,Survivor10MB,所以存活对象直接进入了老年代
(4)由于DirectByteBuffer对象进入了老年代,虽然已经成为了垃圾对象,但是没触发Full GC,就没有被回收,一直占用着大量的堆外内存空间,导致内存溢出
(5)由于JVM设置参数-XX:+DisableExplicitGC,所以导致Java NIO中的System.gc()无法生效
优化
本次问题有两个
(1)内存设置不合理,导致DirectByteBuffer对象进入老年代,导致堆外内存释放不掉
(2)-XX:+DisableExplicitGC导致Java NIO没法主动回收垃圾对象
(1)合理分配内存,给年轻代更多的内存,让Survivor有更多的空间
(2)开放-XX:+DisableExplicitGC,使System.gc()生效
扩展知识
Direct buffer memory
堆外内存,他是JVM堆内存之外的一块内存空间,这块内存空间不是JVM管理的,但是java代码可以在JVM堆之外使用一些内存空间。直接被操作系统管理
jetty
和Tomcat一样,是一个web容器,java编写的,会监听一个端口,比如9090,当监听到9090端口发送请求,就会把请求转到Spring MVC之类的框架,然后Spring MVC之类的框架去调用写好的Controller之类的代码
解决OOM问题的底层技术修为的建议
一般来说解决OOM问题,都是有点技术难度的。但是排查思路都是类似的,如果要解决这些问题,需要有较为扎实的技术内功修为
堆外内存是如何申请和释放的
在Java代码中要申请一块堆外内存空间,可以使用DirectByteBuffer这个类,通过它创建一个对象,这个对象本身是在JVM堆内存的
当DirectByteBuffer对象没人引用就成为了垃圾对象,在Young GC或者是Full GC的时候就会被回收
Java NIO的处理
每次分配新的堆外内存的时候,都会调用System.gc()去提醒JVM主动执行gc回收掉无人引用的DirectByteBuffer对象,释放堆外内存空间。
微服务架构下的RPC调用引发的OOM故障排查实践
案例背景
发生在微服务架构下的一次RPC调用过程中的OOM。
一般线上系统OOM,都不是简单的由自己编写的代码导致,可能是因为系统使用的某个开源技术的内核代码有一定的故障和缺陷,此时要解决OOM问题,就必须深入到开源技术的源码中去分析。
系统架构
早期的系统,在进行服务间的PRC通信时,采用的是基于Thrift框架自己封装出来的一个RPC框架。
故障
平时服务A通过RPC框架去调用服务B,但是有一天,负责服务A的工程师更新了一些代码,然后将服务A重新上线,服务A没有任何问题,但是服务B宕机了
故障排查
(1)登录到服务B的机器查看服务B的日志,发现了OOM异常:java.lang.OutOfMemoryError Java heap space。堆内存溢出
(2)尝试重启,发现很快fuwuB又宕机了,而且还是因为OOM。但是服务B是没有动过得,只是改过服务A的代码。
(3)查找内存溢出的故障发生点
(1)一般在日志中,都会有详细的异常栈信息
(2)可以看到,引发OOM异常的是自研的RPC框架
(4)分析内存快照,找到占用内存最大的对象
(1)通过内存快照分析,发现占用内存最大的是一个超大的byte[]数组。当时的机器堆内存4GB,这个byte[]数组就占据了差不多4GB
(2)分析这个byte[]数组的引用者,发现这个数组就是RPC框架内部的类引用的
(5)分析源代码找出原因
通过分析,服务B在接受到请求之后,会先反序列化,接着把请求读出来放入一个byte[]数组。但是这里RPC框架有一个bug,就是发现自己发送过来的字节流反序列化的时候失败了,此时会开辟一个byte[]数组,默认4GB,然后把对方的字节流原封不动的放进去。
这个一般是因为服务A对Reqeust类做了修改,服务B没有同步。比如服务A有15个字段,服务B10个字段,有的字段名字还不一样,就会反序列化失败。
概括:服务A和服务B的传输对象没有同步,导致服务B一直在缓存服务A的字节流,导致OOM
解决方案
(1)将RPC框架中那个数组的默认值从4GB调整到4MB,一般请求都不会超过4MB
(2)同步服务A和服务B的Request类保持一致
自研RPC框架流程
服务A发送请求的时候,会对传输过来的对象进行序列化。类似Reqeust的对象变成一个byte[]数组
服务B会根据我们自定义的序列化协议(Protobuf)对发送过来的数组进行反序列化,接着把请求数据读取到一个byte[]缓存中去,然后调用业务逻辑代码处理请求,最后请求处理完毕,清理byte[]缓存。
流程图
RPC框架类的定义
RPC框架要进行对象传输,就必须要让服务A和服务B都知道有这么一个对象。
示例
(1)服务A要把Request对象传输给服务B,那么首先需要使用一种语法定义一个对象文件,当时用的是ProtoBuf支持的语法
(2)通过上面这个特殊语法写的文件反向生成一个Java类处理,此时会生成一个Java语法的Reqeust类
(3)这个Reqeust类需要在服务A和服务B的工程里都要引入,然后才能把Reqeust对象序列化成字节流和反序列化成一个Request对象
没有where条件的SQL语句引发的OOM问题排查实践
案例背景
系统在使用mybatis写SQL语句的时候,在某些情况下允许不加where条件就可以执行,结果导致一下子查询出来上百万条数据引发了系统的OOM
故障
(1)收到反馈说线上一个系统崩溃不可用了,此时立即登录到线上机器去查看日志,日志中发现了OOM的异常:java.lang.OutOFMemoryError, java heap space。堆内存溢出
(2)把自动导出的内存快照拷贝到自己电脑,用MAT进行分析
故障排查
(1)检查内存中到底是什么对象太多了
(1)用MAT的Histogram功能,检查占用内存最多的对象有哪些
(2)此时可以瞬间看到是谁占用内存过多
(2)深入查看占用内存过多的对象
(1)进入domainator_tree界面,会展示出来当前JVM中所有的线程
(2)此时可以看到哪些线程创建了过多的对象
(3)展开这个线程,看看他创建了哪些对象
(4)发现了有一个java.util.ArrayList @ 0x5c00206a8,说经线程创建了一个巨大的ArrayList,此时继续展开这个ArrayList,里面是一个java.lang.Object[]数据,继续展开,就能看到大量的Demo1$Data对象了
(3)定位到代码,哪些代码创建了这些对象
(1)进入thread_overrview,这里会展示所有线程以及每个线程当时的方法调用栈,以及每个方法中创建了哪些对象
(2)此时看到main Thread,先执行了一个Demo1.Java类中的第12行处的一个Demo1.main()方法,接着main方法有执行了一个java.lang.Thread类的sleep()方法
(3)可以看到线程调用每个方法的时候创建和引用了哪些对象
(4)发现Demo1.maim方法执行的时候创建了一个ArrayList,展开发现是一个java.lang.Object[]数组,再次展开就是一大堆的Demo1$Data对象
通过上述步骤,立马定位到了是系统中的一个业务方法,在执行查询操作的时候,因为没有带上where条件,导致查询出来过多数据,造成OOM
优化
修改SQL语句,带上where条件
使用MAT工具对OOM故障的实践意义
如果系统触发OOM是由于Tomcat、Jetty、RPC框架之类的底层技术,那么MAT的用处并没有那么大,最多是使用MAT查找占用过多的对象,然后结合异常日志调用栈和MAT中的对象引用情况,初步定位是底层技术中心哪部分代码导致的内存溢出。真正解决问题,需要仔细研究Tocmat、Jetty、RPC框架之类技术的底层源码,结合线上系统的负载情况、访问压力以及GC情况,以及底层技术的源码细节,真正分析清楚发生OOM的原因,才能解决。
如果是自己的系统代码问题导致的,值需要依托MAT层层分析,定位到代码的问题所在,就可以进行解决了
每天10亿数据的日志分析系统的OOM问题排查实践
案例背景
一个每天10亿数据量的日志清洗系统,他主要是从Kafka中不停地消费各种日志数据,然后对日志的格式进行清洗,比如设计到用户敏感的字段(姓名、手机号、身份证号码)进行脱敏处理,然后将清洗后的数据交付给其他的系统去使用
事故现场
(1)发现日志清洗系统发生了OOM的异常,查看日志发现是java.lang.OutOfMemoryError: java heap space的问题,堆内存溢出
(2)发现XXXCalss.process()方法反复出现,导致了堆内存溢出的问题,初步判定是出现大量的递归
排查
内存快照分析
(1)分析发现,因为有大量的XXClass.process()方法的递归执行,每个XXClass.process()中都创建了大量的char数组,导致堆内存耗尽
(2)但是发现递归调用的次数并不多,大概就十几次到几十次,所有递归加起来创建的char[]数组综合只有1G左右。判断可能是给堆内存分配的空间太少了
分析JVM的GC日志
由于系统已经宕机了,所以只能去查看GC日志
完整参数如下
GC日志如下
可以发现,Allocation Failure触发的Full GC很多,且每次Full GC只能回收掉少量的对象,堆内存基本上都是占满的,日志显示的每秒钟都会进行一次Full GC。可能是每秒钟执行Young GC之前,发现老年代空间不足,提前触发Full GC,也可能是Young GC之后存活对象无法放入Survivor ,要进入老年代,只能Full GC。知道某次Full GC后内存依然不足,造成OOM
分析JVM运行时的内存使用模型
(1)使用jstat打印统计信息
(2)可以看到当Young GC从36到37,Old区直接从占比69到99,说明YGC后存活对象,Survivor放不下,直接进入老年代
(3)接着老年触发了一次Full GC,但是回收后依然占用87
(4)反复几次以上流程,导致老年代无法回收,触发OOM
优化
(1)增加堆内存
从JVM运行情况看,是因为内存分配不足的问题,所以在4核8G的机器上,给堆内存加大空间到5G,接着通过jstat观察发现,每次Young GC后存活对象都落入Survivor区域,没有进入老年代,基本运行一段时间不会有OOM
(2)改写代码
减少他的内存占用。但是代码递归是因为在一条日志中,可能有很多用户的信息,所以会递归十几次到几十次去吃力这个日志,导致每次递归都产生大量的char[]数组
对一条日志切割处理就可以了,没必要递归
总结
(1)先通过OOM的排查方法去分析,发现因为内存调小导致
(2)然后用GC日志和jstat分析,明显发现是内存不够用
(3)优化JVM参数和代码即可
服务类加载器过多引发的OOM问题排查实践
案例背景
(1)一个非常正常的线上服务,采用Web系统部署在Tomcat中的方式来启动
(2)收到反馈,服务不稳定,经常出现访问服务的接口的时候假死问题。相当于服务不可用。但是是一段时间内无法访问这个服务的接口,果断时间又可以了
排查
(1)使用top命令检查机器资源使用
(1)因为当时的生成清楚是服务假死,接口无法调用,并不是直接抛出OOM异常,所以很难去看线上日志,根据日志就定位问题,所以先查看机器的资源使用情况
(2)如果服务出现无法调用接口假死的情况,首先要考虑的是两种问题
(1)服务使用了大量的内存,内存无法释放,导致了频繁的GC。频繁Stop the World,接口调用出现频繁假死的问题
(2)这台机器的CPU负载太高了,也许是某个进程耗尽了CPU资源,导致这个服务的线程时钟无法得到CPU资源去执行,也就无法响应调用的请求
(3)进过top命令分析发现,这个服务的进程对CPU耗费很少,仅仅耗费了1%的CPU,但是他耗费了50%以上的内存资源
(4)当时的机器时4核8G,这种机器通常给部署服务的JVM总内存5G~6G,刨除掉Metaspace区域之类,堆内存大概会有4G~5G,所以堆内存基本上使用了机器一半的内存
一般Metaspace区域512MB以上的空间,堆内存假设4G,栈内存一个线程一般1M,一般预留1G左右
(2)分析内存使用这么高下可能出现的情况
可能存在的三种情况
(1)内存使用率居高不下,导致频繁Full GC,gc带来的stop theworld问题影响了服务
(2)内存使用率过多,导致JVM自己放上了OOM
(3)内存使用率过高,也许有的使用会导致这个进程因为申请内存不足,直接被操作系统把这个进程给杀掉了
(1)使用jstat分析一下JVM运行的情况,虽然经常发生gc,但是实际上gc耗时每次也就几百毫秒,并没有耗费过多的时间。排除第一种情况
(2)检查了应用服务自身的日志,并没有看到任何日志输出OOM异常。排除第二种情况
(3)可能是在进行被杀掉的过程中,出现了上游服务无法访问的情况,但是我们的进程都是有监控脚本的,一旦进程被杀掉,会有脚本自动把进程重新启动拉起来,所以过一会儿后服务又可以访问了
(3)排查高内存对象
(1)通过top和jhstat命令观察JVm耗费超过50%时,从线上导出一份内存快照
(2)使用MAT进行内存快照分析,发现了一大堆的ClassLoader(类加载器),有几千个,大量的byte[]数组,加起来占用超过50%
(4)结论:代码中没有限制的创建了大量的自定义类加载器,去重复加载了大量的数据,导致内存耗尽,进程被杀掉
解决方案
修改代码,避免重复创建几千个自定义类加载器,避免重复加载大量的数据到内存
类加载器
处理可以加载类之外,还可以用来加载一些其他的资源,比如外部配置文件
数据同步系统频繁OOM内存溢出的排查实践
案例背景
一个线上的数据同步系统,专门负责从另外一个系统去同步数据,另外一个系统会不停的发布自己的数据到Kafka中,然后这个系统专门从Kafka里去消费数据,接着保存到自己的数据库中去
时不时报内存溢出的错误,重启一段时间后又会再次出现,随着系统处理的数据量越来越多,频率越来越高
现象看本质
(1)每次重启后在一段时间之后OOM,所以内存是不断的上涨的。一般高到JVM内存溢出,通常两种情况
(1)并发太高,瞬间大量并发创建过多的对象,导致系统崩溃
(2)内存泄漏问题,很多对象都在内存中,GC回收不掉
(2)分析了当时的系统,负载并不高,所以应该是随着时间推移,某种对象越来越多,赖在内存,无法回收掉,导致内存溢出
(3)使用jstat分析
分析发现老年代对象一直在增长,每次Young GC后,老年代就会增长不少,当触发Full GC后,老年代无法回收太多对象,导致OOM
(4)使用MAT查找大对象
(1)在内存快照中发现,有一个队列数据结构,直接引用了大量的数据,就是这个队列数据结构占满了内存
(2)从Kafka消费出来的数据会先写入这个队列,然后再从这个队列慢慢写入数据库,中间做一些额外的数据处理和转换。
(3)我们从Kafka中一下就消费了几百万条数据,然后变成了一个List,将这个List放入了一个List中,导致内存中积压了几十万甚至几百万条数据,发生了内存溢出,因为只要数据还停留在队列中,就没办法被回收。
解决方案
将内存队列改为一个定长的阻塞队列,比如1024个元素,然后每次消费出来的数据,一条一条的吸入,而不是直接将list作为一个元素
流程图
发生OOM时
优化后
总结
JVM和GC
JVM内存区域划分
年轻代
(1)Eden和2个Survivor,默认比例8:1:1
(2)写好的系统会不停的运行,运行的时候不停的在年轻代的Eden区域中创建对象
(3)一般创建对象都是咋一个钟方法里执行,一旦方法运行完毕,方法局部变量引用搞得对象会成为Eden的垃圾对象,可以被回收
(4)随着Eden不断创建对象,被塞满,触发一次Young GC,Young GC采用复制算法,从GC Roots(方法的局部变量、类的静态变量)开始追踪,标记出来存活的对象,然后把存活对象放入第一个Survivor区域,然后直接回收掉Eden区的全部垃圾对象,垃圾回收过程中,系统进入Stop the World(系统代码停止运行,不允许创建新的对象)
(5)Young GC执行完毕,系统恢复工作,继续在Eden区创建对象,如果下次Eden塞满,再次触发Young GC,Eden + s0的存活对象放入s1
(6)Young GC垃圾回收器很多,常用的是ParNew,基于多线程并发执行垃圾回收
老年代
对象进入老年代的时机
(1)一个对象在年轻代躲过15次垃圾回收,年龄太大了,进入老年代
(2)对象太大了,超过一定阈值,直接进入老年代,不走年轻代
(3)一次Young GC后存活对象太多了,导致Survivor区域放不小,这批对象会进入老年代
(4)动态年龄判定规则。几次YoungGC过后,Survivor区域的对象占用超过50%,判断年龄1 +年龄2 +年龄3... 年龄N的对象综合超过了50%,此时年龄N以及之上的对象都进入老年代
老年代如何触发GC
一旦老年代对象过多就会触发Full GC,Full GC必然会带着Old GC,也是针对老年代的GC,而且一般跟着一次Young GC和永久代的GC
Full GC触发的条件
(1)老年代自身可以设置一个阈值,有一个JVM参数可以控制,一旦老年代内存使用打到这个阈值,就会触发Full GC,一般建议调大点儿:92%
(2)在Young GC之前,如果判断发现老年代可用空间小于历次Young GC后升入老年代的平均对象大小的话,就会在Young GC之前触发Full GC,先回收掉一批老年代对象,然后再执行Young GC
(3)如果过Young GC过后存活对象太多,Survivor区域放不下,就要放入老年代,但是如果老年代也放不下,就会触发Full GC,回收老年代对象
频繁Full GC的问题
(1)Old GC速度很慢,少则几百毫秒,多则几秒,一旦Full GC频繁,会导致系统性能很差,因为频繁要停止系统工作线程,导致卡顿
(2)导致机器CPU负载过高,机器性能下降,处理请求能力下降
Metaspace
正常系统GC频率
Young GC:几分钟或者几十分钟,一次耗时几毫秒到几十毫秒
Full GC:几十分钟,几个小时,一次耗时几百毫秒
JVM优化
核心:减少Full GC的频率
新系统开发完毕如何设置JVM参数
(1)估算一下系统每隔核心接口每秒多少次请求,每次请求会创建多少个对象,每个对象多大,每秒会使用多少内存空间
(2)估算多久会发生一次Young GC,会有多少对象存活,会有多少对象升入老年代,以及老年代对象增长的速率,多久会触发Full GC
原则:尽可能让每次Young GC后存活对象远远小于Survivor区域,避免对象频繁进入老年代触发Full GC
压测后合理调整JVM参数
使用jstat等工具观察JVM的运行内存模型
Eden区的对象增长速率有多快
Young GC频率多高
一次Young GC多长耗时
Young GC过后多少对象存活
老年代对象增长速率多高
Full GC频率多高
一次Full GC耗时
针对结果,尽可能优化JVM的内存分配,尽量避免对象频繁进入老年代,尽量让系统仅有Young GC
线上系统的监控和优化
使用Zabbix、Open-Falcon之类的工具来监控,频繁Full GC就报警。或者使用jstat,每天定时检查
一旦发现频繁Full GC就进行优化,优化思路:通过jstat分析处理系统的JVM运行指标,找到Full GC的核心问题,然后优化JVM参数,尽量让对象别进入老年代,减少Full GC的频率
频繁Full GC的表现
(1)机器CPU负载过高
(2)频繁Full GC报警
(3)系统无法处理骑牛或者处理过慢
频繁Full GC的场景原因
(1)系统承载高并发请求,或者处理数据量过大,导致Young GC很频繁,而且每次Young GC过后存活对象太多,内存分配不合理,Survivor,导致对象频繁进入老年代,触发Full GC
(2)系统一次性加载过多数据进入内存,搞出来很多大对象,导致频繁大对象频繁进入老年代,频繁触发Full GC。
(3)系统发生内存泄漏,莫名其妙创建大量的对象,始终无法回收,一直占用老年代,频繁触发Full GC
(4)Metaspace因为加载过多类导致Full GC
(5)误调用System.gc()触发Full GC
解决方案
第一种:合理分配内存,调大Survivor
第二种、第三种:利用MAT工具分析,找出来哪些对象占用内存过多,然后通过一些对象的引用和线程执行堆栈的分析,找到代码,进行代码优化
第四种、第五种,对应的进行优化
统一的JVM参数模板
-Xms4096M
-Xmx4096M
-Xmn3072M
-XX:SurvivorRatio=8
-Xss1M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=92
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSParallelInitialMarkEnabled
-XX:+CMSScavengeBeforeRemark
-XX:+DisableExplicitGC
-XX:+PrintGCDetails
-Xloggc:gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/app/oom
注意点
真正的JVM优化,就是一些内存分配 + 垃圾回收期的选择(ParNew、CMS、G1) + 垃圾回收器的常见参数设置,还有一些代码层面的内存泄漏问题。
面试
将JVM优化案例融合到自己负责的系统
(1)将所负责的系统访问量和数据量暴增10倍或100倍。考虑会不会出现频繁Full GC的问题,不同场景下的频繁Full GC,以及问题发生后的定位、分析和解决。整理出来一套解决方案。
常见的三个问题
(1)生产环境系统的JVM参数怎样设置的,为什么要这么设置
(2)你再生产环境中的JVM优化经验可以聊聊
(3)生产环境解决过的JVM OOM问题
常见参数
-XX:+CMSParallellnitialMarkEnabled
表示在初始标记的多线程执行,减少STW;
-XX:+CMSScavengeBeforeRemark
在重新标记之前执行minorGC减少重新标记时间;
-XX:+CMSParallelRemarkEnabled
在重新标记的时候多线程执行,降低STW;
-Xx: CMSInitiatingOccupancyFraction=92和-XX:+UseCMSInitiatingOccupancyOnly
配套使用,如果不设置后者,jvm第一次会采用92%但是后续jvm会根据运行时采集的数据来进行GC周期,如果设置后者则jvm每次都会在92%的时候进行gc;
-XX:+PrintHeapAtGC
在每次GC前都要GC堆的概况输出
0 条评论
下一页