【jvm】-从原理到实践深入剖析jvm调优(小白也适用)

1.why?

为什么要进行Jvm调优?因为jdk默认的jvm参数并不能很好的满足每个项目的实际性能需求,因为不同的项目本身占用内存cpu资源就不一样,加上服务器配置的多种多样,jvm提供的初始参数很难达到定制的效果,在项目生产环境中,除了对代码,sql,web容器等优化以外,对Jvm的优化也同样重要,而且在一些情况下会出现内存溢出报错,不论你怎么修改代码,还是无法解决,这时候就得借助于Jvm调优参数了,另外如果你想涨薪的话Jvm也是必须必会的一项技能,jvm的调优本身不算特别难,但想调优调的足够"优",调优次数尽量少,那就是一项经验活了,没有丰富的经验是很难搞定的,所以一般会jvm调优的程序猿也会被打上"高级"程序猿的标签.

2.what?

什么是jvm调优? jvm调优主要是对Jvm启动时的参数进行调整,使java程序在运行时有较高的吞吐量,和较低的暂停时间,使cpu和内存的使用效率最大化,避免浪费资源.

3.How To?

如何优化Jvm呢?这是比较难的,首先得弄明白jvm的内存模型:

堆区:由Young区和Old区构成,Young区包含Survivor区和Eden区

其中Survivor区由两块相同大小的区S0,S1构成,一般新创建的对象会存在于young区中的Eden中,当需要进行gc垃圾回收时,jvm会把S0中正在运行的对象复制到S1中,然后清理S0中的对象,清理完成后释放S0中的内存,伴随着一次次GC,最终Young区中未被回收的对象会慢慢成长并转移到Old区.这里援引网上一段经典的例子来帮助理解对象在JVM堆内存中的生命周期::

我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“To”区,自从去了Survivor区,我就开始漂泊了,因为Survivor的两个区总是交换名字,所以我总是搬家,搬到To Survivor居住,搬来搬去,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

非堆区:由Metaspace(元空间,jdk8出现,在此之前是PermGen,也就是永久代),CCS(压缩类空间,是Metaspace的一部分,主要用于存放堆中对象指向自己class的指针,该空间默认不存在,只有在64位系统中开启短指针时才会出现,为了节省空间可以将64位长指针压缩为32位短指针),CodeCache(存放一些 jar native代码等,默认不存在,被使用时才存在).

除此之外,Metaspace还用于存放:

然后需要弄明白Jvm的垃圾回收算法:

目前主要采用三色标记算法,这里仅抛砖引玉简单介绍一下,感兴趣进一步深入了解的同学可以上网了解一下.

GC算法会从GC Root开始,标记所有目前所有可达的对象。

Root根节点主要包含下述几类:

从根节点开始(在这里仅显示了两个根节点),所有的有引用关系的对象均被标记为存活对象(箭头表示引用)。从根节点起,不可达对象均为垃圾对象。在标记操作完成后,系统回收所有不可达对象。

 回收后→

最后,你还需要弄明白jvm常见的几种垃圾回收器:

1.serial collector 串行垃圾回收器,单线程的垃圾回收器,适用于单核cpu的渣渣服务器,性能效率较差,不多说了...

2.parallel collector 并行垃圾回收器,多线程的垃圾回收器,性能效率较高,垃圾回收时暂停提供服务,也是默认开启的垃圾回收器,jdk9除外,适合在要求吞吐量较高,且垃圾回收暂停时间较短的场景下,其实也就是大多数场景都是适用的.

3.cms collector(concurrent-mark-sweep)并发垃圾回收器,多线程并发的垃圾回收器,垃圾回收时不暂停提供服务,适合那些要求响应时间优先的服务,比如证券交易这种,不允许有延迟,越快越好...

4.G1 collector 从jdk7开始出现,既可以回收Young区垃圾,又可以回收Old区垃圾,在jdk9中完全替代其他gc回收器.

下述情况推荐使用G1GC回收器:

除此之外还有一些其他的GC回收器,至于如何选择,要具体根据你服务器的配置和JDK版本以及项目需求走,如果纠结于选择哪种垃圾回收器的话,你可以参考oracle官方给出的建议:

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28

有了上面这些知识储备之后,接下来开始优化时你才能得心应手.

下面是时候表演真正的技术了:

Jvm调优一般由以下几个步骤:

1.查看当前所用的GC回收器,并根据自己需求选择使用合适的GC回收器:

java -XX:+PrintCommandLineFlags -version

如下图所示,可以开出来当前使用的GC回收器为ParallelGC.

如果你不想用ParallelGC,你可以通过启动jar包时添加参数改变GC回收器类型:

使用SerialGC添加参数:       -XX:+UseSerialGC

使用ParallelGC添加参数:     -XX:+UseParallelGC 

使用CMSGC添加参数:          -XX:+UseConcMarkSweepGC

使用G1GC添加参数:           -XX:+UseG1GC
 

 

如图,在启动java应用时添加该参数:

2.开启GC日志,将GC日志导出到指定文件夹下,以便之后利用工具分析和参考.

-Xloggc:/root/outp/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps

#上面的/root/outp是我自己指定的路径,可以灵活指定,命名也是可以随便取的,只要后缀为.log即可

启动后可以在root/outp文件夹下看到gc.log

用winscp或sz命令将其下载到你本地电脑.

