[Java虚拟机]Java类的加载

[Java虚拟机]Java类的加载

一、类的加载(类初始化)

当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。

在java代码中,类型的加载、连接、与初始化过程都是在程序运行期间完成的(类从磁盘加载到内存中经历的三个阶段)

  1. 类型:定义的类、接口或者枚举称为类型而不涉及对象。
  2. 程序运行期间:并没有在编译器就完成了加载。

注意事项

  • 类加载器并不需要等到某个类被 “首次主动使用” 时再加载它。
  • JVM规范允许类加载器在预料某个类将要被使用时就预先加载它。
  • 如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

二、类的生命周期

(重点是类的主动使用
在这里插入图片描述

加载、验证、准备、初始化和卸载这 5 个阶段的顺序是固定确定的,类的加载过程必须按照这种顺序开始。而解析阶段可以在初始化后再开始,这是为了支持 Java 语言的运行时绑定【也就是java的动态绑定/晚期绑定】。

1. 加载

将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象(HotSpot虚拟机将其放在方法区中),用来封装类在方法区内的数据结构。

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

Class对象是存放在堆区的,不是方法区。 类的元数据才是存在方法区的。类的元数据包括类的方法代码,变量名,方法名,访问权限,返回值等。

JDK7创建Class实例存在堆中;因为JDK7中JavaObjectsInPerm参数值固定为false。
JDK8移除了永久代,转而使用元空间来实现方法区,创建的Class实例依旧在java heap(堆)中

在这里插入图片描述

编写一个新的java类时,JVM就会帮我们编译成class对象,存放在同名的.class文件中。在运行时,当需要生成这个类的对象,JVM就会检查此类是否已经装载内存中。若是没有装载,则把.class文件装入到内存中。若是装载,则根据class文件生成实例对象。

加载阶段总结:

.class文件(二进制数据)——>读取到内存——>数据放进方法区——>堆中创建对应Class对象——>并提供访问方法区的接口

加载 .class文件的方式:

类的加载由类加载器完成,类加载器包括① JVM提供的类加载器(系统类加载器)、② 开发者通过继承ClassLoader基类来创建自己的类加载器。通过使用不同的类加载器,可以从不同来源加载类的二进制数据。

2. 验证

目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  4. 符号引用验证:确保解析动作能正确执行。

验证阶段不是必须的,对程序运行期没有影响,如果所引用的类经过反复验证,可以考虑采用 -Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

3. 准备 *

JVM 便会开始为类变量分配内存并初始化。准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

这里需要注意两个关键点,即内存分配的对象以及初始化的类型

内存分配的对象:Java 中的变量有类变量以及类成员变量两种类型,类变量指的是被 static 修饰的变量,而其他所有类型的变量都属于类成员变量。在准备阶段,JVM 只会为类变量分配内存,而不会为类成员变量分配内存。类成员变量的内存分配需要等到初始化阶段才开始

初始化的类型: 这里的初始化指的是为类变量赋予 Java 语言中该数据类型的默认值,而不是用户代码里初始化的值。但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。

4. 解析

(了解即可)

虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

5.初始化 *

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。

Java程序对类的使用方式可分为两种:主动使用被动使用。一般来说只有当对类的首次主动使用的时候才会导致类的初始化,所以主动使用又叫做类加载过程中“初始化”开始的时机。

类的主动使用 **
  1. 创建类的实例,也就是new的方式

  2. 访问某个类或接口的静态变量,或者对该静态变量赋值(凡是被final修饰或更准确的说是在编译器把结果放入常量池的静态字段除外)

  3. 调用类的静态方法

  4. 反射(如 Class.forName(“com.gx.yichun”))

  5. 初始化某个类的子类,则其父类也会被初始化

  6. Java虚拟机启动时被标明为启动类的类(JavaTest),还有就是Main方法的类会首先被初始化

**注意:**对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),这句话在继承、多态中最为明显。

6. 使用与卸载

JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。

7. 结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

  1. 执行了System.exit()方法
  2. 程序正常执行结束
  3. 程序在执行过程中遇到了异常或错误而异常终止
  4. 由于操作系统出现错误而导致Java虚拟机进程终止

三、接口的加载过程

当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。

四、类的主动使用例题

package com.jvm.classloader;
class Father2{
    public static String strFather="HelloJVM_Father";
    static{
        System.out.println("Father静态代码块");
    }
}

class Son2 extends Father2{
    public static String strSon="HelloJVM_Son";
    static{
        System.out.println("Son静态代码块");
    }
}

public class InitativeUseTest2 {
    public static void main(String[] args) {
       System.out.println(Son2.strSon);
    }
}

运行结果:
        Father静态代码块
        Son静态代码块
        HelloJVM_Son

Son2.strSon是调用了Son类自己的静态方法属于主动使用,所以会初始化Son类,又由于继承关系,类继承原则是初始化一个子类,会先去初始化其父类,所以会先去初始化父类。

