Java classloader详解

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: Java程序并不是一个可执行文件,而是由很多的Java类组成,其运行是由JVM来控制的。而JVM从内存中查找到类,而真正将类加载进内存的就是ClassLoader,可以说我们每天都在接触ClassLoader,但是很多时候我们没有明白其执行的流程和原理。

Java程序并不是一个可执行文件,而是由很多的Java类组成,其运行是由JVM来控制的。而JVM从内存中查找到类,而真正将类加载进内存的就是ClassLoader,可以说我们每天都在接触ClassLoader,但是很多时候我们没有明白其执行的流程和原理。

01

为什么需要ClassLoader?

ClassLoader的有以下的作用:

  1. 从本地系统加载类文件,甚至是从网络上加载Java类文件,例如Applet,加载Class是程序执行的前提。
  2. 进行应用的隔离,不同的应用使用ClassLoader加载的类实现相互隔离不可见,典型的例子如tomcat,启动的时候会启动一个JVM,Tomcat下面会部署多个应用,但是多个应用之间的类是相互不可见的,也不能相互调用,这就是依靠自定义的ClassLoader  WebappX进行隔离的。
  3. 自定义ClassLoader在执行非信任代码前验证数字签名。
  4. 自定义ClassLoader根据用户提供的密码解密代码,从而可以对.class文件加密,避免被反编译。
  5. 根据用户的需要动态的创建类,增强代码能力。

02

Java ClassLoader运行机制

Java提供了三个ClassLoader,分别是BootStrapClassLoader、ExtClassLoader和AppClassLoader。

1、BootStrapClassLoader

启动类装载器,主要加载jre的lib目录下的Java类,使用C++编写,是JVM自带的类加载器,用来装载核心类库。Java程序可以通过以下代码查看这个类加载器加载了哪些jar包:

URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
     for (int i = 0; i < urls.length; i++) {
       System.out.println(urls[i].toExternalform());
     }

执行结果如下:

file:/C:/Program%20Files%20(x86)/Java/jre7/lib/resources.jar
  file:/C:/Program%20Files%20(x86)/Java/jre7/lib/rt.jar
  file:/C:/Program%20Files%20(x86)/Java/jre7/lib/sunrsasign.jar
  file:/C:/Program%20Files%20(x86)/Java/jre7/lib/jsse.jar
  file:/C:/Program%20Files%20(x86)/Java/jre7/lib/jce.jar
  file:/C:/Program%20Files%20(x86)/Java/jre7/lib/charsets.jar
  file:/C:/Program%20Files%20(x86)/Java/jre7/lib/jfr.jar
  file:/C:/Program%20Files%20(x86)/Java/jre7/classes

可以明显看出BootStrapClassLoader在jre目录下加载了lib目录的特定jar和classes目录下的所有的类,这其实是在JVM中定义的,加载类的路径源代码定义如下:

static const char classpathFormat[] =
  "%/lib/rt.jar:"
  "%/lib/i18n.jar:"
  "%/lib/sunrsasign.jar:"
  "%/lib/jsse.jar:"
  "%/lib/jce.jar:"
  "%/lib/charsets.jar:"
  "%/classes";

有时候我们希望使用BootStrapClassLoader加载额外的一些指定类或者jar包,此时可以在执行java启动命令的时候指定-Xbootclasspath参数。

-Xbootclasspath参数有三种使用方式:

  • 直接使用-Xbootclasspath参数。完全取代基本核心的Java class 搜索路径,如果使用这种方式,需要自己全新编写核心类或者重新制定核心类jar包路径,基本不使用。
  • 使用-Xbootclasspath/a。以冒号作为path分隔符,指定包含类的目录路径,jar或者zip的路径,此参数表示将这些额外的类附加到默认的类路径中,先Load默认的jar文件,然后再Load   -Xbootclasspath/a:path指定的jar文件,这种方式不会导致覆盖系统提供的默认类。可以编写执行类A,由类A引用类B,将B类打成jar包B.jar,执行以下命令,如果能够正常查找到类B,证明加载成功。
java -verbose:class -Xbootclasspath/a:B.jar com.fantuantech.A
  • 使用-Xbootclasspath/p。以冒号作为path分隔符,指定包含类的目录路径,jar或者zip的路径,此参数表示先加载参数指定的jar文件,然后再去加载系统默认的jar和类文件,一旦系统在参数指定的路径中load了全权限定名与JRE提供的默认的类文件相同的文件的时候,将不会去load  JRE提供的相同文件,所以这个参数会造成覆盖JRE提供的默认文件的情况,官方是不建议这么做的。

2、ExtClassLoader

