什么是 Java 类加载机制?

Java 虚拟机一般使用 Java 类的流程为:首先将开发者编写的 Java 源代码(.java文件)编译成 Java 字节码(.class文件),然后类加载器会读取这个 .class 文件,并转换成 java.lang.Class 的实例。有了该 Class 实例后,Java 虚拟机可以利用 newInstance 之类的方法创建其真正对象了。

ClassLoader 是 Java 提供的类加载器,绝大多数的类加载器都继承自 ClassLoader,它们被用来加载不同来源的 Class 文件。

Class 文件有哪些来源呢?

上文提到了 ClassLoader 可以去加载多种来源的 Class,那么具体有哪些来源呢?

首先,最常见的是开发者在应用程序中编写的类,这些类位于项目目录下;

然后,有 Java 内部自带的核心类如 java.lang、java.math、java.io 等 package 内部的类,位于 $JAVA_HOME/jre/lib/ 目录下,如 java.lang.String 类就是定义在 $JAVA_HOME/jre/lib/rt.jar 文件里;

另外,还有 Java 核心扩展类,位于 $JAVA_HOME/jre/lib/ext 目录下。开发者也可以把自己编写的类打包成 jar 文件放入该目录下;

最后还有一种,是动态加载远程的 .class 文件。

既然有这么多种类的来源,那么在 Java 里,是由某一个具体的 ClassLoader 来统一加载呢?还是由多个 ClassLoader 来协作加载呢?

哪些 ClassLoader 负责加载上面几类 Class?

实际上,针对上面四种来源的类,分别有不同的加载器负责加载。

首先,我们来看级别最高的 Java 核心类,即$JAVA_HOME/jre/lib 里的核心 jar 文件。这些类是 Java 运行的基础类,由一个名为 BootstrapClassLoader 加载器负责加载,它也被称作 根加载器/引导加载器。注意,BootstrapClassLoader 比较特殊,它不继承 ClassLoader,而是由 JVM 内部实现;

然后,需要加载 Java 核心扩展类,即 $JAVA_HOME/jre/lib/ext 目录下的 jar 文件。这些文件由 ExtensionClassLoader 负责加载,它也被称作 扩展类加载器。当然,用户如果把自己开发的 jar 文件放在这个目录,也会被 ExtClassLoader 加载;

接下来是开发者在项目中编写的类,这些文件将由 AppClassLoader 加载器进行加载,它也被称作 系统类加载器 System ClassLoader;

最后,如果想远程加载如(本地文件/网络下载)的方式,则必须要自己自定义一个 ClassLoader,复写其中的 findClass() 方法才能得以实现。

因此能看出,Java 里提供了至少四类 ClassLoader 来分别加载不同来源的 Class。

那么,这几种 ClassLoader 是如何协作来加载一个类呢?

这些 ClassLoader 以何种方式来协作加载 String 类呢?

String 类是 Java 自带的最常用的一个类,现在的问题是,JVM 将以何种方式把 String class 加载进来呢?

我们来猜想下。

首先,String 类属于 Java 核心类,位于 $JAVA_HOME/jre/lib 目录下。有的朋友会马上反应过来,上文中提过了,该目录下的类会由 BootstrapClassLoader 进行加载。没错,它确实是由 BootstrapClassLoader 进行加载。但,这种回答的前提是你已经知道了 String 在 $JAVA_HOME/jre/lib 目录下。

那么,如果你并不知道 String 类究竟位于哪呢?或者我希望你去加载一个 unknown 的类呢?

有的朋友这时会说,那很简单,只要去遍历一遍所有的类,看看这个 unknown 的类位于哪里,然后再用对应的加载器去加载。

是的,思路很正确。那应该如何去遍历呢?

比如,可以先遍历用户自己写的类,如果找到了就用 AppClassLoader 去加载;否则去遍历 Java 核心类目录,找到了就用 BootstrapClassLoader 去加载,否则就去遍历 Java 扩展类库,依次类推。

这种思路方向是正确的,不过存在一个漏洞。

假如开发者自己伪造了一个 java.lang.String 类,即在项目中创建一个包java.lang,包内创建一个名为 String 的类,这完全可以做到。那如果利用上面的遍历方法,是不是这个项目中用到的 String 不是都变成了这个伪造的 java.lang.String 类吗?如何解决这个问题呢?

解决方法很简单,当查找一个类时,优先遍历最高级别的 Java 核心类,然后再去遍历 Java 核心扩展类,最后再遍历用户自定义类,而且这个遍历过程是一旦找到就立即停止遍历。

