Java核心 -- Class类

导论

在周志明的《深入理解Java虚拟机》书中的类加载机制章节上有提到,在“类加载”过程的加载阶段,虚拟机需要完成以下三件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

在《Java核心技术 卷一》关于反射的内容中提到:

    在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类,虚拟机利用运行时类型信息选择相应的方法执行。而保存这些信息的类被称为Class

Class类简介

在java世界里,一切皆对象。从某种意义上来说,java有两种对象:实例对象和Class对象。每个类的运行时的类型信息就是用Class对象表示的。它包含了与类有关的信息。其实我们的实例对象就通过Class对象来创建的。Java使用Class对象执行其RTTI(运行时类型识别,Run-Time Type Identification),多态是基于RTTI实现的。

  每一个类都有一个Class对象,每当编译一个新类就产生一个Class对象,基本类型 (boolean, byte, char, short, int, long, float, and double)有Class对象,数组有Class对象,就连关键字void也有Class对象(void.class)。Class对象对应着java.lang.Class类,如果说类是对象抽象和集合的话,那么Class类就是对类的抽象和集合。

  Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。一个类被加载到内存并供我们使用需要经历如下三个阶段:

  1. 加载,这是由类加载器(ClassLoader)执行的。通过一个类的全限定名来获取其定义的二进制字节流(Class字节码),将这个字节流所代表的静态存储结构转化为方法去的运行时数据接口,根据字节码在java堆中生成一个代表这个类的java.lang.Class对象
  2. 链接。在链接阶段将验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求,为静态域分配存储空间并设置类变量的初始值(默认的零值),并且如果必需的话,将常量池中的符号引用转化为直接引用。
  3. 初始化。到了此阶段,才真正开始执行类中定义的java程序代码。用于执行该类的静态初始器和静态初始块,如果该类有父类的话,则优先对其父类进行初始化。

所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。这一点与许多传统语言都不同。动态加载使能的行为,在诸如C++这样的静态加载语言中是很难或者根本不可能复制的。

  在类加载阶段,类加载器首先检查这个类的Class对象是否已经被加载。如果尚未加载,默认的类加载器就会根据类的全限定名查找.class文件。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良java代码。一旦某个类的Class对象被载入内存,我们就可以它来创建这个类的所有对象。
 

 

虚拟机为每个类创建唯一的Class对象

一旦类被加载了到了内存中,那么不论通过哪种方式获得该类的Class对象,它们返回的都是指向同一个java堆地址上的Class引用。jvm不会创建两个相同类型的Class对象。

其实对于任意一个Class对象,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个Class对象来源于同一个Class文件,只要加载它们的类加载器不同,那这两个Class对象就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。所以在java虚拟机中使用双亲委派模型来组织类加载器之间的关系,来保证Class对象的唯一性。
--------------------- 
作者:mcryeasy 
来源:CSDN 
原文:https://blog.csdn.net/mcryeasy/article/details/52344729 
版权声明:本文为博主原创文章,转载请附上博文链接!

JVM为每个加载过的类生成一个Class类的对象实例,比如Student类被加载后,JVM就生成一个和Stundent信息相对应的Class对象,不论是通过Student.class还是通过student.getClass()获取该对象,都是获取同一个Class对象实例的引用。也就是说,对于一个加载到虚拟机中的类,不论对其创建了多少个实例,内存中都有且只有一个与之相对应的Class对象。

public class Student {

    public static void main (String[] args) {

        Class c0 = Student.class;
        Student s1 = new Student();
        Student s2 = new Student();
        Class c1 = s1.getClass();
        Class c2 = s2.getClass();

        // Student的实例对象1:
        System.out.println(s1.hashCode());
        // Student的实例对象1:
        System.out.println(s2.hashCode());
        System.out.println();
        // Student类未被创建任何实例之前获取的Class对象(仅被加载到虚拟机):
        System.out.println(c0.hashCode());
        // Student的实例对象1获取的Class对象:
        System.out.println(c1.hashCode());
        // Student的实例对象2获取的Class对象:
        System.out.println(c2.hashCode());
    }
}

输出结果:
1163157884
1956725890

460141958
460141958
460141958


分析结果可知,同一个类的不同对象实例获取的对应的Class对象是同一个引用,而这个Class对象是该类被虚拟机加载时,就创建至内存中。