加载$JAVA_HOME/jre/lib/ext目录下的类或者是其他任何通过java.ext.dirs系统属性指定的目录下的Java类。这是通过实现了sun.misc.Launcher$ExtClassLoader接口实现的。如果有自定义的公共类希望通过ExtClassLoader自动加载的话,可以采用以下两种方式:

  • java -Djava.ext.dirs=/home/externalDir,在程序启动的时候通过参数指定扩展类加载器额外加载的类路径
  • 将额外的jar包放在jre/lib/ext目录下,当AppClassLoader加载这些类加载不到时会委托ExtClassLoader加载,ExtClassLoader就会去这个目录下查找对应的类并加载。

ExtClassLoader的parent是BootStrapClassLoader,但是由于BootStrapClassLoader是由c++编写的,通过native方式加载的。而并不是由java编写的,所以当调用classLoader的getParent()方法时,获取到的是null。

3、AppClassLoader

加载java.class.path(一般映射到系统classPath)指定的目录下的所有Java类,实现sun.misc.Launcher$AppClassLoader接口。在程序执行时通过-classpath或者-cp或者-Djava.class.path可以指定系统类路径。

03

Java ClassLoader双亲委派机制

既然Java同时存在多种ClassLoader,那么这些ClassLoader什么关系,Class类的加载顺序是怎么样的,会不会存在冲突呢?为了解决这些问题,Java ClassLoader采用了双亲委派机制,如下图所示:

双亲委派机制是指某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

每个ClassLoader都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类,否则就会出现冲突。

ClassLoader有一些重要的方法,主要是以下几个:

  • loadCass
    loadClass(String  name ,boolean  resolve)其中name参数指定了JVM需要的类的名称,该名称以包表示法表示,如Java.lang.Object;resolve参数告诉方法是否需要解析类,在初始化类之前,应考虑类解析,并不是所有的类都需要解析,如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要解析。这个方法是ClassLoader  的入口点
  • defineClass
    这个方法接受类文件的字节数组并把它转换成Class对象。字节数组可以是从本地文件系统或网络装入的数据。它把字节码分析成运行时数据结构、校验有效性等等。
  • findSystemClass
    findSystemClass方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass将字节数组转换成Class对象,以将该文件转换成类。当运行Java应用程序时,这是JVM 正常装入类的缺省机制。
  • resolveClass
    resolveClass(Class c)方法解析装入的类,如果该类已经被解析过那么将不做处理。当调用loadClass方法时,通过它的resolve 参数决定是否要进行解析。
  • findLoadedClass
    当调用loadClass方法装入类时,调用findLoadedClass 方法来查看ClassLoader是否已装入这个类,如果已装入,那么返回Class对象,否则返回NULL。如果强行装载已存在的类,将会抛出链接错误。

其中类加载主要是loadClass方法,以下是方法的定义,可以看到查找类主要分为4个步骤:

  1. findLoadedClass判断类是否已经被加载。
  2. 如果类未被加载则通过parent.loadClass加载类。
  3. 如果父ClassLoader为空,则调用findBootstrapClass0从BootstrapClassLoader去加载。
  4. 如果还是找不到,则调用findClass由当前加载器去加载相应的类。
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
   c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
   }
} catch (ClassNotFoundException e) {
   // If still not found, then invoke findClass in order    to find the class.
   c = findClass(name);
   }
}
if (resolve) {
   resolveClass(c);
   }
return c;
}

采用双亲委派机制有以下的好处:

  • 安全
    不允许任意的创建类替换Java的基础类,例如创建一个java.lang.Integer,新建的整个Integer是不会被加载的,因为根据双亲委派原则,会委托给BootStrapClassLoader去加载这个类,而BootstrapClassLoader是JVM实现的,无法更改,它默认去加载jre/lib目录下的类,包括了java.lang.Integer。
  • 隔离
    每个类在JVM中的唯一表示与类的权限定名以及类加载器有关,可以说一个类加载器加上一个类的全限定名唯一确定了一个类,通过自定义不同的ClassLoader,即使加载了相同限定名的类也不会造成冲突。比如tomcat的StandardClassLoader就是使用ClassLoader来为每一个应用做隔离。

04

自定义ClassLoader

这里我们编写一个简单的案例用来说明如何自定义ClassLoader,代码仅供演示,先创建一个目录,所有演示代码都在同个目录下:

1、创建test目录

mkdir /Users/lucas-os/workspace/test

2、自定义类MyClassLoader,继承ClassLoader

