重温Java基础(三)之Java虚拟机类加载机制探究:生命周期、初始化、使用与验证

本文深度解析Java虚拟机中类的生命周期,包括加载、验证、初始化等阶段。强调主动和被动使用触发条件,总结类加载器类型和获取方法。

本文内容参考:深入探究Java虚拟机类加载过程

一、java虚拟机与程序的生命周期

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

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

二、类的加载,链接,初始化

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

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

2.2 连接:

将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。然后要经过一系列的验证。

2.2.1 验证:确保被加载的类的正确性(验证字节码)
  • 类文件的结构检查:确保类文件遵从java类文件的固定格式。
  • 语义检查:确保类本身符合java语言的语法规定,比如验证final类型的类没有子类,以及final类型的方法没有被覆盖。(虽然编译时就可以发现错误,但不经过编译,手动生成class文件,那么就会发现不了final类型的方法被覆盖,但是语义检查就可以发现)
  • 字节码验证:确保字节码流可以被java虚拟机安全的执行。字节码流代表java方法(报空静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码后跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。
  • 二进制兼容性的验证:确保相互引用的类之间的协调一致,例如在Wroker类的gotoWork()方法中会调用Car类的run()方法。java虚拟机在验证work()类时,会检查在方法区内是否存在Car类的run()方法,假如不存在(当worker类和Car类的版本不兼容,就会出现这种问题),就会抛出NoSuchMethodError方法。
csharp 复制代码
public class Wroker{
    public void gotoWork(){
       Car car = new Car(); 
       car.run();//这段代码在worker类的二进制数据中表示为符号引用
    }
}
2.2 准备:为类的静态变量分配内存,并将其初始化为默认

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

arduino 复制代码
public class Sample{
    private static int a=1;
    public static long b;
    static{
        b=2;
    }
}
2.3 解析:把类中的符号引用转换为直接引用

在解析阶段,java虚拟机会把类的二进制数据中的符号引用替换为直接引用。例如在Worker类的gotoWork()方法中会引用Car类的run()方法。

在Worker类中的二进制数据中,包含了一个对Car类的run()方法的符号引用,它由run()方法的全名和相关描述符组成。在解析阶段,Java虚拟机会把这个符号替换为一个指针,该指针指向Car类的run()方法在方法区内的内存位置。这个指针就是直接引用。

csharp 复制代码
public class Wroker{
    public void gotoWork(){
       Car car = new Car(); 
         car.run();//这段代码在worker类的二进制数据中表示为符号引用
    }
}

2.3 初始化:为类的静态成员变量赋予正确的初始值

在初始化阶段,java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量初始化有两种途径:

  • 在静态变量的声明处进行初始化
  • 在静态代码快中进行初始化。例如在以下代码中,静态变量a和b都被显示初始化,而静态变量c没有被显示初始化,它将保持默认值为0;但是如果要使用c,则必须进行初始化。
arduino 复制代码
public class Sample{
    private static int a=1; //在静态变量声明出进行初始化
    public static long b;
    public static long c;  //但是如果要使用c,则必须进行初始化
    static{
        b=2; //在静态代码块中进行初始化
    }
}

示例:

csharp 复制代码
public class ClassLoaderTest {
    public static void main(String[] args) {
        Singleton singleton=Singleton.getInstance();
        System.out.println("counter1= "+singleton.counter1);
        System.out.println("counter2= "+singleton.counter2);
    }
}

/**

*程序是从上向下顺序执行
* new Singleton()时,counter1,counter2初始值均为0
* 在通过构造方法Singleton(),均加1.则返回的值counter1,counter2均为1
* 然后再程序在继续向下执行,由于counter1没有显示初始化,则值还是为1
* 但是counter2经过显示初始化后,其值为0
* @author coderacademy
*/
class Singleton{

    private static Singleton singleton=new Singleton();//new语句在这是结果为counter1= 1;counter2= 0
    public static int counter1;
    public static int counter2=0;
    //private static Singleton singleton=new Singleton();//new语句在这是结果为counter1= 1;counter2= 1
    private Singleton(){
        counter1++;
        counter2++;
    }
    public static Singleton getInstance(){
        return singleton;
    }
}
  • 静态变量的声明语句,以及静态代码块都被看做类的初始化语句,java虚拟机会按照初始化语句在类文件中的先后顺序来一次执行他们。
  • 类的初始化步骤
  • 假如这个类还没有被加载和连接,那就先进行加载和连接
  • 假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类。
  • 假如父类中存在初始化语句,那就依次执行这些初始化语句。
arduino 复制代码
public class FinalTest {
    public static void main(String[] args) {
    System.err.println(Test.X);
    }
}

/**
* 当X=6/3时,编译时即可算出X=2,即编译时常量,即不需要运行类,所以不打印静态代码块中的内容
*当X=new Random().nextInt(100)时,编译时不能算出X的值,只有运行程序才知道,所以打印结果为:FinalTest static final 2
* @author coderacademy
*/
class Test{
    public static final int X=6/3;//打印结果: 2
    //public static final int X=new Random().nextInt(100);//打印结果为FinalTest static final 2
    static{
        System.err.println("FinalTest static final");
    }
}
  • 类的初始化时机:当java虚拟机初始化一个类时,要求他的所有父类都已经被初始化,但是这条规则并不适用于接口。
  • 在初始化一个类时,并不先初始化它所实现的接口
  • 在初始化一个接口时,并不会先初始化他的父接口
    因此,一个父接口并不会因为他的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
arduino 复制代码
public class Test4 {
    static {
        System.err.println("Test4 static block");
    }

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

/**
* Test4 static block
* Parent static block
* Child static block
* 4
* @author coderacademy
*/
class Parent{
    public static final int a=3;
    static{
        System.err.println("Parent static block");
    }
}

    class Child extends Parent{
    public static int b=4;
    static{
        System.err.println("Child static block");
    }
}

如以下示例赋值的执行流程:

csharp 复制代码
public class test(){
    private static int a=3;
}

//首先在准备阶段java虚拟在内存中为a分配内存,int的初始值是0,所以此时a的值是0;在初始化阶段,给赋值为3
//相当于:

public class test(){
    private static int a;
    //从上到下执行
    static{
        a=3;
    }
}
image.png

image.png

三、java程序对类的使用方式可分为两种:

3.1 主动使用

  • 创建类的实例。比如:new Test()
  • 访问某个类或者接口的静态变量,或者对该静态变量赋值。比如:int b=Test.a
  • 调用类的静态方法。例如:Test.doSomething();
  • 反射(如class.forName("com.jvm.classloader.test"))
  • 初始化一个类的子类(对父类的主动使用)。例如
ini 复制代码
class Parent {
}

class Child extends Parent{
    public static int a=4;
}
Child.a=8;
  • java虚拟机启动时被表明为启动类的类

程序中对子类的"主动使用"会导致父类被初始化,但对父类的"主动使用"并不会导致子类初始化,不可能说生成一个Object类的对象就导致系统中所有的子类都会被初始化。

arduino 复制代码
public class Test5 {
    static{
        System.err.println("Test5 static block");
    }

    public static void main(String[] args) {
            Parent2 parent;
            System.err.println("-------------");
            parent=new Parent2();
            System.err.println(Parent2.a);
            System.err.println(Child2.b);
        }
}

/**

* Test5 static block
* -------------
* Parent2 static block
* 3
* Child2 static block
* 4
*
*/
class Parent2{
    public static final int a=3;
    static{
        System.err.println("Parent2 static block");
    }

}

class Child2 extends Parent2{
    public static int b=4;
    static{
        System.err.println("Child2 static block");
    }
}

只有当程序访问的静态变量或静态方法确实在当前接口定义时,才可以认为是对类或接口的主动使用。

typescript 复制代码
public class Test6 {
    public static void main(String[] args) {
        System.err.println(Child3.a);
        Child3.doSomething();
    }
}

/**
* Parent3 static block
* 3
* doSomething
* @author coderacademy
*/
class Parent3{
    static int a=3;
    static {
        System.err.println("Parent3 static block");
    }

    static void doSomething(){
        System.err.println("doSomething");
    }

}

class Child3 extends Parent3{
    static{
        System.err.println("Child3 static block");
    }
}

调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

arduino 复制代码
public class Test7 {
  public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader loader=ClassLoader.getSystemClassLoader();
        Class<?> clazz=loader.loadClass("com.jvm.classloader.Z");
        System.err.println("------------------------");
        clazz=Class.forName("com.jvm.classloader.Z");
  }
}

/**
* ------------------------
*Z static block
* @author coderacademy
*/
class Z{
  static{
      System.err.println("Z static block");
  }
}

3.2 被动使用

除去以上六种主动使用以外的使用都是被动使用,都不会导致类的初始化。所有的java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化他们。

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其方法存进运行时数据区的方法区内。然后在堆区创建一个Java.lang.Class对象,用来封装在类在方法区内的数据结构。

image.png

image.png

四、 加载class文件的方式

4.1 本地系统中直接加载

  • 通过网络下载.class文件(java.net.URLClassLoader(URL[] urls))
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将java源文件动态编译为.class文件。
    类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

4.2、两种类型的类加载器

4.2.1 Java虚拟机自带的加载器
  • 根类加载器(Bootstrap)。使用C++编写,程序员无法在java代码中获得该类。
  • 扩展类加载器(Extension),使用java代码实现
  • 系统类加载器(System),应用加载器,使用java代码实现
4.2.2 用户自定义的类加载器
  • java.lang.ClassLoader的子类
  • 用户可以定制类的加载方式
    public ClassLoader getClassLoader()方法。针对这个类返回一个个加载器,但是某些实现可能会返回null代表根类加载器。如果使用根类加载器加载类,那么这个方法就会返回null;例:
ini 复制代码
public class BootStrapTest {
    public static void main(String[] args) throws Exception {
        Class clazz=Class.forName("java.lang.String");
        ClassLoader loader=clazz.getClassLoader();
        /**
        * 打印结果为null
        */
        System.err.println(loader);
        Class clazz2=Class.forName("com.jvm.classloader.C");
        ClassLoader loader2=clazz2.getClassLoader();
        /**
        * 打印结果为:sun.misc.Launcher$AppClassLoader@54a5f709 应用加载器
         */
        System.err.println(loader2);
    }
}

class C{

}

参考

1、深入探究Java虚拟机类加载过程

相关推荐
亲爱的非洲野猪22 分钟前
Kafka消息积压的多维度解决方案:超越简单扩容的完整策略
java·分布式·中间件·kafka
wfsm24 分钟前
spring事件使用
java·后端·spring
微风粼粼42 分钟前
程序员在线接单
java·jvm·后端·python·eclipse·tomcat·dubbo
缘来是庄1 小时前
设计模式之中介者模式
java·设计模式·中介者模式
rebel1 小时前
若依框架整合 CXF 实现 WebService 改造流程(后端)
java·后端
代码的余温2 小时前
5种高效解决Maven依赖冲突的方法
java·maven
慕y2742 小时前
Java学习第十六部分——JUnit框架
java·开发语言·学习
paishishaba3 小时前
Maven
java·maven
张人玉3 小时前
C# 常量与变量
java·算法·c#
Java技术小馆3 小时前
GitDiagram如何让你的GitHub项目可视化
java·后端·面试