一:ClassLoader

从JVM结构图中可以看到,类加载器的作用是将Java类文件加载到Java虚拟机。

HotSpot JVM结构,图片来自Java Garbage Collection Basics

只有当类被加载进虚拟机内存,才能使用对应的类。

在Java中,类加载过程大概分为以下几步:通过全限类名获取类文件字节数组。可来自本地文件、jar包、网络等。

在方法区/元空间保存类的描述信息、静态属性。

在JVM堆中生成一个对应的java.lang.Class对象。

理解Java的类加载机制,对理解JVM有很大帮助。

二:Java默认的类加载器

Java默认提供三个类加载器,分别为:Bootstrap ClassLoader

Extension ClassLoader

App ClassLoader

Bootstrap ClassLoader 负责加载Java基础类,主要是 %JRE_HOME%/lib/ 目录下的rt.jar、resources.jar、charsets.jar等。

Extension ClassLoader 负责加载Java扩展类,主要是 %JRE_HOME%/lib/ext 目录下的jar。

App ClassLoader 负责加载当前应用的ClassPath中的所有类。

三个ClassLoader所负责加载的类,可以通过以下方式进行查看。public class ClassPath {    public static void main(String[] args) {

System.out.println("Bootstrap ClassLoader path: ");

System.out.println(System.getProperty("sun.boot.class.path"));

System.out.println("----------------------------");

System.out.println("Extension ClassLoader path: ");

System.out.println(System.getProperty("java.ext.dirs"));

System.out.println("----------------------------");

System.out.println("App ClassLoader path: ");

System.out.println(System.getProperty("java.class.path"));

System.out.println("----------------------------");

}

}

具体原因,在源码分析章节说明。

其中Bootstrap ClassLoader是JVM级别的,由C++撰写。

Extension ClassLoader和App ClassLoader都是Java类。

JVM启动Bootstrap ClassLoader,然后初始化sun.misc.Launcher。

接着,Launcher初始化Extension ClassLoader和App ClassLoader。

三:源码分析

sun.misc.Launcher类是Java程序的入口。

其构造器如下:public Launcher() {

Launcher.ExtClassLoader var1;    try {

var1 = Launcher.ExtClassLoader.getExtClassLoader();

} catch (IOException var10) {        throw new InternalError("Could not create extension class loader", var10);

}    try {        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

} catch (IOException var9) {        throw new InternalError("Could not create application class loader", var9);

}

Thread.currentThread().setContextClassLoader(this.loader);

……

}

其中有两行比较重要的代码:Launcher.ExtClassLoader.getExtClassLoader();this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

第一行初始化了ExtClassLoader,但没有指定其parent。

一些文章表示ExtClassLoader的父加载器是Bootstrap ClassLoader,这个说法其实并不完全准确。

第二行初始化了AppClassLoader,指定ExtClassLoader作为其父加载器。并将AppClassLoader作为系统类加载器。

AppClassLoader将会成为自定义ClassLoader的默认父加载器。

具体逻辑可按照以下顺序查看源代码:Launcher类的getClassLoader()方法。

ClassLoader类的initSystemClassLoader()方法。

ClassLoader类的getSystemClassLoader()方法。

ClassLoader类的ClassLoader()方法。

其中getSystemClassLoader()方法的注释为:/**

* Returns the system class loader for delegation.  This is the default

* delegation parent for new ClassLoader instances, and is

* typically the class loader used to start the application.

**/

ExtClassLoader和AppClassLoader都继承了URLClassLoader类。

URLClassLoader支持从文件目录和jar包加载class。

ExtClassLoader和AppClassLoader都调用了父类的构造函数。public URLClassLoader(URL[] urls, ClassLoader parent,

URLStreamHandlerFactory factory)

URLClassLoader类中有个属性为ucp,表示该ClassLoader负责搜索的路径。

ExtClassLoader和AppClassLoader最大的不同,即它们负责的路径不同。/* The search path for classes and resources */private final URLClassPath ucp;

查看源码可得:

