9. 类的加载过程

9.1 类加载过程简介

Java高并发编程详解(二)Java ClassLoader - 图1

  • 加载阶段:主要负责查找并且加载类的二进制数据文件,即class文件。
  • 连接阶段:可细分为三个阶段
    • 验证:确保类文件的安全性,如:class的版本,class文件的魔术因子是否正确。
    • 准备:为类的静态变量分配内存,并为其初始化默认值。
    • 解析:把类中的符号引用转换为直接引用
  • 初始化阶段:为类的静态变量赋予正确的初始值(代码编写阶段给定的值)。

    JVM对类的初始化是一个延迟的机制,即:lazy,当一个类在首次使用的时候才会被初始化,在同一个运行时包下,一个Class只会被初始化一次(运行时包 != 类的包)

9.2 类的主动使用和被动使用

JVM在运行期间提前预判并且初始化某个类:

  1. 通过new关键字会导致类的初始化。
  2. 访问静态变量,包括读取和更新会导致类的初始化;如:

public class _Simple {
_static
{
System.out.println(“初始化~”);
}
//静态变量
public static int _x = 10;
}
_System.out.println(Simple.x);
输出:
初始化~ 10_

  1. 访问类的静态方法,会导致类的初始化,如:

public class _Simple {
_static
{
System.out.println(“初始化~”);
}
//静态方法
_public static void _test() {}
}

Simple.test();
输出:初始化~

  1. 对某个类进行反射操作,会导致类的初始化;如:

public static void _main(String[] args) _throws _ClassNotFoundException {
_Class.forName(“com.zoro.concurrent.chapter09.Simple”);
}

  1. 初始化子类会导致父类的初始化;如: ```java package com.zoro.concurrent.chapter09;

/**

  • parent *
  • @author yx.jiang
  • @date 2021/7/27 10:26 */ public class Parent { static {

    1. System.out.println("父类初始化");

    }

    public static int y = 100; }

  1. ```java
  2. package com.zoro.concurrent.chapter09;
  3. /**
  4. * parent
  5. *
  6. * @author yx.jiang
  7. * @date 2021/7/27 10:26
  8. */
  9. public class Child extends Parent{
  10. static {
  11. System.out.println("子类初始化");
  12. }
  13. public static int x = 10;
  14. }
  1. package com.zoro.concurrent.chapter09;
  2. /**
  3. * test
  4. *
  5. * @author yx.jiang
  6. * @date 2021/7/27 10:26
  7. */
  8. public class ClassLoadTest {
  9. public static void main(String[] args) {
  10. //父类初始化
  11. //子类初始化
  12. //10
  13. //System.out.println(Child.x);
  14. //父类初始化
  15. //100
  16. System.out.println(Child.y);
  17. }
  18. }
  1. 启动类:即执行main函数所在的类会导致该类的初始化,如:使用java命令运行ActiveLoadTest类

    除了上述6中情况,其余的都被称为被动引用,不会导致类的加载和初始化。

  2. 构造某个类的数组时并不会导致该类的初始化:

_public static void _main(String[] args) {
Simple[] simples = _new _Simple[10];
System.out.println(simples.length);
}
输出:10