在 Java 中,这种实现方式也称作 双亲委托。其实很简单,把 BootstrapClassLoader 想象为核心高层领导人, ExtClassLoader 想象为中层干部, AppClassLoader 想象为普通公务员。每次需要加载一个类,先获取一个系统加载器 AppClassLoader 的实例(ClassLoader.getSystemClassLoader()),然后向上级层层请求,由最上级优先去加载,如果上级觉得这些类不属于核心类,就可以下放到各子级负责人去自行加载。

如下图所示:

真的是按照双亲委托方式进行类加载吗?

下面通过几个例子来验证上面的加载方式。

开发者自定义的类会被 AppClassLoader 加载吗?

在项目中创建一个名为 MusicPlayer 的类文件,内容如下:

package classloader;

public class MusicPlayer {

public void print() {

System.out.printf("Hi I'm MusicPlayer");

}

}

然后来加载 MusicPlayer。

private static void loadClass() throws ClassNotFoundException {

Class> clazz = Class.forName("classloader.MusicPlayer");

ClassLoader classLoader = clazz.getClassLoader();

System.out.printf("ClassLoader is %s", classLoader.getClass().getSimpleName());

}

打印结果为:

ClassLoader is AppClassLoader

可以验证,MusicPlayer 是由 AppClassLoader 进行的加载。

验证 AppClassLoader 的双亲真的是 ExtClassLoader 和 BootstrapClassLoader 吗?

这时发现 AppClassLoader 提供了一个 getParent() 的方法,来打印看看都是什么。

private static void printParent() throws ClassNotFoundException {

Class> clazz = Class.forName("classloader.MusicPlayer");

ClassLoader classLoader = clazz.getClassLoader();

System.out.printf("currentClassLoader is %s\n", classLoader.getClass().getSimpleName());

while (classLoader.getParent() != null) {

classLoader = classLoader.getParent();

System.out.printf("Parent is %s\n", classLoader.getClass().getSimpleName());

}

}

打印结果为:

currentClassLoader is AppClassLoader

Parent is ExtClassLoader

首先能看到 ExtClassLoader 确实是 AppClassLoader 的双亲,不过却没有看到 BootstrapClassLoader。事实上,上文就提过, BootstrapClassLoader比较特殊,它是由 JVM 内部实现的,所以 ExtClassLoader.getParent() = null。

如果把 MusicPlayer 类挪到 $JAVA_HOME/jre/lib/ext 目录下会发生什么?

上文中说了,ExtClassLoader 会加载$JAVA_HOME/jre/lib/ext 目录下所有的 jar 文件。那来尝试下直接把 MusicPlayer 这个类放到 $JAVA_HOME/jre/lib/ext 目录下吧。

利用下面命令可以把 MusicPlayer.java 编译打包成 jar 文件,并放置到对应目录。

javac classloader/MusicPlayer.java

jar cvf MusicPlayer.jar classloader/MusicPlayer.class

mv MusicPlayer.jar $JAVA_HOME/jre/lib/ext/

这时 MusicPlayer.jar 已经被放置与 $JAVA_HOME/jre/lib/ext 目录下,同时把之前的 MusicPlayer 删除,而且这一次刻意使用 AppClassLoader 来加载:

private static void loadClass() throws ClassNotFoundException {

ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); // AppClassLoader

Class> clazz = appClassLoader.loadClass("classloader.MusicPlayer");

ClassLoader classLoader = clazz.getClassLoader();

System.out.printf("ClassLoader is %s", classLoader.getClass().getSimpleName());

}

打印结果为:

ClassLoader is ExtClassLoader

说明即使直接用 AppClassLoader 去加载,它仍然会被 ExtClassLoader 加载到。

从源码角度真正理解双亲委托加载机制

上面已经通过一些例子了解了双亲委托的一些特性了,下面来看一下它的实现代码,加深理解。

打开 ClassLoader 里的 loadClass() 方法,便是需要分析的源码了。这个方法里做了下面几件事:

检查目标class是否曾经加载过,如果加载过则直接返回;

如果没加载过,把加载请求传递给 parent 加载器去加载;

如果 parent 加载器加载成功,则直接返回;

如果 parent 未加载到,则自身调用 findClass() 方法进行寻找,并把寻找结果返回。

代码如下:

protected Class> loadClass(String name, boolean resolve)

throws ClassNotFoundException

