浅谈JVM中的垃圾回收

字数 8615阅读 857

作者:一字马胡

转载标志 【2017-11-12】

更新日志

日期更新内容备注

2017-11-12新建文章初版

导入

作为Java语言的使用者,不像C++那样需要自己负责内存的申请和释放,因为Java语言有垃圾收集器(garbage collector GC),GC会负责将那些不再使用的对象释放掉,所以一般情况下,JVM的内存管理对开发者是无感知的,我们只需要设置一些运行时VM参数就可以了,但是,JVM发展至今,GC的类型和数量已经很丰富了,学习和理解GC是一个合格的Java开发者的必修课,因为在学习和理解了不同的GC之后,可以在合适的场景下选择合适的GC,不同的GC适用于不同的场景,并且不同的应用相同的GC也需要设置不同的参数,不理解GC或者参数,就无法正确设置以获得最佳性能。本文并不会对GC做太深入的学习总结,并且学习是一步一步来的,本文对JVM的GC做一个概述,大概知道GC是什么,有哪些GC,参数有哪些,怎么设置等等,更为深入具体的对于GC的学习总结将在未来持续输出。

JVM运行时数据区

下面的图片展示了JVM的运行时数据区:

JVM运行时数据区

程序计数器:指示当前线程所执行的字节码的行号。java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令,所以,为了线程切换后能回到正确的位置,每个线程都需要一个独立的程序计数器,这属于“线程私有”的内存区间。如果正在执行的是一个java方法,那么pc只指向的就是字节码指令的地址,如果正在执行的是一个native方法,那么pc为空(undefined),这个区域是java虚拟机中唯一一个没有规定OutOfMemoryError情况的区域。

java虚拟机栈:属于线程私有的区域。虚拟机栈描述的是java方法执行的内存模型,每个方法在执行时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量表存放了编译期间可知的各种基本数据类型、对象引用、returnAddress类型,局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法所需要在栈帧中分配的局部变量表的大小是完全确定的,在方法运行期间不会改变。该区域会有两种类型的异常:StackOverFlowError和OutOfMemoryError。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常,如果虚拟机栈可以动态扩展,但在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈:与虚拟机栈类似,不同点在于虚拟机栈为虚拟机执行java方法服务,而本地方法栈为虚拟机使用到的native方法服务。java堆:java堆是被所有线程共享的一块大内存,在虚拟机启动时创建,这个区域的唯一目的就是存放对象实例:“所有的对象实例和数组都要在堆上分配”。java堆是垃圾回收器主要管理的区域,所以也称为“GC堆”。java堆可以处于物理上不连续的空间,只要逻辑上连续就可以。当堆无法得到扩展时,会抛出OutOfMemoryError异常。

方法区:是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后产生的代码等。会抛出OutOfMemoryError异常。

运行时常量池:这个方法区的一部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放边缘期间产生的各种字面量和符号引用。java语言并不要求常量一定只有编译期间产生,运行期间也可能将新的常量放入常量池中,比如String的intern方法,该方法确保字符串来自常量池中,如果发现常量池中已经存在该字符串,则直接返回引用,否则,将这个字符串加入常量池中,再返回一个该字符串的引用。

下面的图片展示了HotSpot JVM的内存划分,本文的内容都是基于HotSpot JVM的:

HotSpot JVM内存划分

我们主要关心的是堆空间,因为我们的对象是分配在堆内存上的,可以看出来,在HotSpot JVM的内存划分中,堆被分成了几个部分,首先分成了两个部分,年轻代和老年代,年轻代又分为了Eden区,from区和to区,我们的大部分对象都是分配在Eden区上的,并且很快就会被回收,但是有一些对象总是会存活下来,当满足一定的条件的时候,年轻代的某些对象就会被移动到老年代。发生在年轻代的垃圾回收活动成为Young GC,还有一种垃圾回收活动成为Full GC,Full GC的垃圾收集涉及整个堆。