该操作只是在堆内存中开辟了一段连续的地址空间4byte×10

  1. 引用类的静态常量不会导致类的初始化;如: ```java package com.zoro.concurrent.chapter09;

import java.util.Random;

/**

  • 静态常量 *
  • @author yx.jiang
  • @date 2021/7/27 10:26 */ public class GlobalConstants { static {

    1. System.out.println("GlobalConstants 初始化");

    }

    /**

    • 在其他类中使用MAX不会导致GlobalConstants初始化,静态代码块不会输出 */ public static final int MAX = 100;

      /**

    • 虽然RANDOM时静态常量,但是由于计算复杂,只有初始化之后才能得到结果,因此在其它类中使用RANDOM会导致GlobalConstants的初始化 */ public static final int RANDOM = new Random().nextInt(); }
  1. > 因为RANDOM是需要进行随机函数计算的,在类的加载、连接阶段是无法对其进行计算的,需要进行初始化之后才能对其赋予准确的值
  2. <a name="iqEDK"></a>
  3. ### 9.3 类的加载过程详解
  4. <a name="ZXsDU"></a>
  5. #### 9.3.1 类的加载阶段
  6. ![](https://cdn.nlark.com/yuque/0/2021/jpeg/708204/1627628731485-db5292c5-758b-4346-a7f9-3202a0decf84.jpeg)<br />类的加载(简单来说):将class文件中的二进制数据读取到内存之中,然后将该字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并在堆内存中生成一个该类的java.lang.Class对象,作为访问方法区数据结构的入口。<br />类加载的最终产物就是堆内存中的class对象,对同一个ClassLoader来讲,不管某个类被加载多少次,对应到堆内存中的class对象始终是同一个。<br />class形式:
  7. - 二进制文件
  8. - 运行时动态生成
  9. - 通过网络获取
  10. - 通过读取zip文件获得类的二进制字节流
  11. - 将类的二进制数据存储在数据库的BLOB字段类型中
  12. - 运行时生成class文件,并且动态加载
  13. <a name="op0R4"></a>
  14. #### 9.3.2 类的连接阶段
  15. 1. 验证
  16. 1. 验证文件格式
  17. 1. ……
  18. 2. 元数据的验证
  19. 1. ……
  20. 3. 字节码验证
  21. 1. ……
  22. 4. 符号引用验证
  23. 1. ……
  24. 2. 准备
  25. 当一个class的字节流通过了所有的验证之后,就开始为该对象的类变量,也就是静态变量,分配内存并设置初始值,类变量的内存会被分配到方法区中,不同于实例变量会被分配到堆内存中。<br />设置初始值——为响应的类变量给定一个相关类型在没有被设置值时的默认值;<br />_public class _LinkedPrepare {<br /> _private static int _a = 10;<br /> _private final static int _b = 10;<br />}<br />其中`static int a = 10`在准备阶段不是10,而是初始值0`final static int b`则还是10。因为`final`修饰的静态变量(可直接计算得出结果)不会导致类的初始化,是一种被动引用,因此不存在连接阶段。<br />更严谨的解释:`final static int b` 在类的编译阶段javac会将其value生成一个ConstantsValue属性,直接赋予10
  26. 3. 解析
  27. > 在常量池中寻找类、接口、字段和方法的符号引用,并将这些符号引用替换成直接引用的过程。
  28. 主要针对类接口、字段、类方法和接口方法,对应常量池中的CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_infoCONSTANT_InterfaceMethodred_info这四种类型常量。
  29. 1. 类接口解析
  30. 1. ……
  31. 2. 字段解析
  32. 1. ……
  33. 3. 类方法解析
  34. 1. ……
  35. 4. 接口方法解析
  36. 1. ……
  37. <a name="eSQQZ"></a>
  38. #### 9.3.3 类的初始化阶段
  39. > 执行<clinit>()方法的过程 clinitclass initialize的缩写),在<clinit>()方法中所有的类变量都会被赋予正确的值,也就是程序编写的时候指定的值。
  40. <clinit>()方法是在编译阶段生成的,包含在class文件中了,<clinit>()中包含了所有类变量的赋值动作和静态语句块的执行代码,编译器收集的顺序是由 执行语句在源文件中出现的顺序决定的。<br />**静态语句块只能对后面的变量进行赋值,但是不能对其进行访问。**<br />_static _{<br /> System.out.println(a);<br /> a=100;<br />}<br />_private static int _a = 10;
  41. > 无法编译通过,提示:Illegal forward reference
  42. 另外<clinit>()方法和类的构造函数有所不同,它不需要显式的调用父类的构造器,虚拟机会保证父类的<clinit>()方法最先执行,因此**父类的静态变量总是能够得到优先赋值**。
  43. ```java
  44. package com.zoro.concurrent.chapter09;
  45. /**
  46. * 类加载
  47. *
  48. * @author yx.jiang
  49. * @date 2021/7/27 10:26
  50. */
  51. public class ClassInit {
  52. static class Parent {
  53. static int value = 10;
  54. static {
  55. value = 20;
  56. }
  57. }
  58. static class Child extends Parent {
  59. static int i = value;
  60. }
  61. public static void main(String[] args) {
  62. //输出:20
  63. System.out.println(Child.i);
  64. }
  65. }

某个类中无静态代码块、静态变量,就没有生成()方法的必要了,接口也是如此,由于接口不能定义静态代码块,所以只有当接口中有变量的初始化操作时才会生成()方法。

同一时间,只有一个线程执行到静态代码块中的内容,并且静态代码块只会被执行一次,JVM保证了()方法在多线程的执行环境下的同步语义。单例模式下,采用Holder的方式是一种绝佳的方案。

9.4 例

输出结果:
x = 1
y = 1

  1. package com.zoro.concurrent.chapter09;
  2. /**
  3. * @author yx.jiang
  4. * @date 2021/7/27 10:26
  5. */
  6. public class Singlenton {
  7. //①
  8. private static int x = 0;
  9. private static int y;
  10. private static Singlenton instance = new Singlenton();//②
  11. public Singlenton() {
  12. x++;
  13. y++;
  14. }
  15. public static Singlenton getSinglenton(){
  16. return instance;
  17. }
  18. public static void main(String[] args) {
  19. Singlenton singlenton = Singlenton.getSinglenton();
  20. System.out.println("x = " + singlenton.x);
  21. System.out.println("y = " + singlenton.y);
  22. }
  23. }

②移动到①的位置:
输出结果:
x = 0
y = 1

  1. package com.zoro.concurrent.chapter09;
  2. /**
  3. * @author yx.jiang
  4. * @date 2021/7/27 10:26
  5. */
  6. public class Singlenton {
  7. //①
  8. private static Singlenton instance = new Singlenton();//②
  9. private static int x = 0;
  10. private static int y;
  11. public Singlenton() {
  12. x++;
  13. y++;
  14. }
  15. public static Singlenton getSinglenton(){
  16. return instance;
  17. }
  18. public static void main(String[] args) {
  19. Singlenton singlenton = Singlenton.getSinglenton();
  20. System.out.println("x = " + singlenton.x);
  21. System.out.println("y = " + singlenton.y);
  22. }
  23. }

