目录

  • 7.1 概述
  • 7.2 类加载的时机
    • 类加载生命周期
    • 开始顺序
    • 类初始化时机
    • 主动引用
    • 被动引用
    • 接口加载与类加载过程的区别
  • 7.3 类加载的过程
    • 7.3.1 加载
    • 7.3.2 验证
    • 7.3.3 准备
    • 7.3.4解析
    • 7.3.5 初始化
  • 7.4 类加载器
    • 7.4.1 类与类加载器
    • 7.4.2 双亲委派模型
      • 工作过程
      • 好处
      • 实现
      • 自定义类加载器实现
  • 参考文章

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

7.1 概述

什么是类加载机制
虚拟机把描述类的数据从Class文件加载进内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制;

Java里天生可以动态扩展的语言特性
与那些在编译期需要进行连接工作的语言不同,在Java语言中,类型的加载,链接和初始化过程都是在程序运行期完成的。这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java语言里天生可以动态扩展的语言特性就是依赖运行期可以动态加载和动态链接这个特点完成的。例如编写一个面向接口的应用程序,可以等到运行时再指定其具体实现类。

为了避免语言表达中可能产生的误差,在本章正式开始之前,先设立两个语言上的约定:
第一:Class文件可能表示类或接口
第二:Class文件夹应当是一串二进制字节流,无论以何种形式存在都可以;

7.2 类加载的时机

类加载生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(Unloading)7个阶段。其中验证,准备和解析三个阶段统称为链接(Linking)。

开始顺序

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。注意:这里笔者写的是按部就班地“开始”,而不是按部就班的“进行”或者“完成”,强调这点是因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段的执行过程中调用、激活另外一个阶段。
什么情况下要开始类加载过程的第一个阶段:加载?
Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。

类初始化时机

主动引用

对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(加载,验证和准备自然需要在此之前开始)

  1. 遇到new,getstatic,putstatic,invokestatic这四条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。生成这四条字节码执行的最常用的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则先触发其初始化
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动的时候,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄时,并且这个方法句柄所对应的类没有进行过初始化,则先触发其初始化。
    对于这五种会触发类进行初始化的场景,虚拟机规范种使用了一个很强烈的限定词:”有且只有“,这五种场景中的行为称为对一个类进行主动引用

被动引用

除此之外,所有引用类的方法都不会触发初始化,称为被动引用

  1. 对于静态字段,只有直接定义这个字段的类才会被初始化。通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。(是否要触发子类的加载和验证,在虚拟机规范中并未明确规定,取决于虚拟机的具体实现。对于Sun HotSpot虚拟机来说,可通过-XX:+TraceClassLoading参数看到此操作会导致子类的加载。)
public class SuperClass {static {System.out.println("SuperClass init!");
}public static int value = 123;
}public class SubClass extends SuperClass{static {System.out.println("SubClass init!");}
}public class NotInitialization{public static void main(String[] args){System.out.println(SubClass.value);}
}

执行结果:(并不会输出SubClass init!)
SuperClass init!
123

  1. 通过数组定义来引用类,不会触发此类的初始化。(SuperClass类的定义在上个例子里)
public class NotInitialization{public static void main(String[] args){SuperClass[] sca = new SuperClass[10];}
}

执行结果:没有输出

