深入理解JVM(1):类加载器

文章目录

一、类加载简介

1.简介

  • 在Java代码中,类型加载连接初始化过程都是在程序运行期间完成的。
  • 提供了更大的灵活性,增加了更多的可能性。

要注意理解上面的第一句话,首先类型是什么?

  • 类型:指的是类/接口/枚举这类的信息,这些被称为类型,这里暂时涉及到任何的对象,这些是类本身。这里要和平日里用到最多的对象概念区分开来。可以这样理解,要想创建对象,应该有类的相关信息。所有这里的类型应该理解为一个具体的Class,大多数情况下类都是提前编写好的,当然也有运行期动态生成类型的情况,典型的就是动态代理。其他的一些语言,加载和连接的过程都是在编译阶段做好的,Java中是在运行期才完成的。这就给Java的语言带来了更多的灵活性,可以在运行期对提前已经存在好的一些内容做结合。Java是一门拥有动态语言特性的静态语言,让Java拥有动态语言特性的原因就是因为这句话。
  • 类型的加载最常见(不唯一)的一种情况是把磁盘中的字节码问题加载到内存中。
  • 连接:处理好类与类之间的关系,以及完成对字节码文件的检查,校验!注意这里字节码问题不是编译器生成好的吗?它还需要再校验吗?当然,因为字节码文件是可以人为的手动修改的,可能有一些恶意的可能。只有字节码没有问题,Java虚拟机才会去执行它。还有将一些符号引用转换为直接引用也是在这个阶段完成的。
  • 初始化:静态变量的赋值
  • 整个过程大概是按加载、连接、初始化这个顺序,但并不是严格的按照这个顺序的,只要满足Java规范即可。

2.Java虚拟机与程序的生命周期

首先类加载器就是用来加载类的,把类加载到虚拟机中,后续所有的操作由虚拟机来管辖

在如下的几种情况,Java虚拟机(本质上就是一个进程)将结束生命周期

  • Java代码显式的执行了System.exit()方法。(联想try/catch/finally的执行顺序)
    • 在一般情况下,finally代码块中的内容是一定会执行的,但是如果在前存在System.exit()就不会执行finally中的内容了。
  • 程序正常执行结束
  • 程序执行过程遇到异常或错误而异常终止(没能Catch住)(多见)
  • 由于操作系统错误导致虚拟机进程终止(相对不可控制)

3.类的加载、连接与初始化(类加载的最重要的3个阶段)

3.1加载

查找并加载类的二进制数据

3.2连接

分为3个阶段

  • 验证:确保被加载类的正确性

  • 准备:为类的静态变量分配内存,并将其初始化为默认值

    • 静态变量:类的静态变量或者方法分配内存,因为在类的静态变量或方法都是可以直接通过类名去调用或者访问的,不需要实例化的对象。
    • 默认值:我们在定义静态变量的时候可能已经为这个静态变量进行了显式的赋值,但是在这个阶段不会进行赋值,而是使用一些数据类型的默认值,比如int的默认值是0,boolean的默认值是false
    • 实例:
      在这里插入图片描述
  • 解析:把类中的符号引用转换为直接引用

    • 符号引用:Java在编译的时候并不知道一个引用指向的实际地址,所以只好先用一个符号引用来代替。可以认为是一个间接的指针
    • 直接引用:直接指向目标的指针。

3.3 初始化

为类的静态变量赋予正确的初始值

  • 联系上述准备阶段的默认值

4.类的使用和卸载(类加载的剩余两个阶段)

5 类加载阶段小结

类加载分为哪几个阶段?每个阶段的含义?
在这里插入图片描述

6.Java对类的使用方式(主动使用和被动使用)

  • Java对类的使用分为两种方式
    • 主动使用
    • 被动使用
  • 所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化
    • 主动使用:表示被动使用的时候不会初始化
    • 首次:表示初始化只有一次

6.1主动使用(7种)

