上次发布的博客 通过Java程序提交通用Mapreduce,在实施过程中发现,每次提交一次Mapreduce任务,JVM无法回收过程中产生的MapReduceClassLoader对象以及其生成的类。
通过定制如下代码来实现多次任务提交测试:
public class JobSubmitTest {
public static void submit(String classPath, String mainClassName) {
ClassLoader originCL = Thread.currentThread().getContextClassLoader();
try {
MapReduceClassLoader cl = new MapReduceClassLoader();
cl.addClassPath(classPath);
System.out.println("URLS:" + Arrays.toString(cl.getURLs()));
Thread.currentThread().setContextClassLoader(cl);
Class mainClass = cl.loadClass(mainClassName);
System.out.println(mainClass.getClassLoader());
Method mainMethod = mainClass.getMethod("main", new Class[] { String[].class });
mainMethod.invoke(null, new Object[] {new String[0]});
Class jobClass = cl.loadClass("org.apache.hadoop.mapreduce.Job");
System.out.println(jobClass.getClassLoader());
Field field = jobClass.getField(JobAdapter.JOB_FIELD_NAME);
System.out.println(field.get(null));
} catch (Exception e) {
e.printStackTrace();
} finally {
Thread.currentThread().setContextClassLoader(originCL);
}
}
public static void main(String[] args) {
String classPath = args[0];
String mainClassName = args[1];
Scanner scanner = new Scanner(System.in);
String cmd = null;
int i = 0;
while (true) {
cmd = scanner.next();
if ("exit".equalsIgnoreCase(cmd)) {
break;
}
submit(classPath, mainClassName);
i++;
System.out.println("submit index = " + i);
}
}
}
执行命令: java -XX:PermSize=50M -XX:MaxPermSize=50M -Dhadoop.home.dir=$HADOOP_HOME -Djava.library.path=$HADOOP_HOME/lib/native \ -classpath $CLASSPATH JobSubmitTest $MR_CLASSPATH $MR_MAIN_CLASS
执行命令后,3次输入“1” + enter,实现3次mapreduce的提交,并且都创建了独立的类加载器来加载hadoop相关的类。
通过查看永久代的使用情况变化:
$ jstat -gcutil 21225 1000 1000
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 6.05 0.00 6.63 0 0.000 0 0.000 0.000
0.00 0.00 26.15 0.00 8.07 0 0.000 0 0.000 0.000
0.00 0.00 76.55 0.00 16.33 0 0.000 0 0.000 0.000
0.00 78.52 19.82 0.10 28.19 3 0.023 0 0.000 0.023
97.58 0.00 30.21 0.11 36.39 4 0.033 0 0.000 0.033
97.58 0.00 34.18 0.11 36.46 4 0.033 0 0.000 0.033
0.00 99.95 96.01 5.21 52.10 6 0.050 0 0.000 0.050
95.45 0.00 25.96 5.22 57.08 6 0.065 0 0.000 0.065
95.45 0.00 69.92 5.22 65.57 6 0.065 0 0.000 0.065
0.00 99.93 37.95 10.91 77.75 7 0.098 0 0.000 0.098
0.00 99.93 37.95 10.91 77.75 7 0.098 0 0.000 0.098
0.00 99.93 37.95 10.91 77.75 7 0.098 0 0.000 0.098
0.00 99.93 37.95 10.91 77.75 7 0.098 0 0.000 0.098
其中P列表示永久代的使用比例;
执行GC看永久代是否会变小: 执行jcmd $PID GC.run
:
$ jstat -gcutil 21225 1000 1000
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 99.93 41.48 10.91 77.75 7 0.098 0 0.000 0.098
0.00 99.93 41.48 10.91 77.75 7 0.098 0 0.000 0.098
0.00 0.00 0.00 10.62 77.68 8 0.116 1 0.209 0.325
0.00 0.00 0.00 10.62 77.68 8 0.116 1 0.209 0.325
可以看到永久代几乎没有发生任何变化,永久代未被回收。
$ jmap -permstat 21225
Attaching to process ID 21225, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 24.65-b04
finding class loader instances ..done.
computing per loader stat ..done.
please wait.. computing liveness......................................................done.
class_loader classes bytes parent_loader alive? type
<bootstrap> 1301 7691864 null live <internal>
0x0000000085247020 1 1888 null dead sun/reflect/DelegatingClassLoader@0x0000000081e4fc00
0x000000008527f2b0 1744 12519760 0x0000000085031cb0 live com/spiro/test/mr/MapReduceClassLoader@0x0000000082096e50
0x0000000085018b98 1757 12584416 0x0000000085031cb0 live com/spiro/test/mr/MapReduceClassLoader@0x0000000082096e50
0x0000000085128c80 0 0 0x0000000085031cb0 live java/util/ResourceBundle$RBClassLoader@0x00000000820f5030
0x0000000085021cc0 1 3032 null dead sun/reflect/DelegatingClassLoader@0x0000000081e4fc00
0x00000000852a6f50 1 3056 null dead sun/reflect/DelegatingClassLoader@0x0000000081e4fc00
0x0000000085031cb0 83 873544 0x0000000085031d00 live sun/misc/Launcher$AppClassLoader@0x0000000082013318
0x0000000085021c80 1 1888 null dead sun/reflect/DelegatingClassLoader@0x0000000081e4fc00
0x00000000852a6f90 1 3056 null dead sun/reflect/DelegatingClassLoader@0x0000000081e4fc00
0x0000000085021c40 1 3056 0x0000000085018b98 dead sun/reflect/DelegatingClassLoader@0x0000000081e4fc00
0x00000000852a6fd0 1 3056 0x00000000852a67e0 dead sun/reflect/DelegatingClassLoader@0x0000000081e4fc00
0x0000000085031d00 0 0 null live sun/misc/Launcher$ExtClassLoader@0x0000000081fb5c08
0x0000000085021c00 1 3032 null dead sun/reflect/DelegatingClassLoader@0x0000000081e4fc00
0x00000000852a6e48 1 3056 0x000000008527f2b0 dead sun/reflect/DelegatingClassLoader@0x0000000081e4fc00
0x00000000852a67e0 1744 12519760 0x0000000085031cb0 live com/spiro/test/mr/MapReduceClassLoader@0x0000000082096e50
total = 16 6638 46214464 N/A alive=7, dead=9 N/A
$ jcmd 21225 GC.class_histogram | grep MapReduceClassLoader
264: 3 240 com.spiro.test.mr.MapReduceClassLoader
num #instances #bytes class name
$ jcmd 21225 GC.class_histogram | grep org.apache.hadoop.mapreduce.Job
772: 2 48 org.apache.hadoop.mapreduce.Job$JobState
785: 1 48 org.apache.hadoop.mapreduce.Job
813: 1 48 org.apache.hadoop.mapreduce.Job
878: 2 48 org.apache.hadoop.mapreduce.Job$JobState
883: 1 48 org.apache.hadoop.mapreduce.Job
961: 2 48 org.apache.hadoop.mapreduce.Job$JobState
1357: 1 24 [Lorg.apache.hadoop.mapreduce.Job$JobState;
1511: 1 24 [Lorg.apache.hadoop.mapreduce.Job$JobState;
1601: 1 24 [Lorg.apache.hadoop.mapreduce.Job$JobState;
可以看到MapReduceClassLoader类型的类加载器有3个,且占用了大部分的容量。org.apache.hadoop.mapreduce.Job对象出现3个,虽然名称相同但是是不同的类,分别由3个类加载器加载。
通过代码来看,MapReduceClassLoader cl = new MapReduceClassLoader();是定义在方法体内,当方法结束时,栈帧中的局部变量表也就消失了,MapReduceClassLoader对象应该就会被GC,并且由其加载的所有类也都应该被回收。但是为什么没有回收呢,根据Java判定对象是否存活的根搜索算法(GC Roots Tracing),肯定有如下GC roots任然持有MapReduceClassLoader对象:
下面通过对java进行的dump文件进行分析。 执行导出dump文件jmap -dump:live,format=b,file=heap.bin $PID
通过jvisualvm工具来分析
在Classes tab页中找到MapReduceClassLoader类,右击鼠标,选择“show in instances view”,
在下面的Refernces中的“this”上右击选择“show nearest gc root”,
可以看到有一个名为“Thread-2”的线程对象的contextClassLoader属性引用指向了MapReduceClassLoader对象。导致MapReduceClassLoader对象无法被回收。
在Summary tab页中可看到线程信息,其中一个名为“Thread-2”的线程调用栈在org.apache.hadoop.net.unix.DomainSocketWatcher类中,通过源码分析,该线程为在执行提交MR任务过程中hadoop框架启动的子线程,创建子线程时会使用父线程的contextClassLoad作为其contextClassLoad。
至此问题分析结束。
问题原因是由于在提交MR任务前执行了Thread.currentThread().setContextClassLoader(cl);
操作,导致提交过程中hadoop开启的一个常驻子线程使用了其父线程的contextClassLoad最为器其上下文线程,也就是MapReduceClassLoader。