例子里包名为:org.fenixsoft.classloading。该例子没有触发类org.fenixsoft.classloading.SuperClass的初始化阶段,但触发了另外一个名为“[Lorg.fenixsoft.classloading.SuperClass”的类的初始化阶段,对于用户代码来说,这并不是一个合法的类名称,它是一个由虚拟机自动生成的、直接继承于Object的子类,创建动作由字节码指令newarray触发。这个类代表了一个元素类型为org.fenixsoft.classloading.SuperClass的一维数组,数组中应有的属性和方法(用户可直接使用的只有被修饰为public的length属性和clone()方法)都实现在这个类里。

  1. 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
public class ConstClass{static {System.out.println("ConstClass init!");}public static final String HELLOWORLD = "hello world";
}public class NotInitialization{public static void main(String[] args){System.out.println(ConstClass.HELLOWORLD);}
}

执行结果:(没有输出ConstClass init!)
hello world

接口加载与类加载过程的区别

  • 接口也有初始化过程,这点是与类是一致的,上面代码都是用静态语句块“static{}”来输出初始化信息的,而接口中不能使用“static{}”语句块,但编译器仍然会为接口生成“()”类构造器,用于初始化接口中所定义的成员变量。
  • 接口与类的真正区别是 主动引用的第三个,当一个接口在初始化时,并不要求其父类全部都完成了初始化,只有在真正使用到父接口的时候(如引用就扣中定义的常量)才会初始化

7.3 类加载的过程

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

7.3.1 加载

加载是类加载的一个阶段,注意不要混淆。
加载阶段虚拟机需要完成以下三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

其中二进制字节流可以从以下方式中获取:

  • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
  • 从网络中获取,最典型的应用是 Applet。
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

7.3.2 验证

验证是连接阶段的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机能否承受恶意代码的攻击

  1. 文件格式验证
    验证class文件格式规范
  2. 元数据验证
    就是对字节码描述的信息进行语义分析,保证描述的信息符合java语言规范。验证点可能包括(这个类是否有父类(除Object)、这个类是否继承了不允许被继承的类(final修饰的)、如果这个类的父类是抽象类,是否实现了父类或接口中要求实现的方法)。
  3. 字节码验证
    进行数据流和控制流分析,这个阶段对类的方法体进行校验,保证被校验的方法在运行时不会做出危害虚拟机的行为。
  4. 符号引用验证
    符号引用中通过字符串描述的权限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private protected public default)是否能被当前类访问。

7.3.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中,应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
  • 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值

初始值一般为 0 值,例如下面的类变量 value 被在准备阶段过后的初始值为 0 而不是 123。

public static int value = 123;

如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。

public static final int value = 123;

7.3.4解析

解析阶段是虚拟机将常量池的符号引用替换为直接引用的过程。符号引用在Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。

符号引用:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。

直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。

  1. 类、接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析

7.3.5 初始化

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 () 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:


public class Test {static {i = 0;                // 给变量赋值可以正常编译通过System.out.print(i);  // 这句编译器会提示“非法向前引用”}static int i = 1;
}

由于父类的 () 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:


static class Parent {public static int A = 1;static {A = 2;}
}static class Sub extends Parent {public static int B = A;
}public static void main(String[] args) {System.out.println(Sub.B);  // 2
}

接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但接口与类不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 () 方法。

虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

7.4 类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

7.4.1 类与类加载器

对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。这是因为每一个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。

7.4.2 双亲委派模型

从虚拟机的角度来说,只存在两种不同的类加载器:

  • 一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。
  • 另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。

从Java开发人员的角度来看,大部分Java程序一般会使用到以下三种系统提供的类加载器:

  • 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
  • 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

双亲委派模型
应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。

下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。

工作过程

如果一个类加载器收到了类请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一层都是如此,因此所有类加载的请求都会传到启动类加载器,只有当父加载器无法完成该请求时,子加载器才去自己加载。

好处

使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。

实现

以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。

public abstract class ClassLoader {// The parent class loader for delegationprivate final ClassLoader parent;public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {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.c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}}protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}
}
自定义类加载器实现

以下代码中的 FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。

public class FileSystemClassLoader extends ClassLoader {private String rootDir;public FileSystemClassLoader(String rootDir) {this.rootDir = rootDir;}protected Class<?> findClass(String name) throws ClassNotFoundException {byte[] classData = getClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {return defineClass(name, classData, 0, classData.length);}}private byte[] getClassData(String className) {String path = classNameToPath(className);try {InputStream ins = new FileInputStream(path);ByteArrayOutputStream baos = new ByteArrayOutputStream();int bufferSize = 4096;byte[] buf
fer = new byte[bufferSize];int bytesNumRead;while ((bytesNumRead = ins.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}private String classNameToPath(String className) {return rootDir + File.separatorChar+ className.replace('.', File.separatorChar) + ".class";}
}

参考文章

JVM类加载过程
类加载过程和类加载器
Java虚拟机

深入理解java虚拟机 第7章 虚拟机类加载机制相关推荐

  1. 深入理解Java类型信息(Class对象)与反射机制

    关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java并发之synchronize ...

  2. java虚拟机类加载机制_《深入理解java虚拟机》学习笔记一/类加载机制