HotSpot JVM使用两种技术来加快内存分配,分别是”bump-the-pointer“和“TLABs(Thread-Local Allocation Buffers)”。Bump-the-pointer技术跟踪在Eden空间创建的最后一个对象。这个对象会被放在Eden空间的顶部。如果之后再需要创建对象,只需要检查Eden空间是否有足够的剩余空间。如果有足够的空间,对象就会被创建在Eden空间,并且被放置在顶部。这样以来,每次创建新的对象时,只需要检查最后被创建的对象。这将极大地加快内存分配速度。但是,如果我们在多线程的情况下,事情将截然不同。如果想要以线程安全的方式以多线程在Eden空间存储对象,不可避免的需要加锁,而这将极大地的影响性能。TLAB(Thread-Local Allocation Buffers) 是HotSpot虚拟机针对这一问题的解决方案。该方案为每一个线程在Eden空间分配一块独享的空间,这样每个线程只访问他们自己的TLAB空间,再与bump-the-pointer技术结合可以在不加锁的情况下分配内存。

初识GC

首先。GC需要负责的工作包括:

分配内存

监控对象,如果对象没有引用的时候需要辨别为垃圾

怎么识别一个对象是垃圾呢?有两种方法来分别,一种称为引用计数法,另外一种称为可达性分析。引用计数法比较好理解,每个对象都有一个引用计数器,当有一个地方引用了该对象,这个计数器就加1,当引用失效的时候减1,任何时候只要这个引用值为0的时候就成垃圾了(无法解决循环引用的问题)。另外一种辨别垃圾对象的算法为可达性分析,这个算法的基本思路是通过一系列名为 “GC Roots” 的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,就可以纳入可回收的范围。在 Java 语言里,可作为 GC Roots 对象的包括如下几种:

虚拟机栈(栈桢中的本地变量表)中的引用的对象;

方法区中的类静态属性引用的对象;

方法区中的常量引用的对象;

本地方法栈中 JNI(一般说的Native方法) 的引用的对象。

GC必须是安全可靠的,存活的对象不能被错误的标记为垃圾而被释放掉,而不再存活的对象应该被及时标记并且被回收。GC的执行应该是高效的,因为一般来说,GC在进行垃圾收集的时候需要Stop-the-world,所以,GC活动的时间越短越好,还有一点需要注意的是,GC过后可能会造成内存碎片问题,为了清除内存碎片,应该选择具备内存压缩能力的GC算法,但是这个过程需要非常高效,因为这不是GC的主要功能,合理的做法应该是GC之后不强制进行内存压缩操作,只有在GC过后一段时间频繁发现因为内存碎片问题而造成内存申请失败的情况下再进行内存压缩,因为这个时候JVM除了内存压缩别无选择。下面列举出了几个设计GC的性能指标:

吞吐量:垃圾回收的过程应该尽量高效,运行正常的应用的时间占比应该尽量高

垃圾收集代价:垃圾收集所占的时间应该尽量少

Stop-the-world的时候应该尽量少

垃圾收集频率:不能频繁进行GC活动,时间应该花在应用的运行上

GC算法分类

GC算法可以分为下面几类:

标记-清除算法(Mark-Sweep):首先标记出所有需要清除的对象,然后进行清除。缺点有两方面,在效率上来说,标记和清除的过程效率都不高,在空间上来说,清除过后会产生很多不连续的内存碎片,会造成很多内存碎片问题。

复制算法(Copying):算法将可用内存分为相同的两部分,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存一次性清理掉。在对象存活率较高的时候效率会较低,因为要进行较多的复制操作。

标记-整理算法(Mark-Compact):和标记-清除一样,前一个过程是一样的,都是将可回收的对象标记起来,但是标记-整理算法在标记起来之后,不是进行简单的清除,而是将所有的存活对象往一端移动,然后将那些需要清除的对象清除。

分代回收算法(Generational GC):当前商业虚拟机的垃圾收集都采用“分代收集”算法。就是讲java堆分为新生代和老年代,新生代中,每次垃圾收集时都发现有大量的对象死去,只有少量存活,就选用复制算法;而在老年代中,对象存活率很高,就使用“标记-清除”或者“标记-整理”算法。

现代JVM的GC大多都是基于分代回收的,所谓分代,就是将堆分成不同的代,然后对不同的代进行不同的回收。在HotSpot JVM的实现中,GC被分为两类:

Partial GC:并不会收集整个堆

(1)、Young GC:只会收集Young Gen的GC算法

(2)、Old GC:只会收集Old Gen的GC(只有CMS的concurrent collection是这个模式)

(3)、Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式

Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

可以根据GC算法的运行模式分为下面几类:

单线程GC:单线程GC在实现上使用单一线程来进行垃圾收集活动,比如Serial GC

并行GC:每次运行时,不管是YGC,还是FGC,会 stop-the-world,暂停所有的用户线程,并采用多个线程同时进行垃圾收集,比如Parallel GC。

