Version: Next

类加载器

类加载

什么是类加载

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。

与那些在编译时需 要进行连接的语言不同,在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的

  • 这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销
  • 但是却为Java应用提供了极高的扩展性和灵活性

类的加载、链接、与初始化

在Java代码中,Class的加载、链接、初始化过程都是在程序运行期间(Runtime)完成的

  • 加载:
    • 加载 不是类加载,它是整个类加载过程中的一个阶段
    • 通过类的 全限定名 来获取定义此类的二进制字节流,具体讲可以从 Jar、War 包、网络、动态代理、反射等中获取流
    • 将这个字节流所代表的的静态存储结构转化为 方法区运行时数据结构
    • 在堆内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
  • 链接:
    • 验证:确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
      • 文件格式验证:是否符合 Class 文件规定的格式
      • 元数据验证:例如是否有父类、父类是否继承了不允许被继承的类、如果不是抽象类是否实现了所有父类或接口要求实现的方法等..
      • 字节码验证:对 Class 文件中的 Code 属性验证,即对类的方法体的验证。例如确保所有操作与其操作数的类型匹配、指令不会跳转到对应方法体之外的地方等
      • 符号引用验证:对应后面的解析阶段:主要是检查方法、属性存不存在、以及其可访问性(public private 等),对应的典型异常为 NoSuchFieldErrorNoSuchMethodError
    • 准备:为类的静态变量分配内存,并将其初始化为默认零值 (数字类为 0、0.0f等,引用类型为 null、布尔类型为 false)
      • 非final变量在准备阶段初始值为0或者null,具体的值在对象初始化阶段赋值 因为JVM在编一阶段会将静态变量的初始化操作定义在构造器
      • final变量在准备阶段就已经赋值完毕 Javac 在编译阶段后会为final类型的变量生成其对应的ConstantValue属性,JVM在准备阶段会根据ConstantValue将对应final属性赋值为具体值
    • 解析:把类中的符号引用转换为直接引用 在编译的时候,每一个Java类都会被编译为一个class文件,但是在编译的时候JVM并不知道所引用类的地址,所以就用符号引用来代替,而解析阶段就是为了将符号引用转化为真正的地址的阶段
  • 初始化:直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序
    • 为类的静态变量赋予正确的初始值
    • 初始化阶段就是执行类构造器<clinit>()方法的过程,它并不是程序员在Java代码中直接编写的方法
    • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
    • JVM 保证先执行父类的 <clinit>() 方法,因此 JVM 中第一个执行的 <clinit>() 方法的类型是 java.lang.Object,也因此,父类中的 静态语句块 优先于子类的变量赋值操作
    • JVM 对 <clinit>() 方法进行了线程安全保证,多线程环境下只会执行一次,其他竞争线程会阻塞,最终一个类只会被初始化一次

代码理解

public static int a = 1;
  1. 加载阶段 编译文件为.class文件,然后通过类加载,加载到JVM
  2. 链接阶段
    1. 验证:确保Class文件安全
    2. 准备:先初始化a = 0,因为int类型的初始值为0
    3. 解析:将符号引用转换为真实引用地址
  3. 初始化阶段 将a的值赋为1

类的加载

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

设置运行参数,查看类的家在信息,并打印出来

参数格式:

-XX:+<option>, 表示开启option选项

-XX:-<option>, 表示关闭option选项

-XX:+<option>=<value> , 表示将option选项的值设置为value

-XX:TraceClassLoading 用于追踪类的加载信息并打印出来

对于静态字段来说,只有直接定义了该字段的类才会被初始化;当一个类在初始化时,要求其父类已经初始化完毕;所有JVM实现必须在每个类或者接口被Java程序首次主动使用时才初始化他们

public class Demo02TestLoad {
public static void main(String[] args) {
System.out.println(MyChild.str1);
}
}
class MyParent{
public static String str = "hello world";
static{
System.out.println("My Parent static");
}
}
class MyChild extends MyParent{
public static String str1 = "welcome";
static {
System.out.println("My Child static");
}
}
[Loaded myJVM.MyParent from file:/Library/bsx/java/JavaProjects/testEnv/out/production/testEnv/]
[Loaded myJVM.MyChild from file:/Library/bsx/java/JavaProjects/testEnv/out/production/testEnv/]
My Parent static
My Child static
welcome

常量池的概念

例1

常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中

本质上,调用类并没有直接用到定义常量的类,因此并不会触发定义常量的类的初始化

注意:这里指的是将常量存放到了Demo03ConstatnPool的常量池中,之后Demo03ConstantPool与MyParent2就没有任何联系了

public class Demo03ConstantPool {
public static void main(String[] args) {
System.out.println(MyParent2.str);
}
}
class MyParent2{
public static final String str = "hello world";
static {
System.out.println("My Parent 2 static block"); // 这一行执行不到
}
}

例2

当一个常量的值并非编译器可以确定的,那么其值就不会被放到调用类的常量池中

这是在程序运行时,会导致主动使用这个常量所在的类,显然就会导致这个类被初始化

