• javaJVM-类加载机制-4


    我们知道,我们写的java文件是不能直接运行的,我们可以在IDEA中右键文件名点击运行,或者可以放到服务器上作为服务运行,这中间其实掺杂了一系列的复杂处理过程。

    这篇文章,我们只讨论我们的代码在运行之前的一个环节,叫做类的加载。按照我写文章的常规惯例,先给出这篇文章的大致结构;

    一、什么是类的加载

    在介绍类的加载机制之前,先来看看,类的加载机制在整个java程序运行期间处于一个什么环节,下面使用一张图来表示:

     从上图可以看,java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。其中类装载器的作用其实就是类的加载。今天我们要讨论的就是这个环节。有了这个印象之后我们再来看类的加载的概念:

    二、类加载的过程

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们的顺序如下图所示:

    分别一一解释各阶段做的事情

    1、加载:

      类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,作为方法区这个类的数据访问的入口

    也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。具体包括以下三个部分:

    (1)通过类的全名产生对应类的二进制数据流(.class文件是字节流)。(根据early load原理,如果没找到对应的类文件,只有在类实际使用时才会抛出错误)

    (2)分析并将这些二进制数据流转换为方法区特定的数据结构

    (3)创建对应类的java.lang.Class对象,作为方法区的入口(有了对应的Class对象,并不意味着这个类已经完成了加载链接)

    通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:

    (1)从本地文件系统加载class文件,这是绝大部分程序的加载方式

    (2)从jar包中加载class文件,这种方式也很常见,例如jdbc编程时用到的数据库驱动类就是放在jar包中,jvm可以从jar文件中直接加载该class文件

    (3)通过网络加载class文件

    (4)把一个Java源文件动态编译、并执行加载

    2、链接:

        链接指的是将Java类的二进制文件合并到jvm的运行状态之中的过程。在链接之前,这个类必须被成功加载。

    类的链接包括验证、准备、解析这三步。具体描述如下:

    2.1  验证:

        验证是用来确保Java类的二进制表示在结构上是否完全正确(如文件格式、语法语义等)。如果验证过程出错的话,会抛出java.lang.VertifyError错误。

    主要验证以下内容:

    • 文件格式验证
    • 元数据验证:语义验证
    • 字节码验证

    2.2  准备:

      准备过程则是创建Java类中的静态域(static修饰的内容),并将这些域的值设置为默认值,同时在方法区中分配内存空间。准备过程并不会执行代码。

    注意这里是做默认初始化,不是做显式初始化。例如:

    public static int value = 12;

    上面的代码中,在准备阶段,会给value的值设置为0(默认初始化)。在后面的初始化阶段才会给value的值设置为12(显式初始化

    2.3  解析:

      解析的过程就是确保这些被引用的类能被正确的找到(将符号引用替换为直接引用)。解析的过程可能会导致其它的Java类被加载。

    符号引用:

        以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)。

    直接引用:

        直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

    3、初始化:

      初始化阶段是类加载过程的最后一步。到了初始化阶段,才真正执行类中定义的Java程序代码(或者说是字节码)。

    在以下几种情况中,会执行初始化过程

    (1)创建类的实例

    (2)访问类或静态变量(读取或者设置静态变量 特例:如果是用static final修饰的常量,那就不会对类进行显式初始化。static final 修改的变量则会做显式初始化))

    (3)调用类的静态方法

    (4)反射(Class.forName(packagename.className))

    (5)初始化某个类的子类,则其父类也会被初始化注:子类初始化问题:满足主动调用,即父类访问子类中的静态变量、方法,子类才会初始化;否则仅父类初始化。

    (6)java虚拟机启动时被标明为启动类的类(main方法所在类,JVM会先初始化这个类)

    代码举例1:

    我们对上面的第(5)种情况做一个代码举例。

    (1)Father.java:

    1 public class Father {
    2 
    3     static {
    4         System.out.println("*******father init");
    5     }
    6     public static int a = 1;
    7 }
    View Code

    (2)Son.java:

    1 public class Son extends Father {
    2     static {
    3         System.out.println("*******son init");
    4     }
    5     public static int b = 2;
    6 }
    View Code

    (3)JavaTest.java:

    1 public class JavaTest {
    2     public static void main(String[] args) {
    3         System.out.println(Son.a);
    4     }
    5 }
    View Code

    上面的测试类中,虽然用上了Son这个类,但是并没有调用子类里的成员,所以并不会对子类进行初始化。于是运行效果是:

    b6de0ab4-1502-46dd-9193-578d1c83ca20

    如果把JavaTest.java改成下面这个样子:

    1 public class JavaTest {
    2     public static void main(String[] args) {
    3         System.out.println(Son.a);
    4         System.out.println(Son.b);
    5     }
    6 }
    View Code

    运行效果:

    b4757ac3-7476-49ff-84a4-659dca412324

    如果把JavaTest.java改成下面这个样子:

    JavaTest.java:

    1 public class JavaTest {
    2     public static void main(String[] args) {
    3         System.out.println(Son.b);
    4     }
    5 }
    View Code

    运行效果:

    e88cc820-e004-4764-9ab3-403d5d660a8a

    代码举例2:

    我们对上面的第(2)种情况做一个代码举例。即:如果是用static final修饰的常量,则不会进行显式初始化。代码举例如下:

    (1)Father.java:

    1 public class Father {
    2     static {
    3         System.out.println("*******father init");
    4     }
    5     public static int a = 1;
    6 }
    View Code

    (2)Son.java:

    1 public class Son extends Father {
    2     static {
    3         System.out.println("*******son init");
    4     }
    5 
    6     public static int b = 2;
    7     public static final int c = 3;
    8 }
    View Code

    这里面的变量c是一个静态常量。

    (3)JavaTest.java:

    1 public class JavaTest {
    2     public static void main(String[] args) {
    3         System.out.println(Son.c);
    4     }
    5 }
    View Code

     上面的运行效果显示,由于c是final static修饰的静态常量,所以根本就没有调用静态代码块里面的内容,也就是说,没有对这个类进行显式初始化

    现在,保持Father.java的代码不变。将Son.java代码做如下修改(静态常量改为final静态变量):

    1 public class Son extends Father {
    2     static {
    3         System.out.println("*******son init");
    4     }
    5 
    6     public static int b = 2;
    7     public static final int c = new Random().nextInt(3);
    8 }
    View Code

    JavaTest.java:

    1 public class JavaTest {
    2     public static void main(String[] args) {
    3         System.out.println(Son.c);
    4     }
    5 }
    View Code

    运行效果如下:

    235bc0d9-0b08-436c-9b6d-380702f4a8c7

    调用 static final 修改的变量则会做显式初始化 

    代码举例3:(很容易出错)

    /**
     * @author hup
     * @data 2020-06-20 10:21
     **/
    public class ClassLoad {
        public static ClassLoad instance = new ClassLoad();
        public static int a;
        public static int b = 0;
    
        public ClassLoad() {
            System.out.println("执行了构造方法");
            a++;
            b++;
        }
    }
    View Code

    测试

        @Test
        public  void test() {
            System.out.println(ClassLoad.a);
            System.out.println(ClassLoad.b);
        }
    View Code

    测试输出结果

    执行了构造方法
    执行了构造方法
    
    2
    1
    View Code

    这里涉及到类加载的顺序:

    (1)在加载阶段,加载类的信息

    (2)在链接的准备阶段给instance、a、b做默认初始化并分配空间,此时a和b的值都为0,instance为null

    (3)在初始化阶段,给静态变量做显式初始化(

                按顺序先执行instance的显示初始化 :   public static ClassLoad instance = new ClassLoad();  会显示调用构造方法  所以此时a=1,b=1)

                执行int a的显示初始化 值不变

                执行int b的显示初始化  b=0

     (4)   执行构造方法   此时a=2,b=1 

    我们改一下代码的执行顺序,改成下面这个样子:

    /**
     * @author hup
     * @data 2020-06-20 10:21
     **/
    public class ClassLoad {
        public static int a;
        public static int b = 0;
        public static ClassLoad instance = new ClassLoad();
    
        public ClassLoad() {
            System.out.println("执行了构造方法");
            System.out.println("a="+a);
            System.out.println("b="+b);
            a++;
            b++;
        }
    }
    View Code

    测试

    View Code

    测试输出结果

    执行了构造方法
    a=0
    b=0
    执行了构造方法
    a=1
    b=1
    
    2
    2
    View Code

    这里涉及到类加载的顺序:

    (1)在加载阶段,加载类的信息

    (2)在链接的准备阶段给instance、a、b做默认初始化并分配空间,此时a和b的值都为0,instance为null

    (3)在初始化阶段,给静态变量做显式初始化(

                执行int a的显示初始化 值为0

                执行int b的显示初始化  b=0

                执行instance的显示初始化 :   public static ClassLoad instance = new ClassLoad();  会显示调用构造方法  所以此时a=1,b=1)

     (4)   执行构造方法   此时a=2,b=2

    注意,这里涉及到另外一个类似的知识点不要搞混了。知识点如下。

    知识点:类的初始化过程(重要)

    Student s = new Student();在内存中做了哪些事情?

    • 加载Student.class文件进内存
    • 栈内存为s开辟空间
    • 堆内存为学生对象开辟内存空间
    • 对学生对象的成员变量进行默认初始化
    • 对学生对象的成员变量进行显示初始化
    • 通过构造方法对学生对象的成员变量赋值
    • 学生对象初始化完毕,把对象地址赋值给s变量

    三  Class类文件结构解析

    什么是Class文件

            Java字节码类文件(.class)是Java编译器编译Java源文件(.java)产生的“目标文件”。它是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间(方便于网络的传输)。

    java源文件在被Java编译器编译之后, 每个类(或者接口)都单独占据一个class文件, 并且类中的所有信息都会在class文件中有相应的描述, 由于class文件很灵活, 它甚至比Java源文件有着更强的描述能力。
     
    class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。 可以把u1, u2, u3, u4看做class文件数据项的“类型” 。

    Class文件的结构

            一个典型的class文件分为:MagicNumber,Version,Constant_pool,Access_flag,This_class,Super_class,Interfaces,Fields,Methods 和Attributes这十个部分,用一个数据结构可以表示如下:

     下面对class文件中的每一项进行详细的解释:

    1、magic
    在class文件开头的四个字节, 存放着class文件的魔数, 这个魔数是class文件的标志,他是一个固定的值: 0XCAFEBABE 。 也就是说他是判断一个文件是不是class格式的文件的标准, 如果开头四个字节不是0XCAFEBABE, 那么就说明它不是class文件, 不能被JVM识别。
     
    2、minor_version 和 major_version
    紧接着魔数的四个字节是class文件的此版本号和主版本号。
    随着Java的发展, class文件的格式也会做相应的变动。 版本号标志着class文件在什么时候, 加入或改变了哪些特性。 举例来说, 不同版本的javac编译器编译的class文件, 版本号可能不同, 而不同版本的JVM能识别的class文件的版本号也可能不同, 一般情况下, 高版本的JVM能识别低版本的javac编译器编译的class文件, 而低版本的JVM不能识别高版本的javac编译器编译的class文件。 如果使用低版本的JVM执行高版本的class文件, JVM会抛出java.lang.UnsupportedClassVersionError 。具体的版本号变迁这里不再讨论, 需要的读者自行查阅资料。
     

    3、constant_pool
    在class文件中, 位于版本号后面的就是常量池相关的数据项。 常量池是class文件中的一项非常重要的数据。 常量池中存放了文字字符串, 常量值, 当前类的类名, 字段名, 方法名, 各个字段和方法的描述符, 对当前类的字段和方法的引用信息, 当前类中对其他类的引用信息等等。 常量池中几乎包含类中的所有信息的描述, class文件中的很多其他部分都是对常量池中的数据项的引用,比如后面要讲到的this_class, super_class, field_info, attribute_info等, 另外字节码指令中也存在对常量池的引用, 这个对常量池的引用当做字节码指令的一个操作数。此外,常量池中各个项也会相互引用。

    常量池是一个类的结构索引,其它地方对“对象”的引用可以通过索引位置来代替,我们知道在程序中一个变量可以不断地被调用,要快速获取这个变量常用的方法就是通过索引变量。这种索引我们可以直观理解为“内存地址的虚拟”。我们把它叫静态池的意思就是说这里维护着经过编译“梳理”之后的相对固定的数据索引,它是站在整个JVM(进程)层面的共享池。

    class文件中的项constant_pool_count的值为1, 说明每个类都只有一个常量池。 常量池中的数据也是一项一项的, 没有间隙的依次排放。常量池中各个数据项通过索引来访问, 有点类似与数组, 只不过常量池中的第一项的索引为1, 而不为0, 如果class文件中的其他地方引用了索引为0的常量池项, 就说明它不引用任何常量池项。class文件中的每一种数据项都有自己的类型, 相同的道理,常量池中的每一种数据项也有自己的类型。 常量池中的数据项的类型如下表:


    java程序是动态链接的, 在动态链接的实现中, 常量池扮演者举足轻重的角色。 除了存放一些字面量之外, 常量池中还存放着以下几种符号引用:
    (1) 类和接口的全限定名
    (2) 字段的名称和描述符
    (3) 方法的名称和描述符

    4、access_flag 保存了当前类的访问权限

    5、this_cass 保存了当前类的全局限定名在常量池里的索引

    6、super class 保存了当前类的父类的全局限定名在常量池里的索引

    7、interfaces 保存了当前类实现的接口列表,包含两部分内容:interfaces_count 和interfaces[interfaces_count]
    interfaces_count 指的是当前类实现的接口数目
    interfaces[] 是包含interfaces_count个接口的全局限定名的索引的数组

    8、fields 保存了当前类的成员列表,包含两部分的内容:fields_count 和 fields[fields_count]
    fields_count是类变量和实例变量的字段的数量总和。
    fileds[]是包含字段详细信息的列表。

    9、methods 保存了当前类的方法列表,包含两部分的内容:methods_count和methods[methods_count]
    methods_count是该类或者接口显示定义的方法的数量。
    method[]是包含方法信息的一个详细列表。

    10、attributes 包含了当前类的attributes列表,包含两部分内容:attributes_count 和 attributes[attributes_count]
    class文件的最后一部分是属性,它描述了该类或者接口所定义的一些属性信息。attributes_count指的是attributes列表中包含的attribute_info的数量。
    属性可以出现在class文件的很多地方,而不只是出现在attributes列表里。如果是attributes表里的属性,那么它就是对整个class文件所对应的类或者接口的描述;如果出现在fileds的某一项里,那么它就是对该字段额外信息的描述;如果出现在methods的某一项里,那么它就是对该方法额外信息的描述

     
     

    欢迎转载,但请保留文章原始出处→_→ 

    参考来源:http://www.cnblogs.com/smyhvae/p/4810168.html

    参考来源:https://www.jianshu.com/p/247e2475fc3a
     
  • 相关阅读:
    js -- 判断数组是否为空?
    vue 初始化高德地图
    git .gitignore 忽略规则的匹配语法
    vue 人脸识别 demo
    vue音乐app——VSCODE中设置vue文件模板
    vue音乐项目——解析stylus中的 &.router-link-active
    十月训练记录
    概率与期望 学习笔记
    山东省队集训整理
    APIO2020 简要题解
  • 原文地址:https://www.cnblogs.com/hup666/p/13155177.html
Copyright © 2020-2023  润新知