{

synchronized (getClassLoadingLock(name)) {

// 1. 检查是否曾加载过

Class> c = findLoadedClass(name);

if (c == null) {

long t0 = System.nanoTime();

try {

if (parent != null) {

// 优先让 parent 加载器去加载

c = parent.loadClass(name, false);

} else {

// 如无 parent,表示当前是 BootstrapClassLoader,调用 native 方法去 JVM 加载

c = findBootstrapClassOrNull(name);

}

} catch (ClassNotFoundException e) {

// ClassNotFoundException thrown if class not found

// from the non-null parent class loader

}

if (c == null) {

// 如果 parent 均没有加载到目标class,调用自身的 findClass() 方法去搜索

long t1 = System.nanoTime();

c = findClass(name);

// this is the defining class loader; record the stats

sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);

sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);

sun.misc.PerfCounter.getFindClasses().increment();

}

}

if (resolve) {

resolveClass(c);

}

return c;

}

}

// BootstrapClassLoader 会调用 native 方法去 JVM 加载

private native Class> findBootstrapClass(String name);

看完实现源码相信能够有更完整的理解。

类加载器最酷的一面:自定义类加载器

前面提到了 Java 自带的加载器 BootstrapClassLoader、AppClassLoader和ExtClassLoader,这些都是 Java 已经提供好的。

而真正有意思的,是 自定义类加载器,它允许我们在运行时可以从本地磁盘或网络上动态加载自定义类。这使得开发者可以动态修复某些有问题的类,热更新代码。

下面来实现一个网络类加载器,这个加载器可以从网络上动态下载 .class 文件并加载到虚拟机中使用。

后面我还会写作与 热修复/动态更新 相关的文章,这里先学习 Java 层 NetworkClassLoader 相关的原理。

作为一个 NetworkClassLoader,它首先要继承 ClassLoader;

然后它要实现ClassLoader内的 findClass() 方法。注意,不是loadClass()方法,因为ClassLoader提供了loadClass()(如上面的源码),它会基于双亲委托机制去搜索某个 class,直到搜索不到才会调用自身的findClass(),如果直接复写loadClass(),那还要实现双亲委托机制;

在 findClass() 方法里,要从网络上下载一个 .class 文件,然后转化成 Class 对象供虚拟机使用。

具体实现代码如下:

/**

* Load class from network

*/

public class NetworkClassLoader extends ClassLoader {

@Override

protected Class> findClass(String name) throws ClassNotFoundException {

byte[] classData = downloadClassData(name); // 从远程下载

if (classData == null) {

super.findClass(name); // 未找到,抛异常

} else {

return defineClass(name, classData, 0, classData.length); // convert class byte data to Class> object

}

return null;

}

private byte[] downloadClassData(String name) {

// 从 localhost 下载 .class 文件

String path = "http://localhost" + File.separatorChar + "java" + File.separatorChar + name.replace('.', File.separatorChar) + ".class";

try {

URL url = new URL(path);

InputStream ins = url.openStream();

ByteArrayOutputStream baos = new ByteArrayOutputStream();

int bufferSize = 4096;

byte[] buffer = new byte[bufferSize];

int bytesNumRead = 0;

while ((bytesNumRead = ins.read(buffer)) != -1) {

baos.write(buffer, 0, bytesNumRead); // 把下载的二进制数据存入 ByteArrayOutputStream

}

return baos.toByteArray();

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

public String getName() {

System.out.printf("Real NetworkClassLoader\n");

return "networkClassLoader";

}

}

这个类的作用是从网络上(这里是本人的 local apache 服务器 http://localhost/java 上)目录里去下载对应的 .class 文件,并转换成 Class> 返回回去使用。

下面我们来利用这个 NetworkClassLoader 去加载 localhost 上的 MusicPlayer 类:

首先把 MusicPlayer.class 放置于 /Library/WebServer/Documents/java (MacOS)目录下,由于 MacOS 自带 apache 服务器,这里是服务器的默认目录;

执行下面一段代码:

String className = "classloader.NetworkClass";

NetworkClassLoader networkClassLoader = new NetworkClassLoader();

Class> clazz = networkClassLoader.loadClass(className);

正常运行,加载 http://localhost/java/classloader/MusicPlayer.class成功。

可以看出 NetworkClassLoader 可以正常工作,如果读者要用的话,只要稍微修改 url 的拼接方式即可自行使用。

小结

类加载方式是 Java 上非常创新的一项技术,给未来的热修复技术提供了可能。本文力求通过简单的语言和合适的例子来讲解其中双亲委托机制、自定义加载器等,并开发了自定义的NetworkClassLoader。

当然,类加载是很有意思的技术,很难覆盖所有知识点,比如不同类加载器加载同一个类,得到的实例却不是同一个等等。

java里加载是什么意思_Java 类加载机制详解相关推荐

  1. Java类加载机制详解【java面试题】

    Java类加载机制详解[java面试题] (1)问题分析: Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数 ...

  2. 【胖虎的逆向之路】01——动态加载和类加载机制详解

    胖虎的逆向之路 01--动态加载和类加载机制详解 一.前言 二.类的加载器 1. 双亲委派模式 2. Android 中的类加载机制 1)Android 基本类的预加载 2)Android类加载器层级 ...

