1.类的加载步骤

image.png

从类的角度来看
image.png

其中:

  1. 第一个过程加载(loading)也称之为装载过程
  2. 验证, 准备, 解析 3个部分统称为链接(Linking)
  3. 都需要加载谁—- 在Java里面分为基本类型数据和引用类型数据. 基本数据类型由虚拟机预先定义, 引用数据类型则需要进行类的加载

2.Loading(装载)阶段

所谓装载, 简而言之就是将Java类的字节码文件加载到内存机器中,并且在内存中构建出Java类的原型 这种称之为类模板对象

1.装载完成的操作

装载阶段,简而言之,查找并加载二进制数据,生成class的实例
在加载类时,Java虚拟机必须完成一下三件事:

  1. 通过类的全名,获取类的二进制数据流
  2. 解析类的二进制数据流为方法区内的数据结构(Java类模型)
  3. 创建java.lang.Class类的实例,表示该类型,作为方法区这个类的各种数据访问入口

1.什么是类模板对象呢

所谓类模板对象,其实就是Java类在JVM内存中的一个快照, JVM将从字节码文件中解析出来的常量池, 类字段,类方法等信息存储到类模板中,这样JVM在运行期间便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用.

反射的机制基于这一基础,如果JVM没有将Java类申明信息存储起来,则JVM在运行期间也无法反射

1. 类模板的位置

加载的类在JVM中创建相应的类结构,类结构会存储在方法区(1.8之前:永久代; 1.8之后,元空间)

2.Class实例的位置在哪里

类将.class文件加载至元空间之后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构, 该Class对象是在加载类的过程中创建的 , 每个类都对应有一个Class类型的对象

image.png

3.数组类的加载有什么不同

创建数组类的情况有些特殊,因为数组类本身并不是由类加载器负责创建的, 而是由JVM在运行时根据需要而直接创建的,但是数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程(如果是基本类型JVM本身就是预定义好了, 如果是引用类型, 就需要类加载器 , 将.class加载到方法区的元空间里面, 并且在堆中放置对象本身)

  1. 如果数组的元素类型是引用类型, 那么就遵循定义的加载过程递归加载和创建数组的元素类型
  2. JVM使用指定的元素类型和数组维度来创建新的数组类
  3. 如果数组的类型是引用类型 , 数组的可访问性就由元素类型的可访问性决定, 否则数组类的可访问性将被缺省定义为public

3.Linking(链接)阶段

1.验证

它的目的是为了保障字节码能够是合法性的加载, 合理并且符合规范
image.png

2.准备

简而言之, 为类的静态变量分配内存, 并将其初始化为默认值
在这个阶段, 虚拟机就会为这个类分配相应的内存空间,并且设置初始化值

注意:

  1. Java并不支持boolean类型,对于boolean类型,内部其实是int, 由于int类型默认是值是0,故对应的 ,boolean的默认值就是false.
  2. 这里不包含基本数据类型的字段用static final修饰的情况, 因为使用final在编译的时候就会分配了,准备阶段就会显示赋值.
  3. 注意这里不会为实例变量分配初始化, 实例变量是会随着对象一起分配到Java堆中的
  4. 在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行

    3.解析

    简而言之, 将类 , 接口, 字段 ,方法的符号引用转为直接引用

以方法为列, Java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,
当需要调用一个方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法,通过解析操作,符号引用就可以转变为目标方法在类中的方法表的位置,从而使得方法被调用成功.

所谓解析就是将符号引用转变为直接引用, 也就是得到类 , 字段 , 方法在内存中的指针或者是偏移量
因此可以说 如果直接引用存在 , 那么可以肯定系统中存在该类 , 方法或者字段.当是只存在符号引用,不能确定系统中一定存在该结构

不过JAVA虚拟机规范并没有明确要求解析阶段一定要求顺序来执行,在HotSpotVM中 , 加载 验证 准备 初始化会有条不紊的执行, 但是解析过程往往会伴随着JVM在执行完初始化之后再执行

4. Initialization(初始化)阶段

初始化阶段, 简而言之, 为类的静态变量赋予正确的显式的初始值

类的初始化是类装载的最后一个阶段, 如果前面的步骤都没有问题, 那么表示类可以顺利装载到系统中,此时, 类才会开始执行Java字节码(即: 到了初始化阶段, 才真正开始执行类中定义的Java程序代码)

初始化阶段中最重要工作是执行类的初始化方法:()方法;

  • 该方法仅能由Java编译器生成并由JVM调用, 程序开发者无法自定义一个同名的方法, 更无法直接在Java程序中调用该方法, 虽然该方法也是由字节码指令所组成
  • 它是由类静态成员的赋值语句以及static语句块合并产生的

(): 只有在给类的中的static的变量显式赋值或者在静态代码块中赋值了,才会赋值此方法
(): 一定会出现在class的method表中

1.子类加载前先加载父类?