大概有7种情况下,都是采取主动使用方式,当然这个划分并不完全精准

  • 创建类的实例:new一个对象嘛
  • 访问某个类或接口的静态变量,或者对该静态变量赋值:一个是取值一个赋值
  • 调用类的静态方法
    • 相关的字节码助记符:getstatic,putstatic,invokestatic
  • 使用反射:Class.forName(com.test.Test)
  • 初始化一个类的子类:比如初始化一个Child类,它的父类是Parent,那么这个父类肯定也要被使用
  • Java虚拟机启动时被标明为启动类的类:入口类,main方法所在的类,Java Test
  • JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有被初始化时,则初始化

除了以上7中情况,其他的Java类的使用方式都被看做是对类的被动使用,都不会导致类的初始化。但是这并不代表不会发生加载和连接过程

6.2程序实例1

代码1
  • 输出什么?
public class MyTest1 {
   
    public static void main(String[] args) {
   
        System.out.println(MyChild1.str);
    }
}

class MyParent1{
   

    public static String  str = "hello world";
    static {
   
        System.out.println("MyParent1 static block");
    }
}
class MyChild1 extends MyParent1{
   

    public static String  str2 = "welcome";
    static {
   
        System.out.println("MyChild1 static block");
    }
}
  • 结果
    在这里插入图片描述
  • 分析
    在这里插入图片描述
代码2
public class MyTest1 {
   
    public static void main(String[] args) {
   
        System.out.println(MyChild1.str2);
    }
}

class MyParent1{
   

    public static String  str = "hello world";
    static {
   
        System.out.println("MyParent1 static block");
    }
}
class MyChild1 extends MyParent1{
   

    public static String  str2 = "welcome";
    static {
   
        System.out.println("MyChild1 static block");
    }
}
  • 输出
    在这里插入图片描述

  • 分析
    在这里插入图片描述

小结
  • 1.对于静态字段来说,只有直接定义了该字段的类才会被初始化
  • 2.当一个类被初始化时,要求其所有的父类都先被初始化完毕了才行。(直到Object类

6.3添加虚拟机参数,查看类被加载的信息:-XX:TraceClassLoading

  • 配置虚拟机参数
    在这里插入图片描述

  • 运行程序,查看输出,可以看到很多很多的类加载信息
    在这里插入图片描述

  • 我们重点关注以下几个

  • Object:因为是所有类的父类嘛。所有肯定最先被加载进来
    在这里插入图片描述

  • 程序入口类:MyTest1,根据Java对类的主动使用的7种情况来看,程序的入口类是主动使用的情况之一所以会先使用

在这里插入图片描述

  • 然后我们可以看到先加载了父类MyParent1
  • 然后再加载了子类MyChild1
  • 这里的先后关系也要注意
  • 然后我们联系代码,这里的MyChild1访问了父类的静态成员变量,根据上面的分析,子类不会被初始化,但是并不表明这个类不被加载(这也是需要强调的一点)

6.4JVM参数的3种形式和含义

因为讲到这里,就顺便对JVM运行时的参数进行一些简单的说明,JVM参数呢有很多很多,不需要刻意去记,在学习中一个一个的去学就可以了。

  • -XX:+<option>:表示开启option选项
  • -XX:-<option>:表示关闭option选项
    • 因为有些选项是默认关闭和开启的,所以需要手动关闭和开启
    • 这两类本质上是赋予了一个boolean
  • -XX:<option> = <value>:将option选项的值设置为value(设置堆空间大小常用)

6.5 程序实例2(常量编译期可确定时常量的位置)

在这里插入图片描述

  • 上述图片左侧的代码和输出,我们都可以很容易的理解,访问类的静态变量会导致类的初始化,那么在类初始化的时候静态代码块会被先加载,随后再输出了该静态变量的值

  • 对于右侧的代码和输出可能有一点超乎想象了!加了一个final修饰后输出的结果就完全变了,这是为什么呢?

    • 常量在编译阶段就会存入调用这个常量的方法所在(此处就是MyTest2)的常量池中
    • 本质上,调用类(MyTest2)并没有直接引用到定义常量的类(MyParnet2),因此不会触发定义常量类的初始化,也就不会加载静态代码块中的内容。因为这里本质上是在访问自己类中的常量池中的一个常量而已
    • 所以,说得更极端一点,在编译阶段的时候,常量就被存放到了MyTest2的常量池中,之后MyTest2MyParent2就没有任何关系了
    • 为了证明上面这一点,我们完全可以把MyTest2的编译后的class文件删除掉
      在这里插入图片描述
  • 经过试验后,完美的证明了上述的结论

  • 为了更深入的理解,我们尝试查看MyTest2的字节码文件(反编译后查看),这里需要再编译一次项目,因为上面把MyParent2的字节码文件删除了
    在这里插入图片描述

6.5.1 反编译MyTest2.class(以及助记符:ldc)

字节码文件中有大量的助记符,遇到一个学一个

  • 注意路径
    在这里插入图片描述
  • 反编译命令和结果

javap -c class文件所在路径

在这里插入图片描述

  • 字节码解读
    在这里插入图片描述
  • ldc:将int/float/String类型的常量从常量池中推送到栈顶
6.5.2 常用助记符探析
bipush

将单字节(-128~127)的常量值推送到栈顶

在这里插入图片描述

sipush

将整型int(-32678 ~ 32676)的常量值推送到栈顶

在这里插入图片描述

iconst_m1/iconst_0/iconst_1/iconst_2/…/iconst_5

只有这7个,从-1到5;因为1-5使用得比较多,所以就专门有5个专用的助记符

在这里插入图片描述

6.5.3助记符的本质(了解)

助记符的本质也是有底层类的定义才能实现的,可以在IDEA中搜索相关类。了解即可,平时开发不会用到

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述

6.6 程序实例3(常量编译期不可确定时常量的位置)

  • 问下述代码的输出
public class MyTest3 {
   
    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("MyParent3 static code");
    }
    
}
  • 本质上的问题和例2的区别在于MyParent3这个类会不会被初始化

  • 结果
    在这里插入图片描述

  • 发现静态代码块被加载了,说明该类完成了初始化了,但是这是为什么呢?常量不应该是在调用类的常量池中吗?

  • 原因在于:

  • 当一个常量的值并非编译期可以确定的,那么其值就不会被放入调用类MyTest3的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,显然导致这个类被初始化**

  • 这里,我们同样可以尝试删除MyParent3的字节码文件,然后再运行MyTest3,可以发现运行结果报错,就是因为这个常量是属于这个类,类的字节码文件没了,自然找不到
    在这里插入图片描述