3.借助工具进行分析,这里主要介绍2款分析GC日志的工具,一款是gceasy在线分析,一款是gcviewer,关于这两个工具我专门写了一篇文章,感兴趣的可以参考:https://blog.csdn.net/lovexiaotaozi/article/details/82862196

4.根据分析建议和结果进行调优:

这里我自己新建了个EXCEL对比10次调整各种参数后的数据表现,并最终得出一个适合生产环境的jvm启动参数:

使用的分析工具是gcviewer:

也可以用gceasy去分析,方便强大:http://gceasy.io

分析完会给出调优建议,如下图红框中所示:

工具分析表明,Metaspace分配的太小了,建议调大,然后你可以看看当前已用的Metaspace空间大小,图上显示是47.9MB,你可以适当调大两倍(并非越大越好),比如我调整为128MB,再次启动项目并分析日志,发现吞吐量,GC次数都有明显改善.

GC调优其实是没有最佳实践的,只有相对最佳的实践,需要根据需求去调整,比如我们公司现在用的服务器是4核16G内存的,在生产和测试环境中发现启动12个左右的springboot项目内存就撑爆了,但cpu空闲率却很高,因此我调优的目的主要就是降低内存占用率,牺牲点cpu使用率无所谓...在这样一种目的下,我可以分配给每个应用相对较少的内存,然后增加GC次数(会牺牲CPU使用率),这样就能满足我目前这种需求了. 当然在CPU和内存都允许的前提下,最好还是追求最高的吞吐量和最低的暂停时间为主.

下面提供一下jvm调优主要使用的参数,以jdk1.8为例,在Jdk1.8之前还可以优化PermGen(永久代),如果你觉得太麻烦,我建议你直接升级JDK1.9及以上,在JDK1.9中仅使用G1垃圾回收器,废弃了其它垃圾回收器.

具体的可以查看官网的建议:

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html

jdk1.8 下,常用的优化参数和优化建议参考:

-Xms 
初始堆大小,默认为物理内存的1/64(<1GB),测试时也可以跟-Xmx保持一样大,这样可以避免每次垃圾回收完成后JVM重新分配内存
-Xmx 
最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制,建议不要超过系统总可用内存的1/2
新生代 
-XX:NewSize 
新生代空间大小初始值
-XX:MaxNewSize 
新生代空间大小最大值
-Xmn 
新生代空间大小,此处的大小是(eden+2 survivor space) 
指定后可以不用指定上面两条参数
-XX:YoungGenerationSizeIncrement=30
指定YoungGC的增长率,默认为20%

当然,需要调优的参数其实非常多的,官网上对每个调优参数都有介绍,感兴趣深入了解的自行去官网学习,或者用到的时候去看下就行,对于大多数情况来说,jvm的调优其实调整-Xms,-Xmx,-Xmn -XX:MetaspaceSize=128M (根据具体情况调整) 这几个参数的值就够用了,甚至都不需要调整这么多参数,jvm在垃圾回收这方面已经趋于成熟,一般公司用用默认的或者微调即可满足需求.

最后,说一下最为简单粗暴的方式,如果你使用的是jdk1.8及以上版本,又不想花时间精力在Jvm调优上,又想得到比较好的性能表现,不妨将你项目的垃圾回收器换成GC1,只需要指定合理的暂停时间,(太苛刻的话会增加GC次数,降低cpu效率)剩下的交给Jvm自己去做就是了,官方建议:

java -jar -XX:+UseG1GC -XX:MaxGCPauseMillis=200 xxx.jar


华丽的分割线,来纪念下,2018年9月29日的一次调优,调优数据真是惊艳到我了,所以在此分享给大家,并极度推荐G1GC.

前面介绍中已经提到了,G1GC是后来才出现的垃圾回收器,出现的目的是为了代替CMSGC回收器,且已在JDK1.9中一统天下,于是我就想试试看G1GC的性能,把原来的ParalleGC切换成了G1GC,然后简单的设置了最大堆大小和最大暂停时间200毫秒:

-Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200

完整的如下:

nohup java -jar  -Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xloggc:/root/gclogs/你的项目名-gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps  你的项目名.jar &

启动项目后然后我把gc日志从服务器上下载下来,用http://gceasy.io/ 在线分析了下,分析结果惊艳了我,直接上图:

99.977%的吞吐量还能保持16毫秒的低延迟,这组数据简直完美,不仅如此,内存的使用率比原来降低了整整一半,不得不说oracle这些年在GC上的优化越来越厉害,从JDK1.9以后,Jvm调优这种印象中本来大神才可以做的事情小学生都可以做了...

于是果断的把公司所有项目都改为G1GC,并对参数适当调优,调优后各项表现均大幅提升,而且内存整整比原来多了一半,原来一台4核16G的服务器只够部署10来个springboot项目,现在可以部署20几个,当然仅如此还是不够的,因为一旦内存最终被撑爆,所有服务还是得瞬间雪崩,所以docker还是很有必要的,但是docker下,springboot项目的Jvm调优有bug,感兴趣的同学可以看下我另一篇,《docker下的jvm一件自动部署并调优》:https://blog.csdn.net/lovexiaotaozi/article/details/82963472

实现智能化的部署,一键部署+自动调优,每个项目启动节省一半内存,同时吞吐量和暂停时间都有提升,若有服务崩了也不会影响其它容器,不至于一崩全崩.

猜你喜欢

转载自blog.csdn.net/lovexiaotaozi/article/details/82883365