并发GC:在新生代进行垃圾收集时和并行收集器类似,都是并行收集,而且都会stop-the-world,主要的区别在于老年代的收集上,在老年代进行垃圾收集时,大部分时间可以和用户线程并发执行的,只有小部分的时间stop-the-world,这就是它的优势,可以大大降低应用的暂停时间,比如CMS GC。

Serial GC

Serial GC属于单线程的GC,在进行垃圾收集的时候需要Stop-the-world直到Serial GC工作完成。使用串行收集器的年轻代垃圾收集。Eden区的活跃对象(live状态的对象)会被拷贝到初始为空的Survivor区中,这其中,那些体积过大以至于Survivor区装不下的对象不会进行拷贝。这些对象会被拷贝到老年代中。相对于已经被拷贝到To区的对象,源Survivor区中的live对象仍然比较年轻,而被拷贝到老年代中对象则相对年纪大一些。注意,若To区已经满了,来自Eden区或From区的对象就无法被拷贝到To区了,那么这些对象会被调整,无论经过多少次年轻代的垃圾收集,这些对象都不会被释放掉。在live对象被拷贝之后,Eden区和From区中还存在的对象就不再是live的了,它们不会再被检测。在年轻代垃圾收集完成后,Eden区和From区会被清空,只有To区会继续持有live状态的对象。此时,From区和To区在逻辑上交换,To区变成From区,原From区变成To区。

对于串行收集器,老年代和永生代会在进行垃圾收集时使用标记-清理-压缩(Mark-Sweep-Compact)算法。在标记阶段,收集器会标识哪些对象是live状态的。清理阶段会跨代清理,标识垃圾对象。然后,收集器执行移动压缩(sliding compaction),将live对象移动到老年代内存空间的起始部分(永生代中情况于此类似),这样在老年代内存空间的尾部会产生一个大的连续空间。

在非服务器类使用的机器上,默认选择的是串行垃圾收集器。在其他类型使用的机器上,可以通过添加参数-XX:+UseSerialGC来显式的使用串行垃圾收集器。

Serial Old GC

Serial Old GC是Serial GC的老年代版本,它同样是一个单线程GC,使用”标记-整理“算法,这个GC的主要意义在于给Client模式下的JVM使用,如果是在Server模式下,它还有两个用途,一种用在JDK 1.5以及之前的版本中与Parallel Scavenge GC搭配使用,另外一种用途是作为CMS GC的后备预案GC,在并发收集发生Concurrent Mode Failure的时候使用。

ParNew GC

ParNew GC是Serial GC的多多线程版本,除了使用多线程进行垃圾收集之外,其余的行为都和Serial GC一致。

Parallel Scavenge GC

Parallel Scavenge GC是用于新生代的一种GC,它属于并行的多线程收集器,使用复制算法,和ParNew GC类似。Parallel Scavenge GC的特别之处在于,它关注的是如何达到一个可控的GC吞吐量,而其他比如CMS等GC关注的则是尽量缩短GC导致的用户线程的停顿时间。

Parallel Old GC

Parallel Old GC是Parallel Scavenge GC的老年代版本,使用多线程和”标记-整理“算法,在比较注重吞吐量的场景下可以使用Parallel Scavenge GC + Parallel Old GC的GC组合,年轻代使用Parallel Scavenge GC,老年代则使用 Parallel Old GC。下面的图片展示了Serial GC和Parallel GC的区别:

CMS GC

下面的图片首先展示了Serial GC 和CMS GC的区别:

下面的图片展示了CMS中的内存结构:

CMS内存结构

CMS GC的执行流程如下:

第一步初始化标记(initial mark):这一步骤只是查找那些距离类加载器最近的幸存对象。因此,停顿的时间非常短暂。

第二步并行标记( concurrent mark ):所有被幸存对象引用的对象会被确认是否已经被追踪和校验。这一步的不同之处在于,在标记的过程中,其他的线程依然在执行。

第三步重新标记(remark):会再次检查那些在并行标记步骤中增加或者删除的与幸存对象引用的对象。

第四步并发清除( concurrent sweep ):转交垃圾回收过程处理。垃圾回收工作会在其他线程的执行过程中展开。

一旦采取了这种GC类型,由GC导致的暂停时间会极其短暂。CMS GC也被称为低延迟GC。它经常被用在那些对于响应时间要求十分苛刻的应用之上。当然,这种GC类型在拥有stop-the-world时间很短的优点的同时,也有如下缺点:

