一、废话
应大神RednaxelaFX要求写下自己调试JVM GC的一些心得体会。
我工作本身不是JVM/GC,但在08年开始要处理外场一些java/jvm crash,先拿着不完全一致的开源代码对照反汇编逆向分析,开始对jvm有些兴趣,也开始自己动手编译调试分析jvm的解释执行/gc/热点编译,虽然远远谈不上熟悉,更没有吃透,但也学习到很多东西,似乎比学习windows内核还多些收获。
三年前曾经有个初步心得总结,但环境都变化了,这次重新整理一把,温故而知新。
本来打算元宵前就开始弄,结果忙于windows上一个开发32位到64位移植,耽误了不少时间,所以赶得比较匆忙,而且水平有限,有些也不细致,有些可能不正确,大家尽量指点,我也好有进步。
这里先把新生代回收的调试写下,老生代用的cms,其实cms更有趣,相对要复杂得多,在入门的时候看了Poonam写的一篇博客《Understanding CMS GC Logs》受益匪浅,这个比较耗费时间,如果有空打算下个月中旬前完成。
二、环境
编译调试jvm版本是sun的jdk6 hotspot 6u20版本
我在windows下只编译了jvm,其实在solaris下也如此,如果要编译整个jdk有些组件找不到。
jdk采用的是java version "1.6.0_11",两个版本不一致,对调试gc关系不大哈。
jvm在windows xpsp3下用vc2003编译,虽然我在solaris下用sun的编译器和mdb调试(gcc似乎编译不过),不过,感觉还是在windows下调试更方便些。
三、几个术语和概念
堆分为新生代、老生代和perm代。
新生代又由eden和from/to区三个部分组成。
注意,还有个东东虽然不是java堆内存里面,但辅助内存分配回收,也很重要,它就是card table,后续会说到。
注意,以上几个图都是以前从sun的公开文档下载来的,后续图都是原创。
四、准备工作
1、运行参数如下
./java -server -Xmx600m -Xms600m -XX:PermSize=64M -XX:MaxPermSize=128M -XX:NewSize=128m -XX:MaxNewSize=128m -XX:SurvivorRatio=8 -XX:-UseTLAB -XX:+UseSerialGC -XX:CMSInitiatingOccupancyFraction=50 -XX:+PrintGCDetails -Xloggc:gc.log test.MyHelloM
其中参数说明:
-XX:-UseTLAB是为了更好调试new如何分配的;
-XX:+UseSerialGC为了调试最简单的回收,其实一般来说新生代不够大,用ParNew足够了,而它除了多线程外,原理和DefNew一样,而后者显然更好调试;
-XX:+PrintGCDetails 这个为了更好看回收日志;
新生代和老生代以及perm的最大最小都一致,这个不仅更好调试,而且即使在实际应用中,如果内存足够,也推荐这样,免得反复回收频繁。
2、进程地址空间
注意,在我的环境中,在进程的地址空间中,eden/from/to/tenured/perm的实际地址如下
gclog最后部分
def new generation total 118016K, used 218K [0x08430000, 0x10430000, 0x10430000)
eden space 104960K, 0% used [0x08430000, 0x08466b68, 0x0eab0000)
from space 13056K, 0% used [0x0eab0000, 0x0eab0000, 0x0f770000)
to space 13056K, 0% used [0x0f770000, 0x0f770000, 0x10430000)
tenured generation total 483328K, used 0K [0x10430000, 0x2dc30000, 0x2dc30000)
the space 483328K, 0% used [0x10430000, 0x10430000, 0x10430200, 0x2dc30000)
compacting perm gen total 65536K, used 2112K [0x2dc30000, 0x31c30000, 0x35c30000)
the space 65536K, 3% used [0x2dc30000, 0x2de40218, 0x2de40400, 0x31c30000)
---------------------------------------------------------------------------------------------------------
大致整理下各个区的开始地址:
eden from to tenured perm
0x08430000 0x0eab0000 0x0f770000 0x10430000 0x2dc30000
以后调试的时候可以很容易在内存中看到相关对象在那个区。
ps: 也可以把把各个代的最小和最大设置成不一样,然后用vmmap看很容易看到,如下图
3、java代码
package test; import java.util.Vector; public class MyHelloM { public static Vector ve ; private int _count; private String _name; MyHelloM(int count){ _count = count; } public void setName(String name){ _name = name; } static { } public static void main( String[] arg) { int i =0; while(true){ try { if (i == 0){ System.gc(); ve = new Vector(10); Thread.sleep(1* 1 * 1000); MyHelloM my1 = new MyHelloM(i); String testString1 = new String("test0"); my1.setName(testString1); ve.add(my1); } if (i == 1){ MyHelloM my2 = new MyHelloM(i); String testString2 = new String("test1"); my2.setName(testString2); ve.add(my2); } if (i == 2){ System.out.println("before sleep i = 2\n"); Thread.sleep(1* 30 * 1000); Byte []bb = new Byte[100 * 1024 * 1024]; ve.add(bb); System.out.println("after sleep i = 2\n"); } if (i == 3){ Thread.sleep(1* 1 * 1000); ve.remove(2); ve.remove(1); System.out.println("after sleep i = 3\n"); } if (i > 3 ){ System.out.println("before sleep i = " + i + " ...\n"); if (i == 6) Thread.sleep(1* 40 * 1000); Byte []bb = new Byte[10 * 1024 * 1024]; Thread.sleep(1* 20 * 1000); System.out.println("after sleep i = " + i + " ...\n"); } i++; } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
代码说明:
Vector ve是static变量;
循环第4次的时候,i=3, 会把my2从ve中remove,但my1仍然在ve中;
循环第7次的时候, i=6, new Byte[10 * 1024 * 1024]触发新生代回收;
加上Thread.sleep,是为了方便sleep的时候打jmap,看到各个对象的引用;
如果不打算用mat/jmap来看,非要自己调试各个对象怎么new出来的,跟踪每个bytecode的解释执行太累,注意每次new不一定都会到InterpreterRuntime::_new这里,所以在JVM_Sleep完毕的JVM_END处下断点很容易跟踪到下一个bytecode的解释执行那里,也可以在new/newarry的解释执行开始那里设置断点,不过每次跟踪还是有些累。
再次啰嗦下,为什么不在bytecode new那里设置断点呢?
比如在下面这里bb这个的解释那里设置断点
00982884 mov edx,dword ptr [esi+1] //__ get_unsigned_2_byte_index_at_bcp(rdx, 1)
00982887 bswap edx
00982889 shr edx,10h
是因为中间可能会有很多其它不关心的对象产生,所以还是建议在JVM_Sleep完毕的JVM_END处下断点。
4、修改下jvm的代码
为了代码容易测试,在ClassFileParser.cpp
ClassFileParser::parseClassFile函数开头地方加上如下代码
//added by jl
char *tmp = name->as_utf8();
if ( strstr(tmp, "test/MyHello") > 0) {
printf("test--parseClassFile class %s\n", tmp);
//_has_print_constpool = true;
}
//added end
在JavaCalls.cpp
JavaCalls::call_helper加上如下代码
char * tmp = method->name()->as_C_string();
if (strstr(tmp, "main") > 0) //main或者其它想要调试的函数名字都可以
{
printf("call myfunction----%s-----------\n", tmp);
}
这两处代码加上就是为了方便容易设断点:
parseClassFile那里修改后,可以方便下断点看到自己的class的constpool和各个东东(比如cp/klassOop/methods)以及放到perm的位置;
JavaCalls::call_helper那里修改后,可以方便调试自己的函数的bytecode的执行。
注意,这些代码修改并不影响任何逻辑,只是方便调试。
5、其它工具
除了vc2003和jdk自代工具外(jmap/jstack/jconsole这些),需要vmmap/MemoryAnalyzer,后者简称mat,特别重要,完全可以免除很多费神的手工操作,比如跟踪每个对象的new/newarray,很烦,我在sleep某个时刻,自己jmap打出来,然后用mat一清二楚。
准备工作差不多了,可以开始动手了。