java – 理解classloader

最近学习大数据,意识到java不仅解决了C++高昂的内存管理成本之外,其”类动态加载”和”反射”机制无疑有效的支撑了”计算向存储移动”思路的落地,难怪java语言在大数据领域成为主流选择。

如果我们接触过mapreduce的话,就不得不产生一个这样的疑惑:

业务编写的mapper和reducer类,是如何被mapreduce框架分发到任意计算节点、实例化为对象并且执行相关函数的?

本文简单清晰的揭示classloader原理,看完之后将会明白java如何是将class移动到远端并实例化执行的。

classloader原理

编译好的.java源代码保存为.class字节码文件,代码中import一个类时就是由classloader去相应的磁盘路径下读取.class文件到内存中来实现的。

java自带了3个classloader,它们呈现分层关系,分别负责去不同的磁盘路径加载.class。

  • bootstrap classloader:最顶层的loader,它伴随java启动而生效,负责加载JRE/lib/rt.jar等运行时核心类。
  • extension classloader:负责去JRE/lib/ext目录下搜索加载一些功能扩展类。
  • app classloader:负责帮应用程序加载类,一般通过环境变量CLASSPATH或者-Djava.class.path来指定搜索路径。
  • custom classloader:我们可以自定义classloader类,通过任意方式找到.class文件并加载它。

当我们import或者反射1个classname时,java默认会帮我们调用app classloader的loadClass方法来加载.class,但是app classloader并不会立即做查找而是递归调用extension classloader的loadClass,同样extension classloader也会先递归调用bootstrap classloader的loadClass,如果递归返回没有找到,那么classloader才会通过自己的方式查找.class文件。

这种先递归向上再回溯的方式可以保证,如果bootstrap里面已经加载过该.class,那么下层的extension/app/custom classloader就不需要自己去查找了。这样也带来了一些安全性,比如我们在下层放一个伪造的String.class文件并不会覆盖标准库,因为查找String类的过程是自顶向下的,在bootstrap loader层就已经找到了。

验证ClassLoader

看如下代码:

TestLoader就是我们程序写的类,它是由app ClassLoader负责加载的,当然app ClassLoader会委托上层的loader先行查找,最后才轮到它自己。

上述程序输出:

sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader

app ClassLoader的父亲是ext ClassLoader,ext classloader的父亲应该是bootstrap ClassLoader,然而因为bootstrap ClassLoader是C++实现在JVM内核心代码,所以JAVA层无法真的获取到它。


另外一个例子,我们利用当前的app classloader加载一下java.lang.String类,实际会由bootstrap ClassLoader完成真正加载:

打印:

null

app classloader委托查找给bootstrap classloader,因为String类放在bootstrap classloader的加载路径下,所以被它实际加载,因此打印String类的classLoader是null(bootstrap classloader是c++实现的),可见class的loader是谁取决于真正加载者,并不一定是我们直接调用的loader。

自定义classloader

我们可以自定义ClassLoader类,只有上层loader都找不到要加载的class时,才轮到我们自定义的查找方法执行。

首先自定义了MyClassLoader类,我们只需要实现findClass方法,通过任意方式把类对应的.class内容读取进来即可;我们不需要覆盖loadClass方法,因为loadClass方法已经实现先查找上层loader最后再调用findClass的逻辑。

程序输出:

sun.misc.Launcher$AppClassLoader
null
sun.misc.Launcher$AppClassLoader
java.lang.ClassNotFoundException

  • MyClassLoader的父loader默认是sun.misc.Launcher$AppClassLoader,也可以构造MyClassLoader的时候手动传入父loader。
  • String会被委托给祖父bootstrap loader加载得到。
  • cc.yuerblog.TestLoader是我们程序入口类,当然是委托给父sun.misc.Launcher$AppClassLoader搞定的。
  • cc.yuerblog.fake压根不存在,所以app classloader也不会成功,而轮到我们的MyClassLoader时因为findClass压根没实现加载.class逻辑直接抛出了class not found。

class相等性问题

只有同1个classloader实例加载的class对象才会相等,就像这样:

虽然我new了2个MyClassLoader,但是它们加载cc.yuerblog.TestLoader都是委托给同一个app classloader实例对象完成的,因此返回的是同1个class对象。

如果某个class是通过MyClassLoader的findClass方法加载的,而我们通过2个不同MyClassLoader实例加载同一个class,则2个class一定是不等的。

所以我们要注意classloader的单例化是非常重要的,java自带的都是单例的,并且classloader基类的loadClass方法默认会记住曾经加载过的class对象,因此同一个类总是返回同一个class对象。

实战:动态加载jar包中的类

我们模拟mapreduce框架的工作流程:

  • 将业务开发的mapper和reducer类打包成jar
  • 由另外一个程序通过classloader来加载jar包中的类,反射创建实例并执行map和reduce方法。

项目地址:https://github.com/owenliang/classloader-demo

打包mapper和reducer

在mapreduce项目中,我们实现业务的mapper和reducer类:

将它们通过maven打包到jar包里:C:\Users\xx\IdeaProjects\classloader-demo\mapreduce\target\mapreduce-1.0-SNAPSHOT.jar。

上传至hdfs

上述mapreduce jar包放在本地磁盘上,我们假想这就是hdfs分布式文件系统。

运行mapreduce

当MR框架选定任意计算节点之后,会从hdfs上(也就是我们的本地磁盘)找到jar包,并通过自定义classloader加载jar包中的class。

业务的mapper和reducer类名,需要通过命令行告知MR框架,然后MR框架会通过classloader从HDFS下载Jar包然后去里面找到这两个类,因此运行命令是:

java -jar C:\Users\xx\IdeaProjects\classloader-demo\hadoop\target\hadoop-1.0-SNAPSHOT.jar cc.yuerblog.Mapper cc.yuerblog.Reducer
map
reduce

能够扫描jar包中.class文件的Classloader不用我们自己实现,标准库自带一个URLClassLoader,因此我们直接把要查找的所有路径与JAR包拼成一个URL数组,构造一个URLClassLoader对象即可。

显然URLClassLoader能够从指定的jar包中找到mapper和reducer类,经过反射创建实例后调用它们的对应方法即可。

如果你用过hadoop就会知道,hadoop提交MR任务的命令是:
hadoop -jar xxxx.jar,实际上hadoop命令会将jar包保存到HDFS,然后任意worker就可以通过自定义的classloader从hdfs上的xxxx.jar中加载到业务的mapper和reducer类了。

 

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~