6.7 程序实例4

  • 下述代码输出什么
public class MyTest4 {
   
    public static void main(String[] args) {
   
        MyParent4 myParent4 = new MyParent4();
        System.out.println("------------");
        MyParent4 myParent5 = new MyParent4();
    }
}

class MyParent4{
   
    static {
   
        System.out.println("MyParent4 static code");
    }
}
  • 结果
    在这里插入图片描述
  • 首先使用new关键字来创建对象显然是对类的主动使用,那么MyParent5被主动使用,然后被初始化,静态代码块被加载
  • 其次,这个例子非常完美的阐述了类的主动使用的前提:所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化。
  • 显然只有第一次创建该类的对象才触发了初始化操作,所以静态代码块中的内容只被输出了一次

6.8 程序实例5(数组类型的本质)

  • 下述代码的输出
public class MyTest4 {
   
    public static void main(String[] args) {
   

        MyParent4[] myParent4s = new MyParent4[1];
    }
}

class MyParent4{
   
    static {
   
        System.out.println("MyParent4 static code");
    }
}

  • 结果
    在这里插入图片描述

  • 发现居然什么都没有输出,也就是在数组创建的过程中,并没有触发对类MyParent4的主动使用,所以没有导致初始化。其实这一点也可以在前面7种Java主动使用类的情况中看到,前面没有提到任何和数组相关的情况。

  • 但是既然都new了一个数组,那么肯定生成了一个对象呀,那这个对象又是什么呢,我们不妨用类.getClass()方法来查看

