java gc的工作原理、如何优化GC的性能、如何和GC进行有效的交互
一个优秀的Java 程序员必须了解GC 的工作原理、如何优化GC的性能、如何和GC进行有效的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统、实时系统等。只有全面提升内存的管理效 率,才能提高整个应用程序的性能。 本篇文章首先简单介绍GC的工作原理,然后再对GC的几个关键问题进行深入探讨,最后提出一些Java程序设计建议,从GC角度提高Java程序的性能。
GC的基本原理
Java
的内存管理实际上就是对象的管理,其中包括对象的分配和释放,对于程序员来说,分配对象使用new关键字;释放对象时,只要将对象所有引用赋值为null,让程序不能够再访问到这个对象,我们称该对象为"不可达的".GC将负责回收所有"不可达"对象的内存空间。
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对
象,通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。但是,为了保证
GC能够区别平台实现的问题,Java规范标准对GC的很多行为都没有进行严格的规定。例如,对于采用什么类型的回收算法、什么时候进行回收等重要问题都
没有明确的规定。因此,不同的JVM的实现者往往有不同的实现算法。这也给Java程序员的开发带来很多不确定性。本文研究了几个和GC工作相关的问题,
努力减少这种不确定性给Java程序带来的负面影响。
@@增量式GC( Incremental GC )
GC在JVM中通常由一个或一组进程来实现,它本身也和用户程序一样占用heap空间,运行时也占用CPU,当GC进程运行时,应用程序停止运行。因此,
当GC运行时间较长时,用户能够感到Java程序的停顿,另一方面,如果GC运行时间太短,可能对象回收率太低,这意味着还有很多应该回收的对象没有被回
收,仍然占用大量内存。因此,在设计GC的时候,就必须在停顿时间和回收率之间进行权衡。
一个好的GC实现允许用户定义自己所需要的设置,例如内存有限的设备,对内存的使用量非常敏感,希望GC能够准确的回收内存,它并不在意程序速度的放慢,
另外一些实时网络游戏,就不能够允许程序有长时间的中断。
增量式GC就是通过一定的回收算法,把一个长时间的中断,划分为很多个小的中断,通过这种方式减少GC对用户程序的影响。虽然,增量式GC在整体性能上可
能不如普通GC的效率高,但是它能够减少程序的最长停顿时间。
Sun JDK提供的HotSpot JVM就能支持增量式GC。HotSpot
JVM缺省GC方式为不使用增量GC,为了启动增量GC,我们必须在运行Java程序时增加-Xincgc的参数。HotSpot
JVM增量式GC,实现是采用Train
GC算法,它的基本想法:将堆中的所有对象按照创建和使用情况进行分组(分层),将使用频繁和具有相关性的对象放在一队中,随着程序的运行,不断对组进行
调整,当GC运行时,它总是先回收最老的(最近很少访问的)对象,如果整组都为可回收对象,GC将整组回收,这样,每次GC运行只回收一定比例的不可达对
象,保证程序的顺畅运行。
finalize()函数
finalize是位于Object类的一个思路方法,该思路方法的访问修饰符为protected,由于所有类为Object的子类,因此用户类很容易
访问到这个思路方法。由于,finalize函数没有自动实现链式调用,我们必须手动实现,因此finalize函数的最后一个语句通常是
super.finalize()。通过这种方式,我们可以实现从下到上实现finalize的调用,即先释放自己的资源,然后再释放父类的资源。
根据Java语言规范标准,JVM保证调用finalize函数之前,这个对象是不可达的,但是,JVM不保证这个函数一定会被调用。另外,规范标准还保证finalize函数最多运行一次。
很多Java初学者会认为这个思路方法类似和C++中的析构函数,将很多对象、资源的释放都放在这一函数里面。其实,这不是一种很好的方式,原因如下:其
一,GC为了能够支持finalize函数,要对覆盖这个函数的对象作很多附加的工作;其二,在finalize运行完成之后,该对象可能变成可达
的,GC还要再检查一次该对象是否是可达的,因此,使用finalize会降低GC的运行性能;其三,由于GC调用finalize的时间是不确定的,因
此通过这种方式释放资源也是不确定的。
通常,finalize用于一些不容易控制,并且非常重要资源的释放,例如一些I/O操作、数据连接等,这些资源的释放对整个应用程序是非常关键的。在这
种情况下,程序员应该以通过程序本身管理(包括释放)这些资源为主,以finalize函数释放资源方式为辅,形成一种双保险的管理机制,而不应该仅仅依
靠finalize来释放资源。
程序如何和GC进行交互(不懂...)
Java2增强了内存管理功能,增加了一个java.lang.ref包,其中定义了3种引用类。这3种引用类分别为SoftReference、
WeakReference和PhantomReference.通过使用这些引用类,程序员可以在一定程度和GC进行交互,以便改善GC的工作效率。这
些引用类的引用强度介于可达对象和不可达对象之间。
一些Java编码的建议
根据GC的工作原理,我们可以通过一些窍门技巧和方式,让GC运行更加有效率,更加符合应用程序的要求。以下就是一些程序设计的几点建议:
1、最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后自动设置为null。我们
在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组、队列、树、图等,这些对象之间有相互引用,关系较为复杂。对于这类对象,GC回收它们一般
效率较低。如果程序允许,尽早将不用的引用对象赋为null。这样可以加速GC的工作。
2、尽量少用finalize函数。Finalize函数是Java提供给程序员一个释放对象或资源的机会,但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。
3、注意集合数据类型,包括数组、树、图、链表等数据结构,这些数据结构对GC来说回收更为复杂。另外,注意一些全局的变量,以及静态变量,这些变量往往容易引起悬挂对象(dangling reference),造成内存浪费。
4、当程序有一定的等待时间,程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范标准并不保证GC一定会执行,此时使用增量式GC可以缩短Java程序的暂停时间。
java 当GC进程运行时,应用程序停止运行吗??
对于创建大量对象的大型应用程序,JVM 花在垃圾收集(GC)上的时间会非常多。默认情况下,进行 GC 时,整个应用程序都必须等待它完成,这可能要有几秒钟甚至更长的时间(Java 应用程序启动器的命令行选项 -verbose:gc
将导致向控制台报告每一次 GC 事件)。要将这些由 GC
引起的暂停(这可能会影响快速任务的执行)降至最少,应该将应用程序创建的对象的数目降至最低。同样,在单独的 JVM
中运行计划代码是有帮助的。同时,可以试用几个微调选项以尽可能地减少 GC 暂停。例如,增量 GC
会尽量将主收集的代价分散到几个小的收集上。当然这会降低 GC 的效率,但是这可能是时间计划的一个可接受的代价
资料引用:http://www.knowsky.com/362375.html
Java虚拟机优化选项,GC说明
引用 http://blog.sina.com.cn/s/blog_6d003f3f0100lmkn.html
有许多 JVM 选项会影响基准测试。比较重要的选项包括:
* 确保有足够的内存可用(-Xmx)。
* 使用的垃圾收集器类型(高级的 JVM 提供许多调优选项,但是要小心使用)。
* 是否允许类垃圾收集(-Xnoclassgc)。默认设置是允许类 GC;使用
-Xnoclassgc 可能会损害性能。
* 是否执行 escape
分析(-XX:+DoEscapeAnalysis)。
*
是否支持大页面堆(-XX:+UseLargePages)。
* 是否改变了线程堆栈大小(例如,-Xss128k)。
* 使用 JIT
编译的方式:总是使用(-Xcomp)、从不使用(-Xint)或只对热点使用(-Xmixed;这是默认选项,产生的性能最好)。
* 在执行 JIT
编译之前(-XX:CompileThreshold)、后台 JIT 编译期间(-Xbatch)或分级的
JIT 编译期间(-XX:+TieredCompilation)收集的剖析数据量。
* 是否执行偏向锁(biased
locking,-XX:+UseBiasedLocking);注意,JDK 1.6
及更高版本会自动执行这个特性。
*
是否激活最近的试验性性能调整(-XX:+AggressiveOpts)。
* 启用还是禁用断言(-enableassertions 和
-enablesystemassertions)。
*
启用还是禁用严格的本机调用检查(-Xcheck:jni)。
* 为 NUMA 多 CPU
系统启用内存位置优化(-XX:+UseNUMA)。
Class Data Sharing类共享.
java5引入了类共享机制,指在java程序第一次启动时, 优化一些最常用的基础类到一个共享文件中,暂只支持Client
VM和serialGC.存放在client/classes.jsa中, 这就是为什么程序在第一次执行较慢的原因.
开启参数-Xshare.
J2SE
6(代号:Mustang野马)主要设计原则之一就是提升J2SE的性能和扩展能力,主要通过最大程度提升运行效率,更好的垃圾收集和一些客户端性能来达到。
1、偏向锁(Biased locking)
Java
6以前加锁操作都会导致一次原子CAS(Compare-And-Set)操作,CAS操作是比较耗时的,即使这个锁上实际上没有冲突,只被一个线程拥
有,也会带来较大开销。为解决这一问题,Java
6中引入偏向锁技术,即一个锁偏向于第一个加锁的线程,该线程后续加锁操作不需要同步。大概的实现如下:一个锁最初为NEUTRAL状态,当第一个线程加
锁时,将该锁的状态修改为BIASED,并记录线程ID,当这一线程进行后续加锁操作时,若发现状态是BIASED并且线程ID是当前线程ID,则只设置
一下加锁标志,不需要进行CAS操作。其它线程若要加这个锁,需要使用CAS操作将状态替换为REVOKE,并等待加锁标志清零,以后该锁的状态就变成
DEFAULT,常用旧的算法处理。这一功能可用-XX:-UseBiasedLocking命令禁止。
2、锁粗化(Lock coarsening)
如果一段代码经常性的加锁和解锁,在解锁与下次加锁之间又没干什么事情,则可以将多次加加锁解锁操作合并成一对。这一功能可用-XX:-EliminateLocks禁止。
3、自适应自旋(Adaptive spinning)
一般在多CPU的机器上加锁实现都会包含一个短期的自旋过程。自旋的次数不太好决定,自旋少了会导致线程被挂起和上下文切换增加,自旋多了耗CPU。为此Java
6中引入自适应自旋技术,即根据一个锁最近自旋加锁成功概率动态调整自旋次数。
4、常用大内存分布的堆(large page heap)
在大内分页是x86/amd64架构上用来减小TLB(虚拟地址到物理地址翻译缓存)大小的TLB失配率。Java
6中的内存堆可以使用这一技术。
5、提高数组拷贝性能
对每种类型大小写一个定制的汇编数组拷贝程序。
6、后台进行代码优化
Background Compilation in HotSpot™ Client Compiler:
后台进行代码优化
7、线性扫描寄存器分配算法(Linear Scan Register
Allocation):
一种新的寄存器分配策略,基于SSA(static single
assignment),性能提高10%左右。常用的寄存器分配算法将寄存器分配看作图着色问题,时间复杂度是O(n^4),不适用于Java的JIT编译。原来的JVM里是根据一些本地启发式规则来分配寄存器,效果不太好,Java
6中使用的线性扫描寄存器算法能够达到与图颜色算法相似的效果,并且时间复杂度是线性的。
8、并行缩并垃圾收集器(Parallel Compaction Collector)
进行Full GC时使用并行垃圾收集(JDK 5里原来非Full GC是并行的但Full
GC是串行的),使用-XX:+UseParallelOldGC开启这一功能
9、并行低停顿垃圾收集器(Concurrent Low Pause Collector)
显式调用gc(如System.gc)时也可以并行进行标记-清扫式垃圾收集,使用-XX:+ExplicitGCInvokesConcurrent开启。
10、Ergonomics in the 6.0 Java Virtual Machine
自动调整垃圾收集策略、堆大小等配置,这一功能在JDK 5中加入,JDK
6中得到显著增强,SPECjbb2005性能提高70%。
11、boot类装载器的优化
jre中增加一个描述package所在jar文件的元索引文件,加快classloader加载类性能,提高桌面Java应用启动速度(+15%)。内存占用也减少了10%
12、图形程序优化
在jvm启动之前显示splash。
OutOfMemoryError是内存溢出, 有多种情况会出现内存溢出.
1.java堆溢出java.lang.OutOfMemoryError: Java heap
space.
2.java永久堆溢出,通常是反射,代理用的较多导致类生成过多,java.lang.OutOfMemoryError:
PermGen space.
3.本地堆溢出,这可能是由于操作系统无法分配足够的内存,可能是系统已无内存,还可能是java进程内存空间耗尽,这里有点意思,一般32位系统进程只
有4G地址空间,而又因为java实现使用本地堆或内存映射区作为java堆的存储空间,再去除内核映射区,java使用的堆一般只有2G以内,而如果
java堆xmx占的过大,导致jni的本地堆过小,也会生成内存溢出.本地堆可以是jni用new,
malloc,也可能是DirectBuffer等实例.
java.lang.OutOfMemoryError: request
<size> bytes for
<reason>. Out of swap space?
这时候,如果java堆足够用的话, 减少xmx的值,反而会解决这种问题.
4.jni方法的溢出.而前者是由jvm检测的本地溢出,而此是在jni方法调用时,无法分配内存.
java.lang.OutOfMemoryError:
<reason> <stack
trace> (Native method)
JDK7性能优化.
1.(Zero Based )Compressed OOPS
在64位CPU中, JVM的OOP(Ordinary object pointer)为64位,
简单的讲,OOP可以被认为为对象的引用,虽然java中基本类型位数是固定的,
但引用类型(简化的C语言指针)用于指向堆中的地址很自然的会被扩展成机器的字长.
32位系统最大可访问内存为4G,为了突破这个限制64位系统已经很常见,但是单单引用从32位转为64位,堆空间占用大概会增加一半,虽然内存已经很便宜,
但是内存带宽,CPU缓存代价是很昂贵的.
Compressed
OOPS压缩可管理的引用到32位以降低堆的占用空间,在JVM执行时加入编/解码指令,类似于8086的段管理,其使用
<narrow-oop-base(64bits)> +
(<narrow-oop(32bits)>
<< 3) +
<field-offset>公式确定内存地址.
JVM在将对象存入堆时编码,在堆中读取对象时解码.
而Zero based compressed oops则进一步将基地址置为0(并不一定是内存空间地址为0,
只是JVM相对的逻辑地址为0,如可用CPU的寄存器相对寻址) 这样转换公式变为:
(<narrow-oop << 3) +
<field-offset>
从而进一步提高了性能.不过这需要OS的支持.
如果java堆<4G,oops使用低虚拟地址空间,而并不需要编/解码而直接使用.
Zero based compressed oops针对不同的堆大小使用多种策略.
1.堆小于4G,无需编/解码操作.
2.小于32G而大于4G,使用Zero based compressed oops
3.大于32G, 不使用compressed oops.
Escape Analysis Improvements
当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸
(Escape),也就是说变量不仅仅在本方法内使用.
Java对象一般被认为总是在堆中分配的,
这使得任何对象都需要进行垃圾回收.而大多数情况下,方法内的对象仅在本方法中使用,完全可以使用栈来存储,栈内变量释放是最自然,性能最好的,C中的
struct即在分配在栈中.如果实现引用逃逸分析,便可以把没有引用逃逸的对象分配在栈中,而且不必在语言上加入新的定义方法,引用逃逸分析是自动
了.JDK7已经开始缺省支持的逃逸分析了.另此还可以消除同步,如果其分析得知对象为非引用逃逸,则所有该对象的同步操作都可以被取消(当然这本是程序
员的任务,比如StringBuffer),另可优化对象的部分甚至全部都保存在CPU寄存器内.
NUMA Collector Enhancements
NUMA(Non Uniform Memory
Access),NUMA在多种计算机系统中都得到实现,简而言之,就是将内存分段访问,类似于硬盘的RAID,Oracle中的分簇,JVM只不过对此加以应用而矣.
以上三个特性也能在有些JDK6中打开,具体需要看各版本的changenotes.
java6中加入了诸如以下的性能优化手段:
轻量锁 使用cas机制减少锁的性能消耗.
偏向锁(biased locking)
锁粗化(lock coarsening)
由逸出(escape)分析产生的锁省略
逸出分析还能够分配内存在栈中,以减少内存回收的压力.
自适应自旋锁(adaptive spinning) 自旋锁只有在物理多CPU中才会效果.
锁消除(lock elimination)
在多核CPU中,锁的获取比单核系统耗费资源相对大的多, 因为在多核系统中,锁的获取需要CPU阻塞数据总线,高速缓存写回.
这样有时候, 我们在单核系统中,经常会得到StringBuffer与StringBuilder性能差不多的用例,
而且由于有了锁消除等技术, 有些情况在多核CPU中也会得到性能相差不多的情况.
据信Java7还将缺省支持OpenGL的加速功能.
在JDK1.5中加入了Class Data Sharing, 也就是把部分常用的java基本类,缓存在文件或共享内存中,
以供所有java进程使用.
从JRE1.5中,java程序启动时,如非使用-client|server指令显示指定,虚拟机会自动选择对应的VM,如在64位系统中,只实现了serverVM,所有的虚拟机都会使用server
VM.
32位的系统则windows缺省使用clientVM,而Linux,solaris则根据CPU个数和内存来确定是否使用serverVM,如jre6以2CPU,2GB物理内存为界.
GC
衡量GC效率的参数主要有两个,一个是吞吐量(即效率),一个是停顿时间,另外还有footprint,就是占用的堆大小.
GC算法.
1.拷贝,将所有仍然生存的对象搬到另外一块内存后,整块内存就可回收。这种方法有效率,但需要有一定的空闲内存,拷贝也有开销.
2.跟踪收集器,跟踪收集成追踪从根节点开始的对象引用图。基本的追踪算法叫作“标记并清除”,也就是垃圾收集的两个阶段。标记阶段,垃圾收集器遍历引用
数,标记每一个遇到的对象。清除阶段,未被标记的对象被释放。可能在对象本身设置标记,要么就是用一个独立的位图来设置标记。 压缩(可选),垃圾收集同
时要应对碎片整理的任务。标记和清除通常使用两种策略来消除堆碎片:压缩和拷贝,这两种方法都是快速移动对象来减小碎片,
加在一起叫做mark-sweep-compact.
3.还有一种引用计数收集器,这种方法时堆中的每个对象都有一个引用计数,在引用赋值时加1,置空或作为基本类型的引用超出生命期(如方法退出而栈回收)时减1,其对多个对象的循环引用无能为力,但引用计数都不为0
,还有引用数的增减带来额外开销,故已不再使用.
分代收集器
根据程序的统计, 大多数对象生命周期都很短,都很快被释放掉.但也有部分对象生命周期较长, 甚至永久有效.
对于拷贝算法来说,每次收集时,所有的活动对象都要移动来移动去。对于短生命的对象还好说,经常可以就地解决掉,可是对于长生命周期的对象就纯粹是个体力
劳动了,把它挪来挪去除消耗大量的时间,没有产生任何效益。分代收集能直接让长生命周期的对象长时间的呆在一个地方按兵不动。GC
的精力可以更多的花在收集短命对象上。
这种方法里,堆被分成两个或更多的子堆,每一个堆为一“代”对象服务。最年幼的那一代进行最频繁的垃圾收集。因为多数对象是短命的,只有很小部分的年
幼对象可以在经历第一次收集后还存活。如果一个最年幼的对象经历了好几次垃圾收集后仍是活着的,那这个对象就成为寿命更高的一代,它被转移到另外一个子堆
中去。年龄更高一代的收集没有年轻一代来得频繁。每当对象在所属的年龄代中变得成熟(多次垃圾收集后仍幸存)之后,就可以转移到更高年龄的一代中去。
分代收集一般在年轻堆中应用于拷贝算法,年老代应用于标记清除算法。不管在哪种情况下,把堆按照对象年龄分组可以提高最基本的垃圾收集的性能。
一般java中分代收集器将堆分为年轻代, 年老代和永久代.
年轻代的收回称为minorGC,因为在此期内,对象生命周期很较,故效率较高,
年老代称为FullGC,对应的效率较低,用时较长,应尽量减少FullGC的次数.
VM,
Client VM 适合桌面程序,启动快, 运行时间短, 故其不会预先装入太多的类,对类进行过多优化.
Server VM 适合服务程序,启动时间不重要,运行时间较长, 会预先装入大多基础类,对类进行优化.
GC种类.
Serial 串行回收器(缺省)
在GC运行时,
应用逻辑全部暂停,利用单线程通过"拷贝"进行年轻代的垃圾收集,单线程使用"标记-清除-压缩"进行年老代(tenured)垃圾回收.
吞吐率较高.适合单CPU硬件.
Parallel 并行回收器
针对年轻代使用多个GC线程进行"拷贝"垃圾收集,针对年轻代的GC运行时,程序暂停,
年老代依然是单线程使用"标记-清除-压缩"进行年老代垃圾回收,GC运行时,
应用同样暂停.在大内存,多处理器的机器上,可以考虑使用此Parallel
GC(使用参数-XX:+UseParallelGC指定),这种GC在对YoungGen进行GC时,可以对多处理器加以利用,从而相对降低了停顿时间,但重点是提高了吞吐量,但是,在其对OldGen进行GC时,依然使用了和Serial
GC同样的算法。所以在Jdk5U6中,又引入了Parallel Compacting
Collector(使用参数-XX:+UseParallelOldGC指定),这种GC对OldGen的GC也可以受益于多处理器。由于对OldGen的GC远比YoungGen更耗时间,所以理论上这种Garbage
Collector可以提供更优的性能,而且,值得注意的是,Parallel Compacting GC最终会取代Parallel
GC。
Concurrent mark-sweep 并发回收器.
对于年轻代使用和多GC线程"拷贝"回收,此GC也需要暂停应用,但由于minorGC效率较高,故不会产生大的停顿,对于年老代使用与应用程序同时运行
的并发方式标记-回收机制,其将步骤再次分细,部分阶段(初始标记,重新标记)也会完全导致应用暂停,但时间较短,大部分时间都是应用程序与单GC线程并
发,降低了应用程序暂停的时间。这种GC使用了和Parallel
GC一致的YoungGen的收集算法,而在对OldGen进行GC时,它采用了较为复杂的算法,提供了极短的停顿时间。但是,复杂的算法也造成了更大的
开销,而且这种
Parallel GC是non-compacting的,所以它使用一个空闲块链表来管理OldGen
Heap,分配空间的开销也加大了.在某些场景中,较短的停顿时间比较大的吞吐量更加重要,这时可以考虑使用此GC,即所谓的CMS
GC。
增量收集器(Train算法)已逐渐被弃用,-XincGC 在1.5中会选中并发GC.
在SUN J2SE 5.0中,引入了所谓Behavior-based Parallel Collector
Tuning,这种调优方式基于三个Goal:
Maximum Pause Time Goal:
使用参数-XX:MaxGCPauseMillis=n指定,默认值为空。这个参数被指定后,三个内存区的GC停顿时间都会尽力的保持在n毫秒以内,如果无法满足,则相应的内存区会缩小,以缩短GC的停顿时间;
Throughput Goal:
使用参数-XX:GCTimeRatio=n指定,默认值为99,即GC时间占总的应用运行时间为1%。如果无法满足,相应的内存区会扩大,以提高应用在两次GC间的运行时间;
Footprint Goal: 由于眼下内存泛滥,所以这个Goal一般就不值得关注了;
这三个Goal的优先级为从上到下,即首先满足Maximum Pause Time Goal,再满足Throughput
Goal,最后再满足Footprint Goal。
使用参数-Xloggc:file和-XX:+PrintGCDetails打印gclog,然后使用gcviewer对gclog进行查看,它的优势在于可以生成统计数据,吞吐量,最大的和最小的停顿时间,Full
GC时间占整个GC时间的百分比等,都可以使用这个工具查看,但目前只支持到1.5。
JConsole是允许您监测各种各样的VM资源运行时使用情况的Java监视和管理控制台。实际在java5中, 需要加一个参数,
在java6中由于支持了attach API,jconsole会自动加载JVM内部的JMX代理.
jstat命令打印各种各样的VM统计数据,包括内存使用、垃圾回收时间、类加载和及时编译器统计。 jmap
命令允许您获得运行时的堆直方图和堆转储。jhat命令允许您分析堆转储。jstack命令允许您获得线程堆栈跟踪。这些诊断工具可以附加到任何应用程序,不需要以特别方式启动。