在加载一个类之前,虚拟机总是会试图加载该类的父类 , 因此父类的总是在子类之前被调用. 也就说,父类的static优先级高于子类
由父及子, 静态先行.

  1. class FatherInitialization {
  2. static {
  3. System.out.println("father..........");
  4. }
  5. }
  6. public class SubInitialization extends FatherInitialization {
  7. static {
  8. System.out.println("sub..........");
  9. }
  10. public static void main(String[] args) {
  11. }
  12. }
  13. console===============
  14. father..........
  15. sub..........

2.那些类不会生成方法?

Java编译器并不会为所有的类产生()初始化方法. 那些类在编译字节码后,字节码文件中将不会包含()方法?

  • 一个类中并没有声明任何类变量,也没用静态代码块.
  • 一个类中声明类变量,但是没有明确使用变量的初始化语句以及静态代码块来执行初始化操作时
  • 一个类中包含static fianl修饰的基本数据类型的字段, 这类字段初始化语句采用编译时常量表达式

3.代码举列 final+static的搭配问题

使用static+final对修饰的字段的显式赋值的操作, 到底是那个阶段赋值

  1. 在链接的准备阶段进行赋值
  2. 在初始化阶段进行赋值

总结:

  1. 在链接的准备环节赋值的情况
    1. 在使用final+static修饰的情况下, 如果是基本类型数据和String类型数据, 并且在显式赋值的情况下进行赋值(表示是使用常量赋值, 不是使用方法或者构造器的情况下进行赋值),这种情况下是在链接的准备环节进行赋值
  2. 在初始化阶段进行赋值的情况
    1. 在final+static修饰的情况下,如果在基本类型或者包装类型使用构造器方式, 显式方法或者隐式方法(static final Integer i = 100底层调用valueOf方法进行装箱操作)进行显式赋值,这种情况下的赋值就是在初始化环节进行赋值的
    2. 在static 修饰的情况字面量或者静态代码块, 在基本类型或者是包装类型下使用常量进行显式赋值 , 这种情况下的赋值就是在初始化环节进行赋值的
  1. public static int a = 1; // 在初始化阶段进行赋值
  2. public static final int INT_CONSTANT = 10; // 在链接阶段进行赋值
  3. public static final Integer INTEGER_CONSTANT= Integer.valueOf(100); // 在初始化阶段进行赋值
  4. public static final Integer INTEGER = 400; // 在初始化阶段进行赋值
  5. public static final Integer INTEGER_CONSTANT_ = Integer.valueOf(300); // 在初始化阶段进行赋值
  6. public static final String s0 = "helloWorld"; // 在链接阶段进行赋值
  7. public static final String s1 = new String("======"); // 在初始化阶段进行赋值
  8. public static String s2 = "======"; // 在初始化阶段进行赋值
  9. public static final int NUM_2 = new Random().nextInt(10); // 在初始化阶段进行赋值

4.调用会死锁吗

对于()方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁 , 同步 , 如果多个线程同时去初始化一个类, 那么只会有一个线程去执行这个类()方法,其他线程都是需要阻塞等待的,直到活动线程执行()方法完毕.

正是因为函数()带锁线程安全的,因此,如果在一个类的()方法中有着很长的操作,就可能造成多个线程阻塞, 引发死锁.

