【JVM】探究数组的本质

之前写过一篇深入理解数组的博文【Java核心技术卷】深入理解Java数组, 这篇文章主要从理论的角度, 探讨了Java的数组。

这篇文章主要从实战的角度去探究数组的本质。


在正文开始之前,我们有必要先关注一下类的加载机制:

在Java代码中,类型加载连接初始化过程都是在程序运行期间完成的

这里的类型指的是我们定义的class interface,枚举等等,这里不涉及到对象的概念,是一种runtime的阶段。这种加载机制提供了更大的灵活性,增加了更多的可能性。

简单地说类型的加载 最常见的就是把字节码文件从磁盘中加载到内存,连接就是将类与类之间的关系确定好,并且对字节码的一些处理,校验等 也就是在这一阶段完成了初始化 就是对类型中的静态字段赋值等等

具体流程如下:

类的加载、连接与初始化

  1. 加载:查找并加载类(class文件)的二进制数据
  2. 连接
    ·-验证:确保被加载的类的正确性(class文件的格式等)

·-准备:为类的静态变量分配内存,并将其初始化为默认值(准备阶段 还没有类的概念)
·-解析:把类中的符号引用转换为直接引用

  1. 初始化:为类的静态变量赋予正确的初始值

用图示表述为
【JVM】探究数组的本质


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

  1. 主动使用
  2. 被动使用

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

