从零开始带你成为JVM实战高手笔记

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”

从零开始带你成为JVM实战高手笔记_第1张图片

从零开始带你成为JVM实战高手笔记_第2张图片

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 默认垃圾收集器G1

不要忘记指定垃圾回收器,新生代可以使用ParNew,老年代使用CMS。

 

我们写的Java代码到底是如何运行起来的?

1、我们编写.java文件

2、打包成.class文件

3、JVM类加载器把.class字节码文件加载到内存中

4、JVM基于自己的字节码执行引擎,来执行加载到内存里我们写好的类

 

JVM类加载机制

一个类从加载到使用,一般会经历下面的这个过程:

加载-->验证-->准备-->解析-->初始化-->使用-->卸载

验证:校验加载进来的.class文件是否符合规范

准备:分配内存空间,分配变量类型对应的初始值(如int分配0,Integer分配null)

解析:把符号引替换为直接引用

初始化:初始我们分配的值(如int a = 3,那么这时a就初始为3)

 

Java有如下类加载器

从零开始带你成为JVM实战高手笔记_第3张图片

它们遵循双亲委派机制。

 

大厂面试题:JVM中有哪些内存区域,分别都是用来干嘛的?

方法区(jdk1.8以后,这块区域叫元数据空间):存放类似常量池的东西和各种类相关的信息

程序计数器:记录当前执行的字节码指令的位置

Java虚拟机栈:存放方法创建时的栈帧,栈帧有(方法的局部变量表、操作数栈、动态链接、方法出口等)

堆内存:存放代码创建的各种对象

本地方法栈:存放各种native方法的局部变量表之类的信息

 

JVM的垃圾回收机制是用来干嘛的?为什么要垃圾回收?

它是后台自动运行的线程, 回收无引用的实例对象。没有垃圾回收,分配的内存将得不到释放,导致内存资源耗尽。

 

JVM整体运行图

从零开始带你成为JVM实战高手笔记_第4张图片

 

Java支持多线程,每个线程都有自己的Java虚拟机栈和本地方法栈。

新建的实例在堆内存,实例变量也是在堆内存

 

方法区内进行垃圾回收条件

1、首先该类的所有实例对象都已经从Java堆内存里被回收

2、其次加载这个类的ClassLoader已经被回收

3、最后,对该类的Class对象没有任何引用

 

大厂面试题:你的对象在JVM内存中如何分配?如何流转的?

新生代内存空间的垃圾回收,也称之为Minor GC,有时候也叫Young GC,它会尝试把新生代里那些没有人引用的垃圾对象,都给回收掉。

如果有对象躲过了十多次垃圾回收,就会放入老年代里

 

对象分配这块,有很多其它的复杂机制,如

1、新生代垃圾回收之后,因为存活对象太多,导致大量对象直接进入老年代

2、特别大的超大对象直接不经过新生代就进入老年代

3、动态对象年龄判断机制

4、空间担保机制

 

从零开始带你成为JVM实战高手笔记_第5张图片

 

每日百万交易的支付系统,如何设置JVM堆内存大小?

假设每天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。

 

每日百万交易的支付系统,JVM栈内存与永久代大小又该如何设置?

永久代上线上前没太多可以参考的规范,一般你设置几百MB,大体够用的。

栈内存一般默认512kb到1MB就差不多了。

 

大厂面试题:什么情况下JVM内存中的一个对象会被垃圾回收?

只要你的对象被方法的局部变量、类的静态变量给引用了,就不会回收它们。

强引用:垃圾回收的时候绝对不会去回收这个对象

软引用:正常情况下垃圾回收是不会回收软引用对象,如果内存不足,哪怕他被变量引用了,但是因为他是软引用,所以还是要回收的。

从零开始带你成为JVM实战高手笔记_第6张图片

 弱引用:这个引用就跟没引用类似的,如果发生垃圾回收,就会把这个对象回收掉

虚引用:略 

 

大厂面试题:JVM中有哪些垃圾回收算法,每个算法各自的优劣?

