首页 > 其他 > 详细

test

时间:2021-04-25 10:14:36      阅读:18      评论:0      收藏:0      [点我收藏+]

类的加载机制

引言

我们写的代码会首先编译成.class文件,然后加载到虚拟机中后才能运行和使用,那么虚拟机是如何加载这些class文件的呢?会有哪些过程呢?今天我们来探究一下虚拟机的类加载机制。

生命周期

类被加载到虚拟机中,它的整个生命周期分为:加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备和解析部分统称为连接。

我们主要关注加载、连接和初始化部分。

加载

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

加载.class文件的方式

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件,如动态代理技术

验证

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

验证阶段分为4个动作:

  1. 文件格式验证

    验证字节流是否符合Class文件格式的规范,保证输入的字节流能够被正确的解析和存储于方法区之内。

  2. 元数据验证

    对字节码描述的信息进行语义分析,如检查这个类的父类是否继承了不允许被继承的类(被Final修饰的类),主要是对类的元数据信息进行语义校验。

  3. 字节码验证

    通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,主要是对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出伤害虚拟机安全的事件。

  4. 符号引用验证

    确保解析动作能正常执行,主要对类自身以外的信息(常量池中的各种符号引用)进行校验

验证阶段不是必要的,如果代码已经被反复使用和验证过,可考虑使用-Xverify:none参数关闭验证,缩短类加载的时间

准备

这一阶段主要是为类的静态变量分配内存和设置默认值,如int类型默认值为0,引用类型默认值为null等

解析

这一阶段主要是将常量池中的符号引用替换为直接引用,如将字符串全类名(com.xxx.xxx)替换为内存地址

初始化

这一阶段就是为类的静态变量显式地赋予其初始值,即程序员设定的值

类的初始化时机

Java程序对类的使用方式可分为两种:

  • 主动使用
  • 被动使用

虚拟机规范中要求每个类或接口被Java程序首次主动使用时需要进行初始化

主动使用

  • 创建类的实例
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射
  • 初始化一个类的子类
  • Java虚拟机启动时被标明为启动类的类
  • JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化

被动使用

  • 除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化

下面举几个例子来理解一下:

主动使用例子:

public class Test {
    public static void main(String[] args) {
        System.out.println(Child.str2);
    }
}

class Parent {
    public static String str = "hello world";
  
    static {
        System.out.println("parent static block");
    }
}


class Child extends Parent {
    public static String str2 = "hello beauty";
   
    static {
        System.out.println("child static block");
    }
}

//parent static block
//child static block
//hello beauty

主动使用了子类,在初始化子类之前会先初始化父类,再初始化子类

被动使用例子1:

// 对于静态字段来说,只有直接定义了该字段的类才会被初始化
public class Test {
    public static void main(String[] args) {
        System.out.println(Child.str);
    }
}

class Parent {
    public static String str = "hello world";
  
    static {
        System.out.println("parent static block");
    }
}

class Child extends Parent {
    static {
        System.out.println("child static block");
    }
}

//parent static block
//hello world

通过子类引用父类的静态变量,并不会初始化子类,因为没有直接使用到了子类的静态变量。

被动使用例子2:

public class TestMain {

    public static void main(String[] args) {
        Parent[] parents = new Parent[1];
    }
}

class Parent {
    static {
        System.out.println("parent static block");
    }
}
//无输出

通过数组定义来引用类,并不会触发此类的初始化,对于数组来说,Java虚拟机会自动生成一个类来进行初始化,比如Parent数组会生成一个名为[Lcom.dxg.demo.Parent的类。

被动使用例子3:

//代码1
public class TestMain {

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

class Parent {
    public static final int i = 1;

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

//1

这个例子只输出了hello world,并不会输出静态块的内容。因为常量在编译阶段会存入调用这个常量所在方法的类的常量池中,本质上并没有直接引用到定义常量的类,所以不会触发该类的初始化。

但是要注意运行时常量并不是如此,如下代码:

//代码2
public class TestMain {
    public static void main(String[] args) {
        System.out.println(Child.j);
    }
}

class Parent {
    public static final int i = 1;

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

class Child extends Parent {
    public static final int j = new Random().nextInt();

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

//parent static block
//child static block
//1394803640

运行时常量在编译阶段是不知道其值的,只有在运行时才能获取到值,所以使用运行时常量会触发类的初始化

我们也可以看看javap -c反编译后的结果:

//代码1
public class com.guodx.studydemo.jvm.TestMain {
  public com.guodx.studydemo.jvm.TestMain();
    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: iconst_1
       4: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
       7: return
}


//代码2
public class com.guodx.studydemo.jvm.TestMain {
  public com.guodx.studydemo.jvm.TestMain();
    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: getstatic     #3                  // Field com/guodx/studydemo/jvm/Child.j:I
       6: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
       9: return
}

注意代码1main方法的行号3调用的是iconst_1,说明编译时就把常量放到常量池中了,再看代码2main方法行号3则是getstatic,说明主动使用了类,调用了类的静态字段,会初始化类。

特殊情况

当一个类或接口在初始化时,并不要求其父接口都完成了初始化,只有当真正使用到父接口的时候(如引用父接口中定义的常量时,才会初始化)

public class TestMain {
    public static void main(String[] args) {
        System.out.println(Child.i);
    }
}

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

}

class Child implements Parent {
    public static int i = 5;

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

//child static block
//5

类的初始化顺序

public class TestMain {

    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 final Singleton singleton = new Singleton();

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

    public static int counter2 = 0;

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

先来一段代码,思考一下其运行结果?理解了这段代码也就理解了类的初始化顺序了

....

揭晓答案

counter1=1
counter2=0

首先我们来回顾一下类的生命周期,加载、验证、准备、解析、初始化、使用和卸载七个阶段,在准备阶段会给类的静态变量赋予初始值,此时counter1=0singleton=nullcounter2=0,然后进入初始化阶段,该阶段给类的静态变量赋予正确的值,此时counter1=0singleton=new Singleton(),进入构造方法 counter1++; counter2++;,现在counter1=1counter2=1了,但是最后轮到counter2显式设置为0则此时counter2=0

前面我们知道了子类在初始化的时候会先初始化父类,再结合上面的类初始化顺序,我们可以得出类初始化时会先初始化其父类,再初始化子类,并且类中的静态变量是按顺序依次进行初始化的。

总结

这个小节我们学习了三大块内容:

  1. 类的生命周期分为:加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中加载、连接(验证、准备、解析)和初始化是我们重点关注的。
  2. 类的初始化时机分为主动使用和被动使用的情况,主动使用会触发类的初始化,而被动使用则不会,这块内容有大量的例子帮助理解,需要关注每个例子的情况。
  3. 类初始化时会先初始化其父类,再初始化子类,并且类中的静态变量是按顺序依次进行初始化的。

test

原文:https://www.cnblogs.com/dxGo/p/14698518.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!