Java反射
Oracle 官方对反射的解释是:
Reflection is commonly used by programs which require the ability to examine or modify the runtime behavior of applications running in the Java virtual machine.This is a relatively advanced feature and should be used only by developers who have a strong grasp of the fundamentals of the language. With that caveat in mind, reflection is a powerful technique and can enable applications to perform operations which would otherwise be impossible.
Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
正射
存在说反射,那么相对应的也就会有“正射"
那么“正射"的概念是啥?
其实在我们编写代码的时候,每当我们需要用到某一个类的时候,首先肯定需要实例化这么类,然后用实例化好的对象进行操作,这个过程其实就是正射。
//正射
Animals animal = new Animals();
animal.shout("狗");
//狗叫
反射
反射就是我们一开始不知道要初始化的类对象是啥,所以也没办法直接将对象给new出来
//反射
Class<?> clazz = Class.forName("serialize.Animals");
System.out.println(clazz.toString());
Method shout = clazz.getMethod("shout", String.class);
System.out.println(shout.toString());
Constructor<?> constructor = clazz.getConstructor();
System.out.println(constructor.toString());
Object o = constructor.newInstance();
System.out.println(o.toString());
shout.invoke(o,"猫");
//class serialize.Animals
//public void serialize.Animals.shout(java.lang.String)
//public serialize.Animals()
//serialize.Animals@4554617c
//猫叫
从输出的流程大致可以看出反射的调用是,首先从serialize.Animals
获取到要操作的类对象,然后在获取需要执行的方法
对比
两种方式的执行效果的是一样的,当时在实现的过程还是有挺大区别的
- 正射代码在未运行前就已经知道了要运行的类是
Animals
; - 反射代码则是到整个程序运行的时候,从字符串
reflection.Animals
,才知道要操作的类是Animals
。
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
类的动态加载
类加载
类加载的时机
类加载,即虚拟机加载.class文件。什么时候虚拟机需要开始加载一个类呢?虚拟机对此没有规范约束,交给虚拟机把握。
Javac原理
javac是用于将源码文件.java编译成对应的字节码文件.class。
其步骤是:源码——>词法分析器组件(生成token流)——>语法分析器组件(语法树)——>语义分析器组件(注解语法树)——>代码生成器组件(字节码)。
类加载过程
先在方法区找class信息,有的话直接调用,没有的话则使用类加载器加载到方法区(静态成员放在静态区,非静态成功放在非静态区),静态代码块在类加载时自动执行代码,非静态的不执行;先父类后子类,先静态后非静态;静态方法和非静态方法都是被动调用,即不调用就不执行。
类加载的流程如下图:
-
加载
- 通过类的全名限定获取定义此类的二进制字节流。
- 将字节流代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存(方法区)生成一个代表这个类的class对象,作为方法区这个类的各种数据访问入口。
加载和连接是交叉进行的,加载未完成可能连接已经开始了。
-
验证
检查class是否符合要求,非必须阶段,对程序的运行期没有影响,-Xverif:none 关闭(可以提高启动速度)
-
文件格式验证(魔数、常量类型);
-
元数据验证(语义分析);
-
字节码验证(语义合法);
-
符号引用验证;
-
-
准备
正式为类变量(static成员变量)分配内存并设置类变量初始值(零值)的阶段,这个变量所使用的内存都将在方法区进行分配,这时候的内存分配仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起在堆中进行分配。
-
解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
-
初始化
初始化阶段是执行类构造器\
()方法的过程,虚拟机会保证一个类的类构造器\ ()在多线程环境中被正确的加锁,同步;如果多个线程同时初始化一个类,那么只会有一个线程区执行这个类的类构造器,其他线程阻塞等待,直到\ ()方法完毕,同一个类加载器,一个类只会被初始化一次。
代码分析
- 类加载的时候会执行代码
- 初始化:静态代码块
- 实例化:构造代码块、无参构造函数
构建一个Person类
package loadclassTest;
import java.io.Serializable;
public class Person implements Serializable {
public String name;
private int age;
public static int id;
static {
System.out.println("静态代码块");
}
public static void staticMethod(){
System.out.println("静态方法");
}
{
System.out.println("构造代码块");
}
public Person() {
System.out.println("无参构造");
}
public Person(String name, int age) {
System.out.println("有参构造");
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试
new Person(); //输出 静态代码块 构造代码块 无参构造
new Person("a", 18); //输出 静态代码块 构造代码块 有参构造
Person.staticMethod(); //输出 静态代码块 静态方法
Person.id = 1; //输出 静态代码块
可以看到无论是无参构造、有参构造、调用静态方法还是给静态变量赋值都会调用静态代码块
尝试直接调用class加载类Class c = Person.class;
,发现没有输出,没有进行初始化只进行了加载
动态加载
动态类加载方法:Class.forname
使用Class.forName
来动态加载类
Class<?> c = Class.forName("serialize.ClassLoadText.Person");
//输出 静态代码块
这里可以看出Class.forName,跟进下查看调用关系
这里我们可看到进入函数之后又调用了forName0
函数,找到forName0
函数
可以发现这是由native
修饰的,参数里有个initialize
,应该是判断是否初始化
native
:一个native方法就是一个Java调用非Java代码的接口。一个native方法是指该方法的实现由非Java语言实现,比如用C或C++实现。
然后在函数列表中还可以发现有一个传三个参数的forName
函数
传入类名,是否初始化布尔值以及加载类,当initialize
传入false时,没有输出
类加载分析
ClassLoader cl = ClassLoader.getSystemClassLoader();
System.out.println(cl);
//sun.misc.Launcher$AppClassLoader@18b4aac2
这里可以看到ClassLoader.getSystemClassLoader()
实际上获取的是AppClassLoader
类
进行断点调试、分析
然后课可以看到首先进入的是ClassLoader
调用的是AppClassLoader
的loadClass
方法,然后继续向下跟进
中间是一些安全检查的代码,走到后面调用父类的loadClass
跟进然后又回到了ClassLoade
r,继续往下走,调用了`findLoadedClass
,看这个类是否加载过,这里属于是双亲委派的流程,询问Extension ClassLoader
、Bootstrap ClassLoader
是否加载过,如果没有加载就加载
走到下面判断parant(父属性)
,然后parant
非空,存在ExtClassLoader
,调用ExtClassLoader
的loadClass
,继续跟进
由于ExtClassLoader
没有loadClass
方法所以又回到了ClassLoader
继续往下走,parent
为null
了
然后就会去Bootstrap
里寻找,因为这是个普通的类,不会用系统加载类去加载,所以还是没找到
继续向下找
走到findClass
,由于ClassLoader
的findClass
的方法是需要重写的一个方法,所以会走到子类findClass
但是这原本按照流程来说这是应该走到ExtClassLoader
里的findClass
,但实际上却走到了URLClassLoader
的findClass
里
这个的原因是ExtClassLoader
和AppClassLoader
都没有findClass
方法,但是都继承了URLClassLoader
因为现在是在ExtClassLoader
里,肯定是找不到普通类的
继续往下走,然后退回到AppClassLoader
里了,接着调用findClass
又回到了URLClassLoader
,继续跟进
分析findClass
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");// 这里是获取类路径比如:Person.clss
Resource res = ucp.getResource(path, false); // ucp是一个URLClassPath类,在当前加载路径去找这个类
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
在这里可以找到这个类
最后调用了defineClass
,实际上是在这里进行了类的加载,继续跟进,走到了URLClassLoader
的defindClass
方法,中间都是一些检验的代码,最后又调用了一个defindClass
走到了URLClassLoader
的父类SecureClassLoader
,又调用了一个defindClass
,跟进
然后又回到了ClassLoader
的defindClass
,然后调用了defineClass1
defineClass1
是由native
修饰的,是由c++写的,最后的类加载就是在这完成的,到这这个类的加载过程就结束了
流程:
- 继承关系
- ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoader
- 调用
- loadClass->findClass(重写的方法)->defineClass(从字节码加载类)
使用URLClassLoader
进行任意类加载
知识点
- 支持的协议
- file
- http
- jar
测试
file
首先定义一个Hello类,进行编译,然后移动到别的目录,在把原本的类删了,进行测试
Hello
public class Hello {
static {
System.out.println("Hello");
}
}
成功进行加载
calc
import java.io.IOException;
public class Test {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
尝试下弹出计算器
http
在文件路径起个http服务
成功加载
同时服务端也收到了请求
使用defindClass
进行任意类加载
成功加载类
使用Unsafe
进行任意类加载
Unsafe
是java底层提供的一个类,Unsafe类主要用于执行非常底层、不安全操作的方法,例如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源调度能力方面起到了很大的作用。
在Unsafe
中也有一个defineClass
虽然这个是一个public
方法,但是并不能直接使用,因为Unsafe类的构造被私有化了,Unsafe类对外只提供一个静态方法来获取当前Unsafe实例,我们要获取theUnsafe对象
成功加载类