RTTI,Class类以及类加载过程【学习心得】


部分内容借鉴自《深入理解Java虚拟机》,《Java编程思想》

RTTI

RTTI(Run-Time Type Identification):运行时类型识别,在Java主要用于在程序运行过程中确定对象的类型和类的信息,主要由Class类来完成。

为什么要使用RTTI

在Java存在继承机制,继承机制的存在大大的提升了变成效率,但是也带来了一个问题,对象的确切所属类型问题。现有类继承关系如下:

我们创建一个子类对象:

Animal myDog = new Dog();

对于myDog对象,它在编译期间的类型是Animal,但是在运行时是Dog类型的。当然在这里我们清楚的知道myDog是属于Dog类型,我们可以使用强制类型转换,使myDog成为Dog类型,从而使用Dog类的相关特性,但是如果我们并不知道myDog的明确类型,却想要使用Dog类型的特性,这时我们就要使用RTTI来帮助我们明确myDog明确指向的类型了。

RTTI的三种形式
  • 普通的类型转换
  • instanceof
  • 通过查询Class对象获得运行时所需的信息

1.Class类

介绍:Class引用就是用来表示它所指对象的确切类型。Class类的构造函数由private符修饰,因此我们不能直接创建一个Class对象,只能由JVM(类加载器)创建。
比如说当我们创建一个Animal对象时,JVM会先创建对应Animal的Class对象,用来保存Animal类的类型信息和对象信息,之后JVM加载Class对象,并通过Class对象来创建出Animal类型的对象,需要注意的是,不论在内存中Animal类型对象有多少个,在JVM中只存在一个对应的Class对象

Tips: 这里JVM是使用Class对象创建的对象,因此可以说明构造函数是不依赖于对象的函数,即构造函数是静态的

Class的构造函数

private Class(ClassLoader loader) {
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
    }
创建Class对象的三种方式
Class clazz = Animal.getClass();
Class clazz = Class.forName("Animal");   // 参数是需要创建的对象的全限定类名 
Class clazz = Animal.class; // 通过Object对象的静态常量 -- class 获得,这种方式获得Class对象时,不会初始化Animal类

//Class.forName() 源码:其中forName0()为本地方法
/*
 * 其中true:表示将加载的类进行实例化,也就是说对其进行初始化[即可简单的理解为执行其构造函数]
 * caller:获得调用者的class对象。
 * ClassLoader.getClassLoader(caller):获得加载该class对象的类加载器,Java中类加载器遵循双亲委
 * 派模型,所以该加载器在收到加载请求之后,首先会传给他的“父类”加载器(这里类加载器之间并不是继承
 * 关系,而是通过复用来达到父子类关系),如果父类无法加载,则在由子类加载器处理。
 */
@CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

Tips: 对于上述的第二种创建方式,如果输入的类名不存在,则会抛出ClassNotFoundException,所以在使用第二种方式时,应添加对应的异常声明。

对于forName()方法和ClassLoader的一些想法:

对于forName()方法,它实现了类加载的过程,但是ClassLoader只是完成了通过全限定类名来获得指定的二进制字节流的任务,对于forName()方法,他还会进行连接过程,如果参数为true,他还会进行初始化,从而完成一个类加载的完整过程。

Class类的一些常见方法

getName():获得所指类的全限定类名
getSimpleName():获得所指类的简单类名
getMethods():获得所之类的方法列表
getFields():获得所之类的成员列表
getConstructors():获得所指类的构造函数

Tips: 这里省略了参数列表和返回值类型,大家可以直接去Java API说明文档中查看。

2.类加载过程

类的生命值周期

在这里插入图片描述

对于前五个阶段,它们便是JVM中类加载的全过程,它们的开始时间都是顺序的按部就班的开始,但是并不是顺序的完成,通常是在一个阶段没有结束之前另一个阶段就开始了。

加载阶段

加载阶段需要完成的三个任务:

  • 通过一个全限定名来获得指定的二进制字节流
  • 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表着该类的Class对象,对于HotSpot虚拟机而言,Class对象虽然是对象,但是存储在方法区中!
验证阶段
  • 文件格式验证

验证class文件是否符合文件格式的规范,详情参考 .class文件的具体格式

  • 元数据验证