针对新生代的垃圾回收算法,他叫复制算法。它是把新生代内存划分为两块内存区域,然后只使用其中一块内存,待那块内存满的时候就把里面存活对象一次性转移到另外一块内存区域,保证没有内存碎片。缺点是对内存的利用率低。

复制算法的优化: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,结果有三种:

  1. Minor GC过后,存活对象大小小于Survivor大小,直接进入Survivor区域即可
  2. Minor GC过后,大于Survivor大小且小于老年代可用内存大小,直接进入老年代
  3. Minor GC过后,大于Survivor大小,大于老年代可用内存大小,触发Full GC

 

老年代采用标记整理算法 

 

大厂面试题:JVM中都有哪些常见的垃圾回收器,各自的特点是什么?

 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的核数是一样的。

 

JVM老年代垃圾回收器CMS工作时,内部又干了些啥?

一般老年代我们选择的垃圾回收器是CMS,它采用的是标记清理算法。

CMS在执行一次垃圾回收的过程一共分为4个阶段:

  1. 初始标记:先执行初始标记阶段,这个阶段会让系统的工作线程全部停止,进入“stop the world"的状态
  2. 并发标记:这个阶段系统线程可以随意创建各种新对象,会尽可能的对已有的对象进行GC Roots追踪,这是最耗时的
  3. 重新标记:在并发标记结束之后,会有很多存活对象和垃圾对象,是之前第二阶段没标记出来的。此阶段要“stop the world"。重新标记下第二阶段里新创建的一些对象,还有一些已有对象可能失去引用变成垃圾的情况。
  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?

  1. 老年代可用内存小于新生代全部对象的大小,且没有开启空间担保参数
  2. 老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;
  3. 新生代Minor GC后存活对象大于Survivor,那么就会会进入老年代,此时老年代内存不足
  4. 如果老年代可用内存大小大于历次新生代GC后进入老年代的对象的平均大小,但老年代已用内存空间超过了这个参数(-XX:CMSInitiatingOccupancyFaction)指定的比例,也会自动触发Full 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的触发条件:

  1. 没有打开"-XX:HandlePromotionFailure",导致每次Minor GC前一检查,发现“老年代可用内存<新生代总对象大小”,这会导致Full GC。注意:jdk1.6后废弃这个参数
  2. 每次Minor GC前,检查“老年代可用内存空间<历次Minor GC后升入老年代的平均对象大小”
  3. 某次Minor GC后要升入老年代的对象有几百MB,但是老年代可用空间不足
  4. 设置了“-XX:CMSInitiatingOccupancyFaction"参数,比如设定值为92%,那么前面几个条件都没满足,刚好这个条件满足了,比如老年代空间使用超过92%,此时就会自行触发Full GC

 

一台机器能开多少线程?取决于什么?

对于4核CPU,本身JVM就有一些后台线程,还有使用的框架可能也会有后台线程,自己系统一般开启线程数量在几十个,比如50左右的就差不多了。大概系统所有系统加起来有100+,此时高峰期这么多线程同时工作,CPu负载基本满负荷了。

 

大厂面试题:最新的G1垃圾回收器的工作原理,你能聊聊吗?

G1垃圾回收器可以同时回收新生代和老年代的对象的。它的最大特点就是把Java堆内存拆分为多个大小相等的Region。G1其实也有新生代和老年代的概念,只不过是逻辑上的概念。G1在触发垃圾回收的时候,可以根据设定的预期系统停顿时间,来选择最少回收时间和最多回收对象的Region进行垃圾回收,保证GC对系统停顿的影响在可控范围内,同时还能尽可能回收最多的对象。

 

G1分代回收原理深度图解:为什么回收性能比传统GC更好?

对于G1,JVM最多有2048个Region,Region的大小必须是2的倍数,比如1MB、2MB、4MB。默认新生代对堆内存的占比是5%, 系统运行时,JVM会不停给新生代增加更多的Region,但最多新生代的占比不会超过60%。

按照默认新生代最多只能占据堆内存60%的Region来算,老年代最多可以占据40%的Region。

那么对象什么时候从新生代进入老年代呢?

  1. 对象在新生代躲过了很多次的垃圾回收,达到一定年龄
  2. 动态年龄判定规划,如果某次新生代GC过后,存活对象超过了Survivor的50%

