【JVM学习笔记】类加载过程

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

JVM启动过程包括:加载、连接、初始化

  • 加载:就是将class文件加载到内存。详细的说是,将class文件加载到运行时数据区的方法区内(JDK7是方法区,JDK8对应的是Metaspace),然后创建一个java.lang.Class对象,用来封装类在方法区类的数据结构。JVM规范并未说明Class对象位于何处,Hotspot虚拟机将其放在了方法区。JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误),如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
    • 有两种类型的类加载器:
    • 1.JVM自带提供的三类加载器:
      • 根类加载器Bootstrap Classloader(C++写的, 程序员无法在JAVA代码中获得该类)
      • 扩展加载器Extension Classloader,使用Java代码实现
      • 系统加载器System ClassLoader,也叫应用加载器 Application Classloader,使用Java代码实现
    •  2.用户自定义的类加载器,都是java.lang.ClassLoader的子类
  • 连接:连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去,分为三步  
    • 1.验证:检查被加载的类的正确性,具体来说包括但不限于以下操作:
      • 类文件的结构检查
      • 语义检查
      • 字节码验证
      • 二进制兼容性的验证
    • 2.准备:虚拟机为类的静态变量分配内存,并设置默认的初始值。比如一个类中有一句private static int num=1; 实际上在这一步的时候,只是将num初始化为默认值0
    • 3.解析:把类中的符号引用转换为直接引用。这个也好理解,毕竟所有类都加载好,才能真正进行直接引用,但是类的加载也有现后顺序之分,所以如果先加载的类引用了后加载的类,只有等到后者完成加载,才能真正引用到内存中的地址
  • 初始化:这一步将静态变量最终赋值,比如上面举例钟,num变量将被赋值为1。这一步骤中,有以下要点知识:1.JVM规定,"任何JVM实现,必须在每个类或接口被JAVA程序"首次主动使用时,才初始化它们"。2.静态变量的声明语句,以击静态代码块都被看做类的初始化语句;Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行他们,所以初始化语句和静态块的顺序可能会影响程序结果,这一点在本文后面的例子中有所体现(详见本页"初始化的顺序问题")。类的初始化步骤如下:
    • 假如这个类还没有被加载和连接,那就先进行加载和连接
    • 假如类存在直接父类,并且父类还没有被初始化,那就先初始化直接父类
    • 加入类中存在初始化语句,那就依次执行这些初始化语句

何为类的主动使用/被动使用,类是否可以被卸载

  • 类被加载后,是可以进行卸载的,比如OSGI就这样做了,类卸载相关知识见  https://www.cnblogs.com/heben/p/11438503.html  "类的卸载”
  • Java程序对类的使用分为两种:主动使用,被动使用。所有的Java虚拟机实现,必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
    • 什么是主动使用呢,有7钟情况(但不是非常准确的划分)
      • 创建类的实例,比如new一个对象
      • 访问某个类或者接口的静态变量,或对该静态变量赋值,在虚拟机字节码层面是使用的getstatic/putstatic助记符来操作的
      • 调用类的静态方法,在虚拟机字节码层面是使用invokestatic注记符来操作的
      • 反射的方式获取到一个类的Class对象,如Class.forName("com.test.Test");
      • 初始化一个类的子类
      • Java虚拟机启动时,被标名为启动类的类,即包含了main方法的类
      • JDK7开始提供了动态语言支持,如果java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化
    • 什么是被动使用呢,除了上述主动使用的情况外,其他对类的使用方式都视作被动使用,都不会导致对类的初始化,比如:
      • 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
      • 声明一个类的引用对象,并不是对类的主动使用,例如:Parent parent; 就不是对Parent类的主动使用,不会导致类的初始化

加载.class文件的方式

  • 从本地文件系统中加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将java源文件动态地编译为.class文件,动态代理里面会出现,因为编译阶段这个类不存在,运行期才生成。又如JSP会被转换成Servlet,被编译成一个class文件

对于静态字段来说,只有直接定义了该字段的类才会被初始化,下面代码中,对MyParent1是主动使用,对MyChild1是被动使用

关于static块的执行顺序,可参考之前的笔记 https://www.cnblogs.com/heben/p/5391964.html

