关于 Java 中 Runtime.class.getClass() 的细节分析

栏目: Java · 发布时间: 5年前

内容简介:* 在之前的《浅析Java序列化和反序列化》一文的Payload构造章节中出现了一大堆的我们先重写一下这个问题的代码:通过断点调试观察变量,

* 在之前的《浅析 Java 序列化和反序列化》一文的Payload构造章节中出现了一大堆的 ClassMethodObject ,让很多代码基础较弱的同学一脸懵逼。其中一个比较诡异的逻辑 Runtime.class.getClass() ,有朋友问它的结果为什么是 java.lang.Class 。对于这个问题,有Java语言基础的同学一般会回答『对象的类型本来就是 Class ,而 Class 也是对象,它的类型当然也是 Class 』,道理没错,但仔细想想,这还真是一个挺有意思的问题。

关于 Class 的名称

我们先重写一下这个问题的代码:

Class rt = Runtime.class;
Class clz = rt.getClass();

通过断点调试观察变量, rtclz 同样都是 Class 对象,但 rt 无论是打印输出还是调用 getTypeName() 得到的都是『java.lang.Runtime』,而 clz 则是『java.lang.Class』。

为什么不一样?难道 RuntimeClass 的子类?当然不是, Runtime 可是 Object 的亲儿子。

机智的你一定会跟进 Class 中看看它的 toString()getTypeName() 两个方法的代码逻辑,原来它们都是调用 getName() 返回由这个 Class 所表示的对象的名称。

关于 .class getClass()

由此可知, new Object().getClass() 得到的应该是名称为『java.lang.Object』的 Class ,记作 class java.lang.Object (以下类似) ,而 Runtime.class 拿到的 Class 作为 Object 的子类,调用 getClass() 得到的却是 class java.lang.Class

因此,我们需要对比一下这两种获取 Class 的方法的区别:

  • .class ,又称『类字面量』,只能作用于类的关键字,返回编译时确定的类型

    Object.class
  • getClass()Object 的实例方法,返回运行时确定的类型

    new Object().getClass()

在一般情况下,它俩的结果是可以相等的:

Object obj = new Object();
Object.class == obj.getClass();      // true
Object.class.equals(obj.getClass()); // true

但当存在多态时,后者的区别就体现出来了:

class gyyyy {}

Object obj = new Object();
Object gy = new gyyyy();
obj.getClass(); // class java.lang.Object
gy.getClass();  // class gyyyy

让我们回到最初的那个问题,答案已经呼之欲出了: Runtime.class 获取的是 class java.lang.Runtime ,而该 Class 调用 getClass() 时,运行时确定的类型为 Class 而非方法拥有者 Object ,所以得到的第二个 Classclass java.lang.Class

看到这,一定有同学开始骂我又在水文章了:裤子都脱了你就给我看这个?说来说去都是一堆废话,跟没说一样。

别急,我们继续。

JVM基础

既然上面的两种方法分别提到了编译时和运行时,不妨让我们站在JVM的角度再玩深一点。

先科普几个JVM相关的基础知识,让大家有个整体概念,其他的内容如果在后续分析过程中遇到了再穿插介绍。

Classfile

每个类 (包括内部类、匿名类、接口、注解、枚举和数组等) 经过编译后,都会单独生成一个.class文件,里面是一堆用于表示和描述该类的字节码,Java规范中管它叫Classfile。

Classfile中的核心内容如下:

  • 常量池 (Constant Pool)

  • 访问权限标识 (Access Flags)

  • (This Class)

  • 父类 (Super Class)

  • 接口集合 (Interfaces)

  • 字段集合 (Fields)

  • 方法集合 (Methods)

  • 属性集合 (Attributes)

其中,常量池里存放了该类编译前声明和编译中优化计算的所有值,包括原始类型和引用类型 (符号引用) ,类相关信息都以名称和描述为主,但不涉及任何具体的值或引用  (都依赖常量池索引) 。属性集合中则存放了类、字段和方法所可能需要的属性信息,如类源文件信息、方法代码段、方法代码段的本地变量表等。

运行时内存基本结构

  • 运行时数据区

    • 线程 (Threads)

    • (Frames)

    • 本地变量表 (Local Variables)

    • 操作数栈 (Operand Stacks)

    • 程序计数器 (Program Counter, PC)

    • JVM堆栈 (JVM Stack)

    • (Heap)

    • (Class)

    • 运行时常量池 (Run-Time Constant Pool)

    • 方法区 (Method Area)

    • 对象 (Objects)

    • 线程共享

    • 线程私有