public class Demo03ConstantPool {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3 {
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("My Parent 3 static block"); // 这一行能执行到
}
}

类加载器

Class Loader Subsystem——类加载器子系统

  • 通过一个类的全限定名来获取描述该类的二进制字节流 这个过程放到 JVM 外部实现,让应用程序自己决定如何获取所需的类
  • 对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的 类名称空间
    • 即使两个类来源于同一个 Class 文件,被同一个Java 虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

作用:加载Class文件new Person();,引用存在栈,对象存在堆

image-20200615152034860

  • Class Loader加载、初始化出Class对象
  • 通过new关键字,根据Class对象创建实例化对象
  • 实例化对象通过getClass()方法获得类对象
  • 类对象通过getClassLoader()方法获得ClassLoader

下面这段代码说明Class对象是唯一的

public class Demo01Class {
public static void main(String[] args) {
Person person1 = new Person();
Person person2 = new Person();
Person person3 = new Person();
System.out.println(person1.hashCode());
System.out.println(person2.hashCode());
System.out.println(person3.hashCode());
Class<? extends Person> aClass1 = person1.getClass();
Class<? extends Person> aClass2 = person2.getClass();
Class<? extends Person> aClass3 = person3.getClass();
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
}
}
1639705018
1627674070
1360875712
1625635731
1625635731
1625635731

在如下几种情况下,JVM虚拟机将结束生命周期:

  1. 执行了System.exit()方法
  2. 程序正常执行结束
  3. 程序在执行过程中遇到了异常或者错误而异常终止
  4. 由于操作系统出现错误而导致JVM进程终止

类加载器的分类