/**
 * @author wx
 * @Description 对于静态字段来说,只有直接定义了该字段的类才会被初始化
 * @date 2019/08/30 1:38
 */
public class ClassUsesTest {
    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 {
    static {
        System.out.println("MyChild1 static block");
    }
}
对于静态字段来说,只有直接定义了该字段的类才会被初始化,以上代码输出结果为
MyParent1 static block
hello world

如果在MyChild1中也定义一个静态变量 public static String str = "hell"; 则输出将变为

MyParent1 static block
MyChild1 static block
heel

这是因为初始化一个类的子类,首先要初始化其父类

6.疑问:在第5点的示例代码中,只有父类得到了初始化,子类没有初始化,那子类有没有被加载呢,关于这一点,虚拟机并没有进行明确规定,但是可以用参数-XX:+TraceClassLoading,用于追踪类的加载信息并打印出来

从运行结果可以看出,子类是被加载了的

顺带一提JVM参数的规律,JVM的参数都是-XX:开头的,其中

-XX:+<option>表示开启option

-XX:- <option>表示关闭option

-XX:<option>=<value>表示将option选项的值设置为value

option指的就是例如TraceClassLoading这样的参数

7.final关键字带来的影响

public class MyTest2 {
    public static void main(String[] args) {
        System.out.println(MyParent2.str);
    }
}

class MyParent2 {
    public static final String str = "hello world";

    static {
        System.out.println("MyParent2 static block");
    }
}

与之前代码的区别在于变量带上了final关键字,输出结果为

hello world

这是因为,str变量作为一个常量,在编译阶段,这个常量就会被存入到调用这个常量的那个方法的类所在的常量池中,也就是"hello world"这个常量,会被放置到 MyTest2 这个类的常量池中。

本质上,调用类并没有直接引用到定义常量的那个类,因此并不会促发对那个类的初始化,并且此后MyTest2 类和MyParent2类就没有关系了,甚至可以将MyParent2的字节码文件删除

通过javap -c MyTest2.class可以看到相关信息

D:workspace-learncommon-learnlearn-jvm	argetclassescomlearnjvmloader>javap -c MyTest2.class
Compiled from "MyTest2.java"
public class com.learn.jvm.loader.MyTest2 {
  public com.learn.jvm.loader.MyTest2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String hello world
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

助记符ldc,表示将int, float, 或是String类型的常量值从常量池推送至栈顶

如果将上面的代码稍作改变,将str变量修改为short类型的数字,则反编译的结果,里ldc助记符将变为bipush

助记符bipush,表示将一个单字节(-128 ~ 127)的常量值推送至栈顶

另外还有:

助记符sipush,表示将一个短整型常量值(-32768 ~ 32767)推送至栈顶

助记符iconst_1,表示将一个int类型的1推送至栈顶,另外jvm可能认为-1到5这7个数字很常见,所以有iconst_m1,iconst_0,iconst_1,iconst_2,一直到iconst_5这7个专门的助记符表示int类型的-1到5

助记符是由相关的类来操作的,比如com.sun.org.apache.bcel.internal.generic.ICONST类

8.如果final变量是运行期才能确定的值

package com.learn.jvm.loader;


import java.util.UUID;

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 block");
    }
}

输出

MyParent3 static block
5bd87094-5c54-4888-a053-387083ca1a28

 由于str的值是运行期才能确定,所以这个常量在编译期无法被直接存入MyTest3的常量池,因此这个例子中,MyParent3类得到了初始化

9.对象数组的情况

public class MyTest4 {

    public static void main(String[] args) {
        System.out.println("自定义类型的数组情况");
        MyParent[] array = new MyParent[5];
        System.out.println(array.getClass());
        System.out.println(array.getClass().getSuperclass());


        System.out.println("基本类型的数组情况");
        int[] nums = new int[5];
        System.out.println(nums.getClass());
        System.out.println(nums.getClass().getSuperclass());
    }
}

class MyParent {
    public MyParent() {
        System.out.println("MyParent constructor");
    }
    static {
        System.out.println("MyParent static block");
    }
}

运行结果