public class MyClassLoader extends ClassLoader {
    private String path;
    public MyClassLoader (String path) {
        this.path = path;
    }
      @Override
    protected Class findClass (String name) throws ClassNotFoundException {
        System.out.println(getSystemClassLoader().getName()+","+getSystemClassLoader().getParent().getName());
        String classPath = path+name+".class";
        InputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        try {
            inputStream = new FileInputStream(classPath);
            outputStream = new ByteArrayOutputStream();
            int temp = 0;
            while((temp = inputStream.read()) != -1){
                outputStream.write(temp);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                outputStream.close();
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        byte[] bytes = outputStream.toByteArray();
        Class clazz =  defineClass(name,bytes,0,bytes.length);
        resolveClass(clazz);
        return clazz;
    }
}

3、自定义HelloWorld类,用于被ClassLoader加载

public class HelloWorld {
    public HelloWorld(){
        System.out.println("Hello ClassLoader!");
    }
}

4、自定义Test类,作为程序入口

源代码如下:

public class Test {
    public static void main(String[] args) {
        MyClassLoader myClassLoader = new MyClassLoader("/Users/lucas-os/workspace/test/");
        try {
            Class clazz = myClassLoader.findClass("HelloWorld");
            clazz.getConstructor().newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

依次使用以下命令编译和执行代码:

javac HelloWorld.java
javac Test.java
java -classpath /Users/lucas-os/workspace/test/ Test

执行结果如下所示,可以看到当前SystemClassLoader是app,其父ClassLoader是platform,程序正常打印出了"class HelloWorld"证明类被正常加载。

app,platform
/Users/lucas-os/workspace/test/HelloWorld.class
Hello ClassLoader!
相关文章
|
7月前
|
Java 关系型数据库 MySQL
在Java的反射中,Class.forName和ClassLoader的区别
在Java的反射中,Class.forName和ClassLoader的区别
109 3
|
4月前
|
数据库 C# 开发者
WPF开发者必读:揭秘ADO.NET与Entity Framework数据库交互秘籍,轻松实现企业级应用!
【8月更文挑战第31天】在现代软件开发中,WPF 与数据库的交互对于构建企业级应用至关重要。本文介绍了如何利用 ADO.NET 和 Entity Framework 在 WPF 应用中访问和操作数据库。ADO.NET 是 .NET Framework 中用于访问各类数据库(如 SQL Server、MySQL 等)的类库;Entity Framework 则是一种 ORM 框架,支持面向对象的数据操作。文章通过示例展示了如何在 WPF 应用中集成这两种技术,提高开发效率。
66 0
|
4月前
|
安全 前端开发 Java
【JVM 探秘】ClassLoader 类加载器:揭秘 Java 类加载机制背后的秘密武器!
【8月更文挑战第25天】本文全面介绍了Java虚拟机(JVM)中的类加载器,它是JVM的核心组件之一,负责将Java类加载到运行环境中。文章首先概述了类加载器的基本工作原理及其遵循的双亲委派模型,确保了核心类库的安全与稳定。接着详细阐述了启动、扩展和应用三种主要类加载器的层次结构。并通过一个自定义类加载器的例子展示了如何从特定目录加载类。此外,还介绍了类加载器的完整生命周期,包括加载、链接和初始化三个阶段。最后强调了类加载器在版本隔离、安全性和灵活性方面的重要作用。深入理解类加载器对于掌握JVM内部机制至关重要。
175 0
|
Java 数据库连接
【Java面试】反射中,Class.forName和classloader的区别是什么?
【Java面试】反射中,Class.forName和classloader的区别是什么?
88 0
|
缓存 前端开发 安全
深入理解Java类加载器(ClassLoader)
深入理解Java类加载器(ClassLoader)
650 0
|
Java 应用服务中间件 数据库
《Java应用提速(速度与激情)》——五、ClassLoader提速
《Java应用提速(速度与激情)》——五、ClassLoader提速
|
XML 前端开发 Java
Java虚拟机系列: ClassLoader类加载机制
改不完的 Bug,写不完的矫情。公众号 杨正友 现在专注移动基础开发 ,涵盖音视频和 APM,信息安全等各个知识领域;只做全网最 Geek 的公众号,欢迎您的关注!
102 0
Java虚拟机系列: ClassLoader类加载机制
|
安全 前端开发 Java
浅析java中ClassLoader如何加载Class
浅析java中ClassLoader如何加载Class
138 0
|
Java
学了这么久的java反射机制,你知道class.forName和classloader的区别吗?
前两天头条有朋友留言说使用class.forName找不到类,可以使用classloader加载。趁此机会总结一下,正好看到面试中还经常问到。
363 0
学了这么久的java反射机制,你知道class.forName和classloader的区别吗?
|
设计模式 Java 开发者
Java学习路线-32:ClassLoader类加载器反射与代理设计模式
Java学习路线-32:ClassLoader类加载器反射与代理设计模式
170 0