它会比其他GC类型占用更多的内存和CPU

默认情况下不支持压缩步骤

在使用这个GC类型之前你需要慎重考虑。如果因为内存碎片过多而导致压缩任务不得不执行,那么stop-the-world的时间要比其他任何GC类型都长,你需要考虑压缩任务的发生频率以及执行时间。

G1 GC

G1 GC是目前为止最复杂、也是最先进的GC,在CMS 算法中,GC 管理的内存被划分为新生代、老年代和永久代/元空间。这些空间必须是地址连续的。在G1算法中,采用了另外一种完全不同的方式组织堆内存,堆内存被划分为多个大小相等的内存块(Region),每个Region是逻辑连续的一段内存,Region的大小可以通过 -XX:G1HeapRegionSize 参数指定,如果没有设置,默认把堆内存按照2048份均分,最后得到一个合理的大小。在G1中,还有一种特殊的区域,叫 Humongous 区域。 如果一个对象占用的空间超过了分区容量 50% 以上,G1 收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1 划分了一个 Humongous 区,它用来专门存放巨型对象。下面的图片展示了G1的内存结构:

G1 GC内存结构

G1 GC的运行可以分为下面几个阶段:

初始标记(STW initial marking):扫描根集合,标记所有从根集合可直接到达的对象并将它们的字段压入扫描栈。在分代式G1模式中,初始标记阶段借用 Young GC 的暂停,因而没有额外的、单独的暂停阶段。

并发标记(concrrent marking):这个阶段可以并发执行,GC 线程 不断从扫描栈取出引用,进行递归标记,直到扫描栈清空。

最终标记(STW final marking,在实现中也叫Remarking):重新标记写入屏障( Write Barrier)标记的对象,这个阶段也进行弱引用处理(reference processing)。

筛选回收(Live Data Counting And evacuation):统计每个 Region 被标记为活的对象有多少,如果发现完全没有活对象的 Region 就会将其整体回收到可分配 Region 列表中。

与其他GC相比,G1 GC有如下特点:

并行与并发:G1 GC能充分利用CPU、多核心等硬件优势,使用多个CPU或者CPU核心来缩短STW的时间,部分其他GC需要停顿java线程执行的GC操作,在G1 GC中任然可以通过并发的方式让java程序继承执行。

分代收集:和其他GC一样,分代的概念在G1 GC中任然保留。

空间整合:与CMS的标记-清理算法不同,G1 GC从整体来看是通过”标记-整理“算法实现的GC,从局部(两个Region之间)来看是通过”复制“算法来实现的,无论如何,这两种算法在运行期间都不会产生内存碎片,GC 活动之后可以提供规整的内存空间。

可预测的停顿:这是G1 GC相对于CMS的另一大优势,降低停顿时间是G1 GC和CMS GC共同关注的,但是G1 GC除了追求低停顿时间外,还建立了可预测的停顿时间模型,能让使用这明确指定在一个长度为M的时间片内,消耗在垃圾收集上的时间不得超过N毫秒。

下面的图片展示了多个GC以及他们工作的分代位置,以及如何组合使用:

多种GC组合模式

JVM GC的触发条件

上文中提到了很多的GC,但是没有提到会在什么时候触发这些GC开始工作,根据HotSpot JVM的Serial GC的实现来看,触发GC工作的条件如下:

Young GC:当Young generation中的Eden区分配满的时候触发。

Full GC:(1)、当准备要触发一次young GC时,如果发现统计数据Young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Young GC而是转为触发Full GC。(2)、如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次Full GC(3)、调用System.gc()默认也是触发Full GC。

每个GC触发的条件都应该不太一样但是整体上是一样的,对于每一个GC的触发条件,需要研究每一个GC的实现,这些内容将在未来合适的时候补充上,本文点到为止。

GC参数说明

下面的表格说明了JVM的GC参数设置细节:

JVM参数描述

UseSerialGC虚拟机运行在Client模式下的默认值,打开此开关后,采用Serial + Serial Old的GC组合进行内存回收

UseParNewGC打开此开关后,使用ParNew + Serial Old的GC组合二

UseConcMarkSweepGC使用ParNew + CMS + Serial Old 的GC组合,Serial Old GC作为CMS GC出现Concurrent Mode Failure后的备用GC

