通过Java程序提交通用Mapreduce无法回收类的问题

问题描述

上次发布的博客 通过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对象:

  • 虚拟机栈(栈帧中的本地变量表)中的引用对象
  • 方法区中的静态属性;
  • 方法区中的常量引用;
  • 本地方法栈中JNI的引用对象;

下面通过对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。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java 成神之路

URL 源码分析

38813
来自专栏青玉伏案

iOS逆向工程之Hopper中的ARM指令

虽然前段时间ARM被日本软银收购了,但是科技是无国界的,所以呢ARM相关知识该学的学。现在看ARM指令集还是倍感亲切的,毕竟大学里开了ARM这门课,并且做了不少...

3357
来自专栏Golang语言社区

Golang语言 之网络

Go语言标准库里提供的net包,支持基于IP层、TCP/UDP层及更高层面(如HTTP、FTP、SMTP)的网络操作,其中用于IP层的称为Raw Socket。...

3669
来自专栏Python研发

Django之Model世界

django为使用一种新的方式,即:关系对象映射(Object Relational Mapping,简称ORM)

1292
来自专栏犀利豆的技术空间

徒手撸框架--实现 RPC 远程调用

微服务已经是每个互联网开发者必须掌握的一项技术。而 RPC 框架,是构成微服务最重要的组成部分之一。趁最近有时间。又看了看 dubbo 的源码。dubbo 为了...

1532
来自专栏Java 源码分析

NioEventLoopGroup 源码分析

NioEventLoopGroup 源码分析 1. 在阅读源码时做了一定的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限。为了方便 IDE 查看...

3867
来自专栏五毛程序员

五毛的cocos2d-x学习笔记07-计时器、数据读写、文件读写

2595
来自专栏Charlie's Road

<Solidity学习系列四>使用编译器

Solidity存储库的一个构建目标是solc,solidity命令行编译器。 使用solc --help为您提供所有选项的解释。 编译器可以生成各种输出,范围...

1582
来自专栏IT杂记

通过Java程序提交通用Mapreduce任务并获取Job信息

背景 我们的一个业务须要有对MR任务的提交和状态跟踪的功能,须要通过Java代码提交一个通用的MR任务(包括mr的jar、配置文件、依赖的第三方jar包),并且...

1.1K5
来自专栏hotqin888的专栏

bootstrap treeview 增删改的正确姿势

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hotqin888/article/det...

4703

扫码关注云+社区

领取腾讯云代金券