虚拟机自带加载器

  1. 启动类(根)加载器 BootStrap ClassLoader 由 C++ 实现是虚拟机的一部分,其余类加载器都通过 Java 实现独立于虚拟机外部并一律继承抽象类 java.lang.ClassLoader 负责加载Java_Home/lib/rt.jar目录中的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库
  2. 扩展类加载器 负责加载Java_Home/ext/*.jar目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库
  3. 应用程序加载器(系统类加载器) 负责加载用户路径(classpath)上的类库,java.class.path

自定义加载器

  • 用户可以继承java.lang.ClassLoader实现自定义加载器

image-20200615160113441

  • 输出一个ClassLoader,观察它属于哪个
public class Demo01Class {
public static void main(String[] args) {
Person person1 = new Person();
Class<? extends Person> aClass1 = person1.getClass();
ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
System.out.println(classLoader.getParent().getParent());
}
}
sun.misc.Launcher$AppClassLoader@18b4aac2 //应用程序加载器
sun.misc.Launcher$ExtClassLoader@61bbe9ba //扩展类加载器
null // 抓不到,并非表示不存在 ,它是JDK rt.jar

双亲委派机制

一层一层的让父类加载器去加载,最顶层父类不能加载时,则父类委派子类加载器加载该类,以此类推

  1. 类加载器收到类加载请求
  2. 把这个请求委托给父加载器去加载,一直向上委托,直到启动类加载器。通过组合而不是继承实现
  3. 启动类加载器检查能不能加载(使用findClass()方法),能就加载(结束) 否则,抛出异常,通知子类加载器进行加载
  4. 重复步骤三

实例:定义一个类,java.lang.String,它跟JDK自己的String类同名同包

package java.lang;
public class String {
@Override
public String toString() {
return "hello";
}
public static void main(String[] args) {
String s = new String();
s.toString();
}
}
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
提示

我们明明写了main方法,却报错说没找到main方法,说明Java跑的不是我们这个String类

info

根据双亲委派机制,JVM会向上到启动类加载器对应的路径去加载JDK自己的String类,而不是我们写的String类。由此可见,双亲委派机制可以确保Java核心类库所提供的类,不会被自定义类所替代

双亲委派机制的核心是包装类的唯一性和安全性。如果在JVM中存在包名和类名相同的两个类,则该类将无法被加载,JVM也无法完成类加载流程

双亲委派机制的三次破坏

双亲委派机制并不是强制性的,但 Java 生态圈一般都选择遵守

  • 双亲委派机制的三次破坏,其实不能说是破坏,应当说是针对具体业务场景做的调整、修改,不应认为是贬义的

第一次破坏

  • 发生在双亲委派机制诞生之前:JDK 1.2 之前,没有双亲委派机制,但有类加载器和抽象类,用户普遍写了自己的自定义类加载器,都自己实现了 loadClass() ,为了兼容这些老代码,JDK 开发者就不能简单粗暴的用双亲委派机制来防止 loadClass() 方法被子类覆盖,简单的说,为了兼容老代码没有硬上双亲委派机制,做了妥协
  • 妥协后,JDK 开发者提供了 protected findClass() 方法,引导用户把代码迁移到这个方法中,这样就可以让双亲委派机制生效,同时用户不必大规模修改代码

第二次破坏

  • 越是基础的类越是放在靠近 Bootstrap ClassLoader 的位置
  • 如果这种很基础的类需要调用加载非常用户级的类就比较麻烦,典型的有 JNDI、JDBC(SPI Service Provier Interface,也是 SpringBoot 自动装配原理的核心)
  • 为了适应这种业务场景,早期通过 线程上下文加载器 实现父类加载器主动请求子类加载器执行加载行为,但这不符合双亲委派的思想
  • JDK 6 后改为配置文件 META-INF/services 中配置 全限定类名 的,通过 java.util.ServiceLoader 加载,配合职责链模式处理 SPI 的加载

第三次破坏

  • 热部署技术的流行
  • 基本思想是直接将 JVM 里的某写具体类加载器整个替换,无需重启
  • 为了实现这种功能,技术厂商自己实现了整个类加载机制,更加复杂:从双亲委派的树形拓展为网状、依然有双亲委派的核心思想,但为了适应热部署技术,添加了很多在同级类加载器中间选择的逻辑

关于Java中的ClassLoader下面的哪些描述是错误的:( )

正确答案: B D F

A 默认情况下,Java应用启动过程涉及三个ClassLoader: Boostrap, Extension, System
B 一般的情况不同ClassLoader装载的类是不相同的,但接口类例外,对于同一接口所有类装载器装载所获得的类是相同的
C 类装载器需要保证类装载过程的线程安全
D ClassLoader的loadClass在装载一个类时,如果该类不存在它将返回null
E ClassLoader的父子结构中,默认装载采用了父优先
F 所有ClassLoader装载的类都来自CLASSPATH环境指定的路径

从java虚拟机的角度讲,只有两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用的是c++实现的,是虚拟机的一部分,另一类是就是所有其他类加载器,这些类加载器都由java语言实现,独立于虚拟机外部,并且全都继承自抽象类。

从开发人员的角度看,类加载器还可以划分为3种系统类加载器,启动类加载器(Bootstrap ClassLoader),负责加载存放在<JAVA_HOME>/lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符的类库即使放在lib目录中也不会被加载)类库加载到虚拟机中内存中。启动类加载器无法被java程序直接引用,用户在编写自定义类加载器是,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。

扩展类加载器(Extension ClassLoader):这个类加载器有sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称它为系统类加载器(System ClassLoader)。他负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。对此,如果有必要开发者可以加入自己定义的类加载器。

一般对于我们java程序员来说,类的加载使用的是双亲委派模型,即当一个类需要加载时,会将类传给Application ClassLoader,但是Application ClassLoader并不会加载,不管它是否能加载,而是传给它的"父类" Extension ClassLoader,Extension ClassLoader同样不会加载,同样传给 Bootstrap ClassLoader(注意不是我们常说的那种父类,但是可以这样理解),这时Bootstrap ClassLoader会判断它是否能加载,能加载就直接加载了,不能加载就传给Extension ClassLoader,Extension ClassLoader同样的判断是否能加载,能加载就直接加载,不能加载就传给Application ClassLoader,然后Application ClassLoader也判断能否加载,如果还是不能加载应该就是报ClassNotFoundException了。这就是双亲委托模型的简单理解了。

对于上面的"父类"为什么要打引号,因为它们并不是真的像java中继承的关系,而是组合的关系,即在"子类"中存在一个成员变量指向"父类"的引用。

所以AE对DF错了。

对于C,很容易理解,因为我们知道一个类只需要加载一次就够了,所以要保证线程安全。

难点就在B了,其实也好理解,就是体现双亲委托模型的优势的时候了,之所以使用双亲委托机制是为了保证java程序的稳定运作,比如当你使用的不是双亲委托模型的时候,然后刚好开发者又定义了一个类,一个java.lang.String这样一个类,如果不使用双亲委托模型,当类加载的时候就有可能会加载开发者定义的String类,这导致了java代码的一片混乱,可读性极差。(PS:但这并不意味着类加载器只要双亲委托模型就行了,没有完美的模型,只有最合适的模型,有不同的需求使用不同的模型。比如破坏双亲委派模型,有兴趣的牛友可以自行了解),所以可以这么说,不同的类加载器加载出来的类是不一样的,不同的类加载器加载同一个类会在方法区产生两个不同的类,彼此不可见,并且在堆中生成不同的Class实例。对于接口,其实就是一个特殊的类,和类一样,在堆中产生不同的class对象。


A.Java系统提供3种类加载器:启动类加载器(Bootstrap ClassLoader) 扩展类加载器(Extension ClassLoader) 应用程序类加载器(Application ClassLoader). A正确

B.《深入理解Java虚拟机》P228:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类必定不相等。接口类是一种特殊类,因此对于同一接口不同的类装载器装载所获得的类是不相同的。B错误

C.类只需加载一次就行,因此要保证类加载过程线程安全,防止类加载多次。C正确

D. Java程序的类加载器采用双亲委派模型,实现双亲委派的代码集中在java.lang.ClassLoader的loadClass()方法中,此方法实现的大致逻辑是:先检查是否已经被加载,若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出ClassNotFoundException异常。D错误

E.双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。E正确

F.应用程序类加载器(Application ClassLoader)负责加载用户类路径(ClassPath)上所指定的类库,不是所有的ClassLoader都加载此路径。F错误