对字节码文件描述的信息进行语义检查,保证其描述的信息符合Java语言规范的要求,即语法检查等等

  • 字节码验证

通过对数据流和控制流分析,确定逻辑和语义是合法的
这个过程及其复杂,所以开发人员为了避免消耗过多的时间在字节码验证上,在方法体的code属性中增加了 StackMapTable 属性,这个属性描述了方法体中所有基本块开始时本地变量表和操作栈应有的状态,因此字节码验证期间只需要检查该属性中的记录是否合法即可。
当然HotSpot虚拟机提供了-XX:UseSplitVerifier 选项来关闭这项优化,或者使用
-XX:+FailOverToOldVerifier 要求在类型检验失败的时候退回到旧的类型推导方式进行检验,但在JDK 1.7 对于主版本号大于50的Class文件,只能使用StackMapTable方式进行字节码验证。

  • 符号引用验证

这个阶段发生在JVM将符号引用转化为直接引用的时候,这个转化动作将在连接阶段的第三阶段----解析阶段发生。这个验证阶段主要是对类自身以外的信息进行匹配性检验。主要包括以下内容:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类
  • 在指定类中是否可以寻找到符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符号引用中的信息的访问性检验(public,private,protected,default)
准备阶段

准备阶段是正式为变量分配内存以及设置类变量初始值的阶段,但是这里的两个概念容易产生混淆

  • 分配内存:这个阶段只为被static修饰的变量(不包括实例变量)进行内存分配(分配在方法区中),实例变量的内存分配是在实例化的时候完成的,并且分配在Java堆中。
  • 设置变量初始值:这里是给变量赋予零值。
public static int value = 123;

例如在这里,value被设置为0而不是123。
但如果value是编译期常量

public static final int value = 123;

则在准备阶段value的值就是123
因此我们可以联想到,如果我们访问一个类的编译期常量,是否可以在不进行对该类进行初始化的条件下访问到我们需要访问的常量呢?答案是可以的:

class person {
    public static final int value = 123;
    public static int arValue = 123456;
    static {
        System.out.println("person loaded!");
    }
}
public class test {
    public static void main(String[] args) {
        System.out.println(person.value);
        System.out.println(person.arValue);
    }
}
Result:
123
person loaded!
123456

可以看到我们在第一次调用value值的时候,并没有运行static代码块中的内容,因此该类并没有进行初始化,在我们接下来访问arValue时,该类进行了初始化。这是因为在编译期间value值通过常量传播优化将value存储到了Test类的常量池中存储,因此访问value时,并没有person类的引用符号入口,所以不需要对person进行初始化。事实上,在JVM中规定了在五种情况下必须对类进行初始化,在之后的初始化阶段中我会写明。

解析阶段

解析阶段是将常量池内的符号引用替换为直接引用的过程

  • 符号引用:用一组符号来描述引用目标,符号可以是任何形式的字面量,只要使用时可以无歧义的定位到目标即可,符号引用的字面量形式是规定的,与使用的虚拟机的内部布局无关
  • 直接引用:直接引用可以是直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄,它与使用的虚拟机的内部布局有关
初始化阶段

在这个阶段会根据程序员的主观计划去初始化变量和其他资源。即执行类构造器<clinit>的过程

<clinit>类构造器

  • <clinit>方法是收集所有类变量(被static修饰的成员变量)的赋值操作以及被static修饰的代码块中的语句产生的,收集的顺序由定义的顺序决定,静态语句块可以使用定义在它之前的变量,定义在其之后的变量只能赋值不能访问
  • <clinit>方法与类的构造函数有区别,前者不需要显式的调用父类的构造函数,JVM会保证父类的<clinit>函数比子类限制性,因此在JVM中第一个被执行方法的类坑定是java.lang.Object。
  • 因为父类的函数先执行,所以父类的static语句块优于子类
  • <clinit>方法对于接口和类来说不是必要的,如果没有static语句块和变量赋值操作,则可以不生成<clinit>方法。
  • 执行接口的<clinit>方法时,不需要先执行父接口的<clinit>方法。
  • JVM保证<clinit>方法会在多线程环境下被正确的加锁,同步,可以被理解成临界资源。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值