如果之前的线程成功加载了类,则等待在队列中的线程就没有机会在执行()方法了,当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息

  1. import java.util.concurrent.TimeUnit;
  2. class ThreadA {
  3. static {
  4. try {
  5. TimeUnit.SECONDS.sleep(1);
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. try {
  10. Class.forName("com.anda.dachang.classload.java_1.ThreadB");
  11. } catch (ClassNotFoundException e) {
  12. e.printStackTrace();
  13. }
  14. System.out.println("StaticA init OK");
  15. }
  16. }
  17. class ThreadB {
  18. static {
  19. try {
  20. TimeUnit.SECONDS.sleep(1);
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. try {
  25. Class.forName("com.anda.dachang.classload.java_1.ThreadA");
  26. } catch (ClassNotFoundException e) {
  27. e.printStackTrace();
  28. }
  29. System.out.println("StaticB init OK");
  30. }
  31. }
  32. public class ClInitDeadLock extends Thread {
  33. private String type;
  34. public ClInitDeadLock(String type) {
  35. this.type = type;
  36. this.setName("Thread" + type);
  37. }
  38. @Override
  39. public void run() {
  40. try {
  41. Class.forName("com.anda.dachang.classload.java_1.Thread" + type);
  42. } catch (ClassNotFoundException e) {
  43. e.printStackTrace();
  44. }
  45. System.out.println(this.getName());
  46. }
  47. public static void main(String[] args) {
  48. ClInitDeadLock a = new ClInitDeadLock("A");
  49. a.start();
  50. ClInitDeadLock b = new ClInitDeadLock("B");
  51. b.start();
  52. }
  53. }

image.png

5. 类的初始化情况: 主动使用VS被动使用

1.主动使用:

Class只有在必须要首次使用的时候才会被装载, Java虚拟机不会无条件地装载Class类型, Java虚拟机规定 , 一个类或者接口在初次使用前, 必须要进行初始化 , 这里的”使用” ,是指主动使用.

主动使用只有下列几种情况: 即(如果出现如下的情况,则会对类进行初始化操作,而初始化操作之前的装载,解析,准备,验证已经完成)

  1. 当创建一个类的使用,使用关键词new, 或者使用反射, 克隆, 序列化
  2. 当调用类的静态方法时,即当使用了字节码invokestatic指令
  3. 当使用类, 接口的静态字段时(final修饰特殊考虑), 比如 , 使用getstatic或者putstatic指令.
  4. 当使用java.lang.reflect包中的方法反射时, 比如:
    1. Class.forName(“com.anda.Test”)
  5. 当初始化子类时,如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化
  6. 如果一个接口定义了default 方法, 那么直接实现或者间接实现改接口的类的初始化 , 改接口要在之前被初始化
  7. 当虚拟机启动的时候, 用户需要执行主类(包含main()方法的那个类) , 虚拟机会先找机会初始化这个主类

    2.被动使用:

    除了以上的情况属于主动使用, 其他的情况均属于被动使用,被动使用不会引起类的初始化.
    也就是说: 并不是在代码中出现的类, 就一定会被加载或者被初始化. 如果不符合条件, 类就不会初始化

  8. 当访问一个静态字段时, 只有真正声明这个字段的类才会被初始化

    1. 当通过子类引用父类的静态变量, 不会导致子类初始化
  9. 通过数组定义类引用, 不会触发此类的初始化
  10. 引用常量不会触发此类或者接口的初始化 . 因为常量在链接阶段就已经被显式赋值了(如果是Integer类型就不会 ,底层会调用valueOf, 装箱操作)
  11. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化

    被动的使 用, 意味着不会进行初始化操作, 也就是不会有

5.类的使用(Using)

任何一个类型在使用之前必须经历完整的加载, 链接 , 初始化3个类加载步骤. 一旦一个类型成功经历这3个步骤之后,就可以在程序中使用了

6.类的卸载(Unloading)

1.类 , 类的加载器 , 类的实例之间的关系

在类的加载器的内部实现中,用一个Java集合来存放加载类的引用,另一个方面,一个Class对象总是会引用它的类加载器, 调用Class对象的getClassLoader()方法,就能获得他的类加载器. 由此可见 , 代表某个类的Class实例与其类的加载之间的双向关联关系

一个类的实例总是引用代表这个类的Class对象. 在Object类中定义了getClass方法, 这个方法返回代表对象所属类的Class对象的引用, 此外, 所有的Java类都有一个静态属性class,它引用代表这个类的Class对象.

image.png

2.何时会被卸载

loader1变量和obj变量间接引用代表sample类的class对象,而objClass变量则直接引用它.

如果程序运行过程中,将上图左侧三个引用变量为置位null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期, 代表Sample类的Class对象也结束生命周期, Sample类在方法区的二进制数据也会被卸载.

当再次有需要的时候, 会检查Sample类的Class对象是否存在,如果存在则会直接使用,不会在重新加载,如果不存在Sample类会被重新加载,在Java虚拟机的堆区中重新生成一个新的代表Sample类的Class实例(可以通过哈希码查看是否是同一个实例)

3.类的卸载在实际生产的情况如何?

  1. 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范)
  2. 被系统类加载和扩展类加载器加载的类型在运行期间不太可能被卸载 , 因为系统类加载器实例或者扩展的实例基本上在整个运行期间总能直接或者间接的访问到,其到达unreachable的可能性极小.
  3. 被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才能做到,

4.方法区的垃圾回收

方法区的垃圾收集主要回收两个部分内容: 常量池中废弃的常量 不在使用的类型
HotSpot虚拟机对常量池的回收策略是很明确, 只要常量池中的常量没有被任何地方引用,就可以被回收了.

判定一个常量是否”废弃”还是简单(没有被任何地方引用了).而且判断一个类型是否属于”不再被使用的类”的条件比较苛刻了. 需要同时满足下面三个条件.

  • 该类所有实例都已经被回收. 也就是Java堆中的不存在该类型以及任何派生子类的实例
  • 加载该类的类加载器已经被回收了. 这个条件除非都是经过精心设计的可替换类加载器的场景,如OSGI, JSP的重加载等.否则通常很难达成的.
  • 改类对应的java.lang.Class对象没有任何地方引用,无法在任何地方通过反射访问该类的方法

Java虚拟机被允许对满足上述三个条件的无用类进行回收, 这里说的是”被允许”, 而不是和对象一样,没有了引用就会被回收了.