【JVM】探究数组的本质
之前写过一篇深入理解数组的博文【Java核心技术卷】深入理解Java数组, 这篇文章主要从理论的角度, 探讨了Java的数组。
这篇文章主要从实战的角度去探究数组的本质。
在正文开始之前,我们有必要先关注一下类的加载机制:
在Java代码中,类型的加载
、连接
与初始化
过程都是在程序运行期间完成的
这里的类型指的是我们定义的class interface,枚举等等,这里不涉及到对象的概念,是一种runtime的阶段。这种加载机制提供了更大的灵活性,增加了更多的可能性。
简单地说类型的加载 最常见的就是把字节码文件从磁盘中加载到内存,连接就是将类与类之间的关系确定好,并且对字节码的一些处理,校验等 也就是在这一阶段完成了初始化 就是对类型中的静态字段赋值等等
具体流程如下:
类的加载、连接与初始化
- 加载:查找并加载类(class文件)的二进制数据
- 连接
·-验证:确保被加载的类的正确性(class文件的格式等)
·-准备:为类的静态变量分配内存,并将其初始化为默认值(准备阶段 还没有类的概念)
·-解析:把类中的符号引用转换为直接引用
- 初始化:为类的静态变量赋予正确的初始值
用图示表述为
Java程序对类的使用方式可分为两种
- 主动使用
- 被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
不严格划分,主动使用分为七种
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如Class.forName(“com.test.Test"))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类(包含main方法的类)
- 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"); } }
运行结果发现控制台没有输出任何东西
没有任何的输出 证实Test类并没有对TestValue类主动使用
我们已知的有两点:
- 静态代码块是在类加载时自动执行的,非静态代码块是在创建对象时自动执行的代码,不创建对象不执行该类的非静态代码块。
- 所有的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"); } }
打印结果:
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"); } }
运行直接报错
改变一下
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"); } }
是不是清晰多了??
二维数组与一维数组类似,我们测试一下二维数组
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()); } }
结果:
有很多人疑惑,为什么学习底层? 相信,这篇文章已经告诉你答案了,这也是为什么有的人写的代码,bug很少,遇见了也很快解决。有的人写的代码,bug不仅多,却要花了大量的时间去debug的原因了。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
【JVM】探究Java常量本质及三种常量池
可以从他人的博文,还有一些书籍中了解到 常量是放在常量池 中,细节的内容无从得知,相信每个人都会觉得面前的东西是一个几乎完全的黑盒,总是觉得不舒服,翻阅《深入理解Java虚拟机》,会发现这本书中对常量的介绍更多地偏重于字节码文件的结构,还有在自动内存管理机制中也介绍了运行时常量池。下面换种思路来看一下 Java中的常量池分为三种形态:静态常量池,字符串常量池以及运行时常量池。 ? 静态常量池 所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量: 类和接口的全限定名 字段名称和描述符 方法名称和描述符 而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是...
- 下一篇
【设计模式】好好聊一聊单例模式
? 对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。 如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供个访问该实例的方法。这就是单例模式的模式动机。 单例模式是一种对象创建型模式,又名单件模式或单态模式。单例模式( Singleton Pattern)的定义如下:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。 单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。 单例模式包含如下角色: Singleton:单例 ? 我们下面来看一下它的实现,因为单例模式是最重要的一种设计模式,这里要好好地说一下: 懒汉式写法: public clas...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL8.0.19开启GTID主从同步CentOS8
- Linux系统CentOS6、CentOS7手动修改IP地址
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合Redis,开启缓存,提高访问速度
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作