Class类不能被继承,只能由虚拟机创建实例

Class类也只是Java.lang包下的一个普通的类,下图是Class类源码的截图,从图中可以知道:

  • Class类是在java.lang包下的一个类(其父类也是 java.lang.Object);
  • Class类由final修饰,不允许被子类继承;
  • Class类的构造函数是private修饰符,意味着,Class类只能由虚拟机创建实例对象;

获取类的Class对象引用的三种方式

  • 通过调用类对象的getClass()方法,该方法是继承自Object类:getClass()是Object的方法,返回class对象;
  • 通过调用Class类的forName()类静态方法,方法入参必须有类的全限定类名:forName()是Class类的方法,返回class对象;
  • 通过类字面常量:类.class返回class对象;
// Person是一个抽象类,含有静态域
public abstract class Person {

    static {
        System.out.println("初始化。。。");
    }

}


// Student中含有main主函数,对比演示通过类字面量class和Class.forName()获取Class对象的不同
public class Student {

    public static void main (String[] args) {

        // 通过类字面量获取Class对象
        Class c1 = Person.class;
        System.out.println("类字面量演示:\n" + c1.hashCode() + "\n");

        // 通过Class.forName()类静态方法获取Class对象
        Class c2 = null;
        try {
             c2 = Class.forName("Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("Class.forName()静态方法演示:\n" + c2.hashCode());
    }
}

输出结果:
类字面量演示:
1163157884

初始化。。。
Class.forName()静态方法演示:
1163157884

分析结果:通过类字面量获取Class对象不会触发类初始化,而Class.forName()会在类加载的初始化阶段完成后才会被调用

JVM在运行Java程序时,不是一次性加载所有的类,而是按需加载,JVM的类加载过程可以分为如下7个阶段:

其中在加载阶段(Loading),在本文的导论部分有介绍,加载阶段会为类创建相应的Class对象;而初始化阶段是属于类加载的最后阶段,会对类的静态变量进行初始化。所以对比上述实验结果,可以得知通过类字面量class获取类的Class对象,会触发类加载完成Loading阶段,但是不会触发Initialization阶段,因为实验中没有打印“初始化。。。”,而通过Class.forName()方法则会触发Initialization阶段的执行。并且由于Class.forName()方法在虚拟机的编译期无法检测其传递的类全限定名字符串对应的类是否存在,只能在程序运行时进行检查,所以方法在使用时需要捕获一个名称为ClassNotFoundException的异常。

将上述实验中对Person类的Class对象获取,修改成对Student类(该类中含有main入口函数)的Class对象的获取,并且在Student类中也增添静态域,实验如下:

public class Student {

    static {
        System.out.println("初始化。。。");
    }
    
    public static void main (String[] args) {

        // 通过类字面量获取Class对象
        Class c1 = Student.class;
        System.out.println("类字面量演示:\n" + c1.hashCode() + "\n");

        // 通过Class.forName()类静态方法获取Class对象
        Class c2 = null;
        try {
             c2 = Class.forName("Student");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("Class.forName()静态方法演示:\n" + c2.hashCode());
    }
}


输出结果:
初始化。。。
类字面量演示:
460141958

Class.forName()静态方法演示:
460141958

分析结果:main方法在执行函数体之前,静态域就已经被执行,也就是说在Student.class执行之前,Student就已经完成了类加载的初始化阶段。

为什么对Person类和Student类分别进行试验,会有不一样的现象?答案是,当Java虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类,由于Student类中包含有main方法,所以即便main方法中没有任何关于Student类的代码,Student类都已经完成了类加载的初始化阶段。相反的,Person不是程序的主类,不会被事先触发类加载的过程。

因此,对比获取类的Class对象的三种方式,通过getClass()和forName()方法的方式都需要类被初始化,而类字面量的方式则更简单且安全,因为它在编译时就会接受检查(因此不需要置于try语句块中),不会自动初始化该类,更加有趣的是字面常量的获取Class对象引用方式不仅可以应用于普通的类,也可以应用用接口,数组以及基本数据类型,这点在反射技术应用传递参数时很有帮助。

什么情况下需要开始类加载过程的第一个阶段:加载?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

对于这5种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。

猜你喜欢

转载自blog.csdn.net/WalleIT/article/details/87533988