package com.jvm.classloader;
class YeYe{
    static {
        System.out.println("YeYe静态代码块");
    }
}
class Father extends YeYe{
    public static String strFather="HelloJVM_Father";
    static{
        System.out.println("Father静态代码块");
    }
}
class Son extends Father{
    public static String strSon="HelloJVM_Son";
    static{
        System.out.println("Son静态代码块");
    }
}
public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather); 
    }
}

运行结果:
	YeYe静态代码块
    Father静态代码块
    HelloJVM_Father

要注意子类Son类没有被初始化,也就是Son的静态代码块没有执行!

Son.strFather是子类Son访问父类Father的静态变量strFather,之前提过对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),所以对于静态字段strFather,直接定义这个字段的类是父类Father,所以在执行 System.out.println(Son.strFather); 这句代码的时候会去初始化Father类而不是子类Son

package com.jvm.classloader;
class YeYe{
    static {
        System.out.println("YeYe静态代码块");
    }
}
class Father extends YeYe{
    public final static String strFather="HelloJVM_Father";
    static{
        System.out.println("Father静态代码块");
    }
}
class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son静态代码块");
    }
}
public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather);
    }
}

运行结果:HelloJVM_Father

Son.strFather所对应的变量便是final static修饰的,在准备的时候就已经分配内存且初始化为"HelloJVM_Father"了,因此并不会初始化任何类(除了main),仅仅执行System.out.println(Son.strFather);。这是主动使用的第二点的例外实例,但是final不是重点,重点是编译器把结果放入常量池,如下面这题

package com.jvm.classloader;
import sun.applet.Main;
import java.util.Random;
import java.util.UUID;
class Test{
    static {
        System.out.println("static 静态代码块");
    }
//  public static final String str= UUID.randomUUID().toString();
    public static final double str=Math.random();  //编译期不确定
}
public class FinalUUidTest {
    public static void main(String[] args) {
        System.out.println(Test.str);
    }
}

运行结果:
static 静态代码块
0.7338688977344875

因为当一个常量的值并非编译期可以确定的,那么这个值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,所以这个类会被初始化。因此final不是重点。

package com.jvm.classloader;
public class ClassAndObjectInitialize {
        public static void main(String[] args) {
            System.out.println("输出的打印语句");
        }
        public ClassAndObjectInitialize(){
            System.out.println("构造方法");
            System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);
        }
        {
            System.out.println("普通代码块/初始化块");
        }
        int ZhiShang = 250;
        static int QingShang = 666;        
        static
        {
            System.out.println("静态初始化块");
        }     
}

运行结果:
静态初始化块
输出的打印语句

先看 十、初始化块与执行顺序

实际上Java代码编译成字节码之后,最开始是没有构造方法的概念的,只有类初始化方法对象初始化方法

类初始化方法:编译器会按照其出现顺序,收集:类变量(static变量)的赋值语句、静态初始化块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行

对象初始化方法:编译器会按照其出现顺序,收集:成员变量的赋值语句、普通代码块(初始化块),最后收集构造函数的代码,最终组成对象初始化方法,值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在实例化类对象的时候执行

这题因为没有对类ClassAndObjectLnitialize 进行实例化!只是单纯的写了一个输出语句,因此没有执行对象初始化。

首次主动使用

Java程序对类的使用方式可分为两种:主动使用与被动使用。一般来说只有当对类的首次主动使用的时候才会导致类的初始化
也就是当第二次对类进行主动使用时,类的初始化并不会发生!

package com.jvm.classloader;

class Father{
    public static int a = 1;
    static {
        System.out.println("父类静态代码块");
    }
}
class Son{
    public static int b = 2;
    static {
        System.out.println("子类静态代码块");
    }
}
public class OverallTest {
    static {
        System.out.println("Main方法静态代码块");
    }
    public static void main(String[] args) {
        Father father;
        System.out.println("======");
        father=new Father();
        System.out.println("======");
        System.out.println(Father.a);
        System.out.println("======");
        System.out.println(Son.b);
    }
}

运行结果:
Main方法静态代码块
======
父类静态代码块
======
1
======
子类静态代码块
2

① 类的主动使用的第六点:Main方法的类会首先被初始化,首先执行main方法静态代码块
Father father只是声明了一个引用不会执行什么。
③ 类的主动使用的第一点:new实例变量,父类Father被初始化,执行类初始化方法。
④ 类的主动使用的第三点:调用类的静态方法。但是父类不会再次初始化一次println(Father.a)输出1
⑤ 同④,先初始化Son,然后输出2。

参考链接
java类的加载以及ClassLoader源码分析

发布了54 篇原创文章 · 获赞 3 · 访问量 3633

猜你喜欢

转载自blog.csdn.net/magic_jiayu/article/details/104258188