前言
这周主要的技术内容是分享JVM的基础知识和一些生产事故案例
JVM
先问一个问题:在公司,你负责的项目JVM的参数数多少?比如堆的大小MaxHeapSize,新生代的大小,晋升年龄,垃圾收集器是什么?
如果你不清楚,什么看都没看到过,甚至怎么查看JVM的参数都不清楚,那你就看对文章了。
首先,我们知道JVM的参数,无非就是读写。接下来,我们先看查看JVM的命令有哪些?
怎么查看JVM参数
命令一:java -XshowSettings:vm -version
aaron@aarondeMBP ~ % java -XshowSettings:vm -version
VM settings:
Max. Heap Size (Estimated): 3.56G
Ergonomics Machine Class: server
Using VM: Java HotSpot(TM) 64-Bit Server VM
java version "1.8.0_281"
Java(TM) SE Runtime Environment (build 1.8.0_281-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)
aaron@aarondeMBP ~ %
这个命令,相对来说,展示的JVM配置信息比较少:只有最大的堆大小,jdk的版本,jdk的运行模式。这个命令,在生产上用得比较少,你就了解一下就好。
命令二:jcmd
可以用jcmd -l命令查询正在运行的程序的实时JVM参数
[work(tanghailin)@tjtx167-48-41 ~]$ jcmd -l
1041671 sun.tools.jcmd.JCmd -l
1467 com.bj58.spat.scf.server.bootstrap.Main -Dscf.service.name=testdemo //项目名称
[work(tanghailin)@tjtx167-48-41 ~]$ jcmd 1467 VM.flags
1467:
-XX:CICompilerCount=4 //最大并行编译数
-XX:CMSInitiatingOccupancyFraction=80 //当老年代空间被占用80%时,就需要进行垃圾回收
-XX:InitialHeapSize=2147483648 // -Xms=2G
-XX:MaxHeapSize=2147483648// -Xmx=2G
-XX:MaxNewSize=1073741824 //新生代=1G
-XX:MaxTenuringThreshold=6 //晋升年龄=6
-XX:MinHeapDeltaBytes=196608 //
-XX:NewSize=1073741824 //新生代=1G
-XX:OldPLABSize=16
-XX:OldSize=1073741824 //老年代=1G
-XX:ParallelGCThreads=20 //并发线程数=20
-XX:ThreadStackSize=1024 //等价于-Xss,堆栈的大小
-XX:+UseCMSCompactAtFullCollection //在发生FULL GC时,进行压缩 https://blog.csdn.net/weixin_33978044/article/details/94321406
-XX:+UseCompressedClassPointers //压缩对象中的类型指针KlassPoniter
-XX:+UseCompressedOops //oop:ordinary object pointer 压缩对象的。一般UseCompressedOops和UseCompressedClassPointers是一起使用的。二者都是为了提高内存的利用率
-XX:+UseConcMarkSweepGC //使用CMS垃圾收集器,收集老年代
-XX:+UseFastUnorderedTimeStamps
-XX:+UseParNewGC //使用并发NewGC垃圾收集器收集新生代
jcmd命令用法:
- 第一步:先查出对应的进程号pid。
- 第二步:jcmd pid VM.flags 来查对应进程的JVM参数配置
你看,上面的参数,明显多了很多,但不是全部JVM参数。
如果你想要查看全部的JVM参数,可以使用命令
java -XX:+PrintFlagsFinal -version
注意一下:
=代表初始参数值;
:=代表修改后的参数值
aaron@aarondeMBP ~ % java -XX:+PrintFlagsFinal -version
[Global flags]
intx ActiveProcessorCount = -1 {product}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
{product}
uintx InitialHeapSize := 268435456 {product}
uintx InitialRAMFraction = 64 {product}
double InitialRAMPercentage = 1.562500 {product}
uintx InitialSurvivorRatio = 8 {product}
uintx InitialTenuringThreshold = 7 {product}
uintx InitiatingHeapOccupancyPercent = 45 {product}
bool Inline = true {product}
ccstr InlineDataFile = {product}
intx InlineSmallCode = 2000 {pd product}
bool InlineSynchronizedMethods = true {C1 product}
bool InsertMemBarAfterArraycopy = true {C2 product}
intx InteriorEntryAlignment = 16 {C2 pd product}
intx InterpreterProfilePercentage = 33 {product}
bool JNIDetachReleasesMonitors = true {product}
uintx MaxHeapSize := 4294967296 {product}
{product}
intx SelfDestructTimer = 0 {product}
uintx SharedBaseAddress = 34359738368 {product}
ccstr SharedClassListFile = {product}
uintx SharedMiscCodeSize = 122880 {product}
uintx SharedMiscDataSize = 4194304 {product}
uintx SharedReadOnlySize = 16777216 {product}
uintx SharedReadWriteSize = 16777216 {product}
bool ShowMessageBoxOnError = false {product}
intx SoftRefLRUPolicyMSPerMB = 1000 {product}
bool SpecialEncodeISOArray = true {C2 product}
bool SplitIfBlocks = true {C2 product}
intx StackRedPages = 1 {pd product}
intx StackShadowPages = 20 {pd product}
bool StackTraceInThrowable = true {product}
intx StackYellowPages = 2 {pd product}
bool StartAttachListener = false {product}
intx StarvationMonitorInterval = 200 {product}
bool StressLdcRewrite = false {product}
uintx StringDeduplicationAgeThreshold = 3 {product}
uintx StringTableSize = 60013 {product}
bool SuppressFatalErrorMessage = false {product}
uintx SurvivorPadding = 3 {product}
uintx SurvivorRatio = 8 {product}
intx SuspendRetryCount = 50 {product}
intx SuspendRetryDelay = 5 {product}
bool UseCompressedClassPointers := true {lp64_product}
bool UseCompressedOops := true {lp64_product}
{C1 product}
intx ValueMapMaxLoopSize = 8
java version "1.8.0_281"
Java(TM) SE Runtime Environment (build 1.8.0_281-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)
aaron@aarondeMBP ~ %
上面三个命令,我比较常用的是第二个命令jcmd,因为这个命令提供的参数足以让我排查JVM问题。第三个命令信息太多了,是在第二个命令提供的参数信息不够时,再使用的。
怎么修改JVM的参数
知道怎么读后,就要开始修改JVM参数,然后进行调优。
- 方式一:就是启动参数(生产中不常用)
- 方式二:配置catalina.sh文件
在catalina.sh文件中增加:
JAVA_OPTS=-Xms64m -Xmx256m -XX:PermSize=128M -XX:MaxNewSize=256m -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./
根据不同的需要配置不同的参数即可。参数的具体含义和作用,这里就不展开了。
在这里需要解答2个疑惑点:
- 1.如果在此设置了jvm最大堆内存限制为2G,那么是该tomcat中所有war分享2G,还是每个war都可以独立有2G?
- 2.tomcat本身也是一个运行在jvm上的程序,既然如此,它自己本身的jvm参数要如何调整?
问题1:是所有war包共享2G。问题2:与war包共享2G,因为都在同一个容器中。
知道JVM参数的读写后,应该如何进行调优
说到调优,肯定得有参照物对吧。否则怎么知道调整JVM参数后,是好还是坏呢。
其实就3个参照物。延迟,吞吐量,内存占用。
内存占用:程序正常运行需要的内存大小。
延迟:由于垃圾收集而引起的程序停顿时间。
吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。
当然,和CAP原则一样,同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的。就是三个参照物,不可能同时满足的,必须牺牲其中一个。
那用什么来看这三个参照物呢?网上有一堆工具,比如MAT,VisualVM。
本人比较懒,所以都是直接用在线工具的。比如: http://gceasy.io/
因为我设置的是,有OOM时,会生成一个dump文件,然后我下载文件到本地,用工具查看即可。
但是如果你的项目没设置dump文件的话,发生OOM异常时。记得先摘流量,再去dump,尽可能减少对业务的影响。
因为业务的不同,导每个项目用的垃圾收集器也是不同的。
这里提供2个GC的指导原则。
Parallel GC调优的指导原则
1、除非确定,否则不要设置最大堆内存
2、优先设置吞吐量目标
3、如果吞吐量目标达不到,调大最大内存,不能让OS使用Swap,如果仍然达不到,降低目标
4、如果吞吐量达到,GC的时间太长,设置停顿时间的目标
CMS GC 调优的指导原则
直接参考:CMS GC调优指导原则
好了,这周的JVM的技术周报,大概就是这样子。就是JVM的读写,然后根据不同的指导原则进行调优,根据参照物进行调就好。调优时,用控制变量法来调就好,别一次改变多个值,否则你也不清楚究竟是哪个值优化的。
生产事故
JVM生产事故
这个报警,有点厉害啊,都直接告诉我原因。
新生代内存被占满,被置换到老生代。
看到这个,就立马想起了空间分配担保规则。
当时我看完数据视图和堆文件的分析后,根据引用树和可疑报告,定位到了一个项目中的本地消息队列。(相关图片,因涉及项目机密,不太方便透露)
造成JVM新生代内存被占满,被置换到老生代的原因如下:
上游系统作为生产者生产消息,平均700条/S,高峰时期高达1300条/s。我们这边的系统是下游系统,作为消费者,主动去拉取消息,进行消费。拉取的消息对象就有很多字段,从而使得对象本身很大。而且,消费者系统是在本地建立了一个本地消息队列,正常情况下,每秒700条,是可以正常消费完毕的,此时消费者的消费速度大于生产者的生产速度。从而不会导致消息堆积。而当流量大时,消息高达每秒1300条,此时消费速度小于生产速度。从而造成本地消息队列的消息堆积,又因为对象本身比较大。从而使得新生代的内存很快满了,满了后,因为还有更多的新消息被主动拉取到下游系统,且消息没被及时消费,因此对象还不能被及时回收掉。此时新生代已满,且对象不能被回收掉,从而使得对象直接进入老年代。从而使得老年代的空间很快满了。最后是怎么解决的呢?简单粗暴的横向扩张,说人话就是加机器。
我就简单总结一下:其实遇到JVM。一般排查思路如下:首先你肯定会收到OOM的报警,接着你需要通过分析dump文件,进行定位OOM是内存泄露,还是内存溢出原因。确定这个原因后,需要通过一些可视化的工具,比如MAT这些工具,通过分析引用树,可疑报告来定位到具体的业务代码。然后定位到业务代码是什么问题,根据不同的问题来进行设计方案即可。
Long和long类型引发的的订单不能支付问题
先上图哈
bug的现象:就是订单无法进行支付
严重性:这是非常严重的bug,该收钱的时候,竟然不能收。
其实,你一看代码,就能发现清楚是什么问题了。Long类型,可不可以用==和!=比较。可以是可以。但只限于-128-127之间。超出这个范围的数都是一个新的Long对象。
拓展一下:在Java中,只有整形的包装类才有缓存,浮点型都没缓存。简单点说,就是:Short,Integer,Long才有缓存,并且范围都是-128-127。
Integer类
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Long类
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}
Short
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
}
现在,我们既然知道问题的所在了。那肯定知道如何解决。直接用equals比较即可。
那我们再去关心一个事儿,为什么开发人员会犯这个错误呢?
其实,开发人员并没有犯!=和==的比较错误。一开始其实updateOrderState的buserId其实是long基础类型,因此完全是可以用!=和==来进行比较的。只不过后来,把buserId修改为Long包装类型了。修改Long类型后,也没去检查,开发时间急迫,可以理解。
总结一下:一,接口之前定义好了,后续就不应该在这个接口上进行直接修改。你可以新开一个接口,然后兼容老接口。但绝不能在老接口上直接修改,即使是数据类型的修改,也是不应该的。因为你也不清楚这个接口,谁在用,以及这个接口的内部逻辑是如何的。二,包装类型和基础类型的区别和底层,我们也应该掌握好。这就是为什么要知其然知其所以然的原因。
这周的技术周报就是这样子。拜拜,下周见。
絮叨
非常感谢你能看到这里,如果觉得文章写得不错 求关注 求点赞 求分享 (对我非常非常有用)。
如果你觉得文章有待提高,我十分期待你对我的建议,求留言。
如果你希望看到什么内容,我十分期待你的留言。
各位的捧场和支持,是我创作的最大动力!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。