9.4.1 分析

调换位置前:
_private static int _x = 0;
_private static int _y;
_private static _Singlenton instance = _new _Singlenton();
在连接准备阶段, 每个变量被赋予了对应的初始值:
x=0, y=0, instance=null
类的初始化阶段, 为每一个类变量赋予正确的值(()方法执行):
x=0, y=0, instance = new Singleton();
在new Singleton时会执行构造函数,所以:
x=1,y=1
调换位置后:
_private static _Singlenton instance = _new _Singlenton();
_private static int _x = 0;
_private static int _y;
在连接准备阶段:
instance=null, x=0, y=0
类的初始化阶段, 为每一个类变量赋予正确的值(()方法执行), 首先会进入instance的构造函数中:
instance = Singleton@4f88db32, x=1, y=1
然后,为x初始化, 由于x没有显式的进行赋值,所以0才是所期望的正确赋值:
instance = Singleton@4f88db32, x=0, y=1

10. JVM类加载器

10.1 JVM内置三大类加载器

Java高并发编程详解(二)Java ClassLoader - 图2

10.1.1 根类加载器介绍

最顶层的加载器, 没有任何父加载器, 由C++编写, 负责虚拟机核心类库的加载

  1. package com.zoro.concurrent.chapter10;
  2. /**
  3. * @author yx.jiang
  4. * @date 2021/7/27 10:26
  5. */
  6. public class BootStrapClassLoader {
  7. public static void main(String[] args) {
  8. //String.class的类加载器是根加载器,根加载器是获取不到引用的,所以输出为null
  9. System.out.println("BootStrap: " + String.class.getClassLoader());
  10. //根加载器所在的加载路径
  11. System.out.println(System.getProperty("sun.boot.class.path"));
  12. //BootStrap: null
  13. //C:\Program Files\Java\jdk1.8.0_151\jre\lib\resources.jar;
  14. //C:\Program Files\Java\jdk1.8.0_151\jre\lib\rt.jar;
  15. //C:\Program Files\Java\jdk1.8.0_151\jre\lib\sunrsasign.jar;
  16. //C:\Program Files\Java\jdk1.8.0_151\jre\lib\jsse.jar;
  17. //C:\Program Files\Java\jdk1.8.0_151\jre\lib\jce.jar;
  18. //C:\Program Files\Java\jdk1.8.0_151\jre\lib\charsets.jar;
  19. //C:\Program Files\Java\jdk1.8.0_151\jre\lib\jfr.jar;
  20. //C:\Program Files\Java\jdk1.8.0_151\jre\classes
  21. }
  22. }

10.1.2 扩展类加载器介绍

扩展类加载器的父加载器是根加载器, 它主要用于加载JAVA_HOME下的jre\lb\ext子目录里的类库. 由纯java实现, 是java.lang.URLClassLoader的子类, 它的完整类名是sun.misc.Launcher$ExtClassLoader, 加载的类库可通过系统属性java.ext.dirs获得

10.1.3 系统类加载器介绍

负责加载classpath下的类库资源, 引入第三方jar包, 系统类加载器的父加载器是扩展类加载器, 同时也是自定义类加载器的默认父加载器 通过系统属性java.class.path获得

10.2 自定义类加载器

10.2.1 略.

  • java.lang.String : 包名.类名
  • javax.swing.JSpinner$DefaultEditor : 包名.类名$内部类
  • java.security.KeyStore$Builder$FileBuilder$1 : 包名.类名$内部类$内部类$匿名内部类
  • java.net.URLClassLoader$3$1 : 包名.类名$匿名内部类$匿名内部类

    10.2.2 双亲委派机制

    Java高并发编程详解(二)Java ClassLoader - 图3

    10.2.3 破坏双亲委托机制

    10.2.4 类加载器命名空间、运行时包、类的卸载等

  1. 类加载器命名空间
  2. 运行时包
  3. 类的卸载(GC回收)
    1. 该类所有的实例都已经被GC
    2. 加载该类的ClassLoader实例被回收
    3. 该类的class实例没有在其他任何地方被引用

      11. 线程上下文类加载器

      由于双亲委托机制, 导致第三方厂商包中的实现不会被加载;

      由于JDK定义了SPI标准接口,加之这些接口作为JDK核心标准类库的一部分, 既想完全透明标准接口的实现, 又想与JDK核心库进行捆绑, 由于JVM类加载器双亲委托机制的限制, 启动类加载器不可能加载到第三方厂商的具体实现. 有了线程上下文类加载器, 启动类加载器(根加载器)反倒需要委托子类加载器区加载厂商提供的SPI具体实现.

可根据 数据库驱动的初始化源码 去分析;