Skip to content

Latest commit

 

History

History
152 lines (105 loc) · 8.54 KB

about-runtime-class-getclass.md

File metadata and controls

152 lines (105 loc) · 8.54 KB

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

Category Research Language Timestamp Progress

* 在之前的《浅析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所表示的对象的名称。

关于.classgetClass()

由此可知,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)

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

运行时内存基本结构

  • 运行时数据区
    • 线程共享
      • (Heap)
        • 方法区 (Method Area)
          • (Class)
            • 运行时常量池 (Run-Time Constant Pool)
        • 对象 (Objects)
    • 线程私有
      • 线程 (Threads)
        • 程序计数器 (Program Counter, PC)
        • JVM堆栈 (JVM Stack)
          • (Frames)
            • 本地变量表 (Local Variables)
            • 操作数栈 (Operand Stacks)

其中,线程共享部分随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又是唯一的单例,因此同一个类以及它不同的实例获取到的类对象都是同一个。

结论不变。