自定义类型的数组情况
class [Lcom.learn.jvm.loader.MyParent;
class java.lang.Object
基本类型的数组情况
class [I
class java.lang.Object

代码没有输出静态块的内容,这是由于MyParent类没有得到初始化,没有初始化的原因是没有MyParent类的实例生成。这里生成的实例是MyParent[]类型的实例,这个实例的类型就是[Lcom.learn.jvm.loader.MyParent,这是虚拟机运行期间生成的类型

 ,打头一个左方括号表示一维数组,二维数组则是两个左方括号

对于这个例子的总结:对于数组来说,其类型是由JVM在运行时创建的,表示为[Lcom.xxx]这种形式,其父类型为Object,对于数组来说,JavaDoc经常将构成数组的元素成为Component,实际上就是将数组降低一个维度后的类型

反编译结果如下:

D:workspace-learncommon-learnlearn-jvm	argetclassescomlearnjvmloader>javap -c MyTest4.class
Compiled from "MyTest4.java"
public class com.learn.jvm.loader.MyTest4 {
  public com.learn.jvm.loader.MyTest4();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String 自定义类型的数组情况
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: iconst_5
       9: anewarray     #5                  // class com/learn/jvm/loader/MyParent
      12: astore_1
      13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: aload_1
      17: invokevirtual #6                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      20: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      23: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      26: aload_1
      27: invokevirtual #6                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      30: invokevirtual #8                  // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
      33: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      36: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      39: ldc           #9                  // String 基本类型的数组情况
      41: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      44: iconst_5
      45: newarray       int
      47: astore_2
      48: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      51: aload_2
      52: invokevirtual #6                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      55: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      58: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      61: aload_2
      62: invokevirtual #6                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      65: invokevirtual #8                  // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
      68: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      71: return
}

助记符anewarray,表示创建一个引用类型的数组,并将其引用的值压入栈顶

助记符newarray,表示创建一个指定的原始类型(如int,float,char等)的数组,并将其引用值压入栈顶

当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口

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

 以下代码佐证了上面的第一种说法

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

interface MyParent5 {
    // 定义一个一个静态内部类,并实例化它,并赋值给thread变量
    // 如果MyParent5接口被初始化,即thread被初始化,就一定会打印出hello world
    public static Thread thread = new Thread() {
        {
            System.out.println("hello world");
        }
    };
}

class MyChild5 implements MyParent5 {
    public static int b = 5;
}

运行结果,只打印出了5

11.初始化的顺序问题

public class MyTest6 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("counter1: " + Singleton.counter1);
        System.out.println("counter2: " + Singleton.counter2);
    }
}

class Singleton {
    public static int counter1;
    public static int counter2 = 0;
    
    private static Singleton singleton = new Singleton();

    private Singleton() {
        counter1++;
        counter2++;
    }

    public static Singleton getInstance() {
        return singleton;
    }
}

运行结果:

counter1: 1
counter2: 1

但如果稍微改变一下counter2申明和初始化的位置:

public class MyTest6 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("counter1: " + Singleton.counter1);
        System.out.println("counter2: " + Singleton.counter2);
    }
}

class Singleton {
    public static int counter1;

    private static Singleton singleton = new Singleton();

    private Singleton() {
        counter1++;
        counter2++;
    }

    public static int counter2 = 0;

    public static Singleton getInstance() {
        return singleton;
    }
}

运行结果如下

counter1: 1
counter2: 0

仅仅调整了代码的位置,结果就发生了变化

这是因为在对Singleton类的初始化过程中,是按照变量声明的顺序进行初始化的,首先是初始化counter1,然后接着初始化single对象,导致构造函数被调用,然后才是初始化counter2变量。从这里也可以看出“连接”步骤中,“准备“阶段,也就是赋初值的意义所在,如果没有这一步,显然counter2++就是没有意义的语句

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

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

  • 执行了System.exit方法
  • 程序正常执行结束
  • 程序运行中发生错误/异常导致异常终止
  • 操作系统出现错误导致jvm进程终止

一张总结了类和对象有关加载,连接,初始化的知识的图

原文地址:https://www.cnblogs.com/heben/p/11432232.html