深入理解Class对象
RRIT及Class对象的概念
RRIT(Run-Time Type Identification)运行时类型识别。在《Thinking in Java》一书第十四章中有提到,它的功能是在运行时识别对象的类型和类信息。有两种主要方式:“传统的”RTTI(它假定我们在编译时已经知道所有类型)和“反射”机制(它允许我们在运行时发现和使用类信息)。
类是程序的一部分,每个类都有一个类对象。换句话说,无论何时编写和编译新类,都会生成一个Class对象(更恰当地说,保存在相同名称的A.class文件中)。当第一次使用所有类时,它们都被动态地加载到JVM中。例如,我们编写了一个Test类并编译它来生成Test。班级。此时,Test类的Class对象保存在类文件中。当我们新建一个对象或引用一个静态成员变量时,Java虚拟机(JVM)中的类加载器子系统将相应的类对象加载到JVM中,然后JVM从这个类型的信息中创建我们需要的类对象,或者提供静态变量的参考值。应当注意,无论创建了多少实例对象,手动编写的每个类类类在JVM中都只有一个Class对象,也就是说,每个类在内存中都具有并且只有一个对应的Class对象。
1 Test t1 = new Test();
2 Test t2 = new Test();
3 Test t3 = new Test();
如上所示,实际上JVM内存中只存在一个Test的Class对象。
Class类,类类也是Java中存在的一个真实类。JDK的Lang软件包。类类的实例表示Java应用程序运行时的类枚举或接口和注释(每个Java类运行时被表示为JVM中的类对象),类对象可以通过类名来获得。类,类型。getClass(),Class.forName(“类名”)。数组还映射到一个类对象,该类对象由具有相同元素类型和维度的所有数组共享。基本类型布尔、字节、char、.、int、long、float、double和关键词void也表示为类对象。
1 ublic final class Class<T> implements java.io.Serializable,
2 GenericDeclaration,
3 Type,
4 AnnotatedElement {
5 private static final int ANNOTATION= 0x00002000;
6 private static final int ENUM = 0x00004000;
7 private static final int SYNTHETIC = 0x00001000;
8
9 private static native void registerNatives();
10 static {
11 registerNatives();
12 }
13
14 /*
15 * Private constructor. Only the Java Virtual Machine creates Class objects. //私有构造器,只有JVM才能调用创建Class对象
16 * This constructor is not used and prevents the default constructor being
17 * generated.
18 */
19 private Class(ClassLoader loader) {
20 // Initialize final field for classLoader. The initialization value of non-null
21 // prevents future JIT optimizations from assuming this final field is null.
22 classLoader = loader;
23 }
到这我们也就可以得出以下几点信息:
- Class类也是类的一种,与class关键字是不一样的。
- 手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且这个Class对象保存在同名.class的文件中(字节码文件)
- 每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。
- Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载
- Class类的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要(关于反射稍后分析)。
Class对象的加载及获取
Class对象的加载
正如我们前面提到的,类对象是由JVM加载的,所以什么时候加载呢?实际上,所有类在第一次使用时都动态地加载到JVM中。当程序创建对该类的第一个静态成员引用时,它加载使用的类(实际加载该类的字节码文件)。注意,使用新操作符创建类的新实例对象也被视为对类的静态成员(构造函数也是一个类)的引用。看来Java程序在开始运行之前没有完全加载到内存中,而且它们的所有部分都按需加载。因此,当使用这个类时,类加载器首先检查这个类的Class对象是否已经被加载(类的实例对象是根据Class对象中的类型信息创建的)。如果未加载,则默认的类加载Class对象将以相同的名称保存。编译后的类文件。当该类的字节码文件被加载时,它们必须接受相关的验证,以确保它们不被破坏,并且不包含坏的Java代码(这是Java的安全机制检测)。在没有问题之后,它们将被动态地加载到内存中,这相当于Cl。ass对象被加载到内存中(毕竟,类字节码文件保存Cl ass对象),并且还可以用于创建类的所有实例对象。
类加载的过程 :
1. 加载
在加载阶段,虚拟机需要完成3件事:
(1)通过一个类的全限定名(org/fenixsoft/clazz/TestClass)获取定义此类的二进制字节流(.class文件);
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
(3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口;
2. 验证
验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占了相当大的一部分。验证阶段大致上完成下面4个阶段的验证动作:
(1)文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理;
这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证,字节流才会进入内存的方法区进行储存,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。
(2)元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,保证不存在不符合Java语言规范的元数据信息;
(3)字节码验证
通过数据流和控制流分析,确定程序是语义是合法的、符合逻辑的,保证被校验的方法在运行时不会做出危害虚拟机安全的事件;
(4)符号引用验证
可以看作是对类自身以外(常量池中各种符号引用)的信息进行匹配性校验,确保解析动作能正常执行;
3. 准备
准备阶段是正式为类变量分配内存并设置类变量初始值阶段,这些变量所使用的内存都将在方法区中进行分配。这里进行内存分配仅仅是类变量(被static修饰的变量),而不包括实例变量,实例变量将在对象实例化时随着对象一起分配在Java堆中;
4. 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用进行;
5. 初始化
初始化阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。初始化是如何被触发的:
(1)遇到new、getstatic、putstatic或involestatic这4条指令时;
(2)使用 java.lang.reflect 包的方法对类进行反射调用的时候;
(3)初始化一个类时,如果父类还没被初始化,则先触发父类的初始化;
(4)虚拟机启动时,用户需要指定一个要执行的主类 (包含main()方法的那个类),虚拟机会先初始化这个主类;
(5)如果一个 java.lang.invoke.MethodHandle 实例最后解析的结果是 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,若句柄所对应的类没有进行过初始化,则将它初始化;
上文源自《深入理解java虚拟机》一书,大家可以去读一下,这本书基本上是java程序员学习必读之一了。在此就不深入展开了,因为这又是另一个JVM领域了。 以后如果写了该方面的文章,会贴到这里。
Class对象的获取
Class对象的获取主要有3种:
- 通过实例getClass()方法获取
- Class.forName方法获取
- 类字面常量获取
通过实例getClass()方法获取
1 Test t1 = new Test();
2 Class clazz=test.getClass();
3 System.out.println("clazz:"+clazz.getName());
getClass()是从顶级类Object继承而来的,它将返回表示该对象的实际类型的Class对象引用。
Class.forName方法获取
forName方法是Class类的静态成员方法,记住所有Class对象都源自这个Class类,因此Class类中定义的方法适用于所有Class对象。这里,通过forName方法,我们可以获得Test类的相应Class对象引用。注意,当调用forName方法时,您需要捕获一个名为ClassNotFoundException的异常,因为forName方法无法检测编译器中与其传递的字符串(是否有.class文件)对应的类的存在,并且只能在程序的运行时进行检查。如果不存在,将引发ClassNotFoundException异常。
使用forName方式会触发类的初始化,与之相比的是使用类字面常量获取
类字面常量获取
1 //字面常量的方式获取Class对象
2 Class clazz = Test.class;
这不仅更简单,而且更安全,因为它是在编译时检查的(因此不需要放在try语句块中)。而且它消除了对forName()方法的调用,因此也更有效。注意,当您使用“.“类”创建对Class对象的引用,Class对象不会自动初始化。注意,当您使用“.“类”创建对Classs对象的引用,Class对象不会自动初始化。使用该类的准备实际上包括三个步骤:
- 加载,这是由类加载器执行的,该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非是必需的),并从这些字节码中创建一个Class对象。
- 链接。在链接阶段将验证类中的字节码,为静态域分布存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。
- 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
1 class Initable{
2 static final int staticFinal = 47;
3 static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
4 static {
5 System.out.ptintln("Initializing Initable");
6 }
7 }
8
9 class Initable2 {
10 static int staticNonFinal = 147;
11 static {
12 System.out.println("Initializing Initable2");
13 }
14 }
15
16 class Initable3 {
17 static int staticNonFinal = 74;
18 static {
19 System.out.println("Initializing Initable3");
20 }
21 }
22
23 public class ClassInitialization {
24 public static Random rand = new Random(47);
25 public static void main(String[] args) throws Exception {
26 Class initable = Initable.class;
27 System.out.println("After creating Initable ref");
28 System.out.println(Initable.staticFinal);
29 System.out.println(Initable.staticFinal2);
30 System.out.println(Initable2.staticNonFinal);
31 Clas initable3 = Class.forName("Initable3");
32 System.out.println("After creating Initable3 ref");
33 System.out.println(Initable3.staticNonFinal);
34 }
35 }
36
37 /* output
38 After creating Initable ref
39 47
40 Initializing Initable
41 258
42 Initializing Initable2
43 147
44 Initializing Initable3
45 After creating Initable ref
46 74
感谢各位的观看啦!
希望可以动动小手点赞一下呀!
你们的点赞转发就是我更文的动力!