类加载机制

一、前言

1、Write Once, Run Anywhere

我们都知道Java的最大的特点之一就是跨平台性,Java在诞生之时有一个很牛的口号:“Write Once, Run Anywhere!”.

那么为什么可以做到如此呢?关键点就在于JVM。存有我们业务逻辑的源文件,在通过编译器编译成字节码文件后,可以在不同平台(Windows、Linux等)上运行的 Java 虚拟机(JVM)——负责载入和执行 Java 编译后的字节码——运行。跨平台性的原理就是在JVM层面上屏蔽了不同操作系统上的差异。
类加载机制_第1张图片

图1 Java跨平台性的根本条件——JVM

2、字节码

操作系统中,是根据文件的类型后缀去判断文件类型的。那么JVM是如何判断一个文件是字节码文件的呢?

所有的字节码文件,都是以0xcafebabe开头的,这被称为“魔数”(Magic Number),是JVM识别字节码文件的标识。

​ 字节码由10部分组成,依次是魔数、版本号、常量池、访问权限、类索引、父类索引、接口索引、字段表索引、方法、Attribute。
类加载机制_第2张图片

图2 字节码文件

二、什么是类加载

​ 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

通俗点来说,类加载就是把编译后的字节码文件装在JVM里的过程。

类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

三、类加载过程

当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过 加载,连接,初始化 三步来实现对这个类进行初始化。

1、加载 Loading

一个字节码文件,它肯定是存在于磁盘中或者某个区域,JVM肯定要找到它并导入它,那么是通过什么样的方式去找到它呢?——这个类的全限定路径名,那么加载在干什么事我们就知道了——通过一个类的全限定名来获取定义此类的二进制的字节流。这个二进制字节流所代表的静态结构,转化为内存中的一个代表这个类的java.lang.Class对象,作为方法区里这个类的各种数据的访问入口。

2、连接 Linking

2.1 验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。简而言之是确保被加载类的正确性、合法性、安全性。验证阶段大致会完成4个阶段的检验动作:

文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

2.2 准备

负责为类的静态成员分配内存并设置默认初始化值

2.3 解析

​将类中的符号引用替换为直接引用 (直接引用:直接指向目标的指针,指的是地址

3、初始化 Initializing

给静态成员变量赋初值,执行类的初始化(静态代码块)内容。

初始化的详细过程:

  • 如果类还没有被加载和连接,那就先进行加载和连接;
  • 如果类存在父类,且父类还没有初始化,那就先初始化直接父类;
  • 如果类中存在初始化语句,顺序执行初始化语句。

类加载机制_第3张图片

图3 类的生命周期

四、类加载机制

1、类加载的时机

  • 创建类的实例
  • 访问类的静态变量
  • 主动调用类的静态方法
  • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
  • 初始化某个类的子类,会先触发父类的加载
  • 直接使用java.exe命令来运行某个主类

2、类加载器

刚刚所说的加载、连接、初始化最终加载到JVM是怎么做到的呢,其实就是通过类加载器。可以说,类加载器是类加载流程的实现者。

对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals)。

JDK自带的Class Loader:

2.1 启动类加载器(Bootstrap Class-Loader)

  • JVM自带的引导类加载器。由C/C++语言实现,在Java中打印null。
  • 负责Java核心类库的加载,加载 jre/lib 包下面的 jar 文件,比如说常见的 rt.jar、resources.jar,或者java程序运行指定的Xbootclasspath选项jar包。
  • 指定加载java,javax,sun等开头的包名类。

2.2 扩展类加载器(Extension or Ext Class-Loader)

  • Java语言编写的类加载器 sun.misc.Launcher$ExtClassLoader

  • 指定Bootstrap Classloader为Parent加载器 --> getParent()可获取Bootstrap Classloader

  • 负责加载Java平台中扩展功能的一些jar包,包括 jre/lib/ext 包下面的 jar 文件 或者 -Djava.ext.dirs指定目录下的jar包 。

    • 如果我们自定义的类需要交给Ext来加载可以放置到ext的目录下。

    • 还有需要注意的是,使用-Djava.ext.dirs指令会覆盖Java本身的ext设置。

      • 解决方法:

        1、把相关的lib复制到新的ext directory下;

        2、用冒号拼接多个directory,e.g. -Djava.ext.dirs=./plugin**:$JAVA_HOME/jre/lib/ext**

2.3 应用类加载器(Application or App Class-Loader)

  • Java语言编写的类加载器 sun.misc.Launcher$AppClassLoader
  • Java程序默认的类加载器,Java应用的类都是该类加载器加载的
  • 指定Extension Classloader 为parent加载器 --> getParent()可以获取Extension Classloader
  • 负责加载环境变量classpath指定的目录,或者java.class.path指定的目录类库。

通过一个简单例子了解:

public class Test {
     