其中,线程共享部分随JVM启动而创建,线程私有部分随线程创建而创建。Frame中存放的是方法数据而非Class数据,但一般来说,Object和方法的代码实现中都会存放它所属Class的引用。

需要注意的是,上面列出的Class和Object大致分别对应在Java代码中使用 classinterface 关键字声明的类和根据它们创建的类实例,而Java语言规范中所描述的 ClassObject 严格意义上来说都属于Class。

加载、链接和初始化

  • 加载,是指根据指定名称寻找并读取Classfile,将其转换成Class的过程

  • 链接,是指解析Class中的符号引用,并转换为运行时状态的过程

  • 初始化,是指执行Class的 <cinit> 方法的过程

在这个阶段中,可以为Class创建一个新的 java/lang/Class 的Object,在其中定义一个字段中存放当前Class的引用,并将这个Object的引用放入Class中作为其类对象  (非JVM规范,由实现方自行决定) ,而这个所谓的类对象,就是我们最开始通过 .classgetClass() 获取到的那个 Class 对象。

方法执行过程

由于篇幅原因,这里只简单介绍实例方法的执行过程:

  • 从常量池中取出方法引用,计算该方法参数个数

  • 从操作数栈弹出当前类对象引用和其他参数,组成参数列表

  • 为该方法创建新的 Frame ,将参数放入它的本地变量表中,将其压入JVM栈顶

  • 解析并执行该方法代码段的指令集

方法的执行结果并不会直接返回给调用方,而是由 return 系列的指令将当前操作数栈顶元素取出,压入JVM栈中调用方所属Frame的操作数栈中。

刨根问底

现在,我们将示例代码放入 main 函数中,这段代码经过编译后会变成以下指令:

ldc #2
astore_1
aload_1
invokevirtual #3
astore_2
...

#x 代表常量池索引值,可能会因为示例代码差异而不同。如果使用链式结构 Runtime.class.getClass() ,第2、3条指令会省略)

大致解释一下:

  • ldc 指令会从常量池中取索引为 2 的元素,此时取到的是名为 java/lang/Runtime 的类引用类型常量,根据JVM规范的描述,如果是类引用类型常量,需要获取它的类对象引用  (在前面加载、链接和初始化部分提到过的那个Object) ,再将其压入操作数栈  (对应 Runtime.class

  • astore_1 指令会弹出操作数栈顶元素,放入本地变量表的 1 位置  0 位置是 main 方法参数 args ,此时该位置的变量名为 rt (对应 Class rt =

  • aload_1 指令会从本地变量表中读取元素压入操作数栈  (对应 rt

  • invokevirtual 指令会从常量池中取索引为 3 的元素,此时取到的是名为 java/lang/Object.getClass 的方法引用类型常量,再弹出操作数栈顶获得之前 ldc 得到的类对象引用作为第一个参数,为该方法创建新的 Frame 并压入JVM堆栈,执行该方法的指令集, return 时将结果压入方法调用方的操作数栈  (对应 .getClass()

  • astore_2 指令会弹出栈顶元素,放入本地变量表的 2 位置,此时该位置的变量名为 clz (对应 Class clz =

由此,我们可以明确的知道变量 rt 存放的是 java/lang/Runtime 的类对象引用,变量 clz 存放的是 java/lang/Class 的类对象引用。由于类对象是在Class的链接过程中创建的,而在JVM中每个Class又是唯一的单例,因此同一个类以及它不同的实例获取到的类对象都是同一个。

结论不变。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

计算机程序设计艺术・卷3

计算机程序设计艺术・卷3

[美] 高德纳(Donald E. Knuth) / 贾洪峰 / 人民邮电出版社 / 2017-2 / 198.00元

《计算机程序设计艺术》系列被公认为计算机科学领域的权威之作,深入阐述了程序设计理论,对计算机领域的发展有着极为深远的影响。本书为该系列的第3卷,全面讲述了排序和查找算法。书中扩展了卷1中数据结构的处理方法,并对各种算法的效率进行了大量的分析。一起来看看 《计算机程序设计艺术・卷3》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

在线进制转换器
在线进制转换器

各进制数互转换器