UseParallelGCJVM 运行在Server模式下的默认值,使用Parallel Scavenge GC + Serial Old 的GC组合

UseParallelOldGC使用Parallel Scavenge GC + Parallel Old GC的GC组合

UseG1GC使用G1 GC来进行垃圾收集

MaxGCPauseMillis设置期望的GC停顿时间,仅对G1 GC有用

SurvivorRatio新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden : Survivor  = 8 : 1

PretenureSizeThreshold直接晋升到老年代的对象大小阈值,设置这个参数后,大于这个参数的对象将直接晋升为老年代

MaxTenuringThreshold晋升到老年代的对象年龄,每个对象在坚持一次Young GC之后年龄增加1,超过这个参数就会晋升到老年代

UseAdaptiveSizePolicy动态调整java堆中各个区域的大小以及进入老年代的年龄

ParallelGCThreads设置并行GC时进行内存回收的线程数量

GCTimeRatioGC时间占总时间的比率,默认为1%,仅在使用Parallel Scavenge GC的时候有效

MaxGCPauseMillis设置GC的最大停顿时间,仅对Parallel Scavenge GC有效

CMSInitiatingOccupancyFraction设置CMS GC在老年代空间被使用多少后出发GC默认值为68%,仅对于CMS有效

UseCMSCompactAtFullCollection设置CMS GC在进行一次垃圾收集之后是否需要进行内存碎片整理

CMSFullGCsBeforeCompaction设置CMS GC在进行了若干次垃圾收集之后进行一次内存碎片整理

理解GC日志

在设置JVM参数的时候,可以设置GC打印日志参数:-XX:+PrintGCDetails。下面是两条典型的GC输出日志:

33.125: [GC [DefNew:3324k->152k(3712k),0.0025925secs]3324k->152k(11904k),0.0031680secs]100.667: [Full GC [Tenured:0k->210k(10240k),0.0149142secs]4603k->210k(19456k), [Perm:2999k->2999k(21248k)],0.0150007secs] [Times: user=0.01sys=0.00real=0.02secs]

