Java 类加载
类加载(类的生命周期)
类从被加载到虚拟机内存开始,到卸载出内存结束。

一、Creation and Loading(加载)
JVM 需要完成 3 件事:
- 通过类的全限定名获取该类的二进制字节流。
- 将二进制字节流所代表的静态结构转化为方法区的运行时数据结构。
- 在内存中创建一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
怎样获取类的二进制字节流,JVM 没有限制。除了从编译好的 .class 文件中读取,还有以下几种方式:
- 从 zip 包中读取,如 jar、war 等
- 从网络中获取
- 通过动态代理生成代理类的二进制字节流
- 从数据库中读取
- 。。。
数组类本身不通过类加载器创建,由 JVM 直接创建,再由类加载器创建数组中的元素类。
加载阶段与连接阶段的部分内容交叉进行,但这两个阶段的开始仍然保持先后顺序。
二、Linking(连接)
1、Verification(验证)
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
2、Preparation(准备)
为类变量(静态成员变量)分配内存并设置初始值的阶段。这些变量(不包括实例变量)所使用的内存都在方法区中进行分配。
基本类型初始值(JDK8)https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.5
- 对于 byte 类型,默认值为零,即(byte)0。
- 对于 short 类型,默认值为零,即(short)0。
- 对于 int 类型,默认值为零,即 0。
- 对于 long 类型,默认值为零,即 0L。
- 对于 float 类型,默认值为正零,即 0.0f。
- 对于 double 类型,默认值为正零,即 0.0d。
- 对于 char 类型,默认值为空字符,即 '\u0000'。
- 对于 boolean 类型,默认值为 false。
- 对于所有引用类型,默认值为 null。
存在特殊情况 https://www.jianshu.com/p/520295a63967
/** * 准备阶段过后的初始值为 0 而不是 123,这时候尚未开始执行任何 Java 方法 */ public static int value = 123; /** * 同时使用 final 、static 来修饰的变量(常量),并且这个变量的数据类型是基本类型或者 String 类型,就生成 ConstantValue 属性来进行初始化。 * 没有 final 修饰或者并非基本类型及 String 类型,则选择在 <clinit> 方法中进行初始化。 * 准备阶段虚拟机会根据 ConstantValue 的设置将 value 赋值为 123 */ public static final int value = 123;
3、Resolution(解析)
虚拟机将常量池内的符号引用替换为直接引用。会把该类所引用的其他类全部加载进来( 引用方式:继承、实现接口、域变量、方法定义、方法中定义的本地变量)
https://www.cnblogs.com/shinubi/articles/6116993.html
符号引用:一个 java 文件会编译成一个class文件。在编译时,java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
直接引用:直接指向目标的指针(指向方法区,Class 对象)、指向相对偏移量(指向堆区,Class 实例对象)或指向能间接定位到目标的句柄。
解析阶段在某些情况下可以在初始化后再开始,这是为了支持 Java 语言的运行时绑定。
三、Initialization(初始化)
JVM 规范没有强制约束类加载过程的 Creation and Loading 阶段什么时候开始,但对于 Initialization 阶段,有严格规定。
有且仅有 5 种情况必须立即对类进行初始化:
- 在遇到 new、putstatic、getstatic、invokestatic 字节码指令时,如果类尚未初始化,则需要先触发初始化。
- 对类进行反射调用时,如果类还没有初始化,则需要先触发初始化。
- 初始化一个类时,如果其父类还没有初始化,则需要先初始化父类。
- 虚拟机启动时,用于需要指定一个包含 main() 方法的主类,虚拟机会先初始化这个主类。
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发初始化。
这 5 种场景中的行为称为对一个类进行主动引用,除此之外,其它所有引用类的方式都不会触发初始化,称为被动引用。
几种被动引用:
通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化。
class SuperClass { static { System.out.println("SuperClass init!"); } public static int value = 123; } class SubClass extends SuperClass { static { System.out.println("SubClass init!"); } } public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); // SuperClass init! } }
通过数组定义来引用类,不会触发此类的初始化。
class SuperClass2 { static { System.out.println("SuperClass init!"); } public static int value = 123; } public class NotInitialization2 { public static void main(String[] args) { SuperClass2[] superClasses = new SuperClass2[10]; } }
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLO_BINGO = "Hello Bingo"; } public class NotInitialization3 { public static void main(String[] args) { System.out.println(ConstClass.HELLO_BINGO); } }
编译通过之后,常量存储到 NotInitialization 类的常量池中,NotInitialization 的 Class 文件中并没有 ConstClass 类的符号引用入口,这两个类在编译成 Class 之后就没有任何联系了。
关于接口加载
当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。
类加载过程的最后一步,是执行类构造器 <clinit>() 方法的过程。
<init>() 与 <clinit>() 介绍: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.9
https://blog.csdn.net/u013309870/article/details/72975536
<init>():为 Class 类实例构造器,对非静态变量解析初始化,一个类构造器对应个。
<clinit>():为 Class 类构造器对静态变量,静态代码块进行初始化,通常一个类对应一个,不带参数,且是 void 返回。当一个类没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成 <clinit>() 方法
加载顺序:
<clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作语句和静态块(static {})中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。
静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。
static { i = 0; // 给后面的变量赋值,可以正常编译通过 System.out.println(i); // 使用后面的变量,编译器会提示“非法向前引用” } static int i = 1;
虚拟机会保证在子类的 <clinit>() 方法执行之前,父类的 <clinit>() 方法已经执行完毕。
由于父类的 <clinit>() 方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
static class Parent { static { A = 2; } public static int A = 1; } static class Sub extends Parent { public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); // 输出 1 }
来看一个类属性加载顺序的问题
public class JvmTest { public static JvmTest jt = new JvmTest(); public static int a; public static int b = 0; static { a++; b++; } public JvmTest() { a++; b++; } public static void main(String[] args) { /** * 准备阶段:为 jt、a、b 分配内存并赋初始值 jt=null、a=0、b=0 * 解析阶段:将 jt 指向内存中的地址 * 初始化:jt 代码位置在最前面,这时候 a=1、b=1 * a 没有默认值,不执行,a还是1,b 有默认值,b赋值为0 * 静态块过后,a=2、b=1 */ System.out.println(a); // 输出 2 System.out.println(b); // 输出 1 } }
关于接口初始化:
接口中不能使用静态代码块,但接口也需要通过 <clinit>() 方法为接口中定义的静态成员变量显式初始化。
接口与类不同,接口的 <clinit>() 方法不需要先执行父类的 <clinit>() 方法,只有当父接口中定义的变量被使用时,父接口才会初始化。
虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法。
类加载器
类的生命周期中的第一步,就是要被 JVM 加载进内存,类加载器就是来干这件事。系统提供了 3 种类加载器:
一、启动类加载器(Bootstrap ClassLoader)
由 C 和 C++ 编写,是在 JVM 启动后初始化的。可在这里查看到源码(OpenJDK):https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/tip/src/share/native/java/lang/ClassLoader.c负责将存放在 <JAVA_HOME>\jre\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且能被虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。
二、扩展类加载器(Extension ClassLoader)
由 sun.misc.Launcher$ExtClassLoader 实现,负责加载 <JAVA_HOME>\jre\lib\ext 目录中的所有类库,以及系统变量 java.ext.dirs 指定路径中的所有类库,开发者可以直接使用扩展类加载器。三、应用程序类加载器(Application ClassLoader)
由 sun.misc.Launcher$AppClassLoader 实现,可以通过 ClassLoader 类中的 getSystemClassLoader() 方法的获得,所以一般也称它为“系统类加载器”。
它负责加载用户类路径(classpath:CLASSPATH 环境变量指定的, 由 -classpath 或 -cp 选项定义的,或者是 jar 中的 Manifest 的 classpath 属性定义的)上所指定的类库,以及系统变量 java.class.path 指定路径中的所有类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器(父子关系一般不会以继承的关系实现,而是以组合关系来复用父加载器的代码),结构如图:

getParent() 可获得父加载器
public class ClassLoaderTest { public static void main(String[] args) { ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); // 默认由 AppClassLoader 加载类 System.out.println(classLoader); // ExtClassLoader System.out.println(classLoader.getParent()); // Bootstrap ClassLoader,由 JVM 启动 System.out.println(classLoader.getParent().getParent()); } }

类加载机制(类加载器的工作方式)
一、委托机制(委派模型 或 父委派模型)
委派模型是描述类加载器之间的层次关系。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。
因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(找不到所需的类)时,子加载器才会尝试自己去加载。

在 java.lang.ClassLoader 中的 loadClass() 方法中实现该了过程。
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查是否已加载该类 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) { // 如果从非 null 的父类加载器中找不到该类,则抛出 ClassNotFoundException } if (c == null) { long t1 = System.nanoTime(); // 如果仍未找到,则调用 findClass 查找该类 c = findClass(name); // 这是定义的类加载器; 记录统计数据 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { // 解析类,属于类加载的 link 阶段 resolveClass(c); } return c; } } /** * ClassLoader 的子类建议重写 findClass 方法,而不是 loadClass */ protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
自己写的 java.lang.String 类,是否可以替换 JDK 自带的类?
答案是不行的。但这非委托机制解决的,因为委托机制是可以被打破的,完全可以写一个 classLoader 来加载自己写的 java.lang.String 类。
二、可见性机制
子类加载器可以看到父类加载器加载的类,而反之则不行。当 Abc.class 已经被 Application 类加载器加载过了,然后想要使用 Extension 类加载器加载这个类,将会抛出 java.lang.ClassNotFoundException 异常。
三、单一性机制
根据委托机制,父加载器加载过的类不能被子加载器加载第二次。虽然重写 loadClass() 的类加载器可以做到不遵守委托机制和单一性机制,但这样做并不可取。
判断类是否“相等”
因此,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。
这里的“相等”,包括代表类的 Class 对象的 equals() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。
关于破坏委派模型
java 引入了线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过 Thread 类的 setContextClassLoader 进行设置,默认继承父线程类加载器,也可由父类加载器请求子类加载器完成类加载动作。
自定义类加载器
除系统提供的加载器外,还可以自己定义类加载器(继承 java.lang.ClassLoader 类实现)。
ExtClassLoader 和 AppClassLoader 由 sun.misc.Launcher 创建。
自定义类加载器需要继承 ClassLoader 类,重写 findClass 方法,不推荐重写 loadClass 方法,会破坏委派机制。
测试时,使用 javac 把 .java 文件编译成 .class 文件,注意加载类的文件路径名与包名。
Class.forName() 和 ClassLoader.loadClass()
调用了 forName0,第二个参数为 true,默认会初始化,可使用其重载方法指定为 false
@CallerSensitive public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
调用了 loadClass 的重载方法,默认不会链接,就不会初始化了
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
以上面的 Hello 类为例,在 com 包下新建同样的文件,命名为 Hello1
public static void main(String[] args) throws Exception { // 加载,链接,初始化 Class.forName("com.Hello1"); System.out.println("=========================================="); // 加载,链接 Class.forName("com.Hello1", false,ClassLoader.getSystemClassLoader()); System.out.println("=========================================="); // 加载 ClassLoader.getSystemClassLoader().loadClass("com.Hello1"); }

线程上下文类加载器(ThreadContextClassLoader)
在 Java 中存在着很多的服务提供者接口 SPI,全称 Service Provider Interface,是Java 提供的一套用来被第三方实现或者扩展的API,这些接口一般由第三方提供实现,常用 SPI 有 JDBC、JNDI 等。
这些 SPI 的接口(比如 JDBC 中的 java.sql.Driver)属于核心类库,一般存在 rt.jar 包中,由根类加载器加载。
而第三方实现的代码一般作为依赖 jar 包存放在 classpath 路径下,由于 SPI 接口中的代码需要加载具体的第三方实现类并调用其相关方法,SPI 的接口类是由根类加载器加载的,Bootstrap 类加载器无法直接加载位于 classpath 下的具体实现类。
由于委派模式的存在,Bootstrap 类加载器也无法反向委托 AppClassLoaser 加载 SPI 的具体实现类。
在这种情况下,Java 提供了上下文类加载器用于解决以上问题。这种加载类的方式破坏了委托模型,但它使得 Java 类加载器变得更加灵活。
java.lang.Thread 中的方法 getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 用来获取和设置线程的上下文类加载器。
如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。
Java 应用运行的初始线程的上下文类加载器是系统类加载器(AppClassLoader)。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html
https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html
https://docs.oracle.com/javase/tutorial/ext/basics/load.html
https://blog.csdn.net/lengxiao1993/article/details/86689331

浙公网安备 33010602011771号