jvm启动参数时强烈建议加入如下的一些参数:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/app/oom
加入了这两个参数,在jvm OOM崩溃时,你能够去找到OOM时的内存快照
jvm参数的标准模板如下:
“-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=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”
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
不要忘记指定垃圾回收器,新生代可以使用ParNew,老年代使用CMS。
1、我们编写.java文件
2、打包成.class文件
3、JVM类加载器把.class字节码文件加载到内存中
4、JVM基于自己的字节码执行引擎,来执行加载到内存里我们写好的类
一个类从加载到使用,一般会经历下面的这个过程:
加载-->验证-->准备-->解析-->初始化-->使用-->卸载
验证:校验加载进来的.class文件是否符合规范
准备:分配内存空间,分配变量类型对应的初始值(如int分配0,Integer分配null)
解析:把符号引替换为直接引用
初始化:初始我们分配的值(如int a = 3,那么这时a就初始为3)
Java有如下类加载器
它们遵循双亲委派机制。
方法区(jdk1.8以后,这块区域叫元数据空间):存放类似常量池的东西和各种类相关的信息
程序计数器:记录当前执行的字节码指令的位置
Java虚拟机栈:存放方法创建时的栈帧,栈帧有(方法的局部变量表、操作数栈、动态链接、方法出口等)
堆内存:存放代码创建的各种对象
本地方法栈:存放各种native方法的局部变量表之类的信息
它是后台自动运行的线程, 回收无引用的实例对象。没有垃圾回收,分配的内存将得不到释放,导致内存资源耗尽。
Java支持多线程,每个线程都有自己的Java虚拟机栈和本地方法栈。
新建的实例在堆内存,实例变量也是在堆内存。
方法区内进行垃圾回收条件
1、首先该类的所有实例对象都已经从Java堆内存里被回收
2、其次加载这个类的ClassLoader已经被回收
3、最后,对该类的Class对象没有任何引用
新生代内存空间的垃圾回收,也称之为Minor GC,有时候也叫Young GC,它会尝试把新生代里那些没有人引用的垃圾对象,都给回收掉。
如果有对象躲过了十多次垃圾回收,就会放入老年代里
对象分配这块,有很多其它的复杂机制,如
1、新生代垃圾回收之后,因为存活对象太多,导致大量对象直接进入老年代
2、特别大的超大对象直接不经过新生代就进入老年代
3、动态对象年龄判断机制
4、空间担保机制
假设每天100万个支付订单,每天高峰期大概3个小时,平均下来每秒100订单左右。如果部署了3台机器,那么每台处理30笔订单。
支付订单假设20个实例变量,一个支付订单对象占据500字节。30个支付订单,大概占用内存空间是30*500=15000字节,大概15kb。真实的支付系统线上运行,肯定每秒会创建大量其他的对象。可以把之前的计算结果扩大10倍-20倍。在几百kb~1MB之间。如果用2核4G机器,那么JVM进程最多就是2G内存,堆内存最多1G,除去老年代,新生代就几百MB内存。而系统大致每秒占据1MB空间,导致运行几百秒之后,新生代内存空间就满了,如此频繁minor gc,会影响线上系统性能稳定性。
如果采用4核8G机器,JVM可分配4G以上内存,新生代2G,这需要近半小时到1小时才会让新生代触发Minor GC,这大大降低了GC的频率。所以机器采用4核8G,就可用设-Xms和-Xmx为3G,-Xmn为2G。
永久代上线上前没太多可以参考的规范,一般你设置几百MB,大体够用的。
栈内存一般默认512kb到1MB就差不多了。
只要你的对象被方法的局部变量、类的静态变量给引用了,就不会回收它们。
强引用:垃圾回收的时候绝对不会去回收这个对象
软引用:正常情况下垃圾回收是不会回收软引用对象,如果内存不足,哪怕他被变量引用了,但是因为他是软引用,所以还是要回收的。
弱引用:这个引用就跟没引用类似的,如果发生垃圾回收,就会把这个对象回收掉
虚引用:略
针对新生代的垃圾回收算法,他叫复制算法。它是把新生代内存划分为两块内存区域,然后只使用其中一块内存,待那块内存满的时候就把里面存活对象一次性转移到另外一块内存区域,保证没有内存碎片。缺点是对内存的利用率低。
复制算法的优化:Eden区和Survivor区
一般躲过15次GC之后进入老年代。
动态对象年龄判断:当前放对象的Survivor区域里,相同年龄的一批对象的总大小大于这块Survivor区域的内存大小的50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了。
大对象直接进入老年代。
如果Eden区的的对象无法放入Survivor区,那么就会直接转移到老年代去。
如果老年代里空间也放不下这些对象咋办?
在Minor GC之前,都要检查老年代可用的内存空间是否大于新生代所有对象的总大小。如果老年代空间够用,就放心Minor GC,这是怕Minor GC后大量对象存活下来,无法全部放入老年代。
(1)如果老年代空间不足,就会检查“-XX:-HandlePromotionFailure”的参数是否设置了。
(2)设置了(1)中参数,就判断老年代空间是否大于每一次Minor GC后进入老年代对象的平均大小。
(3)如果(1)未设置参数或(2)判断失败,则进行Full GC,再进行Minor GC。
(4)如果(1)已设置参数和(2)判断成功,就可以冒险进行Minor GC,结果有三种:
老年代采用标记整理算法
Serial和Serial Old垃圾回收器:分别用来回收新生代和老年代的垃圾对象。工作原理不是单线程运行,垃圾回收的时候会停止我们自己写的系统的其它工作线程,让我们系统直接卡死不动,然后让它们垃圾回收,这个现在一般写后台Java系统几乎不用。
ParNew和CMS垃圾回收器:ParNew现在一般都是用在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器,它们都是多线程并发的机制,性能更好,现在一般是线上生产系统的标配组合。
G1垃圾回收器:统一收集新生代和老年代,采用了更加优秀的算法和设计机制。
面试题:ParNew+CMS的gc,如何保证只做young gc,jvm参数如何配置
(1)加大分代年龄,比如默认15加到30;延长对象在新生代的停留时间,以更minor gc时能直接移除
(2)修改新生代老年代比例,比如新生代老年代比例改为2:1
(3)修改eden区和survivor区比例,比如6:2:2。即增大survivor的大小
什么时候会尝试触发Minor GC?
当新生代的Eden区和其中一个Survivor区空间不足时
触发Minor GC之前会如何检查老年代大小,涉及哪几个步骤和条件?
1、判断新生代存活是否大于老年代剩余
2、条件1成立且设置空间担保的情况下,判断老年代剩余是否大于之前进入老年代平均存活大小
什么时候在Minor GC之前就会提前触发一次Full GC?
新生代现有存活对象>老年代内存情况下或未设置空间担保或空间担保失败
Minor GC过后可能对应哪几种情况?
放入新对象前进行判断,新对象大小+存活对象是否可以分配在新生代。可以则放入,否则判断是否可以放入老年代。可以则放放,否则OOM
哪些情况下Minor GC后的对象会进入老年代?
1、新生代放不下,老年代可以放下的情况下。
2、动态年龄机制
默认情况下ParNew的垃圾回收线程数与CPU的核数是一样的。
一般老年代我们选择的垃圾回收器是CMS,它采用的是标记清理算法。
CMS在执行一次垃圾回收的过程一共分为4个阶段:
CMS默认启动的垃圾回收线程的数量是(CPU核数 + 3) / 4。并发清理阶段,由于系统一直在运行,所以此时会让一些对象进入老年代,同时还变成垃圾对象,这种垃圾对象是”浮动垃圾“,只能下次gc清理了。为了保证CMS垃圾回收期间,还有一定的内存空间让一些对象可以进入老年代,一般会预留一些空间。默认老年代占用92%空间,就自动进行CMS垃圾回收。如果CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,会发生Concurrent Model Failure,此时就会自动用”Serial Old"垃圾回收器替代CMS,直接强行把系统程序"stop the world",进行垃圾回收。
CMS其实在Full GC之后要再次进行"stop the world",把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片
CMS是标记清理算法
什么时候触发老年代GC?
电商系统大促期间有接近每秒1000的下单请求。基本上可以按3台算,就是每台机器每秒需要300个下单请求,假设订单系统部署的就是最普通的标配4核8G机器。假设每个订单按1kb大小算,300个订单就有300kb的内存开销。算上订单对象连带的订单条目对象、库存、促销、优惠券等等,一般需要对单个对象开销放大10倍~20倍。除了下单外,订单系统还会有很多订单相关的其它操作,如订单查询,可以往大估算,再扩大10倍的量。那么每秒钟有大概300kb*20*10=60mb的内存开销。但一秒后,60mb的对象就处于可回收的状态。
对于4核8G的机器,给JVM内存分配4G,堆内存3G,其中新生代1.5G,老年代1.5G,每个线程的java虚拟机栈1M,那么几百个线程大概几百M,然后永久代256M内存,这4G就差不多用完了。
60MB对象需要25秒才会把1.5G新生代占满。Minor GC后,估计存活对象就100MB。新生代Eden区为1.2G,每个Survivor是150MB,实际要20秒就会发生Minor GC。存活的100MB放到S1中。每次新生代垃圾产生在100MB,有可能会突破150MB,此时就会频繁让对象进入老年代。即使是100MB的对象进入Survivor区,因为这是同一批同龄对象,直接超过了Survivor的50%,此时也可能会导致对象进入老年代。此时可以考虑把新生代调整为2G,老年代为1G,那么Eden为1.6G,每个Survivor为200MB,大大降低了新生代GC过后存活对象在Survivor里放不下的问题或者同龄对象超过50%的问题。
不要忘记指定垃圾回收器,新生代可以使用ParNew,老年代使用CMS。
Full GC的触发条件:
一台机器能开多少线程?取决于什么?
对于4核CPU,本身JVM就有一些后台线程,还有使用的框架可能也会有后台线程,自己系统一般开启线程数量在几十个,比如50左右的就差不多了。大概系统所有系统加起来有100+,此时高峰期这么多线程同时工作,CPu负载基本满负荷了。
G1垃圾回收器可以同时回收新生代和老年代的对象的。它的最大特点就是把Java堆内存拆分为多个大小相等的Region。G1其实也有新生代和老年代的概念,只不过是逻辑上的概念。G1在触发垃圾回收的时候,可以根据设定的预期系统停顿时间,来选择最少回收时间和最多回收对象的Region进行垃圾回收,保证GC对系统停顿的影响在可控范围内,同时还能尽可能回收最多的对象。
对于G1,JVM最多有2048个Region,Region的大小必须是2的倍数,比如1MB、2MB、4MB。默认新生代对堆内存的占比是5%, 系统运行时,JVM会不停给新生代增加更多的Region,但最多新生代的占比不会超过60%。
按照默认新生代最多只能占据堆内存60%的Region来算,老年代最多可以占据40%的Region。
那么对象什么时候从新生代进入老年代呢?
G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。而且一个大对象如果太大,可能会横跨多个Region来存放。在新生代、老年代在回收时,会顺带着大对象Region一起回收。
G1有一个参数,是”-XX:InitiatingHeapOccupancyPercent",它的默认值是45%。意思是说,如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。
G1整体是基于复制算法进行Region垃圾回收,不会出现内存碎片问题,不需要像CMS那样标记-清理之后再进行内存碎片的整理。
g1垃圾回收器新生代初始占比默认为5%,新生代最大占比默认为60%。
如果堆内存为4G,此时除以2048,得出每个region的大小为2mb,刚开始新生代就占5%的region,可以认为新生代就是只有100个region,有200mb的内存空间。
g1有一个参数"-XX:MaxGCPauseMills",它的默认值是200ms
一旦老年代频繁达到占用堆内存45%的阈值,那么就会频繁触发mixed gc。
类似kafka、elasticsearch之类的大数据相关的系统,都是部署在大内存的机器上的,此时如果你的系统负载非常高,对于大数据系统是很有可能,比如每秒几万的访问请求到kafka、elasticsearch上去。那么可能导致你eden区的几十G内存频繁塞满要触发垃圾回收,假设1分钟会塞满一次。然后每次垃圾回收要停顿kafka、elasticsearch的运行,然后执行垃圾回收大概需要几秒钟,此时你发现,可能每过一分钟,你的系统就要卡顿几称钟。解决大内存机器新生代GC过慢就是使用G1垃圾回收器。
Mixed GC是G1中特有的概念,一旦老年代占据堆内存的45%了,就要触发Mixed GC,此时对年经代和老年代都会进行回收。
Young GC一般在新生代的Eden区域满了之后就会触发,采用复制算法来回收新生代的垃圾。
Old GC和Full GC的触发时机:
针对大内存机器通常建议采用G1垃圾回收器。
g1在gc时不会产生碎片,但是由于每个region存在存活率85%不清理的机器,会导致内存没有充分释放问题。因此,对于cpu性能高,内存大,对应用响应度高的系统使用g1。而内存小,cpu性能低下使用pn+cms更合适
据此分析一下young gc情况。
我们看GC日志中的如下几行:
0.268: [GC (Allocation Failure)
0.269: [ParNew: 4030K->512K(4608K), 0.0015734 secs]
4030K->574K(9728K), 0.0017518 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
0.268表示系统运行以后多少秒发生本次gc, ParNew就是触发young gc,年经代可用空间4608k(即Eden区加上一个Survivor),4030k->512k,就是gc之前使用4030k,gc之后只有512k对象存活下来。0.0015734 secs就是本次gc耗费时间。4030K->574K(9728K), 0.0017518 secs] 是指整个Java堆内存的情况,堆内存总可用空间9728k(老年代+年经代(eden区加上一个Survivor区)),整个堆gc前使用4030k,gc之后使用574kb。
例子
老年代使用率达到92%的阈值,也会触发full gc。
jps能找出Java进程的pid
jstat -gc PID可以看到这个Java进程的内存和GC情况了
jmap -heap PID可以查看堆内存里的一些基本各个区域的情况,jmap的主要用途不是这个,一般用jstat去看这些信息。
jmap -histo PID 了解系统运行时的对象分布,他会按照各种对象占用内存大小降序排列,把占用内存最多的对象放在最上面。
可以使用jmap生成堆内存转储快照
jmap -dump:live,format=b,file=dump.hprof PID
使用jhat在浏览器中分析堆转出快照
jhat dump.hprof -port 7000
常见的部署专门的监控系统,比较常见的有Zabbix、OpenFalcon(重点)、Ganglia等等
8G机器JVM参数模板
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark
频繁full gc不光是老年代触发的,有时候也会因为Metaspace区域的类太多而触发。
g1对于大对象的判定规则是超过region的50%
CPU负载过高的原因
频繁full gc的原因
导致对象进入老年代的原因
什么是内存溢出?在哪些区域会发生内存溢出?
1. 永久代(从jdk1.8后叫做Metaspace)用来存放你系统里的各种类的信息,此区域可以会发生内存溢出,即OOM。
2. 每个线程的虚拟机栈内存也可能OOM。
3. 堆内存空间会OOM。
查看java程序的jvm启动参数
jcmd pid VM.flags or jinfo -flags pid or jmap -heap pid
公司应该最好有一种监控平台,比如Zabbix、Open-Falcon之类的监控平台.