这篇文章中介绍下如何使用JDK自带工具来分析和定位Java程序的问题。 使用JDK自带工具查看JVM情况 JDK自带了很多命令行甚至是图形界面工具,帮助我们查看JVM的一些信息。比如,在我的机器上运行ls命令,可以看到JDK8提供了非常多的工具或程序: 接下来,我会与你介绍些常用的监控工具。你也可以先通过下面这张图了解下各种工具的基本作用: 为了测试这些工具,我们先来写一段代码:启动10个死循环的线程,每个线程分配一个10MB左右的字符串,然后休眠10秒。可以想象到,这个程序会对GC造成压力:启动10个线程IntStream。rangeClosed(1,10)。mapToObj(inewThread((){while(true){每一个线程都是一个死循环,休眠10秒,打印10M数据StringpayloadIntStream。rangeClosed(1,10000000)。mapToObj(a)。collect(Collectors。joining())UUID。randomUUID()。toString();try{TimeUnit。SECONDS。sleep(10);}catch(InterruptedExceptione){e。printStackTrace();}System。out。println(payload。length());}}))。forEach(Thread::start);TimeUnit。HOURS。sleep(1); 修改pom。xml,配置springbootmavenplugin插件打包的Java程序的main方法类:plugingroupIdorg。springframework。bootgroupIdspringbootmavenpluginartifactIdconfigurationmainClassorg。geekbang。time。commonmistakes。troubleshootingtools。jdktool。CommonMistakesApplicationmainClassconfigurationplugin 然后使用javajar启动进程,设置JVM参数,让堆最小最大都是1GB:javajarcommonmistakes0。0。1SNAPSHOT。jarXms1gXmx1g 完成这些准备工作后,我们就可以使用JDK提供的工具,来观察分析这个测试程序了。 jps 首先,使用jps得到Java进程列表,这会比使用ps来的方便:jps1270722261Launcher23864commonmistakes0。0。1SNAPSHOT。jar15608RemoteMavenServer3623243Main23868Jps22893KotlinCompileDaemon jinfo 然后,可以使用jinfo打印JVM的各种参数:jinfo23864JavaSystemProperties:WedJan2912:49:47CST2020。。。user。namezhuyepath。separator:os。version10。15。2java。runtime。nameJava(TM)SERuntimeEnvironmentfile。encodingUTF8java。vm。nameJavaHotSpot(TM)64BitServerVM。。。VMFlags:XX:CICompilerCount4XX:ConcGCThreads2XX:G1ConcRefinementThreads8XX:G1HeapRegionSize1048576XX:GCDrainStackTargetSize64XX:InitialHeapSize268435456XX:MarkStackSize4194304XX:MaxHeapSize4294967296XX:MaxNewSize2576351232XX:MinHeapDeltaBytes1048576XX:NonNMethodCodeHeapSize5835340XX:NonProfiledCodeHeapSize122911450XX:ProfiledCodeHeapSize122911450XX:ReservedCodeCacheSize251658240XX:SegmentedCodeCacheXX:UseCompressedClassPointersXX:UseCompressedOopsXX:UseG1GCVMArguments:javacommand:commonmistakes0。0。1SNAPSHOT。jarXms1gXmx1gjavaclasspath(initial):commonmistakes0。0。1SNAPSHOT。jarLauncherType:SUNSTANDARD 查看第15行和19行可以发现,我们设置JVM参数的方式不对,Xms1g和Xmx1g这两个参数被当成了Java程序的启动参数,整个JVM目前最大内存是4GB左右,而不是1GB。最近整理了一份最新的面试资料,里面收录了2021年各个大厂的面试题,打算跳槽的小伙伴不要错过,点击领取吧! 因此,当我们怀疑JVM的配置很不正常的时候,要第一时间使用工具来确认参数。除了使用工具确认JVM参数外,你也可以打印VM参数和程序参数:System。out。println(VMoptions);System。out。println(ManagementFactory。getRuntimeMXBean()。getInputArguments()。stream()。collect(Collectors。joining(System。lineSeparator())));System。out。println(Programarguments);System。out。println(Arrays。stream(args)。collect(Collectors。joining(System。lineSeparator()))); 把JVM参数放到jar之前,重新启动程序,可以看到如下输出,从输出也可以确认这次JVM参数的配置正确了:targetgit:(master)javaXms1gXmx1gjarcommonmistakes0。0。1SNAPSHOT。jartestVMoptionsXms1gXmx1gProgramargumentstest jvisualvm 然后,启动另一个重量级工具jvisualvm观察一下程序,可以在概述面板再次确认JVM参数设置成功了: 继续观察监视面板可以看到,JVM的GC活动基本是10秒发生一次,堆内存在250MB到900MB之间波动,活动线程数是22。我们可以在监视面板看到JVM的基本情况,也可以直接在这里进行手动GC和堆Dump操作: jconsole如果希望看到各个内存区的GC曲线图,可以使用jconsole观察。jconsole也是一个综合性图形界面监控工具,比jvisualvm更方便的一点是,可以用曲线的形式监控各种数据,包括MBean中的属性值: jstat 同样,如果没有条件使用图形界面(毕竟在Linux服务器上,我们主要使用命令行工具),又希望看到GC趋势的话,我们可以使用jstat工具。 jstat工具允许以固定的监控频次输出JVM的各种监控指标,比如使用gcutil输出GC和内存占用汇总信息,每隔5秒输出一次,输出100次,可以看到YoungGC比较频繁,而FullGC基本10秒一次:jstatgcutil239405000100S0S1EOMCCSYGCYGCTFGCFGCTCGCCGCTGCT0。00100。000。3687。6394。3081。0653914。021333。9728370。97618。9680。00100。000。6069。5194。3081。0654014。029333。9728390。97818。9790。000。000。5099。8194。2781。0354814。143344。0028400。98119。1260。00100。000。5970。4794。2781。0354914。177344。0028440。98519。1640。00100。000。5799。8594。3281。0955014。204344。0028450。99019。1960。00100。000。6577。6994。3281。0955914。469364。1988470。99319。6590。00100。000。6577。6994。3281。0955914。469364。1988470。99319。6590。00100。000。7035。5494。3281。0956714。763374。3788531。00120。1420。00100。000。7041。2294。3281。0956714。763374。3788531。00120。1420。00100。001。8996。7694。3281。0957414。943384。4878591。00720。4380。00100。001。3939。2094。3281。0957514。946384。4878611。01020。442 其中,S0表示Survivor0区占用百分比,S1表示Survivor1区占用百分比,E表示Eden区占用百分比,O表示老年代占用百分比,M表示元数据区占用百分比,YGC表示年轻代回收次数,YGCT表示年轻代回收耗时,FGC表示老年代回收次数,FGCT表示老年代回收耗时。 jstat命令的参数众多,包含class、compiler、gc等。Java8、LinuxUnix平台jstat工具的完整介绍,你可以查看这里。jstat定时输出的特性,可以方便我们持续观察程序的各项指标。 继续来到线程面板可以看到,大量以Thread开头的线程基本都是有节奏的10秒运行一下,其他时间都在休眠,和我们的代码逻辑匹配: 点击面板的线程Dump按钮,可以查看线程瞬时的线程栈: jstack 通过命令行工具jstack,也可以实现抓取线程栈的操作:jstack239402020012913:08:15FullthreaddumpJavaHotSpot(TM)64BitServerVM(11。0。312LTSmixedmode):。。。main1prio5osprio31cpu440。66mselapsed574。86stid0x00007ffdd9800000nid0x2803waitingoncondition〔0x0000700003849000〕java。lang。Thread。State:TIMEDWAITING(sleeping)atjava。lang。Thread。sleep(java。base11。0。3NativeMethod)atjava。lang。Thread。sleep(java。base11。0。3Thread。java:339)atjava。util。concurrent。TimeUnit。sleep(java。base11。0。3TimeUnit。java:446)atorg。geekbang。time。commonmistakes。troubleshootingtools。jdktool。CommonMistakesApplication。main(CommonMistakesApplication。java:41)atjdk。internal。reflect。NativeMethodAccessorImpl。invoke0(java。base11。0。3NativeMethod)atjdk。internal。reflect。NativeMethodAccessorImpl。invoke(java。base11。0。3NativeMethodAccessorImpl。java:62)atjdk。internal。reflect。DelegatingMethodAccessorImpl。invoke(java。base11。0。3DelegatingMethodAccessorImpl。java:43)atjava。lang。reflect。Method。invoke(java。base11。0。3Method。java:566)atorg。springframework。boot。loader。MainMethodRunner。run(MainMethodRunner。java:48)atorg。springframework。boot。loader。Launcher。launch(Launcher。java:87)atorg。springframework。boot。loader。Launcher。launch(Launcher。java:51)atorg。springframework。boot。loader。JarLauncher。main(JarLauncher。java:52)Thread113prio5osprio31cpu17851。77mselapsed574。41stid0x00007ffdda029000nid0x9803waitingoncondition〔0x000070000539d000〕java。lang。Thread。State:TIMEDWAITING(sleeping)atjava。lang。Thread。sleep(java。base11。0。3NativeMethod)atjava。lang。Thread。sleep(java。base11。0。3Thread。java:339)atjava。util。concurrent。TimeUnit。sleep(java。base11。0。3TimeUnit。java:446)atorg。geekbang。time。commonmistakes。troubleshootingtools。jdktool。CommonMistakesApplication。lambdanull1(CommonMistakesApplication。java:33)atorg。geekbang。time。commonmistakes。troubleshootingtools。jdktool。CommonMistakesApplicationLambda410x00000008000a8c40。run(UnknownSource)atjava。lang。Thread。run(java。base11。0。3Thread。java:834)。。。 抓取后可以使用类似fastthread(https:fastthread。io)这样的在线分析工具来分析线程栈。 jcmd 最后,我们来看一下JavaHotSpot虚拟机的NMT功能。 通过NMT,我们可以观察细粒度内存使用情况,设置XX:NativeMemoryTrackingsummarydetail可以开启NMT功能,开启后可以使用jcmd工具查看NMT数据。 我们重新启动一次程序,这次加上JVM参数以detail方式开启NMT:Xms1gXmx1gXX:ThreadStackSize256kXX:NativeMemoryTrackingdetail 在这里,我们还增加了XX:ThreadStackSize参数,并将其值设置为256k,也就是期望把线程栈设置为256KB。我们通过NMT观察一下设置是否成功。 启动程序后执行如下jcmd命令,以概要形式输出NMT结果。可以看到,当前有32个线程,线程栈总共保留了差不多4GB左右的内存。我们明明配置线程栈最大256KB啊,为什么会出现4GB这么夸张的数字呢,到底哪里出了问题呢?jcmd24404VM。nativememorysummary24404:NativeMemoryTracking:Total:reserved6635310KB,committed5337110KBJavaHeap(reserved1048576KB,committed1048576KB)(mmap:reserved1048576KB,committed1048576KB)Class(reserved1066233KB,committed15097KB)(classes902)(malloc9465KB908)(mmap:reserved1056768KB,committed5632KB)Thread(reserved4209797KB,committed4209797KB)(thread32)(stack:reserved4209664KB,committed4209664KB)(malloc96KB165)(arena37KB59)Code(reserved249823KB,committed2759KB)(malloc223KB730)(mmap:reserved249600KB,committed2536KB)GC(reserved48700KB,committed48700KB)(malloc10384KB135)(mmap:reserved38316KB,committed38316KB)Compiler(reserved186KB,committed186KB)(malloc56KB105)(arena131KB7)Internal(reserved9693KB,committed9693KB)(malloc9661KB2585)(mmap:reserved32KB,committed32KB)Symbol(reserved2021KB,committed2021KB)(malloc1182KB334)(arena839KB1)NativeMemoryTracking(reserved85KB,committed85KB)(malloc5KB53)(trackingoverhead80KB)ArenaChunk(reserved196KB,committed196KB)(malloc196KB) 重新以VM。nativememorydetail参数运行jcmd:jcmd24404VM。nativememorydetail 可以看到,有16个可疑线程,每一个线程保留了262144KB内存,也就是256MB(通过下图红框可以看到,使用关键字262144KBforThreadStackfrom搜索到了16个结果): 其实,ThreadStackSize参数的单位是KB,所以我们如果要设置线程栈256KB,那么应该设置256而不是256k。重新设置正确的参数后,使用jcmd再次验证下: 除了用于查看NMT外,jcmd还有许多功能。我们可以通过help,看到它的所有功能:jcmd24781help 除了jps、jinfo、jcmd、jstack、jstat、jconsole、jvisualvm外,JDK中还有一些工具,你可以通过官方文档查看完整介绍。 官方文档:https:docs。oracle。comjavase8docstechnotestools