下面来解析一下GC日志,每个输出字段代表什么意思。首先是最前面的33.125和100.667,代表的是GC发生的时间,这个数字的含义是从Java JVM启动以来经过的秒数。然后是日志开头的“[GC” 和“[Full GC”说明了这次垃圾收集的类型。接下来的“[DefNew”、"[Tenured"、“[Perm”表示GC发生的区域,而后面的"3324k->152k(3712k)"表示的是“GC前该区域已使用量->GC后该区域已使用量(该区域总量)”。而在后面的"3324k->152k(11904k)"表示的是“GC前Java堆已使用量->GC后Java堆的使用量(Java堆的总量)”。后面的0.0025925 secs等表示的是该区域GC的时间。而[Times: user=0.01 sys=0.00 real=0.02 secs]则是更为详细的时间占比统计。在多核心或者多CPU以及使用多线程的情况下,多线程操作会叠加这些时间,所以user、sys以及real之间并没有某种恒等关系。

JVM 性能监控与故障处理工具

本节列出几个用于JVM性能监控与故障处理的有用工具,具体的使用细节还需要分别学习,并且在平时的实战中积累经验。

jps : JVM进程状况工具

jps命令格式:jps [options] [hostid]

jps主要选项:

选项作用

-m输出JVM进程启动时传递给主类main方法的参数

-l输出主类的全名,如果进程执行的是jar包,输出jar包的路径

-v输出进程启动时的JVM参数

jstat:JVM 统计信息监控工具

该工具具有丰富的JVM统计功能,具体支持的统计可以使用man jstat来输出帮助文档,下面以一个使用例子来说明该工具的使用方法:

首先运行一个Java程序,然后使用jps获取这个java进程的进程id,然后使用命令jstat命令来查看JVM的统计信息,比如我在本机启动了一个java进程,id为61659,然后使用jstat命令:jstat -gc 61659 1000 5,则输出内容如下:

S0C    S1C    S0U    S1U      EC      EU        OC        OU      MC    MU    CCSC  CCSU  YGC    YGCT    FGC    FGCT    GCT10752.010752.00.00.065536.01310.82796544.01085.14992.04419.0640.0504.430.00420.0180.02210752.010752.00.00.065536.01310.82796544.01085.14992.04419.0640.0504.430.00420.0180.02210752.010752.00.00.065536.01310.82796544.01085.14992.04419.0640.0504.430.00420.0180.02210752.010752.00.00.065536.01310.82796544.01085.14992.04419.0640.0504.430.00420.0180.02210752.010752.00.00.065536.01310.82796544.01085.14992.04419.0640.0504.430.00420.0180.022

我使用了jstat的gc统计选项,并且输出了所有的统计项信息,关于每个输出列的含义,可以参考下面的说明:

+-------+-------------------------------------------+                                                    |Column |                Description                |                                                    +-------+-------------------------------------------+                                                    |SOC    | Current survivor space0capacity (KB).  |                                                    |S1C    | Current survivor space1capacity (KB).  |                                                    |S0U    | Survivor space0utilization (KB).        |                                                    |S1U    | Survivor space1utilization (KB).        |                                                    |EC    |Current eden spacecapacity(KB).        |                                                    |EU    | Eden spaceutilization(KB).              |                                                    |OC    | Current old spacecapacity(KB).          |                                                    |OU    | Old spaceutilization(KB).              |                                                    |PC    | Current permanent spacecapacity(KB).    |                                                    |PU    | Permanent spaceutilization(KB).        |                                                    |YGC    | Number of young generation GC Events.    |                                                    |YGCT  | Young generation garbage collection time. |                                                    |FGC    | Number of full GC events.                |                                                    |FGCT  | Full garbage collection time.            |                                                    |GCT    | Total garbage collection time.            |                                                    +-------+-------------------------------------------+

jinfo:java配置信息工具

jinfo用于获取当前JVM的配置信息,比如下面的命令:

jinfo -flag PrintGCDetails61659

输出内容为:

-XX:-PrintGCDetails

jmap:java内存映射工具

jmap用于生成堆的转储快照,下面为一个使用示例,用于将当前的JVM的堆的快照输出到文件中去:

jmap -dump:format=b,file=heapdump.data pid

jhat:JVM堆转储快照分析工具

配合jmap使用,jmap用于生成堆的转储快照,而jhat可以将jmap的输出可视化,比如对于上面的输出文件heapdump.data文件,可以使用下面的命令生成可视化html:

jhat heapdump.data

等jhat执行完毕后,就可以打开浏览器查看堆的情况的。

jstack:JVM堆栈追踪工具

jstack用于生成当前堆栈的线程快照,这个命令会将所有在堆上的线程都输出,包括线程的运行状态,持有资源的状态等等,对于java应用调优,jstack是非常有用的。比如在我的机器上执行下面的命令:

jstack61659> threaddump.data

那么java进程id为61659的进程的堆栈的线程信息会被输出到文件threaddump.data文件中去,下面是一小段内容:

"pool-1-thread-1"#11prio=5os_prio=31tid=0x00007f9bad840800nid=0x5a03waiting on condition [0x000070000d8fa000]  java.lang.Thread.State: WAITING (parking)    at sun.misc.Unsafe.park(Native Method)    - parking to waitfor<0x00000006c006ab48> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)    at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081)    at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)    at java.lang.Thread.run(Thread.java:748)

这是线程名字为pool-1-thread-1的线程的堆栈信息,可以看出它目前的状态是WAITING (parking),并且可以看出是因为parking to wait for  <0x00000006c006ab48> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject),线程id的16进制表示为0x5a03,所以在需要查看某个线程的堆栈状态的时候需要先查找到线程id,然后将这个线程id转换为16进制(printf "%x\n" id),然后使用jstack将当前的堆栈内的线程快照输出到文件,然后在这个文件中查找相应的线程来看起状态,看是否在等待锁等信息。

结语

本文在一个较牵线的层面上进行了一些关于java JVM GC的总结记录,主要目的是希望自己对GC以及JVM有一些大概的认识,并且希望自己能在未来的很长一段时间持续学习关于JVM以及GC的内容,并且能够有一些总结文档输出。文章中的大部分内容都出自《深入理解Java虚拟机(第二版)》一书,该书是关于JVM的经典书籍,需要多次研读,文章中还有很多内容来自各种资料,目前流传的关于JVM的资料很多,内容鱼龙混杂,所以本文可能会漏洞百出,并且相等不全面,但是这些都不是问题,有问题的内容将在未来的某个时候被修复,更为具体、深入、全面的关于JVM以及GC的内容将在未来会不断总结输出,学无止境啊!

作者:一字马胡

链接:http://www.jianshu.com/p/2a8d6231d995

來源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

推荐阅读更多精彩内容