    为何要读这本书? 近期看了左萧龙大哥的单例设计模式,后文讲解到了JVM对类实例化相关知识,感觉看着很无力,不懂,于是乎买本书研究下. 如何读? 个人水平一般,理解程度有限,书中说到每章关联不是很大,所 ...

  3. java虚拟机类加载机制浅谈_浅谈Java虚拟机(三)之类加载机制

    在<浅谈Java虚拟机>这篇文章中,我们提到了JVM从操作系统方面来说,可以将其看做是一个进程,分别有类加载器子系统,执行引擎子系统和垃圾收集子系统.这一篇文章就简单的来谈一下类加载器子系 ...

  4. Java 工程师必须掌握的 JVM 类加载机制!

    这篇文章不聊别的,专门来侃侃JVM的类加载机制 概念 类加载器把class文件中的二进制数据读入到内存中,存放在方法区,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据 ...

  5. java类验证和装载顺序_java类加载机制,你会了吗?

    什么是类加载机制呢? java虚拟机将编译后的class文件加载到内存中,进行校验.转换.解析和初始化,到最终的使用.这就是java类加载机制: 下面就开始今天的内容: 1.类加载的生命周期:加载(L ...

  6. java类验证和装载顺序_Java类加载机制实现流程及原理详解

    前言 我们知道,Java项目编译后会生成许许多多的class文件,class文件保存着类的描述信息.虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转化解析和初始化,最终形成可以被虚 ...

  7. java的反射和它的类加载机制

    2019独角兽企业重金招聘Python工程师标准>>> 1. java 的类装载系统: 在java虚拟机中有两种类装载器: 启动类装载器 和 自定义类装载器. 前者是jvm的一部分, ...

  8. 无招胜有招之Java进阶JVM(八)类加载机制

    6.1.ClassLoader的两个作用和一个任务 作用:1.将Class加载到JVM中2.审视每一个类应该由谁加载. 任务:将Class字节码重新解析成统一要求的对象格式 6.2上级委托接待机制又称 ...

  9. java web 加载类_Tomcat类加载机制

    Tomcat Server在启动的时候将构造一个ClassLoader树,以保证模块的类库是私有的 Tomcat Server的ClassLoader结构如下: 代码 Bootstrap | Syst ...

最新文章

  1. qt能使用logback_Spring boot使用logback实现日志配置
  2. 手机没电了 鸿蒙还有用吗,记住这几个技巧,手机没电时可以有效延长使用时间...
  3. python 抽象语法树_用python演示一个简单的AST(抽象语法树)
  4. 对称密码和非对称密码体系_密码学类型:对称和不对称
  5. .xyz域名总量12强:西数连续夺冠 联动天下跌至榜尾
  6. shell命令删除昨日的日志_linux定时自动清理日志文件
  7. 什么是validationQuery
  8. 二叉树中序遍历的下一个节点
  9. java基础知识---IO常用基础操作(二)
  10. abp调用登录接口获取token再调用其他接口
  11. 超算简史:练飞天之技,登峰一战后,终有落地日 | 凌云时刻
  12. python如何实现数据可视化,如何用python做可视化
  13. God.org单域环境攻略(三)
  14. 自定义桌面图标!制作属于自己的个性化桌面吧(=^x^=)
  15. 闲居即兴 - 反卷诗篇
  16. 记一次too many open files 异常
  17. SDL编程入门(27)碰撞检测
  18. https://isux.tencent.com/svg-animate.html(svg动画)
  19. 揭秘-Android刷量有多容易 1
  20. 第二证券|沪指冲上3200点,中字头太火爆!

热门文章

  1. Nature:微生物的胆汁酸代谢物调节宿主肠道特异Treg细胞的稳态
  2. 想知道你手机的身份证号(IMEI)吗?
  3. 打印机扫描功能怎么用
  4. 如何用html制作明信片,用HTML和css选择器写明信片
  5. Linux ubuntu20.04 root密钥登录教程(图文教程)
  6. JS 阻止冒泡 阻止默认
  7. android mi4 分辨率,小米11屏幕分辨率怎么样 小米11屏幕分辨率是多少
  8. 链表反转(指定区间)
  9. String截取字符串的指定字节长度
  10. CAD的秘密小技巧——关联性阵列