深入理解JVM(12)——类加载(2)——类的主动使用和被动使用

本文深入探讨了Java中类的主动使用和被动使用的概念,详细阐述了主动使用包括创建类实例、访问静态变量、调用静态方法等7种情况,并通过代码示例解释了每种情况的触发条件和效果。此外,还讨论了Java虚拟机的生命周期,数组创建的本质以及类加载的准备和初始化阶段的重要性。

目录

1.Java对类的主动使用和被动使用

1.1 主动使用(七种)

(1)上述主动使用规则的典型举例

(2)主动使用与final

(3)初始化时类与接口的区别

1.2 被动使用

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

3.数组创建本质分析

4.类加载的准备阶段和初始化阶段的重要意义


1.Java对类的主动使用和被动使用

所有Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化

1.1 主动使用(七种)

  • 1.通过new关键字创建类的实例
  • 2.访问某个类或接口的静态变量(字节码助记符getstatic),或对该静态变量赋值(字节码助记符putstatic)
  • 3.调用类的静态方法(字节码助记符invokestatic)
    • 其实第3种和第二种本质上可以划分为同一种情况,因为它们反映到字节码助记符的层面上大体上是类似的含义
    • 注意:
      • 只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用
  • 4.反射(如Class.forName("com.test.Test")),通过反射的方式获得类的对象,也是对类的主动使用
  • 5.初始化一个类的子类
  • class Parent{}
    
    class Child extend Parent{}
    
    
    //当我去初始化Child类的时候,也会对Parent进行初始化
  • 6.Java虚拟机启动的时候被标记为启动类的类,即包含Main方法的类
  • 7.JDK1.7开始提供动态语言支持(如我们可以在JVM上使用脚本语言的引擎调用JavaScript(动态语言)代码):
  •     java.lang.invoke.MethodHandle实例的解析结果REF_getStastic、REF_putStastic、REF_invokeStastic句柄对应的类没有初始化,则进行初始化

(1)上述主动使用规则的典型举例

代码演示1:通过new关键字创建类的实例

public class Demo01 {
    public static void main(String[] args) {
        MyParent04 myParent04 =new MyParent04();
        System.out.println("==============");
        MyParent04 myParent04_1 = new MyParent04();
    }
}

class MyParent04{

    static {
        System.out.println("MyParent03 static code");
    }
}

运行结果:

分析1:

  • 类被首次主动使用时(上述代码为new出对象实例),进行初始化,可以看到第二次主动使用该类时,并未再次进行初始化

代码演示2:访问父类的静态变量

public class Demo01 {
    public static void main(String[] args) {
        System.out.println(MyChild.str1);
    }
}

class MyParent
{
    public static String str1="Hello,World!";

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

class MyChild extends MyParent
{
    static{
        System.out.println("MyChild static block");
    }
}

运行结果:

分析2:

  • 对于静态字段来说,只有直接定义了该字段的类才会被初始化
  • 上述静态字段str1,直接定义它的类为MyParent,所以在访问str1的时候,MyParent被主动使用,MyParent会被初始化,虽然使用了MyChild这个名字,但是没有主动使用MyChild中的静态方法或变量,所有Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化,所以MyChild类不会被初始化,所以,最终只会调用MyParent中的静态代码块,而不会调用MyChild中的静态代码块

此处疑问:MyChild类没有被初始化,但有没有被加载呢?

我们可以使用JVM的参数-XX:+TraceClassLoading参数用于追踪类的加载信息并打印

   

我的查看结果:

  • 可以发现MyChild虽然没有被初始化,但被加载了

其他JVM参数:

//JVM的一些选项默认开启,一些默认关闭

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

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

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

代码演示3:访问子类的静态变量

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

class MyParent
{
    public static String str1="Hello,World!";

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

class MyChild extends MyParent
{
    public static String str2="Welcome";

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

运行结果:

分析3:

  • 对子类的静态字段str2的调用是对MyChild类的主动使用,所以子类MyChild会被初始化,由于当初始化一个子类时,要求父类全部都被初始化,所以父类MyParent也会被初始化,所以最终结果先执行了父类静态代码块,后执行了子类静态代码块

代码演示4:访问包含Main的类

package JVMDemo01;

class MyParent
{
    public static String str1="Hello,World!";

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

class MyChild extends MyParent
{
    public static String str2="Welcome";

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

public class Demo01 {

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

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

分析4:

  • 访问包含Main方法的类,即主动使用Demo01,导致了Demo01先进行了初始化
  • 然后对子类的静态字段str2的调用是对MyChild类的主动使用,所以子类MyChild会被初始化,由于当初始化一个子类时,要求父类全部都被初始化,所以父类MyParent也会被初始化,所以最终结果先执行了父类静态代码块,后执行了子类静态代码块

代码演示5:

package JVMDemo01;

class MyParent
{
    public static String str1="Hello,World!";

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

class MyChild extends MyParent
{
    public static String str2="Welcome";

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

public class Demo01 {

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

    public static void main(String[] args) {
        MyParent parent;

        System.out.println("----------");

        parent = new MyParent();

        System.out.println("----------");

        System.out.println(MyParent.str1);

        System.out.println("----------");

        System.out.println(MyChild.str2);
    }
}

分析5:

  • 访问main方法,导致Demo01的初始化
  • 定义一个类MyParent的引用不是对类的主动使用,所以不会导致类的初始化
  • 使用new创建MyParent的实例导致MyParent类的初始化
  • 再次访问MyParent的静态变量,由于MyParent已经进行了初始化,即已经不是首次主动使用,便不用再进行初始化
  • 访问子类的静态变量,导致子类MyChild类的初始化,但子类先去检查父类是否被初始化,父类MyParent已经初始化,所以不用再次初始化

疑问:创建类的引用会不会导致类的加载呢?

package JVMDemo01;

class MyParent
{
    public static String str1="Hello,World!";

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

public class Demo01 {

    public static void main(String[] args) {

        MyParent parent;

    }
}

  • 可以看到未找到任何加载MyParent类的信息,即只定义一个引用,该类都不会被加载

代码演示6:访问父类的静态方法

package JVMDemo01;

class MyParent
{
    public static String str1="Hello,World!";

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

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

class MyChild extends MyParent {

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

public class Demo01 {

    public static void main(String[] args) {
        MyChild.doSomething();
        System.out.println("------------");
        System.out.println(MyChild.str1);
    }
}

分析:

  • 反编译可以看到使用了invokestatic调用了静态方法,即使用子类访问父类中定义的静态方法,是对父类的主动使用,所以导致父类被初始化
  • 当再次访问父类的静态变量,由于父类已经初始化,所以不会再次导致父类初始化

代码演示7:使用反射获取类的对象

package JVMDemo01;

class CL{
    static {
        System.out.println("Class CL");
    }
}

public class Demo01 {

    public static void main(String[] args) throws ClassNotFoundException {

        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Class<?> clazz = loader.loadClass("JVMDemo01.CL");
        System.out.println(clazz);

        System.out.println("-----------------------");

        clazz = Class.forName("JVMDemo01.CL");
        System.out.println(clazz);
    }
}

分析7:

  • 调用ClassLoader类的loadClass方法并不是对类的主动使用,所以不会导致类CL的初始化
  • 使用反射获取类的对象是对类的主动使用,所以对类CL进行了初始化

(2)主动使用与final

代码演示1:

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

class MyParent
{
    public static final String str="Hello,World!";

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

运行结果:

分析1:

  • 常量如果在编译期间是确定的话,在编译阶段会存入到调用这个常量的方法所在的类的方法的常量池中,本质上调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化,所以MyParent的静态代码块没有被执行
  • 这里即将常量存放到了调用常量str的方法所在的类Demo1的常量池中,之后Demo1与MyParent就没有任何关系了,甚至我们可以将MyParent的class文件删除

--------》--------》

反编译:

助记符:

ldc       表示将int,float或String类型的常量从常量池推送至栈顶
bipush   表示将单字节[-128,127]的常量从常量池推送至栈顶
sipush   表示将单字节[-32768,32767]的常量从常量池推送至栈顶
iconst_1 表示将int类型的1推送至栈顶(iconst_1~iconst_5、iconst_0、iconst_m1(-1))
  • 从反编译的结果可以看到,并没有任何访问MyParent中静态变量str的语句,仅仅是从常量池中获得数据,所以不会导致类的初始化

按两下shift打开搜索对应的助记符,可以发现,它们的实现也是由相关的类去实现的,并且这些实现都在rt.jar包中

代码演示2:

import java.util.UUID;

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

class MyParent03{
    public static final String str = UUID.randomUUID().toString();

    static {
        System.out.println("MyParent03 static code");
    }
}

运行结果:

反编译:

分析2:

  • 当一个常量的值并非编译期间可以确定的(如str),那么其值就不会在编译期间放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,导致类初始化,从上述反编译的结果我们也可以看到,底层是调用了putstatic对静态变量赋值,所以会导致类的初始化

代码演示3:

package JVMDemo01;

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

interface MyParent05{
    //默认就是public static final
    int a = 5;
}

interface MyChild5 extends MyParent05{
    int b = 6;
}

删除MyChild5.class和MyParent05.class

分析3:

  • 对于接口,属性默认是,所以对于编译期常量会放到常量池中,所以即使删除class文件上述程序也运行正常

代码演示4:

package JVMDemo01;

import java.util.Random;

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

interface MyParent05{
    //默认就是public static final
    int a = new Random().nextInt(5);
}

interface MyChild5 extends MyParent05{
    int b = 6;
}

  • 可以发现找不到任何加载接口MyParent05和MyChild5的信息,即这两个接口并没有被加载

删除MyParent05.class

删除MyChild5.class和MyParent05.class

package JVMDemo01;

import java.util.Random;

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

class MyParent05{
    public static final int a = new Random().nextInt(5);
}

class MyChild5 extends MyParent05{
    public static final int b = 5;
}

  • 同理,找不到任何加载接口MyParent05和MyChild5的信息,即这两个类并没有被加载

分析4:

由于接口中的变量默认是,所以在编译期就会把常量b的值5放入到调用者Demo01类的常量池中,而在运行时访问的是常量池中的常量的话,本质上调用类(或接口)并没有直接引用到定义常量的类(或接口),甚至不会加载该类(接口),因此并不会触发定义常量的类(接口)以及它的父类(接口)的初始化

(3)初始化时类与接口的区别

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

  • 在初始化一个类时,并不会先初始化它所实现的接口
  • 在初始化一个接口时,并不会先初始化它的父接口

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

补充:代码块{}

package JVMDemo01;

public class Test {
    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
    }
}

class A{

    public A(){
        System.out.println("A");
    }

    {
        System.out.println("Hello");
    }

}

  • 可以看到当我们在定义一个类的时候,如果定义一个代码块{},会在每次创建对象的时候,先执行该代码块中的内容
  • 它与静态代码块的区别时,静态代码块只会在第一次创建对象的时候,执行静态代码块中的内容
package JVMDemo01;

public class Test {
    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
    }
}

class A{

    public A(){
        System.out.println("A");
    }

    static {
        System.out.println("Hello");
    }

}

代码示例1:

package JVMDemo01;


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

interface MyParent5{
    //默认就是public static final
    Thread thread = new Thread(){
        {
            System.out.println("MyParent5 invoked");
        }
    };
}

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

分析1:

  • 如果接口MyParent5中的thread被初始化的话,它就必然会创建Thread对象,执行代码块中的打印语句,但从结果可以发现它并没有打印"MyParent5 invoked",所以证明父类接口MyParent5并没有被初始化,即证实在初始化一个类时,并不会先初始化它所实现的接口
  • 但同时可以看到MyParent5和创建的Thread匿名类MyParent5$1都被进行了加载(注意区分加载和初始化)

为了进一步证实类与接口在初始化时候的区别,我们修改上述接口为类:

package JVMDemo01;

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

class MyParent5{

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

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

可以发现代码块中的内容进行了打印,即初始化一个子类的时候,会先初始化它的父类

代码示例2:

package JVMDemo01;

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

interface MyParent5{