  3. Java虚拟机中类加载机制详解

    Java虚拟机中类加载机制详解 1,什么是java类加载机制 **首先在java中,是通过编译来生成.class文件(可能在本地,或者网页下载),java的类加载机制就是 将这些.class文件加载到 ...

  4. JAVA之JVM垃圾回收(GC)机制详解

    一.为什么需要垃圾回收 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收.除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此.所以,垃圾回收是必须的. 二. ...

  5. java 找不到环境变量_java环境变量详解---找不到或无法加载主类

    默认安装在C:\ProgramFiles\Java\jdk1.7.0目录下 环境变量配置为 PATH=.;%JAVA_HOME%\bin CLASSPATH=.;%JAVA_HOME%\lib\dt. ...

  6. Java 类加载机制详解

    一.类加载器 类加载器(ClassLoader),顾名思义,即加载类的东西.在我们使用一个类之前,JVM需要先将该类的字节码文件(.class文件)从磁盘.网络或其他来源加载到内存中,并对字节码进行解 ...

  7. java反射机制详解_Java反射机制详解

    Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反 ...

  8. Java虚拟机:类加载机制详解

    版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! 大家知道,我们的Java程序被编译器编译成class文件,在class文件中描述的各种信息,最终都需要加载到虚拟机内存才能运行和使用,那么 ...

  9. java反射机制详解_JAVA反射机制详解_JSP/Java编程_互联网开发技术网_传播最新的编程技术_php361.com...

    今天,下午在和朋友聊天的时候,聊起了反射这个话题. 我们就从下面这个段简单的代码开始吧. 这个代码输出什么,想必大部分的读者跟我一样,会很快地知道答案:0 1 2 3 4 5 6 7 8 9.事实也是 ...

最新文章

  1. 封装了一下我佛山人4.0 (支持vs2005)asp.net 页面验证
  2. R语言ggplot2可视化绘制累积计数图(累加图,cumulative counts)
  3. CSP认证201403-4 无线网络[C++题解]:宽搜、bfs最短路、图论
  4. React 16 + Jest单元测试 之 Mock Functions(Mock Names 和 Custom Matchers)
  5. 【项目管理】CMMI内容整理
  6. 栈的应用--进制转换
  7. 判断是否为自然数java_java判断输入的是否是自然数
  8. web开发软件,HTML如何添加锚点,成功入职阿里
  9. 【python】错误、异常和文件---复习笔记
  10. 十一种全球著名商业分析模型
  11. c++工作笔记001---c++相关零碎要点_endl、“\n”和‘\n’区别_extern int a关键字_-的意思_::的意思_指针和引用的区别
  12. 基于阿里云的MQTT远程控制
  13. 什么是 Linux 发行版
  14. windows 系统新建 vue 项目的坑
  15. C# BackgroundWorker的使用 转
  16. 日期对象 date.getDate() date.getDay()的区别
  17. 安卓开发视频教程!十多家大厂Android面试真题锦集干货整理,写给正在求职的安卓开发
  18. 5.大型电商项目之创建前端展示模板并调用
  19. ligh@local-host$ ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.0.3
  20. 软件工程之QA管理(好软件系列二)

热门文章

  1. 当使用malloc提示找不到标识符时?
  2. springboot整合nacos配置实现实时更新
  3. Mail.Ru Cup 2018 Round 1 virtual participate记
  4. C语言文件路径中的”/“和“\“
  5. 2597 团伙(并查集就是好用!)
  6. 创建完maven工程之后,提示[FATAL_ERROR] Cannot start Maven: Project JDK is not specified. a href=''Configure...
  7. 学习SQLite之路(二)
  8. HDU 1358 Period KMP
  9. jquery插件:图片截取工具jquery.imagecropper.js
  10. 大数据之-Hadoop3.x_MapReduce_二次排序案例---大数据之hadoop3.x工作笔记0116