G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。而且一个大对象如果太大,可能会横跨多个Region来存放。在新生代、老年代在回收时,会顺带着大对象Region一起回收。

 

线上系统部署如果采用G1垃圾回收器,应该如何设置参数?

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。

 

糟糕!运行着的线上系统突然卡死无法访问,万恶的JVM GC!

类似kafka、elasticsearch之类的大数据相关的系统,都是部署在大内存的机器上的,此时如果你的系统负载非常高,对于大数据系统是很有可能,比如每秒几万的访问请求到kafka、elasticsearch上去。那么可能导致你eden区的几十G内存频繁塞满要触发垃圾回收,假设1分钟会塞满一次。然后每次垃圾回收要停顿kafka、elasticsearch的运行,然后执行垃圾回收大概需要几秒钟,此时你发现,可能每过一分钟,你的系统就要卡顿几称钟。解决大内存机器新生代GC过慢就是使用G1垃圾回收器。

 

Mixed GC是G1中特有的概念,一旦老年代占据堆内存的45%了,就要触发Mixed GC,此时对年经代和老年代都会进行回收。 

 

Young GC和Full GC分别在什么情况下会发生?

Young GC一般在新生代的Eden区域满了之后就会触发,采用复制算法来回收新生代的垃圾。

Old GC和Full GC的触发时机:

  1. Young GC之前,老年代可用连续内存空间<新生代历次Young GC后升入老年代的对象总和的平均大小。
  2. Young GC之后有一批对象需要放入老年代,此时老年代就没有足够的内存空间存放这些对象
  3. 老年代内存使用率超过92% 

 

针对大内存机器通常建议采用G1垃圾回收器。

 

g1在gc时不会产生碎片,但是由于每个region存在存活率85%不清理的机器,会导致内存没有充分释放问题。因此,对于cpu性能高,内存大,对应用响应度高的系统使用g1。而内存小,cpu性能低下使用pn+cms更合适

 

动手实验:自己动手模拟出频繁Young GC的场景体验一下!

从零开始带你成为JVM实战高手笔记_第7张图片

从零开始带你成为JVM实战高手笔记_第8张图片

据此分析一下young gc情况。

 

高级工程师的硬核技能:JVM的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。

 

动手实验:自己动手模拟出对象进入老年代的场景体验一下(上)

例子

从零开始带你成为JVM实战高手笔记_第9张图片

从零开始带你成为JVM实战高手笔记_第10张图片

 

 老年代使用率达到92%的阈值,也会触发full gc。

 

使用 jstat 摸清线上系统的JVM运行状况

jps能找出Java进程的pid

jstat -gc PID可以看到这个Java进程的内存和GC情况了

 

使用jmap和jhat摸清线上系统的对象分布

jmap -heap PID可以查看堆内存里的一些基本各个区域的情况,jmap的主要用途不是这个,一般用jstat去看这些信息。

jmap -histo PID 了解系统运行时的对象分布,他会按照各种对象占用内存大小降序排列,把占用内存最多的对象放在最上面。

从零开始带你成为JVM实战高手笔记_第11张图片

可以使用jmap生成堆内存转储快照

jmap -dump:live,format=b,file=dump.hprof PID

使用jhat在浏览器中分析堆转出快照

jhat dump.hprof -port 7000 

 

从测试到上线:如何分析JVM运行状况及合理优化?

常见的部署专门的监控系统,比较常见的有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负载过高的原因

  1. 系统创建了大量线程
  2. JVM在运行时频繁的full gc。

频繁full gc的原因

  1. 内存分配不合理
  2. 存在内存泄漏问题
  3. 永久代里的类太多,触发了full gc
  4. 工程师错误的执行System.gc

导致对象进入老年代的原因

  1. 对象在年轻代躲过15次垃圾回收
  2. 对象太大,直接进入老年代
  3. young gc后存活对象survivor放不下
  4. 存活对象年龄超过survivor的50%

 

什么是内存溢出?在哪些区域会发生内存溢出?

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之类的监控平台.

你可能感兴趣的:(从零开始带你成为JVM实战高手笔记)