    Thread thread01 = new Thread(){
        {
            System.out.println("MyParent5 invoked");
        }
    };
}

interface MyChild5 extends MyParent5{
    Thread thread02 = new Thread(){
        {
            System.out.println("MyChild5 invoked");
        }
    };
}

分析2:

  • 可以发现父接口MyParent5并没有被初始化,即证实在初始化一个接口时,并不会先初始化它的父接口
  • 但同样接口MyParent5会被加载

1.2 被动使用

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

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

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

如下情况,Java虚拟机本身是一个进程,在如下情况下,Java虚拟机将结束生命周期:

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

3.数组创建本质分析

public class Demo01 {
    public static void main(String[] args) {
        MyParent05[] myParent05 = new MyParent05[1];
        System.out.println(myParent05.getClass());
        System.out.println(myParent05.getClass().getSuperclass());

        System.out.println("================");
        MyParent05[][] myParent05s1 = new MyParent05[1][1];
        System.out.println(myParent05s1);
        System.out.println(myParent05s1.getClass().getSuperclass());

        System.out.println("================");
        int[] ints = new int[1];
        System.out.println(ints.getClass());
        System.out.println(ints.getClass().getSuperclass());

        boolean[] booleans = new boolean[1];
        System.out.println(booleans.getClass());

        char[] chars = new char[1];
        System.out.println(chars.getClass());

        short[] shorts = new short[1];
        System.out.println(shorts.getClass());


    }
}

class MyParent05{
    static {
        System.out.println("MyParent05 static code");
    }
}

  • 首先可以看到,创建一个数组的实例的时候不会初始化类MyParent05,因为创建数组实例不属于上述主动使用类的7种方式中任何一种
  • 对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为class [LJVMDemo01.MyParent05;
  • 这种形式,动态生成的类型,其父类型就是Object
  • 对于数组来说,JavaDoc经常将构成数组的元素为Component,实际上就是将数组降低一个维度后的类型

反编译:

助记符:

  • anewarray:表示创建一个引用类型的(如类、接口、数组)数组,并将其引用值压入栈顶
  • newarray:表示创建一个指定的原始类型(如int、float、char等)的数组,并将其引用值压入栈顶

4.类加载的准备阶段和初始化阶段的重要意义

代码演示1:

package JVMDemo01;

public class Demo01 {
    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++;

        System.out.println("构造方法中counter1 = "+counter1);
        System.out.println("构造方法中counter2 = "+counter2);
    }

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

分析:

在执行时,

先进入准备阶段,按照定义顺序给类中的变量赋予默认值

  • counter1=0
  • counter2=0
  • singleton=null

然后由于主动使用类的静态方法进入初始化阶段,按照定义顺序为静态变量赋值:

  • counter2=0
  • 调用构造方法
    • counter1=1
    • counter2=1
  • 继续向下执行,没有静态变量需要被初始化

所以最终counter1=1,counter2=1

代码演示2:

package JVMDemo01;

public class Demo01 {
    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();

    public static int counter2 = 0;

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

        System.out.println("构造方法中counter1 = "+counter1);
        System.out.println("构造方法中counter2 = "+counter2);
    }

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

分析:

在执行时,

先进入准备阶段,按照定义顺序给类中的变量赋予默认值

  • counter1=0
  • singleton=null
  • counter2=0

然后由于主动使用类的静态方法进入初始化阶段,按照定义顺序为静态变量赋值:

  • 调用构造方法
    • counter1=1
    • counter2=1
  • counter2=0
  • 继续向下执行,没有静态变量需要被初始化

所以最终counter1=1,counter2=0

可以看到如果没有准备阶段,先初始化调用构造方法时,counter1就会没有值,这也就是准备阶段先赋予默认值的意义——保证静态变量能被正常使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值