在这里插入图片描述

  • 可以看到一维数组和二维数组的类型是[L类的全限定名/[[L类的全限定名
  • 有几个左括号就表示是几维数组
  • 我们接着继续看一下数组类型的父类是什么
    在这里插入图片描述
  • 可以看到不管是针对一维数组还是二维数组其父类都是Object
  • 关于数组,我们有如下几个结论
    • 对于数组对象来说,其类型是由JVM在运行期动态生成的
    • 数组对象的创建不会导致对应的类的主动使用
    • 表示为[L类的全限定名。([对应数组维度)
    • 对于数组来说,JavaDoc经常将构成数组的元素为Component(组件类型),实际上就是将数组降低一个维度后的类型

6.8.1 程序实例5.1(基本数据类型数组的本质)

上面讲述了引用类型的数组创建的对应类型,那么原始的基本数据对应的数组是属于什么类呢?

  • 一一对应查看即可
    在这里插入图片描述

6.8.2 相关助记符

反编译查看数组创建相关的助记符

在这里插入图片描述

  • anewarray:表示创建一个引用类型的(类,接口,数组)的数组,并将其引用值压入栈顶
  • newarray:表示创建一个指定的原始类型(int/char/boolean/fliat等)的数组,并将其引用值压入栈顶。(再次提醒数组中存放的都是引用!)

7.接口初始化规则

7.1 当一个接口在初始化时,并不必须要求其父接口完成了初始化

  • 下述代码输出是?
public class MyTest5 {
   

    public static void main(String[] args) {
   
        System.out.println(MyChild5.b);
    }
}

interface MyParent5{
   

    public static int a = 4;

}

interface MyChild5 extends MyParent5{
   

    public static int b = 5;
}
  • 输出:
    在这里插入图片描述

  • 在类中,一个类被初始化前,要求其所有的父类都要完成初始化才行,显然这一点在接口中是不成立的

  • 为了进一步说明这一点,把MyParent5的字节码文件删除后,再运行
    在这里插入图片描述

  • 删除后,输出仍然是5,说明确实和父接口没有关系

7.2 接口中定义的静态变量默认是常量(final)

  • 在类里,我们知道在编译期可以确定的常量在编译阶段是放在调用该常量的方法所属的类中的,那么在接口中是否也成立这一点呢?

  • 我们尝试在上面已经删除了MyParent5.class的基础上继续删除MyChild5.class,然后再运行
    在这里插入图片描述

  • 上述结果已经足以说明这一点了,在删除MyChild5的基础上,再次运行,依然输出了5,这一点完全印证了前面的分析

7.3 接口中的属性(变量)是默认被public static final修饰的

简单的说,就是接口中的变量一定是静态常量,且访问权限是public的

  • 下面的例子,很好的说明了这一点
    在这里插入图片描述
  • 为什么呢?为什么接口中需要这样规定呢?可以从以下几个方面来分析
  • public:使接口的实现类或者子接口可以使用这个常量,不然定义常量干嘛呢
  • static:接口不涉及任何具体实例的细节,因此接口是不可能被实例化的,所以只可能有静态的变量,因为只有静态变量才属于这个接口(类)本身,随着类的加载而存在。如果是非静态变量的话,那这个变量就只有属于对象,只有当实例化对象的时候才能访问这个变量,但是接口是不可能被实例化的
  • finla:如果没有final修饰的话,子类以及子接口就可以随意改变这个接口,这样就没有意义了,因为接口定义了这个常量就意味着一套规范,所有的实现类和子接口,都只能遵守这种规范,而不能改变它
  • 综上,接口中的属性默认被public static final修饰

7.4只有在真正使用到父接口,(如访问接口中运行期才能确定的常量时),才会初始化父接口

在这里插入图片描述

  • 上面的例子可以继续往下改动
    在这里插入图片描述

  • 再继续改动,把接口改成类
    在这里插入图片描述

8.准备阶段和初始化阶段的过程分析(静态变量赋初值问题)

8.1例1

  • 下述程序输出什么?
public class MyTest6 {
   
    public static void main(String[] args) {
   
        Singleton singleton = Singleton.getInstance();
        System.out.println("count1: " + Singleton.count1);
        System.out.println("count2: " + Singleton.count2);
    }
}

class Singleton{
   
    
    public static int count1;
    public static int count2 = 0;

    private static Singleton singleton = new Singleton();

    private Singleton(){
   
        count1++;
        count2++;
    }
    public static Singleton getInstance(){
   
        return singleton;
    }
}
  • 输出
    在这里插入图片描述

  • 这个不难理解,初始化从上到下

8.2例2

  • 问下述程序输出什么
public class MyTest6 {
   
    public static void main(String[] args) {
   
        Singleton singleton = Singleton.getInstance();
        System.out.println("count1: " + Singleton.count1);
        System.out.println("count2: " + Singleton.count2);
    }
}

class Singleton{
   

    public static int count1;

    private static Singleton singleton = new Singleton();

    private Singleton(){
   
        count1++;
        count2++;
        System.out.println(count1);
        System.out.println(count2);
    }
    public static int count2 = 0;

    public static Singleton getInstance(){
   
        return singleton;
    }
}
  • 输出:

在这里插入图片描述

8.2.1分析:准备阶段,为类的静态变量赋默认值。初始化阶段,为静态变量赋予正确的初值

分析上述程序的执行步骤

在这里插入图片描述

  • 通过这个例子,相信对于准备阶段和初始化阶段认识更深了
  • 那么下述代码输出是:
public class MyTest6 {
   
    public static void main(String[] args) {
   
        Singleton singleton = Singleton.getInstance();
        System.out.println("count1: " + Singleton.count1);
        System.out.println("count2: " + Singleton.count2);
    }
}

class Singleton{
   

    public static int count1 = 1;

    private static Singleton singleton = new Singleton();

    private Singleton(){
   
        count1++;
        count2++;
        System.out.println(count1);
        System.out.println(count2);
    }
    public static int count2 = 0;

    public static Singleton getInstance(){
   
        return singleton;
    }
}
  • 只要理解了第一个例子,这里就很简单了
    在这里插入图片描述

二、 类的加载

1.完整定义

  • 类的加载是指将类的.class文件中的二进制数据读入内存,将其放在运行时数据区的方法区(JDK1.8改造成了元空间),然后在内存中创建一个java.lang.Class对象,(联系反射的知识点,另外要注意的是Java规范中并没说明Class对象应该位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区的数据结构

2.加载.class文件的方式

  • 从本地系统直接加载(大多数人平时使用最多的方式)
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
    • 大量的第三方的api都打包为jar包的方式提供使用
  • 从专有数据库中提取.class文件
  • 将java源文件动态的编译为.class文件
    • 动态代理中,类的生成是在运行期
    • JavaWeb开发中的jsp文件

3类加载的顺序

本质上还是前文的顺序

3.1完整的生命周期

在这里插入图片描述

3.2更深入的每一步

在这里插入图片描述

4 类的加载的最终产品是什么?

  • 类的加载的最终产品是位于内存中的Class对象
  • Class对象封装了类在方法区内的数据结构(成员变量,方法),并且向Java程序员提供了访问方法区内的数据结构接口

5类加载器的类型

5.1Java虚拟机自带的加载器

  • 根类加载器BootStrap
    • 该加载器没有父加载器,它负责加载虚拟机的核心类库,如java.lang.*java.lang.Object就是由根类加载器加载的,根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分。它并没有继承java.lang.ClassLoader
  • 扩展类加载器Extension
    • 它的父加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类
  • 系统(应用)类加载器System
    • 也称应用类记载器。它的父加载器为扩展类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。系统类加载器是纯Java类,是java.lang.ClassLoader类的子类

5.2用户自定义的类加载器

  • java.lang.ClassLoader的子类
  • 用户可以自定义类的加载方式
    • 所有用户自定义的类加载器都应该继承ClassLoader类(抽象类)
5.3类加载器层次关系

表面看是继承关系,实质上是包含关系,下层的包含上层

在这里插入图片描述

在这里插入图片描述

6 类加载器并不需要等到某个类被“首次主动使用”时再加载它

这句话的本质就是前面一直反复提到的一个类不被主动使用代表不会被初始化,但是这不代表这个类不会被加载。详情查看第一节的6.3

  • JVM规范允许类加载器在预料某个类将要被使用时就预先加载了它,如果在预先加载的过程遇到.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误

  • 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

7.父(双)亲委托机制简介

类加载器用来把类加载到java虚拟机中,从JDK1.2版本开始,类的加载采用父亲委托机制,这种机制能更好的保证Java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载器loader1加载某个类的时候,loader1首先委托自己的父加载器去加载该类,若父加载器可以完成加载,则由父加载器完成加载任务,否则才有loader1来加载。
在这里插入图片描述在这里插入图片描述

  • Bootstrap ClassLoader:启动类加载器

    • $JAVA_HOME$jre/lib/rt.jar里的所有的class,由C++实现,不是ClassLoader子类,这个包下面的类是平时开发中用到的绝大多数的类,是JDK的核心类实现
  • Extension ClassLoader:扩展类加载器

    • 负责java平台中扩展功能的一些jar包,包括$JAVA_HOME$中的jre/lib/*.jar-Djava.ext.dirs指定目录下的jar
  • App ClassLoader:系统类加载器

    • 负责加载classpath中指定的jar包以及目录中的class
  • 如果有一个类加载器能够成功加载Test类,那么这个类加载器被称为定义类加载器(真正去加载那个类的加载器),所有能成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始类加载器(了解即可)

在这里插入图片描述

7.1 程序实例:getClassLoader方法

  • 代码
public class MyTest7 {
   

    public static void main(String[] args) throws ClassNotFoundException {
   

        Class<?> clazz =  Class.forName("java.lang.String");
        //返回加载类 clazz的 类加载器
        System.out.println(clazz.getClassLoader());


        Class<?> clazz1 = Class.forName("com.xpt.jvm.Demo01_classloader.C");
        System.out.println(clazz1.getClassLoader());
        
    }
    
}


class C{
   

}
  • 输出
    在这里插入图片描述

  • 关于getClassLoader这个方法,我们根据源码及文档进行学习

    /**
     * Returns the class loader for the class.  Some implementations may use
     * null to represent the bootstrap class loader. This method will return
     * null in such implementations if this class was loaded by the bootstrap
     * class loader.
     *
     * <p> If a security manager is present, and the caller's class loader is
     * not null and the caller's class loader is not the same as or an ancestor of
     * the class loader for the class whose class loader is requested, then
     * this method calls the security manager's {@code checkPermission}
     * method with a {@code RuntimePermission("getClassLoader")}
     * permission to ensure it's ok to access the class loader for the class.
     *
     * <p>If this object
     * represents a primitive type or void, null is returned.
     *
     * @return  the class loader that loaded the class or interface
     *          represented by this object.
     * @throws SecurityException
     *    if a security manager exists and its
     *    {@code checkPermission} method denies
     *    access to the class loader for the class.
     * @see java.lang.ClassLoader
     * @see SecurityManager#checkPermission
     * @see java.lang.RuntimePermission
     */
    @CallerSensitive
    public ClassLoader getClassLoader() {
   
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
   
            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
        }
        return cl;
    }
  • 源码解读
    在这里插入图片描述

三、类的连接

1.类的验证

类被加载后,就进入连接阶段,连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去

1.1类验证阶段的主要内容

  • 类文件的结构检查
  • 语义检查
  • 字节码验证
  • 二进制兼容性的验证

2.类的准备

在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值,例如,对于如下的Sample类,在准备阶段将int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0;为long类型的静态变量b分配8个字节的内存空间,且赋值默认值0

public class Sample{
   
    private static int a = 1;
    public static long b;
    static {
   
        b = 2;
    }

}

四、类的初始化

1初始化方式和顺序

在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,有两种方式对静态变量进行初始化

  • 1.在类的声明处进行初始化
  • 2.在静态代码块中进行初始化

例如,在如下的代码中,ab都被显式的初始化了,而静态变量c没有被显式的初始化,保持默认值0

public class Sample{
   
    private static int a = 1;
    public static long b;
    private static int c;
    static {
   
        b = 2;
    }

}

静态变量的声明语句,静态代码块都被看做是类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序依次执行他们,例如当如下的Sample类初始化后,a= 4

public class Sample{
   
    private static int a = 1;
    
    static {
   
        a = 2;
    }
    static {
   
        a = 4;
    }

}

2初始化步骤

  • 假如这个类还没有被加载和连接,那就先加载和连接
  • 加入类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类
  • 假如类中存在初始化语句,那就依次执行这些初始化语句

3类的初始化时机

和类被主动使用的7中情况完全一致

  • 创建类的实例:new一个对象嘛
  • 访问某个类或接口的静态变量,或者对该静态变量赋值:一个是取值一个赋值
  • 调用类的静态方法
    • 相关的字节码助记符:getstatic,putstatic,invokestatic
  • 使用反射:Class.forName(com.test.Test)
  • 初始化一个类的子类:比如初始化一个Child类,它的父类是Parent,那么这个父类肯定也要被使用
  • Java虚拟机启动时被标明为启动类的类:入口类,main方法所在的类,Java Test
  • JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有被初始化时,则初始化

除了以上7中情况,其他的Java类的使用方式都被看做是对类的被动使用,都不会导致类的初始化。但是这并不代表不会发生加载和连接过程

4. 接口的初始化规则(和类不同)

  • 当Java虚拟机初始化一个类时,要求其所有的父类都已经被初始化,但是这条规则不适合接口
    • 当初始化一个类时,并不会先初始化它所实现的接口
    • 当初始化一个接口时,并不会先初始化它的父接口

因此,一个父接口不会因为它的子接口或者实现类初始化而初始化。只有当程序首次使用特定接口的静态变量时才会导致该接口的初始化

  • 只有当程序访问的静态变量和静态方法确实在当前类或当前接口中定义时,才可以被认为是对类或接口的主动使用
  • 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化(后续讲解)

5. 初始化对于类和接口异同点深入分析

前面关于这个问题,也举过一些例子,但是前面的例子并不能很好的反应出正确的结论,这里我们先具体分析前面例子的问题出在哪里

5.1 第一节7.1中所举例子的问题

  • 在7.1节中,我们举的例子是这样的,接口MyChild5继承了父接口MyParent5,然后通过main方法去访问接口MyChild5中的变量b,继续我们删除了MyParent5的class文件,然后再次运行,仍然可以正常运行,这里我们就认为这里的运行和父接口的class文件是没有关系的。

  • 但是这里有个问题在于!我们现在知道的一个基本事实是**接口中的变量都默认被public static final修饰,也就是说接口中的变量都是常量

  • 同时,例子中的变量b是一个编译期可以确定的常量!也就说这个例子能得出的结论仍然是前面的结论,也就是说这里的输出和子接口MyChild5父接口MyParent5都没有关系,因为常量在编译期就已经被记载到MyTest5类中了

  • 为了说明这一点,我们可以从两方面来验证

    • 删除MyParent5MyChild5的class文件,然后再运行程序,如果仍然正常输出的话,说明和接口都没有关系
      在这里插入图片描述
  • 输出很好的印证了我们的猜想

  • 验证方式2:从类加载角度,此时保留二者的class文件,看类加载过程是否记载了这两个接口

在这里插入图片描述

  • 只加载了入口类,并没有记载到两个接口

  • 所以一系列例子能得到的结论是:当接口中定义的常量是编译期可以确定的,在访问这个常量的时候,不会加载这个接口

5.2 当初始化一个类的时候,并不会先初始化它的父类

这一节通过一个实例来说明这一点

5.2.1Java中的普通代码块{}
  • 静态代码块很熟悉了,普通代码块其实就是一段普通的代码,它和静态代码块的区别在于,静态代码块中的内容属于类,不管创建多少对象,就只有这一份,普通代码块中的内容属于对象,有多少对象,就有多少分
    在这里插入图片描述
  • 有了上面的知识点,下面可以设计这一节的实验
5.2.2 实例1
public class MyTest5 {
   

    public static void main(String[] args) {
   
        System.out.println(MyChild5.b);
    }
}

interface MyParent5{
   
    public static Thread thread = new Thread(){
   
        {
   
            System.out.println("MyParent5 invoked");
        }
    };

}

class MyChild5 implements MyParent5{
   
    public static  int b = 5;
}
  • 首先调用一个类的静态变量,这是对类的主动使用,那么会导致类的初始化
  • 此时核心问题在于这个类的接口会不会被初始化?
  • 具体到这个例子就是MyParent5 invoked是否会被输出?

在这里插入图片描述

  • 结果值输出了5,成功了证明了结论
  • 同时不会被初始化不代表不被加载,可以看到接口还是被加载了的
  • 我们基于上面的例子,查看一下如果是类与类之间的关系,最终的表现会是什么

在这里插入图片描述

5.3 当初始化一个接口时,并不会先初始化它的父接口

  • 下述例子输出?
public class MyTest5 {
   

    public static void main(String[] args) {
   
        System.out.println(MyParent5_1.thread);
    }
}

interface MyGrandPa5_1{
   
    public static Thread thread = new Thread(){
   
        {
   
            System.out.println("MyGrandPa5_1 invoked");
        }
    };
}

interface MyParent5_1 extends MyGrandPa5_1{
   
    public static Thread thread = new Thread(){
   
        {
   
            System.out.println("MyParent5_1 invoked");
        }};
}
  • 结果
    在这里插入图片描述

  • 显然并没有初始化父接口

五、类的加载和初始化深入剖析(大量实例,巩固理论)

前面几节学习了很多理论,这里以实例的方式再次总结一下,以加深理解

1.问题1:常量是否能在编译期确定对类是否被初始化的影响

  • 下述三段程序输出什么?以及为什么?

  • 程序1


class FinalTest{
   

    public static final int x = 3;
    static {
   
        System.out.println("FinalTest static block");
    }

}
public class MyTest8 {
   

    public static void main(String[] args) {
   
        System.out.println(FinalTest.x);
    }

}
  • 程序2
class FinalTest{
   

    public static final int x = new Random().nextInt(3);
    static {
   
        System.out.println("FinalTest static block");
    }

}
public class MyTest8 {
   

    public static void main(String[] args) {
   
        System.out.println(FinalTest.x);
    }

}
  • 程序3:

class FinalTest{
   

    public static int x = 3;
    static {
   
        System.out.println("FinalTest static block");
    }

}
public class MyTest8 {
   

    public static void main(String[] args) {
   
        System.out.println(FinalTest.x);
    }

}
  • 上述程序现在看来以及非常的简单了,其输出也应该完全可以确定下来,下面详细分析一下
    在这里插入图片描述

2. 问题2:入口类,父类,子类的初始化顺序

  • 问下述代码的输出(注意顺序)
class Parent{
   
    static int a = 3;
    static {
   
        System.out.println("parent static block");
    }
}

class Child extends Parent{
   
    static int b = 4;
    static {
   
        System.out.println("child static block");
    }
}
public class MyTest9 {
   
    static {
   
        System.out.println("MyTest9 static block");
    }
    public static void main(String[] args) {
   
        System.out.println(Child.b);
    }
}

在这里插入图片描述

  • 分析

  • 其实很容易理解,类的初始化顺序,一定是先初始化入口类(暂时不考虑入口类的父类)

  • 然后入口类中涉及到其他类(Child)的使用(访问类的静态变量),那么会先去初始化该类的所有父类Parent

  • 最后才初始化该类本身Child

  • 最后完成对类的静态变量的访问

  • 上述过程,从类加载的顺序角度更容易理解,通过JVM参数-XX:+TraceClassLoading可以看到类的加载顺序

在这里插入图片描述

3. 问题3:声明引用不是主动使用,初始化只会在首次主动使用执行一次

  • 下述程序输出什么
class Parent2{
   
    static int a = 3;
    static {
   
        System.out.println("Parent2 static block");
    }
}

class Child2 extends Parent2{
   
    static int b = 4;
    static {
   
        System.out.println("Child2 static block");
    }
}
public class MyTest10 {
   
    static {
   
        System.out.println(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>