借助HotSpot SA来一窥PermGen上的对象

(Disclaimer:如果需要转载请先与我联系;

作者:RednaxelaFX -> rednaxelafx.iteye.com)

接着与,今天也来介绍一个的(以下简称SA)的玩法例子。

昨天用SA把x86机器码反汇编到汇编代码,或许对多数Java程序员来说并不怎么有趣。那么今天就来点更接近Java,但又经常被误解的话题——HotSpot的GC堆的permanent generation。

要用SA里最底层的API来连接上一个Java进程并不困难,不过SA还提供了更方便的封装:只要继承 sun.jvm.hotspot.tools.Tool 并实现一个 run() 方法,在该方法内使用SA的API访问JVM即可。

这次我们就把一个跑在HotSpot上的Java进程的perm gen里所有对象的信息打到标准输出流上看看吧。

测试环境是32位Linux,x86,Sun JDK 6 update 2

(手边可用的JDK版本很多,随便拿了一个来用,呵呵 >_

代码如下:

Java代码

import sun.jvm.hotspot.gc_implementation.parallelScavenge.PSPermGen;

import sun.jvm.hotspot.gc_implementation.parallelScavenge.ParallelScavengeHeap;

import sun.jvm.hotspot.gc_implementation.shared.MutableSpace;

import sun.jvm.hotspot.gc_interface.CollectedHeap;

import sun.jvm.hotspot.memory.Universe;

import sun.jvm.hotspot.oops.HeapPrinter;

import sun.jvm.hotspot.oops.HeapVisitor;

import sun.jvm.hotspot.oops.ObjectHeap;

import sun.jvm.hotspot.runtime.VM;

import sun.jvm.hotspot.tools.Tool;

/**

* @author sajia

*

*/

public class TestPrintPSPermGen extends Tool {

public static void main(String[] args) {

TestPrintPSPermGen test = new TestPrintPSPermGen();

test.start(args);

test.stop();

}

@Override

public void run() {

VM vm = VM.getVM();

Universe universe = vm.getUniverse();

CollectedHeap heap = universe.heap();

puts("GC heap name: " + heap.kind());

if (heap instanceof ParallelScavengeHeap) {

ParallelScavengeHeap psHeap = (ParallelScavengeHeap) heap;

PSPermGen perm = psHeap.permGen();

MutableSpace permObjSpace = perm.objectSpace();

puts("Perm gen: [" + permObjSpace.bottom() + ", " + permObjSpace.end() + ")");

long permSize = 0;

for (VM.Flag f : VM.getVM().getCommandLineFlags()) {

if ("PermSize".equals(f.getName())) {

permSize = Long.parseLong(f.getValue());

break;

}

}

puts("PermSize: " + permSize);

}

puts();

ObjectHeap objHeap = vm.getObjectHeap();

HeapVisitor heapVisitor = new HeapPrinter(System.out);

objHeap.iteratePerm(heapVisitor);

}

private static void puts() {

System.out.println();

}

private static void puts(String s) {

System.out.println(s);

}

}

很简单,假定目标Java进程用的是Parallel Scavenge(PS)算法的GC堆,输出GC堆的名字,当前perm gen的起始和结束地址,VM参数中设置的PermSize(perm gen的初始大小);然后是perm gen中所有对象的信息,包括对象摘要、地址、每个成员域的名字、偏移量和值等。

对HotSpot的VM参数不熟悉的同学可以留意一下几个参数在HotSpot源码中的定义:

C++代码

product(ccstrlist, OnOutOfMemoryError, "",

"Run user-defined commands on first java.lang.OutOfMemoryError")

product(bool, UseParallelGC, false, "Use the Parallel Scavenge garbage collector")

product_pd(uintx, PermSize, "Initial size of permanent generation (in bytes)")

product_pd(uintx, MaxPermSize, "Maximum size of permanent generation (in bytes)")

要让SA连接到一个正在运行的Java进程最重要是提供进程ID。获取pid的方法有很多,今天演示的是利用OnOutOfMemoryError参数指定让HotSpot在遇到内存不足而抛出OutOfMemoryError时执行一段用户指定的命令;在这个命令中可以使用%p占位符表示pid,HotSpot在执行命令时会把真实pid填充进去。

然后来造一个引发OOM的导火索:

Java代码

public class Foo {

public static void main(String[] args) {

Long[] array = new Long[256*1024*1024];

}

}

对32位HotSpot来说,main()方法里的new Long[256*1024*1024]会试图创建一个大于1GB的数组对象,那么只要把-Xmx参数设到1GB或更小便足以引发OOM了。

如何知道这个数组对象会占用超过1GB的呢?Long[]是一个引用类型的数组,只要知道32位HotSpot中采用的对象布局:

-----------------------(+0) | _mark | -----------------------(+4) | _metadata | -----------------------(+8) | 数组长度 length | -----------------------(+12+4*0) | 下标为0的元素 | -----------------------(+12+4*1) | 下标为1的元素 | ----------------------- | ... | -----------------------(+12+4*n) | 下标为n的元素 | ----------------------- | ... | -----------------------

就知道一大半了~

跑一下Foo程序。留意到依赖SA的代码要编译的话需要$JAVA_HOME/lib/sa-jdi.jar在classpath上,执行时同理。指定GC算法为Parallel Scavenge,并指定Java堆(不包括perm gen)的初始和最大值都为1GB:

Command prompt代码

[sajia@sajia ~]$ java -server -version

java version "1.6.0_02"

Java(TM) SE Runtime Environment (build 1.6.0_02-b05)

Java HotSpot(TM) Server VM (build 1.6.0_02-b05, mixed mode)

[sajia@sajia ~]$ javac Foo.java

[sajia@sajia ~]$ javac -classpath ".:$JAVA_HOME/lib/sa-jdi.jar" TestPrintPSPermGen.java

[sajia@sajia ~]$ java -server -XX:+UseParallelGC -XX:OnOutOfMemoryError='java -cp $JAVA_HOME/lib/sa-jdi.jar:. TestPrintPSPermGen %p > foo.txt' -Xms1g -Xmx1g Foo

#

# java.lang.OutOfMemoryError: Java heap space

# -XX:OnOutOfMemoryError="java -cp $JAVA_HOME/lib/sa-jdi.jar:. TestPrintPSPermGen %p > foo.txt"

#   Executing /bin/sh -c "java -cp $JAVA_HOME/lib/sa-jdi.jar:. TestPrintPSPermGen 23373 > foo.txt"...

Attaching to process ID 23373, please wait...

Debugger attached successfully.

Server compiler detected.

JVM version is 1.6.0_02-b05

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

at Foo.main(Foo.java:5)

[sajia@sajia ~]$

得到的foo.txt就是要演示的输出结果。把它压缩了放在附件里,有兴趣但懒得自己实验的同学也可以观摩一下~

在foo.txt的开头可以看到:

Log代码

GC heap name: ParallelScavengeHeap

Perm gen: [0x70e60000, 0x71e60000)

PermSize: 16777216

这里显示了GC堆确实是Parallel Scavenge的,其中perm gen当前的起始地址为0x70e60000,结束地址为0x71e60000,中间连续的虚拟内存空间都分配给perm gen使用。简单计算一下可知perm gen大小为16MB,与下面打出的PermSize参数的值完全吻合。

通过阅读该日志文件,可以得知HotSpot在perm gen里存放的对象主要有:

- Klass系对象

- java.lang.Class对象

- 字符串常量

- 符号(Symbol/symbolOop)常量

- 常量池对象

- 方法对象

等等,以及它们所直接依赖的一些对象。具体这些都是什么留待以后有空再写。

接下来挑几个例子来简单讲解一下如何阅读这个日志文件里的对象描述。

首先看一个String对象。先看看JDK里java.lang.String对象的声明是什么样的:

Java代码

package java.lang;

// imports ...

public final class String

implements java.io.Serializable, Comparable, CharSequence

{

/** The value is used for character storage. */

private final char value[];

/** The offset is the first index of the storage that is used. */

private final int offset;

/** The count is the number of characters in the String. */

private final int count;

/** Cache the hash code for the string */

private int hash; // Default to 0

/** use serialVersionUID from JDK 1.0.2 for interoperability */

private static final long serialVersionUID = -[***********]0L;

private static final ObjectStreamField[] serialPersistentFields =

new ObjectStreamField[0];

public static final Comparator CASE_INSENSITIVE_ORDER

= new CaseInsensitiveComparator();

private static class CaseInsensitiveComparator

implements Comparator, java.io.Serializable {

// ...

}

}

留意到String对象有4个成员域,分别是:

名字类型引用类型还是值类型

valuechar[]引用类型

offsetint值类型

countint值类型

hashint值类型

String类自身有三个静态变量,分别是:

名字类型引用类型还是值类型备注

serialVersionUIDlong值类型常量

serialPersistentFieldsjava.io.ObjectStreamField[]引用类型只读变量

CASE_INSENSITIVE_ORDERjava.lang.String.CaseInsensitiveComparator引用类型只读变量

回到我们的foo.txt日志文件来看一个String的对象实例:

Log代码

"main" @ 0x7100b140 (object size = 24)

- _mark:    {0} :1

- _klass:   {4} :InstanceKlass for java/lang/String @ 0x70e6c6a0

- value:    {8} :[C @ 0x7100b158

- offset:   {12} :0

- count:    {16} :4

- hash:     {20} :0

这是在HotSpot的字符串池里的一个字符串常量对象,"main"。

日志中的“"main"”是对象的摘要,String对象有特别处理显示为它的内容,其它多数类型的对象都是显示类型名之类的。

在@符号之后的就是对象的起始地址,十六进制表示。

紧接着后面是对象占用GC堆的大小。很明显这个String对象自身占用了24字节。这里强调是“占用”的大小是因为对象除了存储必要的数据需要空间外,为了满足数据对齐的要求可能会有一部分空间作为填充数据而空占着。

String在内存中的布局是:

-----------------------(+0) | _mark | -----------------------(+4) | _metadata | -----------------------(+8) | value | -----------------------(+12)| offset | -----------------------(+16)| count | -----------------------(+20)| hash | -----------------------

32位HotSpot上要求64位/8字节对齐,String占用的24字节正好全部都是有效数据,不需要填充空数据。

上面的String实例在内存中的实际数据如下:

偏移量(字节)数值(二进制表示)数值(十六进制表示)宽度(位/字节)

+[***********][***********]0000132位/4字节

+[***********][**************]70e6c6a032位/4字节

+[***********][***********]0b15832位/4字节

+[***********][***********]00000032位/4字节

+[***********][***********]00000432位/4字节

+[***********][***********]00000032位/4字节

OK,那我们来每个成员域都过一遍,看看有何玄机。

第一个是_mark。在HotSpot的C++代码里它的类型是markOop,在SA里以sun.jvm.hotspot.oops.Mark来表现。

它属于对象头(object header)的一部分,是个多用途标记,可用于记录GC的标记(mark)状态、锁状态、偏向锁(bias-locking)状态、身份哈希值(identity hash)缓存等等。它的可能组合包括:

比特域(名字或常量值:位数)标识(tag)状态

身份哈希值:25, 年龄:4, 0:101未锁

锁记录地址:3000被轻量级锁住

monitor对象地址:3010被重量级锁住

转向地址:3011被GC标记

线程ID:23, 纪元:2, 年龄:4, 1:101被偏向锁住/可被偏向锁

例子中的"main"字符串的_mark值为1,也就是说它:

- 没有被锁住;

- 现在未被GC标记;

- 年龄为0(尚未经历过GC);

- 身份哈希值尚未被计算。

HotSpot的GC堆中许多创建没多久的对象的_mark值都会是1,属于正常现象。

接下来看SA输出的日志中写为_klass而在我的图示上写为_metadata的这个域。

在HotSpot的C++代码里,oopDesc是所有放在GC堆上的对象的顶层类,它的成员就构成了对象头。HotSpot在C++代码中用instanceOopDesc类来表示Java对象,而该类继承oopDesc,所以HotSpot中的Java对象也自然拥有oopDesc所声明的头部。

hotspot/src/share/vm/oops/oop.hpp:

C++代码

class oopDesc {

private:

volatile markOop  _mark;

union _metadata {

wideKlassOop    _klass;

narrowOop       _compressed_klass;

} _metadata;

};

_metadata与前面提过的_mark一同构成了对象头。

_metadata是个union,为了能兼容32位、64位与开了压缩指针(CompressedOops)等几种情况。无论是这个union中的_klass还是_compressed_klass域,它们都是用于指向一个描述该对象的klass对象的指针。SA的API屏蔽了普通指针与压缩指针之间的差异,所以就直接把_metadata._klass称为了_klass。

对象头的格式是固定的,而对象自身内容的布局则由HotSpot根据一定规则来决定。Java类在被HotSpot加载时,其对象实例的布局与类自身的布局都会被计算出来。这个计算规则有机会以后再详细写。

现在来看看"main"这个String对象实例自身的域都是些什么。

value:指向真正保存字符串内容的对象的引用。留意Java里String并不把真正的字符内容直接存在自己里面,而是引用一个char[]对象来承载真正的存储。

从Java一侧看value域的类型是char[],而从HotSpot的C++代码来看它就是个普通的指针而已。它当前值是0x7100b158,指向一个char[]对象的起始位置。

offset:字符串的内容从value指向的char[]中的第几个字符开始算(0-based)。int型,32位带符号整数,这从Java和C++来看都差不多。当前值为0。

count:该字符串的长度,或者说包含的UTF-16字符的个数。类型同上。当前值为4,说明该字符串有4个UTF-16字符。

hash:缓存该String对象的哈希值的成员域。类型同上。当前值为0,说明该实例的String.hashCode()方法尚未被调用过,因而尚未缓存住该字符串的哈希值。

String对象的成员域都走过一遍了,来看看value所指向的对象状况。

Log代码

[C @ 0x7100b158 (object size = 24)

- _mark:    {0} :1

- _klass:   {4} :TypeArrayKlass for [C @ 0x70e60440

- _length:  {8} :4

- 0:    {12} :m

- 1:    {14} :a

- 2:    {16} :i

- 3:    {18} :n

这就是"main"字符串的value所引用的char[]的日志。

[C 是char[]在JVM中的内部名称。

在@符号之后的0x7100b158是该对象的起始地址。

该对象占用GC堆的大小是24字节。留意了哦。

看看它的成员域。

_mark与_klass构成的对象头就不重复介绍了。可以留意的是元素类型为原始类型(boolean、char、short、int、long、float、double)的数组在HotSpot的C++代码里是用typeArrayOopDesc来表示的;这里的char[]也不例外。描述typeArrayOopDesc的klass对象是typeArrayKlass类型的,所以可以看到日志里_klass的值写着TypeArrayKlass for [C。

接下来是_length域。HotSpot中,数组对象比普通对象的头要多一个域,正是这个描述数组元素个数的_length。Java语言中数组的.length属性、JVM字节码中的arraylength要取的也正是这个值。

日志中的这个数组对象有4个字符,所以_length值为4。

再后面就是数组的内容了。于是该char[]在内存中的布局是:

-----------------------(+0) | _mark | -----------------------(+4) | _metadata | -----------------------(+8) | 数组长度 length | -----------------------(+12) | char[0] | char[1] | -----------------------(+16) | char[2] | char[3] | -----------------------(+20) | 填充0 | -----------------------

Java的char是UTF-16字符,宽度是16位/2字节;4个字符需要8字节,加上对象头的4*3=12字节,总共需要20字节。但该char[]却占用了GC堆上的24字节,正是因为前面提到的数据对齐要求——HotSpot要求GC堆上的对象是8字节对齐的,20向上找最近的8的倍数就是24了。用于对齐的这部分会被填充为0。

"main"对象的value指向的char[]也介绍过了,回过头来看看它的_metadata._klass所指向的klass对象又是什么状况。

从HotSpot的角度来看,klass就是用于描述GC堆上的对象的对象;如果一个对象的大小、域的个数与类型等信息不固定的话,它就需要特定的klass对象来描述。

instanceOopDesc用于表示Java对象,instanceKlass用于描述它,但自身却又有些不固定的信息需要被描述,因而又有instanceKlassKlass;如此下去会没完没了,所以有个klassKlass作为这个描述链上的终结符。

klass的关系图:

回到foo.txt日志文件上来,找到"main"对象的_klass域所引用的instanceKlass对象:

Log代码

InstanceKlass for java/lang/String @ 0x70e6c6a0 (object size = 384)

- _mark:    {0} :1

- _klass:   {4} :InstanceKlassKlass @ 0x70e60168

- _java_mirror:     {60} :Oop for java/lang/Class @ 0x70e77760

- _super:   {64} :InstanceKlass for java/lang/Object @ 0x70e65af8

- _size_helper:     {12} :6

- _name:    {68} :#java/lang/String @ 0x70e613e8

- _access_flags:    {84} :134217777

- _subklass:    {72} :null

- _next_sibling:    {76} :InstanceKlass for java/lang/CharSequence @ 0x70e680e8

- _alloc_count:     {88} :0

- _array_klasses:   {112} :ObjArrayKlass for InstanceKlass for java/lang/String @ 0x70ef6298

- _methods:     {116} :ObjArray @ 0x70e682a0

- _method_ordering:     {120} :[I @ 0x70e61330

- _local_interfaces:    {124} :ObjArray @ 0x70e67998

- _transitive_interfaces:   {128} :ObjArray @ 0x70e67998

- _nof_implementors:    {268} :0

- _implementors[0]:     {164} :null

- _implementors[0]:     {168} :null

- _fields:  {132} :[S @ 0x70e68230

- _constants:   {136} :ConstantPool for java/lang/String @ 0x70e65c38

- _class_loader:    {140} :null

- _protection_domain:   {144} :null

- _signers:     {148} :null

- _source_file_name:    {152} :#String.java @ 0x70e67980

- _inner_classes:   {160} :[S @ 0x70e6c820

- _nonstatic_field_size:    {196} :4

- _static_field_size:   {200} :4

- _static_oop_field_size:   {204} :2

- _nonstatic_oop_map_size:  {208} :1

- _is_marked_dependent:     {212} :0

- _init_state:  {220} :5

- _vtable_len:  {228} :5

- _itable_len:  {232} :9

- serialVersionUID:     {368} :-[***********]0

- serialPersistentFields:   {360} :ObjArray @ 0x74e882c8

- CASE_INSENSITIVE_ORDER:   {364} :Oop for java/lang/String$CaseInsensitiveComparator @ 0x74e882c0

还记得上文提到过的String类的3个静态变量么?有没有觉得有什么眼熟的地方?

没错,在HotSpot中,Java类的静态变量就是作为该类对应的instanceKlass的实例变量出现的。上面的日志里最后三行描述了String的静态变量所在。

这是件非常自然的事:类用于描述对象,类自身也是对象,有用于描述自身的类;某个类的所谓“静态变量”就是该类对象的实例变量。很多对象系统都是这么设计的。HotSpot的这套oop体系(指“普通对象指针”,不是指“面向对象编程”)继承自,实际上反而比暴露给Java的对象模型显得更加面向对象一些。

HotSpot并不把instanceKlass暴露给Java,而会另外创建对应的java.lang.Class对象,并将后者称为前者的“Java镜像”,两者之间互相持有引用。日志中的_java_mirror便是该instanceKlass对Class对象的引用。

镜像机制被认为是良好的面向对象的反射与元编程设计的重要机制。Gilad Bracha与David Ungar还专门写了篇论文来阐述此观点,参考。

顺带把"main"对象的_klass链上余下的两个对象的日志也贴出来:

Log代码

InstanceKlassKlass @ 0x70e60168 (object size = 120)

- _mark:    {0} :1

- _klass:   {4} :KlassKlass @ 0x70e60000

- _java_mirror:     {60} :Oop for java/lang/Class @ 0x70e76f20

- _super:   {64} :null

- _size_helper:     {12} :0

- _name:    {68} :null

- _access_flags:    {84} :0

- _subklass:    {72} :null

- _next_sibling:    {76} :null

- _alloc_count:     {88} :0

所有instanceKlass对象都是被这个instanceKlassKlass对象所描述的。

Log代码

KlassKlass @ 0x70e60000 (object size = 120)

- _mark:    {0} :1

- _klass:   {4} :KlassKlass @ 0x70e60000

- _java_mirror:     {60} :Oop for java/lang/Class @ 0x70e76e00

- _super:   {64} :null

- _size_helper:     {12} :0

- _name:    {68} :null

- _access_flags:    {84} :0

- _subklass:    {72} :null

- _next_sibling:    {76} :null

- _alloc_count:     {88} :0

而所有*KlassKlass对象都是被这个klassKlass对象所描述的。

klass对象的更详细的介绍也留待以后再写吧~至少得找时间写写instanceKlass与vtable、itable的故事。

嘛,今天的废话到此结束 ^_^

希望这帖能解答先前一帖中的疑问。也希望大家能多多支持圈子,有什么HLL VM相关的话题想讨论的欢迎来转转~

(1.7 MB)

描述: 文中例子的输出

下载次数: 45

(Disclaimer:如果需要转载请先与我联系;

作者:RednaxelaFX -> rednaxelafx.iteye.com)

接着与,今天也来介绍一个的(以下简称SA)的玩法例子。

昨天用SA把x86机器码反汇编到汇编代码,或许对多数Java程序员来说并不怎么有趣。那么今天就来点更接近Java,但又经常被误解的话题——HotSpot的GC堆的permanent generation。

要用SA里最底层的API来连接上一个Java进程并不困难,不过SA还提供了更方便的封装:只要继承 sun.jvm.hotspot.tools.Tool 并实现一个 run() 方法,在该方法内使用SA的API访问JVM即可。

这次我们就把一个跑在HotSpot上的Java进程的perm gen里所有对象的信息打到标准输出流上看看吧。

测试环境是32位Linux,x86,Sun JDK 6 update 2

(手边可用的JDK版本很多,随便拿了一个来用,呵呵 >_

代码如下:

Java代码

import sun.jvm.hotspot.gc_implementation.parallelScavenge.PSPermGen;

import sun.jvm.hotspot.gc_implementation.parallelScavenge.ParallelScavengeHeap;

import sun.jvm.hotspot.gc_implementation.shared.MutableSpace;

import sun.jvm.hotspot.gc_interface.CollectedHeap;

import sun.jvm.hotspot.memory.Universe;

import sun.jvm.hotspot.oops.HeapPrinter;

import sun.jvm.hotspot.oops.HeapVisitor;

import sun.jvm.hotspot.oops.ObjectHeap;

import sun.jvm.hotspot.runtime.VM;

import sun.jvm.hotspot.tools.Tool;

/**

* @author sajia

*

*/

public class TestPrintPSPermGen extends Tool {

public static void main(String[] args) {

TestPrintPSPermGen test = new TestPrintPSPermGen();

test.start(args);

test.stop();

}

@Override

public void run() {

VM vm = VM.getVM();

Universe universe = vm.getUniverse();

CollectedHeap heap = universe.heap();

puts("GC heap name: " + heap.kind());

if (heap instanceof ParallelScavengeHeap) {

ParallelScavengeHeap psHeap = (ParallelScavengeHeap) heap;

PSPermGen perm = psHeap.permGen();

MutableSpace permObjSpace = perm.objectSpace();

puts("Perm gen: [" + permObjSpace.bottom() + ", " + permObjSpace.end() + ")");

long permSize = 0;

for (VM.Flag f : VM.getVM().getCommandLineFlags()) {

if ("PermSize".equals(f.getName())) {

permSize = Long.parseLong(f.getValue());

break;

}

}

puts("PermSize: " + permSize);

}

puts();

ObjectHeap objHeap = vm.getObjectHeap();

HeapVisitor heapVisitor = new HeapPrinter(System.out);

objHeap.iteratePerm(heapVisitor);

}

private static void puts() {

System.out.println();

}

private static void puts(String s) {

System.out.println(s);

}

}

很简单,假定目标Java进程用的是Parallel Scavenge(PS)算法的GC堆,输出GC堆的名字,当前perm gen的起始和结束地址,VM参数中设置的PermSize(perm gen的初始大小);然后是perm gen中所有对象的信息,包括对象摘要、地址、每个成员域的名字、偏移量和值等。

对HotSpot的VM参数不熟悉的同学可以留意一下几个参数在HotSpot源码中的定义:

C++代码

product(ccstrlist, OnOutOfMemoryError, "",

"Run user-defined commands on first java.lang.OutOfMemoryError")

product(bool, UseParallelGC, false, "Use the Parallel Scavenge garbage collector")

product_pd(uintx, PermSize, "Initial size of permanent generation (in bytes)")

product_pd(uintx, MaxPermSize, "Maximum size of permanent generation (in bytes)")

要让SA连接到一个正在运行的Java进程最重要是提供进程ID。获取pid的方法有很多,今天演示的是利用OnOutOfMemoryError参数指定让HotSpot在遇到内存不足而抛出OutOfMemoryError时执行一段用户指定的命令;在这个命令中可以使用%p占位符表示pid,HotSpot在执行命令时会把真实pid填充进去。

然后来造一个引发OOM的导火索:

Java代码

public class Foo {

public static void main(String[] args) {

Long[] array = new Long[256*1024*1024];

}

}

对32位HotSpot来说,main()方法里的new Long[256*1024*1024]会试图创建一个大于1GB的数组对象,那么只要把-Xmx参数设到1GB或更小便足以引发OOM了。

如何知道这个数组对象会占用超过1GB的呢?Long[]是一个引用类型的数组,只要知道32位HotSpot中采用的对象布局:

-----------------------(+0) | _mark | -----------------------(+4) | _metadata | -----------------------(+8) | 数组长度 length | -----------------------(+12+4*0) | 下标为0的元素 | -----------------------(+12+4*1) | 下标为1的元素 | ----------------------- | ... | -----------------------(+12+4*n) | 下标为n的元素 | ----------------------- | ... | -----------------------

就知道一大半了~

跑一下Foo程序。留意到依赖SA的代码要编译的话需要$JAVA_HOME/lib/sa-jdi.jar在classpath上,执行时同理。指定GC算法为Parallel Scavenge,并指定Java堆(不包括perm gen)的初始和最大值都为1GB:

Command prompt代码

[sajia@sajia ~]$ java -server -version

java version "1.6.0_02"

Java(TM) SE Runtime Environment (build 1.6.0_02-b05)

Java HotSpot(TM) Server VM (build 1.6.0_02-b05, mixed mode)

[sajia@sajia ~]$ javac Foo.java

[sajia@sajia ~]$ javac -classpath ".:$JAVA_HOME/lib/sa-jdi.jar" TestPrintPSPermGen.java

[sajia@sajia ~]$ java -server -XX:+UseParallelGC -XX:OnOutOfMemoryError='java -cp $JAVA_HOME/lib/sa-jdi.jar:. TestPrintPSPermGen %p > foo.txt' -Xms1g -Xmx1g Foo

#

# java.lang.OutOfMemoryError: Java heap space

# -XX:OnOutOfMemoryError="java -cp $JAVA_HOME/lib/sa-jdi.jar:. TestPrintPSPermGen %p > foo.txt"

#   Executing /bin/sh -c "java -cp $JAVA_HOME/lib/sa-jdi.jar:. TestPrintPSPermGen 23373 > foo.txt"...

Attaching to process ID 23373, please wait...

Debugger attached successfully.

Server compiler detected.

JVM version is 1.6.0_02-b05

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

at Foo.main(Foo.java:5)

[sajia@sajia ~]$

得到的foo.txt就是要演示的输出结果。把它压缩了放在附件里,有兴趣但懒得自己实验的同学也可以观摩一下~

在foo.txt的开头可以看到:

Log代码

GC heap name: ParallelScavengeHeap

Perm gen: [0x70e60000, 0x71e60000)

PermSize: 16777216

这里显示了GC堆确实是Parallel Scavenge的,其中perm gen当前的起始地址为0x70e60000,结束地址为0x71e60000,中间连续的虚拟内存空间都分配给perm gen使用。简单计算一下可知perm gen大小为16MB,与下面打出的PermSize参数的值完全吻合。

通过阅读该日志文件,可以得知HotSpot在perm gen里存放的对象主要有:

- Klass系对象

- java.lang.Class对象

- 字符串常量

- 符号(Symbol/symbolOop)常量

- 常量池对象

- 方法对象

等等,以及它们所直接依赖的一些对象。具体这些都是什么留待以后有空再写。

接下来挑几个例子来简单讲解一下如何阅读这个日志文件里的对象描述。

首先看一个String对象。先看看JDK里java.lang.String对象的声明是什么样的:

Java代码

package java.lang;

// imports ...

public final class String

implements java.io.Serializable, Comparable, CharSequence

{

/** The value is used for character storage. */

private final char value[];

/** The offset is the first index of the storage that is used. */

private final int offset;

/** The count is the number of characters in the String. */

private final int count;

/** Cache the hash code for the string */

private int hash; // Default to 0

/** use serialVersionUID from JDK 1.0.2 for interoperability */

private static final long serialVersionUID = -[***********]0L;

private static final ObjectStreamField[] serialPersistentFields =

new ObjectStreamField[0];

public static final Comparator CASE_INSENSITIVE_ORDER

= new CaseInsensitiveComparator();

private static class CaseInsensitiveComparator

implements Comparator, java.io.Serializable {

// ...

}

}

留意到String对象有4个成员域,分别是:

名字类型引用类型还是值类型

valuechar[]引用类型

offsetint值类型

countint值类型

hashint值类型

String类自身有三个静态变量,分别是:

名字类型引用类型还是值类型备注

serialVersionUIDlong值类型常量

serialPersistentFieldsjava.io.ObjectStreamField[]引用类型只读变量

CASE_INSENSITIVE_ORDERjava.lang.String.CaseInsensitiveComparator引用类型只读变量

回到我们的foo.txt日志文件来看一个String的对象实例:

Log代码

"main" @ 0x7100b140 (object size = 24)

- _mark:    {0} :1

- _klass:   {4} :InstanceKlass for java/lang/String @ 0x70e6c6a0

- value:    {8} :[C @ 0x7100b158

- offset:   {12} :0

- count:    {16} :4

- hash:     {20} :0

这是在HotSpot的字符串池里的一个字符串常量对象,"main"。

日志中的“"main"”是对象的摘要,String对象有特别处理显示为它的内容,其它多数类型的对象都是显示类型名之类的。

在@符号之后的就是对象的起始地址,十六进制表示。

紧接着后面是对象占用GC堆的大小。很明显这个String对象自身占用了24字节。这里强调是“占用”的大小是因为对象除了存储必要的数据需要空间外,为了满足数据对齐的要求可能会有一部分空间作为填充数据而空占着。

String在内存中的布局是:

-----------------------(+0) | _mark | -----------------------(+4) | _metadata | -----------------------(+8) | value | -----------------------(+12)| offset | -----------------------(+16)| count | -----------------------(+20)| hash | -----------------------

32位HotSpot上要求64位/8字节对齐,String占用的24字节正好全部都是有效数据,不需要填充空数据。

上面的String实例在内存中的实际数据如下:

偏移量(字节)数值(二进制表示)数值(十六进制表示)宽度(位/字节)

+[***********][***********]0000132位/4字节

+[***********][**************]70e6c6a032位/4字节

+[***********][***********]0b15832位/4字节

+[***********][***********]00000032位/4字节

+[***********][***********]00000432位/4字节

+[***********][***********]00000032位/4字节

OK,那我们来每个成员域都过一遍,看看有何玄机。

第一个是_mark。在HotSpot的C++代码里它的类型是markOop,在SA里以sun.jvm.hotspot.oops.Mark来表现。

它属于对象头(object header)的一部分,是个多用途标记,可用于记录GC的标记(mark)状态、锁状态、偏向锁(bias-locking)状态、身份哈希值(identity hash)缓存等等。它的可能组合包括:

比特域(名字或常量值:位数)标识(tag)状态

身份哈希值:25, 年龄:4, 0:101未锁

锁记录地址:3000被轻量级锁住

monitor对象地址:3010被重量级锁住

转向地址:3011被GC标记

线程ID:23, 纪元:2, 年龄:4, 1:101被偏向锁住/可被偏向锁

例子中的"main"字符串的_mark值为1,也就是说它:

- 没有被锁住;

- 现在未被GC标记;

- 年龄为0(尚未经历过GC);

- 身份哈希值尚未被计算。

HotSpot的GC堆中许多创建没多久的对象的_mark值都会是1,属于正常现象。

接下来看SA输出的日志中写为_klass而在我的图示上写为_metadata的这个域。

在HotSpot的C++代码里,oopDesc是所有放在GC堆上的对象的顶层类,它的成员就构成了对象头。HotSpot在C++代码中用instanceOopDesc类来表示Java对象,而该类继承oopDesc,所以HotSpot中的Java对象也自然拥有oopDesc所声明的头部。

hotspot/src/share/vm/oops/oop.hpp:

C++代码

class oopDesc {

private:

volatile markOop  _mark;

union _metadata {

wideKlassOop    _klass;

narrowOop       _compressed_klass;

} _metadata;

};

_metadata与前面提过的_mark一同构成了对象头。

_metadata是个union,为了能兼容32位、64位与开了压缩指针(CompressedOops)等几种情况。无论是这个union中的_klass还是_compressed_klass域,它们都是用于指向一个描述该对象的klass对象的指针。SA的API屏蔽了普通指针与压缩指针之间的差异,所以就直接把_metadata._klass称为了_klass。

对象头的格式是固定的,而对象自身内容的布局则由HotSpot根据一定规则来决定。Java类在被HotSpot加载时,其对象实例的布局与类自身的布局都会被计算出来。这个计算规则有机会以后再详细写。

现在来看看"main"这个String对象实例自身的域都是些什么。

value:指向真正保存字符串内容的对象的引用。留意Java里String并不把真正的字符内容直接存在自己里面,而是引用一个char[]对象来承载真正的存储。

从Java一侧看value域的类型是char[],而从HotSpot的C++代码来看它就是个普通的指针而已。它当前值是0x7100b158,指向一个char[]对象的起始位置。

offset:字符串的内容从value指向的char[]中的第几个字符开始算(0-based)。int型,32位带符号整数,这从Java和C++来看都差不多。当前值为0。

count:该字符串的长度,或者说包含的UTF-16字符的个数。类型同上。当前值为4,说明该字符串有4个UTF-16字符。

hash:缓存该String对象的哈希值的成员域。类型同上。当前值为0,说明该实例的String.hashCode()方法尚未被调用过,因而尚未缓存住该字符串的哈希值。

String对象的成员域都走过一遍了,来看看value所指向的对象状况。

Log代码

[C @ 0x7100b158 (object size = 24)

- _mark:    {0} :1

- _klass:   {4} :TypeArrayKlass for [C @ 0x70e60440

- _length:  {8} :4

- 0:    {12} :m

- 1:    {14} :a

- 2:    {16} :i

- 3:    {18} :n

这就是"main"字符串的value所引用的char[]的日志。

[C 是char[]在JVM中的内部名称。

在@符号之后的0x7100b158是该对象的起始地址。

该对象占用GC堆的大小是24字节。留意了哦。

看看它的成员域。

_mark与_klass构成的对象头就不重复介绍了。可以留意的是元素类型为原始类型(boolean、char、short、int、long、float、double)的数组在HotSpot的C++代码里是用typeArrayOopDesc来表示的;这里的char[]也不例外。描述typeArrayOopDesc的klass对象是typeArrayKlass类型的,所以可以看到日志里_klass的值写着TypeArrayKlass for [C。

接下来是_length域。HotSpot中,数组对象比普通对象的头要多一个域,正是这个描述数组元素个数的_length。Java语言中数组的.length属性、JVM字节码中的arraylength要取的也正是这个值。

日志中的这个数组对象有4个字符,所以_length值为4。

再后面就是数组的内容了。于是该char[]在内存中的布局是:

-----------------------(+0) | _mark | -----------------------(+4) | _metadata | -----------------------(+8) | 数组长度 length | -----------------------(+12) | char[0] | char[1] | -----------------------(+16) | char[2] | char[3] | -----------------------(+20) | 填充0 | -----------------------

Java的char是UTF-16字符,宽度是16位/2字节;4个字符需要8字节,加上对象头的4*3=12字节,总共需要20字节。但该char[]却占用了GC堆上的24字节,正是因为前面提到的数据对齐要求——HotSpot要求GC堆上的对象是8字节对齐的,20向上找最近的8的倍数就是24了。用于对齐的这部分会被填充为0。

"main"对象的value指向的char[]也介绍过了,回过头来看看它的_metadata._klass所指向的klass对象又是什么状况。

从HotSpot的角度来看,klass就是用于描述GC堆上的对象的对象;如果一个对象的大小、域的个数与类型等信息不固定的话,它就需要特定的klass对象来描述。

instanceOopDesc用于表示Java对象,instanceKlass用于描述它,但自身却又有些不固定的信息需要被描述,因而又有instanceKlassKlass;如此下去会没完没了,所以有个klassKlass作为这个描述链上的终结符。

klass的关系图:

回到foo.txt日志文件上来,找到"main"对象的_klass域所引用的instanceKlass对象:

Log代码

InstanceKlass for java/lang/String @ 0x70e6c6a0 (object size = 384)

- _mark:    {0} :1

- _klass:   {4} :InstanceKlassKlass @ 0x70e60168

- _java_mirror:     {60} :Oop for java/lang/Class @ 0x70e77760

- _super:   {64} :InstanceKlass for java/lang/Object @ 0x70e65af8

- _size_helper:     {12} :6

- _name:    {68} :#java/lang/String @ 0x70e613e8

- _access_flags:    {84} :134217777

- _subklass:    {72} :null

- _next_sibling:    {76} :InstanceKlass for java/lang/CharSequence @ 0x70e680e8

- _alloc_count:     {88} :0

- _array_klasses:   {112} :ObjArrayKlass for InstanceKlass for java/lang/String @ 0x70ef6298

- _methods:     {116} :ObjArray @ 0x70e682a0

- _method_ordering:     {120} :[I @ 0x70e61330

- _local_interfaces:    {124} :ObjArray @ 0x70e67998

- _transitive_interfaces:   {128} :ObjArray @ 0x70e67998

- _nof_implementors:    {268} :0

- _implementors[0]:     {164} :null

- _implementors[0]:     {168} :null

- _fields:  {132} :[S @ 0x70e68230

- _constants:   {136} :ConstantPool for java/lang/String @ 0x70e65c38

- _class_loader:    {140} :null

- _protection_domain:   {144} :null

- _signers:     {148} :null

- _source_file_name:    {152} :#String.java @ 0x70e67980

- _inner_classes:   {160} :[S @ 0x70e6c820

- _nonstatic_field_size:    {196} :4

- _static_field_size:   {200} :4

- _static_oop_field_size:   {204} :2

- _nonstatic_oop_map_size:  {208} :1

- _is_marked_dependent:     {212} :0

- _init_state:  {220} :5

- _vtable_len:  {228} :5

- _itable_len:  {232} :9

- serialVersionUID:     {368} :-[***********]0

- serialPersistentFields:   {360} :ObjArray @ 0x74e882c8

- CASE_INSENSITIVE_ORDER:   {364} :Oop for java/lang/String$CaseInsensitiveComparator @ 0x74e882c0

还记得上文提到过的String类的3个静态变量么?有没有觉得有什么眼熟的地方?

没错,在HotSpot中,Java类的静态变量就是作为该类对应的instanceKlass的实例变量出现的。上面的日志里最后三行描述了String的静态变量所在。

这是件非常自然的事:类用于描述对象,类自身也是对象,有用于描述自身的类;某个类的所谓“静态变量”就是该类对象的实例变量。很多对象系统都是这么设计的。HotSpot的这套oop体系(指“普通对象指针”,不是指“面向对象编程”)继承自,实际上反而比暴露给Java的对象模型显得更加面向对象一些。

HotSpot并不把instanceKlass暴露给Java,而会另外创建对应的java.lang.Class对象,并将后者称为前者的“Java镜像”,两者之间互相持有引用。日志中的_java_mirror便是该instanceKlass对Class对象的引用。

镜像机制被认为是良好的面向对象的反射与元编程设计的重要机制。Gilad Bracha与David Ungar还专门写了篇论文来阐述此观点,参考。

顺带把"main"对象的_klass链上余下的两个对象的日志也贴出来:

Log代码

InstanceKlassKlass @ 0x70e60168 (object size = 120)

- _mark:    {0} :1

- _klass:   {4} :KlassKlass @ 0x70e60000

- _java_mirror:     {60} :Oop for java/lang/Class @ 0x70e76f20

- _super:   {64} :null

- _size_helper:     {12} :0

- _name:    {68} :null

- _access_flags:    {84} :0

- _subklass:    {72} :null

- _next_sibling:    {76} :null

- _alloc_count:     {88} :0

所有instanceKlass对象都是被这个instanceKlassKlass对象所描述的。

Log代码

KlassKlass @ 0x70e60000 (object size = 120)

- _mark:    {0} :1

- _klass:   {4} :KlassKlass @ 0x70e60000

- _java_mirror:     {60} :Oop for java/lang/Class @ 0x70e76e00

- _super:   {64} :null

- _size_helper:     {12} :0

- _name:    {68} :null

- _access_flags:    {84} :0

- _subklass:    {72} :null

- _next_sibling:    {76} :null

- _alloc_count:     {88} :0

而所有*KlassKlass对象都是被这个klassKlass对象所描述的。

klass对象的更详细的介绍也留待以后再写吧~至少得找时间写写instanceKlass与vtable、itable的故事。

嘛,今天的废话到此结束 ^_^

希望这帖能解答先前一帖中的疑问。也希望大家能多多支持圈子,有什么HLL VM相关的话题想讨论的欢迎来转转~

(1.7 MB)

描述: 文中例子的输出

下载次数: 45


相关文章

  • 可视化Java垃圾回收的原理和实现 – 码农网
  • 垃圾回收,就像双陆棋一样,只需几分钟来学习,但要用一生来精通. 基础 当谈到释放不再使用的内存,垃圾回收已经在很大程度上取代了早期技术,比如手动内存管理和引用计数. 这是件好事,因为内存管理令人厌烦,学究式地簿记是计算机擅长的,而不是人擅长 ...查看


  • Java中的垃圾回收原理
  • Java中的垃圾回收原理 分类: java 2013-06-19 22:14 446人阅读 评论(2) 收藏 举报 目录(?)[+] 垃圾回收简介 用户程序(mutator)会修改还堆区中的对象集合,从存储管理器处获取空间,创建对象,还可一 ...查看


  • 如何判断 Java 工程师的基础知识是否扎实?
  • 抛开工作经验,项目经验,学历背景,单从技术点分析,哪些方面可以判断一人java程序员的技术扎实程度 本着理论结合实践的方法,我一般都不问上面这种纯知识和理论性问题,而是让他写一段程序来证明HashMap是线程不安全的.然后,再让改一下这个程 ...查看


  • 碳纳米管介绍
  • 碳纳米管的研究 摘 要:本文简要介绍了碳纳米管的发现.结构,重点介绍了其制备.性质.应用和研究热点 关键词:碳纳米管:发现:制备:结构:性质:应用:研究热点 Research of Carbon Nanotubes Abstract: In ...查看


  • 我国的海洋经济
  • HOTSPOT WORKSHOP 我国的海洋经济 文/课题组 海 洋经济是国民经济的重要组成部分.近年来,海洋经济对我 都超过了20%,极大的带动了海洋经济的发展. 济技术关系没有实质性变化和发展.如何扩展产业链条,加强海洋上.下游产业的相 ...查看


  • 制度经济学产权理论
  • 浅谈制度经济学中的产权理论 摘要 改革开放以来,新制度经济学在我国流行并产生了深远的影响.随着研究的进步,发现有关制度对经济行为的影响的分析应该居于经济学的核心地位.产权理论是新制度经济学的主要内容之一,科斯是这一理论的代表人物.科斯的产 ...查看


  • 我想要购买一部中等价位的手机
  • 我想要购买一部中等价位的手机 A Hello sir, may I help you? 先生,你好,需要什么? B Yeah, I accidentally dropped my phone in the toilet. 是的,我不小心将手 ...查看


  • 设计注意事项
  • 毕业设计计算书与外文翻译的要求 按照<河北建筑工程学院毕业设计(论文)规范>的要求,特做以下说明: (一).设计计算书 1.毕业设计计算书(论文)应独立按要求的顺序装订成册: 2.中.英文内容摘要是毕业设计(论文)的内容不加注释 ...查看


  • 海藻多糖在医疗方面的研究进展
  • 海藻多糖在医疗方面的研究进展 摘要:海藻多糖是海藻中的天然生物活性物质,今年来其在医疗方面的研究成为热点.本文通过总结近些年的研究,归纳出海藻多糖在改善人体免疫力,防止胃黏膜损伤,降血脂,治疗类风湿性关节炎,抵抗癌症等方面的作用,体现出海藻 ...查看


热门内容