ExtClassLoader负责搜索的路径为:String var0 = System.getProperty("java.ext.dirs");

AppClassLoader负责搜索的路径为:String var1 = System.getProperty("java.class.path");

所以,上一节可以通过这两个方法获取不同ClassLoader所负责加载的目录。

此外,Bootstrap ClassLoader负责搜索的路径为:String bootClassPath = System.getProperty("sun.boot.class.path");

ClassLoader源码

ClassLoader是一个抽象类,几个主要的方法如下:defineClass(String name, byte[] b, int off, int len)把字节数组b中的内容转换成Java类,返回的结果是java.lang.Class类的实例。

findClass(String name)查找名称为name的类,返回的结果是java.lang.Class类的实例。

loadClass(String name)加载名称为name的类,返回的结果是java.lang.Class类的实例。

resolveClass(Class> c)链接指定的Java 类。

其中,loadClass方法是最常涉及的一个。

其代码如下:protected Class> loadClass(String name, boolean resolve)        throws ClassNotFoundException

{    synchronized (getClassLoadingLock(name)) {        // First, check if the class has already been loaded

Class> c = findLoadedClass(name);        if (c == null) {            long t0 = System.nanoTime();            try {                if (parent != null) {

c = parent.loadClass(name, false);

} else {

c = findBootstrapClassOrNull(name);

}

} catch (ClassNotFoundException e) {                // ClassNotFoundException thrown if class not found

// from the non-null parent class loader

}            if (c == null) {                // If still not found, then invoke findClass in order

// to find the class.

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;

}

}

该方法主要的步骤如下:指定全限类名进行加载,首先调用findLoadedClass(name)判断当前类加载器是否已经加载该类。

如果没有被加载。则判断当前ClassLoader的父加载器是否为null。如果不为null,则委托其父加载器进行加载。如果为null,则使用Bootstrap ClassLoader进行加载。

如果父加载器或Bootstrap ClassLoader都无法加载,则调用findClass(name)方法寻找需要加载的类。

此外,loadClass方法还涉及加锁的过程,使用ConcurrentHashMap对不同的全限类名进行加锁。

具体可查看getClassLoadingLock方法。

四:双亲委托模式

Java类加载机制使用双亲委托模式。

一个ClassLoader加载一个类时,首先需要将任务委托给其父加载器,直到Bootstrap ClassLoader。

如果父加载器未加载该类,则逐层返回给委托发起者即当前ClassLoader进行加载。

在正常应用中,用户不自定义类加载器。

类加载工作首先由App ClassLoader发起,然后委托给Extension ClassLoader,最后委托给Bootstrap ClassLoader。

首先,通过一个例子了解三个ClassLoader所负责加载的类和双亲委托模式。

新建一个jar包,名为acai-cl.jar,包中有个简单的Person类。

写一个简单的程序输出person对象所对应的ClassLoader。import com.acai.Person;public class TestClassLoader {    public static void main(String[] args) {

Person person = new Person();

System.out.println(person.getClass().getClassLoader());

}

}

测试一:将jar包引入项目

jar包引入项目

对应输出:sun.misc.Launcher$AppClassLoader@18b4aac2

可以看到,位于ClassPath的类,是由App ClassLoader负责加载。

测试二:将jar包复制到%JRE_HOME%/lib/ext目录

复制到%JRE_HOME%/lib/ext

对应输出:sun.misc.Launcher$ExtClassLoader@4cc77c2e

可以得出,Extension ClassLoader负责加载%JRE_HOME%/lib/ext目录下的类。

加载Person类时,会首先尝试使用App ClassLoader进行加载。

由于双亲委托模式,最终委托到Extension ClassLoader,而其负责的目录%JRE_HOME%/lib/ext下存在Person类,则进行了类加载操作。

测试三:将jar包追加到Bootstrap ClassLoader加载路径上

追加到Bootstrap ClassLoader加载路径

使用参数:-Xbootclasspath/a:d:\acai-cl.jar,将jar包追加到Bootstrap ClassLoader加载路径。

对应输出:null

可以看出,Person类的加载工作,最终被委托到了Bootstrap ClassLoader。

注:Bootstrap ClassLoader由C++撰写。由Bootstrap ClassLoader负责加载的类,其getClassLoader()方法输出为null。

可以尝试输出String类的类加载器。System.out.println(String.class.getClassLoader());

接下来,再通过debug来验证双亲委托模式。

还是原来那个简单的demo。import com.acai.Person;public class Test {    public static void main(String[] args) {

Person person = new Person();

System.out.println(person.getClass().getClassLoader());

}

}

在ClassLoader类的loadClass方法上打断点。

App ClassLoader尝试加载

Extension ClassLoader尝试加载

Bootstrap ClassLoader尝试加载

可以看出,类的加载过程符合从下到上委托,最终会被委托到Bootstrap ClassLoader。

同时符合从上到下加载,每一层ClassLoader都会尝试进行加载。最终由App ClassLoader加载了Person类。

接着,尝试加载一个特殊的类:Splash.class。

Splash类位于jfxrt.jar,这个jar包在%JRE_HOME%/lib/ext目录下。import com.sun.javafx.applet.Splash;public class ExtTest {    public static void main(String[] args) {

Splash splash = new Splash(null);

System.out.println(splash.getClass().getClassLoader());

}

}

对应输出:sun.misc.Launcher$ExtClassLoader@330bedb4

毫无疑问,Splash类应该由Extension ClassLoader进行加载。

但其加载过程,仍然会从默认的系统类加载器App ClassLoader开始。

可以通过debug进行查看。

App ClassLoader尝试加载

Splash类加载的过程会被委托到Bootstrap ClassLoader,但Bootstrap ClassLoader并不负责加载%JRE_HOME%/lib/ext目录下的类。最终由Extension ClassLoader进行加载。

Bootstrap ClassLoader尝试加载未成功

最终由Extension ClassLoader加载

很多文章在阐述三个ClassLoader之间的关系时候,会给出一个getParent操作的demo。

并且认为Bootstrap ClassLoader是Extension ClassLoader的父加载器。

Extension ClassLoader是App ClassLoader的父加载器。

App ClassLoader是自定义类加载器的父加载器。

这样的解释基本正确,但Bootstrap ClassLoader和Extension ClassLoader之间的关系需要额外解释。

双亲委托机制,图片来自参考7

由于Bootstrap ClassLoader并不是使用Java编写,故无法指定Extension ClassLoader的parent为Bootstrap ClassLoader。

这一层关系在ClassLoader的loadClass方法中做了弥补。

在加载类时,会判断当前ClassLoader的父加载器是否为null,为null则使用Bootstrap ClassLoader进行加载。

在Java提供的三个默认类加载器中,父加载器为null的只有Extension ClassLoader。

该过程可参考ClassLoader的loadClass方法。

为什么使用双亲委托模式?

网上很多例子是关于String类。假设自己写一个java.lang.String类,使用双亲委托模式可以防止这个问题。

但其实双亲委托模式可以被打破,而真正阻止自定义java.lang.String的是“安全机制”。

这里尝试自定义java.lang.String类,并使用自定义ClassLoader进行加载。package java.lang;public class String {

}import java.io.IOException;import java.nio.file.Files;import java.nio.file.Paths;public class StringClassLoader extends ClassLoader {    @Override

public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {        if ("java.lang.String".equals(name)) {            return findClass(name);

} else {            return super.loadClass(name);

}

}    @Override

public Class> findClass(String s) throws ClassNotFoundException {        try {            byte[] classBytes = Files.readAllBytes(Paths.get("d:/String.class"));            return defineClass(s, classBytes, 0, classBytes.length);

} catch (IOException e) {            throw new ClassNotFoundException(s);

}

}    public static void main(String[] args) throws ClassNotFoundException {

StringClassLoader stringClassLoader = new StringClassLoader();

Class clazz = stringClassLoader.loadClass("java.lang.String", false);

System.out.println(clazz.getClassLoader());

}

}

该自定义类加载器破坏了双亲委托机制,具体方式将在下个章节说明。

输出结果为:Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang

可以看到,在被findClass方法调用的defineClass中有这么一段:if ((name != null) && name.startsWith("java.")) {            throw new SecurityException

("Prohibited package name: " +

name.substring(0, name.lastIndexOf('.')));

}

它会检查当前加载类的全限类名是否以java.开头,这也是一种安全机制。

如果按照网上的说法,java.lang.String被Bootstrap ClassLoader加载,demo中自定义的类加载器会被略过,不会输出异常。

所以说,双亲委托模式的作用只是防止类重复加载。

五:自定义ClassLoader

多数情况下,Java默认的三个类加载器已经可以满足需求。

自定义类加载器则可以实现额外的需求,例如:从网络文件加载类。

从任意目录加载类。

对字节码文件做加密处理,由自定义类加载器做解密。

实现自定义类加载器的主要步骤为:继承ClassLoader类。如果只是从目录或者jar包加载类,也可以选择继承URLClassLoader类。

重写findClass方法。

在重写的findClass方法中,无论用何种方法,获取类文件对应的字节数组,然后调用defineClass方法转换成类实例。

自定义类加载器真正好玩的是打破双亲委托机制,也是很多面试官会问到的问题。

上文提到类加载双亲委托模式实现位于ClassLoader的loadClass方法,想要破坏这个机制,则需要重写该方法。

打破双亲委托模式的确有一定的实用价值。

比如有两个class文件,或者两个jar包。

其中两个类的全限类名都一样,如果需要同时使用这两个类,则需要打破双亲委托模式。

有两个Person类,它们的全限类名均为com.acai.Person,唯一的区别是sayHello()方法输出的内容略有不同。package com.acai;import lombok.Data;@Datapublic class Person {    private String name;    private Integer age;    public void sayHello() {

System.out.println("Hello, this is Person in acai-cl");

}

}package com.acai;import lombok.Data;@Datapublic class Person {    private String name;    private Integer age;    public void sayHello() {

System.out.println("Hello, this is Person in acai-cl2");

}

}

将两个Person所在的项目打成jar包。

两个jar包

常规操作是,把两个jar包都引进项目。

写一个小小的demo。import com.acai.Person;public class Main {    public static void main(String[] args) throws Exception {

Person person = new Person();

System.out.println(person.getClass().getClassLoader());

person.sayHello();

}

}

对应输出为:sun.misc.Launcher$AppClassLoader@18b4aac2

Hello, this is Person in acai-cl

可以看到,demo中默认使用了acai-cl.jar中的Person类。

如果想要使用acai-cl2.jar中的Person类,则想到新建一个ClassLoader。

需要从jar包加载类,则优先想到URLClassLoader。import com.acai.Person;import java.io.File;import java.lang.reflect.Method;import java.net.URL;import java.net.URLClassLoader;public class Main {    public static void main(String[] args) throws Exception {

Person person = new Person();

System.out.println(person.getClass().getClassLoader());

person.sayHello();

URL url = new File("d:/acai-cl2.jar").toURI().toURL();

URLClassLoader loader = new URLClassLoader(new URL[]{url});

Thread.currentThread().setContextClassLoader(loader);

Class> clazz = loader.loadClass("com.acai.Person");

System.out.println(clazz.getClassLoader());

Method method = clazz.getDeclaredMethod("sayHello");

method.invoke(clazz.newInstance());

}

}

作者:阿菜的博客

链接:https://www.jianshu.com/p/d98324f5ad23

java加载机制_详解Java类加载机制相关推荐

  1. java双缓存机制_详解JVM类加载机制及类缓存问题的处理方法

    前言 大家应该都知道,当一个Java项目启动的时候,JVM会找到main方法,根据对象之间的调用来对class文件和所引用的jar包中的class文件进行加载(其步骤分为加载.验证.准备.解析.初始化 ...

  2. java 配置文件的路径_详解java配置文件的路径问题

    详解java配置文件的路径问题 详解java配置文件的路径问题 各种语言都有自己所支持的配置文件,配置文件中有很多变量是经常改变的.不将程序中的各种变量写死,这样能更方便地脱离程序本身去修改相关变量设 ...

  3. java同步异步调用_详解java 三种调用机制(同步、回调、异步)

    1:同步调用:一种阻塞式调用,调用方要等待对方执行完毕才返回,jsPwwCe它是一种单向调用 2:回调:一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口: 3:异步调用:一种类似消 ...

  4. java 死锁 内存消耗_详解Java中synchronized关键字的死锁和内存占用问题

    先看一段synchronized 的详解: synchronized 是 java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并 ...

  5. java 配置文件配置路径_详解java配置文件的路径问题

    详解java配置文件的路径问题 各种语言都有自己所支持的配置文件,配置文件中有很多变量是经常改变的.不将程序中的各种变量写死,这样能更方便地脱离程序本身去修改相关变量设置. 那么我们需要读取配置文件, ...

  6. java使用集合存储过程_详解java调用存储过程并封装成map

    详解java调用存储过程并封装成map 发布于 2020-5-1| 复制链接 摘记: 详解java调用存储过程并封装成map           本文代码中注释写的比较清楚不在单独说明,希望能帮助到大 ...

  7. java 线程一直运行状态_详解JAVA 线程-线程的状态有哪些?它是如何工作的?

    线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在. 一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源.更加轻量化,也因 ...

  8. java注解 源码_详解Java注解教程及自定义注解

    详解Java注解教程及自定义注解 更新时间:2016-02-26 11:47:06   作者:佚名   我要评论(0) Java注解提供了关于代码的一些信息,但并不直接作用于它所注解的代码内容.在这个 ...

  9. java的注解方式_详解Java注解的实现与使用方法

    详解Java注解的实现与使用方法 Java注解是java5版本发布的,其作用就是节省配置文件,增强代码可读性.在如今各种框架及开发中非常常见,特此说明一下. 如何创建一个注解 每一个自定义的注解都由四 ...

最新文章

  1. Windows下C 用 Socket 发送图片--基础
  2. Android中RatingBar的自定义效果
  3. This tutorial code needs the xfeatures2d contrib module to be run.
  4. VS中一些提高编码效率的快捷键
  5. Android开发的环境搭建及HelloWorld的实现
  6. 【实习项目记录】(四)Android 实现手机验证时,按钮倒计时60s
  7. AngularJS-liveRoomDirective.js 直播间指令
  8. 如何训练您的医生...使用开源
  9. sql 截取字符串:
  10. layim mysql_ichat系统说明 · ThinkPHP5+workerman+layIM打造聊天系统 · 看云
  11. linux 多线程服务端编程 pdf,Linux 多线程服务端编程.pdf
  12. xshell5 的账号密码搬家
  13. 微信卡券的创建、领取、核销
  14. python-selenium 自动化弹幕
  15. 20中氨基酸名称、简写及化学式
  16. 深度学习之CNN宫颈癌预测
  17. 基于蚁群算法的图像边缘检测
  18. 【小程序】解析二维码decodeURIComponent()
  19. SQL Server阻塞与锁
  20. Ali-Perseus(擎天):统一深度学习分布式通信框架 [弹性人工智能]...

热门文章

  1. .class 字节码文件与Java RTTI(类型信息)(.class 类对象)
  2. Python、Java 在线笔试
  3. Python 基础 —— sorted
  4. SQOOP --hive-import 错误(Sqoop Hive exited with status 1)及解决
  5. 编程规范 —— 变量的命名
  6. python就业方向-为什么这么多人喜欢Python?Python的就业方向是什么?
  7. 自学python需要安装什么-学习python需要什么基础吗?老男孩Python
  8. python编程案例教程-Python程序设计案例教程——从入门到机器学习(微课版)
  9. python常用代码总结-Python基础常见问题总结(一)
  10. 在线语音识别_腾讯云在线语音识别_在线语音识别成文字 - 云+社区 - 腾讯云