目录
参考文章
Java 基础 - 反射机制详解 | Java 全栈知识体系
引言
反射就是把 Java 类的每一部分映射成一个个对象
方法栈区里面存的是数据结构 是虚的 通常是一些静态 static 修饰的常量
堆区里面存的是具体对象 包括实例对象和类对象
类是程序的一部分
每个类都有一个 class 对象
所有类都是在第一次使用的时候 动态加载到 JVM 中的
例如我们写了一个 student 类
程序启动时 类加载机制的第一步会将我们的 Class 对象加载到 JVM 中
然后 JVM 再根据这个类型信息相关的 Class 对象创建我们需要的实例对象或者提供静态变量的引用值
无论创建多少个实例对象,在JVM中都只有一个Class对象,即在内存中每个类有且只有一个相对应的Class对象。
类的生命周期
类加载的过程 :
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的方法句柄,若句柄所对应的类没有进行过初始化,则将它初始化;
Class 对象的获取方式 三种
通过实例 getClass()方法获取
Class clazz=test.getClass();
Class.forName 方法获取
Class clazz=Class.forName("com.hiway.Test");
类字面常量获取
Class clazz = Test.class;
这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。并且它根除了对forName()方法的调用,所以也更高效。 注意,有一点很有趣,当使用“.class”来创建对Class对象的引用时,不会自动地初始化该Class对象。
注意,有一点很有趣,当使用“.class”来创建对Class对象的引用时,不会自动地初始化该Class对象
class Initable {
static final int staticFinal = 47;
static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
static {
System.out.println("Initializing Initable");
}
}
class Initable2 {
static int staticNonFinal = 147;
static {
System.out.println("Initializing Initable2");
}
}
class Initable3 {
static int staticNonFinal = 74;
static {
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
public static java.util.Random rand = new java.util.Random(47);
public static void main(String[] args) throws Exception {
Class initable = Initable.class;
System.out.println("After creating Initable ref");
System.out.println(Initable.staticFinal);
System.out.println(Initable.staticFinal2);
System.out.println(Initable2.staticNonFinal);
Class initable3 = Class.forName("Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
/* output
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable ref
74
*/
细节
如果一个static final值是 编译器常量
,就像Initable.staticFinal那样,那么这个值不需要对Initable类进行初始化就可以被读取。但是,如果只是将一个域设置为static和final的,还不足以确保这种行为,例如,对Initable.staticFinal2的访问将强制进行类的初始化,因为它不是一个编译期常量。
如果一个static域不是final的,那么在对它访问时,总是要求在它被读取之前, 要先进行链接(为这个域分配存储空间)和初始化(初始化该存储空间)
,就像在对Initable2.staticNonFinal的访问中所看到的那样。从输出结果来看,可以发现,通过字面常量获取方式获取Initable类的Class对象并没有触发Initable类的初始化,这点也验证了前面的分析,同时发现调用Initable.staticFinal变量时也没有触发初始化,
这是因为staticFinal属于编译期静态常量,在编译阶段通过常量传播优化的方式将Initable类的常量staticFinal存储到了一个称为 NotInitialization
类的常量池中,在以后对Initable类常量staticFinal的引用实际都转化为对NotInitialization类对自身常量池的引用,所以在编译期后,对编译期常量的引用都将在NotInitialization类的常量池获取,这也就是引用编译期静态常量不会触发Initable类初始化的重要原因。
但在之后调用了Initable.staticFinal2变量后就触发了Initable类的初始化,注意staticFinal2虽然被static和final修饰,但其值在编译期并不能确定,因此staticFinal2并不是编译期常量,使用该变量必须先初始化Initable类。Initable2和Initable3类中都是静态成员变量并非编译期常量,引用都会触发初始化。至于forName方法获取Class对象,肯定会触发初始化,这点在前面已分析过。
总结
为了使用类而做的准备工作实际包含三个步骤:
- 加载,这是由类加载器执行的,该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非是必需的),并从这些字节码中创建一个Class对象。
- 链接。在链接阶段将验证类中的字节码,为静态域分布存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。
- 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。