不严格划分,主动使用分为七种

  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(如Class.forName(“com.test.Test"))
  5. 初始化一个类的子类
  6. Java虚拟机启动时被标明为启动类的类(包含main方法的类)
  7. JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化

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


下面我们试图探究数组的本质

先看一个例子:

public class Test {
    public static void main(String[] args) {
       TestValue[] testValues = new TestValue[10];
    }
}

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

运行结果发现控制台没有输出任何东西
【JVM】探究数组的本质
没有任何的输出 证实Test类并没有对TestValue类主动使用

我们已知的有两点:

  1. 静态代码块是在类加载时自动执行的,非静态代码块是在创建对象时自动执行的代码,不创建对象不执行该类的非静态代码块。
  2. 所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。

你可能会有疑问了 我们这里都new出来一个TestValue[ ] 的实例啊。我们看看它的类型

package com.leetcodePractise.tstudy;

public class Test {
    public static void main(String[] args) {
       TestValue[] testValues = new TestValue[10];
        System.out.println(testValues.getClass());
    }
}

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

打印结果:
【JVM】探究数组的本质
TestValue是我们生成的数组从属的类型,其实这是Java虚拟机帮助我们在运行期声明出来的,但是我们却没有在代码中显式声明出来这种类型。

以下面的测试为例:

public class Test {
    public static void main(String[] args) {
        TestValue[] testValues = new TestValue[10];
    }
}

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

反编译Test.class

Compiled from "Test.java"
public class com.leetcodePractise.tstudy.Test {
  public com.leetcodePractise.tstudy.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: bipush        10
       2: anewarray     #2                  // class com/leetcodePractise/tstudy/TestValue
       5: astore_1
       6: return
}

aload_0 将第一个引用类型本地变量推送至栈顶

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

astore_1表示将栈顶引用型数值存入第二个本地变量

重点关注anewarray ,这里仅仅是创建一个引用型的数组。这个数组的类型在运行期确定为TestValue类型,这是一个引用型的数据,并没有将TestValue类进行加载,所以不会有static静态代码块的执行

不相信的话,看下面的结果

public class Test {
    public static void main(String[] args) {
        TestValue[] testValues = new TestValue[10];
        testValues[0].dosomething();
    }
}

class TestValue{
    static {
        System.out.println("TestValue static code");
    }
    public void dosomething(){
        System.out.println("haha");
    }
}

运行直接报错
【JVM】探究数组的本质
改变一下

public class Test {
    public static void main(String[] args) {
        TestValue[] testValues = new TestValue[10];
        testValues[0] = new TestValue();
        testValues[0].dosomething();
    }
}

class TestValue{
    static {
        System.out.println("TestValue static code");
    }
    public void dosomething(){
        System.out.println("haha");
    }
}

【JVM】探究数组的本质

是不是清晰多了??

二维数组与一维数组类似,我们测试一下二维数组

TestValue[][] testValues2 = new  TestValue[10][10];
System.out.println(testValues2.getClass());

打印结果是:
class [[Lcom.leetcodePractise.tstudy.TestValue;
这里有两个左中括号以示二维数组

探究数组对象的父类

 //进一步探究数组对象的父类型
        System.out.println(testValues.getClass().getSuperclass());
        System.out.println(testValues2.getClass().getSuperclass());

打印结果均为class java.lang.Object

对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为[Lcom leetcodePractise tstudy. Testvalue这种形式。

动态生成的类型,其父类型就是 Object

对于数组来说, JavaDoc经常将构成数组的元素为 Component,实际上就是将数组降低一个维度后的类型。

对下面这段代码编译成的字节码文件进行反编译

package com.leetcodePractise.tstudy;

public class Test {
    public static void main(String[] args) {
       TestValue[] testValues = new TestValue[10];
        System.out.println(testValues.getClass());

        TestValue[][] testValues2 = new  TestValue[10][10];
        System.out.println(testValues2.getClass());

    }
}

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

反编译结果

Compiled from "Test.java"
public class com.leetcodePractise.tstudy.Test {
  public com.leetcodePractise.tstudy.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: bipush        10
       2: anewarray     #2                  // class com/leetcodePractise/tstudy/TestValue
       5: astore_1
       6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: aload_1
      10: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      13: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      16: bipush        10
      18: bipush        10
      20: multianewarray #6,  2             // class "[[Lcom/leetcodePractise/tstudy/TestValue;"
      24: astore_2
      25: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      28: aload_2
      29: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      32: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      35: return
}

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

astore_1表示将栈顶引用型数值存入第二个本地变量

aload_1 将第二个引用类型本地变量推送至栈顶

multianewarray 创建指定类型和指定维度的多维数组(执行指令时,操作栈中必须包含各维度的长度值),并将其引用压入栈顶

astore_2表示将栈顶引用型数值存入第三个本地变量

aload_2 将第三个引用类型本地变量推送至栈顶

复盘一下吧:

数组对象的类型是在运行期确定下来的,这个过程并没有主动使用运行期确定下来的类,因此不会引起类的加载。如果要想通过索引使用对象,还需要new出类相应的实例。


数组的本质已经探讨过了,因为数组对象的类型是在运行期确定下来的,这也留下了一个包袱,就是数组协变

这里也提一下吧

下面就是演示:

class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class CovariantArrays {
  public static void main(String[] args) {
    Fruit[] fruit = new Apple[10];
    fruit[0] = new Apple(); // OK
    fruit[1] = new Jonathan(); // OK
    // Runtime type is Apple[], not Fruit[] or Orange[]:
    try {
      // Compiler allows you to add Fruit:
      //编译时通过,编译时Fruit[]数组可以装入Fruit及其子类
      //运行时Apple[]数组可以装入Apple及其子类,
      //运行时异常,运行时Apple[]数组不可以装入Fruit类
      fruit[0] = new Fruit(); // ArrayStoreException
    } catch(Exception e) { System.out.println(e); }
    try {
      // Compiler allows you to add Oranges:
      fruit[0] = new Orange(); // ArrayStoreException
    } catch(Exception e) { System.out.println(e); }
  }
} 

上面的注释非常详细

fruit[0] = new Fruit(); // ArrayStoreException

因为数组对象的类型是在运行期确定下来的,此时的fruit[0]的类型是Apple类型的。
结果new出来的是它的父类肯定会报错

为了严谨起见,我们测试一下

class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple(); // OK
        fruit[1] = new Jonathan(); // OK
        System.out.println(fruit[0].getClass());
        System.out.println(fruit[1].getClass());
    }
}

结果:
【JVM】探究数组的本质

有很多人疑惑,为什么学习底层? 相信,这篇文章已经告诉你答案了,这也是为什么有的人写的代码,bug很少,遇见了也很快解决。有的人写的代码,bug不仅多,却要花了大量的时间去debug的原因了。

上一篇:周鸿祎:大安全时代漏洞是国家战略资源


下一篇:怎样启动 停止 重启MySQL数据库服务器