	public static void main(String[] args) {
     
		ClassLoader loader = Test.class.getClassLoader();
		while (loader != null) {
     
			System.out.println(loader.toString());
			loader = loader.getParent();
		}
	}

}

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader() 可以获取到此引用;然后通过 loader.getParent() 可以获取类加载器的上层类加载器。

其输出结果:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d

​第一行输出为 Test 的类加载器,即应用类加载器,它是sun.misc.Launcher$AppClassLoader类的实例;第二行输出为扩展类加载器,是 sun.misc.Launcher$ExtClassLoader类的实例。那启动类加载器呢?

按理说,扩展类加载器的上层类加载器是启动类加载器,但启动类加载器由C/C++语言实现,在Java中为null。

3、双亲委派

3.1 双亲委派模型

先来看看ClassLoader的loadClassd()的源码:

    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;
        }
    }

这段代码其实就是双亲委派机制的根本,如果parent加载器不为null,则继续调用parent的loadClass().
类加载机制_第4张图片

图4 双亲委派模型

这种层次关系被称作为双亲委派模型:如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。

3.2 双亲委派的意义

3.2.1 避免类的重复加载

Java类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类的时候,就没有必要子类加载器再加载一次,这对于保证 Java 程序的稳定运作很重要。

如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等——双亲委派模型能够保证同一个类最终会被特定的类加载器加载。

3.2.2 保护程序安全,防止核心的JAVA语言环境遭受破坏

考虑到安全因素,Java核心API中定义类型不会被随意替换。

举个栗子,在classpath路径下自定义一个名为java.lang.S的类,其包名为java.lang:

package java.lang;

public class S {
     
    public S() {
     
        System.out.println("111");
    }
}

运行下面Demo的main方法:

public class Demo {
     
    public static void main(String[] args) {
     
        S s = new S();
    }
}

输出台会报错:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang

这个报错是说S类的自定义包名java.lang是非法的。java.lang.S类的类加载过程是:经过双亲委派机制,传递到启动类加载器中,由于父类加载器路径下并没有该类,也就是核心包中的java.lang包下没有S这个类,所以不会加载,将反向委托给子类加载器,最终会通过系统类加载器加载该类,但是这样做是不允许的,因为java.lang是核心的API包,核心包都应该交给BootStrap Classloader来加载,需要访问权限,强制加载将会报此异常。

4、自定义类加载器

三个重要函数:loadClass(),findClass(),defineClass()

loadClass():调用父类加载器的loadClass,加载失败则调用自己的findClass方法

findClass():根据名称读取文件存入字节数组

defineClass():把一个字节数组转为Class对象

4.1 自定义一个类加载器

自定义一个类加载器,只需继承ClassLoader类;也可以继承URLClassLoader类。后者的方法更加具象。

然后重写findClass()方法即可,其默认实现是会抛出一个异常。

4.2 为什么需要自定义类加载器?

1、 适配环境隔离

比如tomcat中有改写类加载器:
类加载机制_第5张图片

图5 Tomcat的类加载机制

​我们知道Tomcat的源码是用Java语言编写出来的,那么在一个JVM的层面上为什么能够并行地运行多个Web应用呢?来看看《Tomcat类加载机制》这张图:在上方三个JDK自带的类加载器之下,有一个Common ClassLoader,这个类加载器是用来加载Tomcat中公共通用的类;如果是默认用Catalina启动,则用的是Catalina ClassLoader,加载的是Catalina.sh中指定的启动类;JasperLoader是主要加载jsp文件;WebApp ClassLoader 是基于WEB目录或者war工程来进行类加载;Shared ClassLoader 是整个Tomcat层面每一个WEB工程都可以共享使用的。所以说通过这样的一种环境隔离的方式,Tomcat可以做到分层加载,或者说按需加载。

2、 可以从不同数据源来进行字节码的加载

类加载无非找到字节码文件的二进制流,然后读取到JVM内存中,有可能是从数据库、网络、Redis等将这个字节码加载过来。所以说,当我们需要适配各种.class数据源时可以去自定义ClassLoader。

3、 防止源码泄露

​Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。

4.3 打破双亲委派机制

​重写loadClass()方法即可:

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
     
    synchronized (getClassLoadingLock(name)) {
     
        Class<?> c = findLoadedClass(name);
        if (c == null) {
     
            c = findClass(name);
        }
        if (resolve) {
     
            resolveClass(c);
        }
        return c;
    }
}

把loadClass()中使用双亲委派的代码删掉,这样MyClassLoader不用再向上去找类加载器,只会在本类中处理,这样就打破了双亲委派模型。

参考地址:

1、java字节码的魔数是0xCAFEBABE,为什么是4字节,而不是8字节

2、我竟然不再抗拒 Java 的类加载机制了

3、JVM类加载机制

4、Java 类加载机制(阿里)-何时初始化类

5、自定义类加载器以及打破双亲委派模型

6、详解Java类的装载过程及类加载机制

7、Java类加载机制

8、自定义类加载器

你可能感兴趣的:(java,rookie,jvm,java,jdk)