通过前面几个课时的学习,相信你对JVM的理论及实践等相关知识有了一个大体的印象。而本课时将重点讲解JVM的排查与优化,这样就会对JVM的知识点有一个完整的认识,从而可以更好地应用于实际工作或者面试了。 我们本课时的面试题是,生产环境如何排查问题?典型回答 如果是在生产环境中直接排查JVM的话,最简单的做法就是使用JDK自带的6个非常实用的命令行工具来排查。它们分别是:jps、jstat、jinfo、jmap、jhat和jstack,它们都位于JDK的bin目录下,可以使用命令行工具直接运行,其目录如下图所示: 1。jps(虚拟机进程状况工具) jps(JVMProcessStatustool,虚拟机进程状况工具)它的功能和Linux中的ps命令比较类似,用于列出正在运行的JVM的LVMID(LocalVirtualMachineIDentifier,本地虚拟机唯一ID),以及JVM的执行主类、JVM启动参数等信息。语法如下:jps〔options〕〔hostid〕 常用的options选项:l:用于输出运行主类的全名,如果是jar包,则输出jar包的路径;q:用于输出LVMID(LocalVirtualMachineIdentifier,虚拟机唯一ID);m:用于输出虚拟机启动时传递给主类main()方法的参数;v:用于输出启动时的JVM参数。2。jstat(虚拟机统计信息监视工具) jstat(JVMStatisticsMonitoringTool,虚拟机统计信息监视工具)用于监控虚拟机的运行状态信息。 例如,我们用它来查询某个Java进程的垃圾收集情况,示例如下:jstatgc43704S0CS1CS0US1UECEUOCOUMCMUCCSCCCSUYGCYGCTFGCFGCTCGCCGCTGCT10752。010752。00。00。065536。05243。4175104。00。04480。0774。0384。075。800。00000。0000。000 参数说明如下表所示: 参数 说明 S0C 年轻代中第一个存活区的大小 S1C 年轻代中第二个存活区的大小 S0U 年轻代中第一个存活区已使用的空间(字节) S1U 年轻代中第二个存活区已使用的空间(字节) EC Edem区大小 EU 年轻代中Edem区已使用的空间(字节) OC 老年代大小 OU 老年代已使用的空间(字节) YGC 从应用程序启动到采样时younggc的次数 YGCT 从应用程序启动到采样时younggc的所用的时间(s) FGC 从应用程序启动到采样时fullgc的次数 FGCT 从应用程序启动到采样时fullgc的所用的时间 GCT 从应用程序启动到采样时整个gc所用的时间 注意:年轻代的Edem区满了会触发younggc,老年代满了会触发oldgc。fullgc指的是清除整个堆,包括young区和old区。 jstat常用的查询参数有:class,查询类加载器信息;compiler,JIT相关信息;gc,GC堆状态;gcnew,新生代统计信息;gcutil,GC堆统计汇总信息。3。jinfo(查询虚拟机参数配置工具) jinfo(ConfigurationInfoforJava)用于查看和调整虚拟机各项参数。语法如下:jinfooptionpid 查看JVM参数示例如下:jinfoflags45129VMFlags:XX:CICompilerCount3XX:InitialHeapSize268435456XX:MaxHeapSize4294967296XX:MaxNewSize1431306240XX:MinHeapDeltaBytes524288XX:NewSize89128960XX:OldSize179306496XX:UseCompressedClassPointersXX:UseCompressedOopsXX:UseFastUnorderedTimeStampsXX:UseParallelGC 其中45129是使用jps查询的LVMID。 我们可以通过jinfoflag〔〕name来修改虚拟机的参数值,比如下面的示例:jinfoflagPrintGC45129查询是否开启GC打印XX:PrintGCjinfoflagPrintGC45129开启GC打印jinfoflagPrintGC45129查询是否开启GC打印XX:PrintGCjinfoflagPrintGC45129关闭GC打印jinfoflagPrintGC45129查询是否开启GC打印XX:PrintGC4。jmap(堆快照生成工具) jmap(MemoryMapforJava)用于查询堆的快照信息。 查询堆信息示例如下:jmapheap45129AttachingtoprocessID45129,pleasewait。。。Debuggerattachedsuccessfully。Servercompilerdetected。JVMversionis25。101b13usingthreadlocalobjectallocation。ParallelGCwith6thread(s)HeapConfiguration:MinHeapFreeRatio0MaxHeapFreeRatio100MaxHeapSize4294967296(4096。0MB)NewSize89128960(85。0MB)MaxNewSize1431306240(1365。0MB)OldSize179306496(171。0MB)NewRatio2SurvivorRatio8MetaspaceSize21807104(20。796875MB)CompressedClassSpaceSize1073741824(1024。0MB)MaxMetaspaceSize17592186044415MBG1HeapRegionSize0(0。0MB)HeapUsage:PSYoungGenerationEdenSpace:capacity67108864(64。0MB)used5369232(5。1204986572265625MB)free61739632(58。87950134277344MB)8。000779151916504usedFromSpace:capacity11010048(10。5MB)used0(0。0MB)free11010048(10。5MB)0。0usedToSpace:capacity11010048(10。5MB)used0(0。0MB)free11010048(10。5MB)0。0usedPSOldGenerationcapacity179306496(171。0MB)used0(0。0MB)free179306496(171。0MB)0。0used2158internedStringsoccupying152472bytes。 我们也可以直接生成堆快照文件,示例如下:jmapdump:formatb,fileUsersadminDocuments2020。dump47380DumpingheaptoUsersadminDocuments2020。dump。。。Heapdumpfilecreated5。jhat(堆快照分析功能) jhat(JVMHeapAnalysisTool,堆快照分析工具)和jmap搭配使用,用于启动一个web站点来分析jmap生成的快照文件。 执行示例如下:jhatUsersadminDocuments2020。dumpReadingfromUsersadminDocuments2020。dump。。。DumpfilecreatedTueMay2616:12:41CST2020Snapshotread,resolving。。。Resolving17797objects。。。Chasingreferences,expect3dots。。。Eliminatingduplicatereferences。。。Snapshotresolved。StartedHTTPserveronport7000Serverisready。 上述信息表示jhat启动了一个http的服务器端口为7000的站点来展示信息,此时我们在浏览器中输入:http:localhost:7000,会看到如下图所示的信息: 6。jstack(查询虚拟机当前的线程快照信息) jstack(StackTraceforJava)用于查看当前虚拟机的线程快照,用它可以排查线程的执行状况,例如排查死锁、死循环等问题。 比如,我们先写一段死锁的代码:publicclassNativeOptimize{privatestaticObjectobj1newObject();privatestaticObjectobj2newObject();publicstaticvoidmain(String〔〕args){newThread(newRunnable(){Overridepublicvoidrun(){synchronized(obj2){System。out。println(Thread。currentThread()。getName()锁住obj2);try{Thread。sleep(1000);}catch(InterruptedExceptione){e。printStackTrace();}synchronized(obj1){执行不到这里System。out。println(1秒钟后,Thread。currentThread()。getName()锁住obj1);}}}})。start();synchronized(obj1){System。out。println(Thread。currentThread()。getName()锁住obj1);try{Thread。sleep(1000);}catch(InterruptedExceptione){e。printStackTrace();}synchronized(obj2){执行不到这里System。out。println(1秒钟后,Thread。currentThread()。getName()锁住obj2);}}}} 以上程序的执行结果如下: main:锁住obj1 Thread0:锁住obj2 此时我们使用jstack工具打印一下当前线程的快照信息,结果如下:binjstackl500162020052618:01:41FullthreaddumpJavaHotSpot(TM)64BitServerVM(25。101b13mixedmode):AttachListener10daemonprio9osprio31tid0x00007f8c00840800nid0x3c03waitingoncondition〔0x0000000000000000〕java。lang。Thread。State:RUNNABLELockedownablesynchronizers:NoneThread09prio5osprio31tid0x00007f8c00840000nid0x3e03waitingformonitorentry〔0x00007000100c8000〕java。lang。Thread。State:BLOCKED(onobjectmonitor)atcom。example。optimize。NativeOptimize1。run(NativeOptimize。java:25)waitingtolock0x000000076abb62d0(ajava。lang。Object)locked0x000000076abb62e0(ajava。lang。Object)atjava。lang。Thread。run(Thread。java:745)Lockedownablesynchronizers:NoneServiceThread8daemonprio9osprio31tid0x00007f8c01814800nid0x4103runnable〔0x0000000000000000〕java。lang。Thread。State:RUNNABLELockedownablesynchronizers:NoneC1CompilerThread27daemonprio9osprio31tid0x00007f8c0283c800nid0x4303waitingoncondition〔0x0000000000000000〕java。lang。Thread。State:RUNNABLELockedownablesynchronizers:NoneC2CompilerThread16daemonprio9osprio31tid0x00007f8c0300a800nid0x4403waitingoncondition〔0x0000000000000000〕java。lang。Thread。State:RUNNABLELockedownablesynchronizers:NoneC2CompilerThread05daemonprio9osprio31tid0x00007f8c0283c000nid0x3603waitingoncondition〔0x0000000000000000〕java。lang。Thread。State:RUNNABLELockedownablesynchronizers:NoneSignalDispatcher4daemonprio9osprio31tid0x00007f8c0283b000nid0x4603runnable〔0x0000000000000000〕java。lang。Thread。State:RUNNABLELockedownablesynchronizers:NoneFinalizer3daemonprio8osprio31tid0x00007f8c03001000nid0x5003inObject。wait()〔0x000070000f8ad000〕java。lang。Thread。State:WAITING(onobjectmonitor)atjava。lang。Object。wait(NativeMethod)waitingon0x000000076ab08ee0(ajava。lang。ref。ReferenceQueueLock)atjava。lang。ref。ReferenceQueue。remove(ReferenceQueue。java:143)locked0x000000076ab08ee0(ajava。lang。ref。ReferenceQueueLock)atjava。lang。ref。ReferenceQueue。remove(ReferenceQueue。java:164)atjava。lang。ref。FinalizerFinalizerThread。run(Finalizer。java:209)Lockedownablesynchronizers:NoneReferenceHandler2daemonprio10osprio31tid0x00007f8c03000000nid0x2f03inObject。wait()〔0x000070000f7aa000〕java。lang。Thread。State:WAITING(onobjectmonitor)atjava。lang。Object。wait(NativeMethod)waitingon0x000000076ab06b50(ajava。lang。ref。ReferenceLock)atjava。lang。Object。wait(Object。java:502)atjava。lang。ref。Reference。tryHandlePending(Reference。java:191)locked0x000000076ab06b50(ajava。lang。ref。ReferenceLock)atjava。lang。ref。ReferenceReferenceHandler。run(Reference。java:153)Lockedownablesynchronizers:Nonemain1prio5osprio31tid0x00007f8c00802800nid0x1003waitingformonitorentry〔0x000070000ef92000〕java。lang。Thread。State:BLOCKED(onobjectmonitor)atcom。example。optimize。NativeOptimize。main(NativeOptimize。java:41)waitingtolock0x000000076abb62e0(ajava。lang。Object)locked0x000000076abb62d0(ajava。lang。Object)Lockedownablesynchronizers:NoneVMThreadosprio31tid0x00007f8c01008800nid0x2e03runnableGCtaskthread0(ParallelGC)osprio31tid0x00007f8c00803000nid0x2007runnableGCtaskthread1(ParallelGC)osprio31tid0x00007f8c00006800nid0x2403runnableGCtaskthread2(ParallelGC)osprio31tid0x00007f8c01800800nid0x2303runnableGCtaskthread3(ParallelGC)osprio31tid0x00007f8c01801800nid0x2a03runnableGCtaskthread4(ParallelGC)osprio31tid0x00007f8c01802000nid0x5403runnableGCtaskthread5(ParallelGC)osprio31tid0x00007f8c01006800nid0x2d03runnableVMPeriodicTaskThreadosprio31tid0x00007f8c00010800nid0x3803waitingonconditionJNIglobalreferences:6FoundoneJavaleveldeadlock:Thread0:waitingtolockmonitor0x00007f8c000102a8(object0x000000076abb62d0,ajava。lang。Object),whichisheldbymainmain:waitingtolockmonitor0x00007f8c0000ed58(object0x000000076abb62e0,ajava。lang。Object),whichisheldbyThread0Javastackinformationforthethreadslistedabove:Thread0:atcom。example。optimize。NativeOptimize1。run(NativeOptimize。java:25)waitingtolock0x000000076abb62d0(ajava。lang。Object)locked0x000000076abb62e0(ajava。lang。Object)atjava。lang。Thread。run(Thread。java:745)main:atcom。example。optimize。NativeOptimize。main(NativeOptimize。java:41)waitingtolock0x000000076abb62e0(ajava。lang。Object)locked0x000000076abb62d0(ajava。lang。Object)Found1deadlock。 从上述信息可以看出使用jstack,可以很方便地排查出代码中出现deadlock(死锁)的问题。考点分析 Java虚拟机的排查工具是一个合格程序员必备的技能,使用它我们可以很方便地定位出问题的所在,尤其在团队合作的今天,每个人各守一摊很容易出现隐藏的bug(缺陷)。因此使用这些排查功能可以帮我们快速地定位并解决问题,所以它也是面试中常问的问题之一。 和此知识点相关的面试题还有以下这些:除了比较实用的命令行工具之外,有没有方便一点的排查工具?JVM常见的调优手段有哪些?知识扩展可视化排查工具 JVM除了上面的6个基础命令行工具之外,还有两个重要的视图调试工具,即JConsole和JVisualVM,它们相比于命令行工具使用更方便、操作更简单、结果展现也更直观。 JConsole和JVisualVM都位于JDK的bin目录下,JConsole(JavaMonitoringandManagementConsole)是最早期的视图调试工具,其启动页面如下图所示: 可以看出我们可以用它来连接远程的服务器,或者是直接调试本机,这样就可以在不消耗生产环境的性能下,从本机启动JConsole来连接服务器。 JVisualVM的启动图如下图所示: 由上图可知,JVisualVM既可以调试本地也可以调试远程服务器。JVM调优 JVM调优主要是根据实际的硬件配置信息重新设置JVM参数来进行调优的,例如,硬件的内存配置很高,但JVM因为是默认参数,所以最大内存和初始化堆内存很小,这样就不能更好地利用本地的硬件优势了。因此,需要调整这些参数,让JVM在固定的配置下发挥最大的价值。 JVM常见调优参数包含以下这些:Xmx,设置最大堆内存大小;Xms,设置初始堆内存大小;XX:MaxNewSize,设置新生代的最大内存;XX:MaxTenuringThreshold,设置新生代对象经过一定的次数晋升到老生代;XX:PretrnureSizeThreshold,设置大对象的值,超过这个值的对象会直接进入老生代;XX:NewRatio,设置分代垃圾回收器新生代和老生代内存占比;XX:SurvivorRatio,设置新生代Eden、FormSurvivor、ToSurvivor占比。 我们要根据自己的业务场景和硬件配置来设置这些值。例如,当我们的业务场景会有很多大的临时对象产生时,因为这些大对象只有很短的生命周期,因此需要把XX:MaxNewSize的值设置的尽量大一些,否则就会造成大量短生命周期的大对象进入老生代,从而很快消耗掉了老生代的内存,这样就会频繁地触发fullgc,从而影响了业务的正常运行。小结 本课时我们讲了JVM排查的6个基本命令行工具:jps、jstat、jinfo、jmap、jhat、jstack,以及2个视图排查工具:JConsole和JVisualVM;同时还讲了JVM的常见调优参数。