类的加载机制与反射

当java程序运行时,将会启动一个jvm虚拟机。不管该程序多复杂,有多少线程,都处于同一个进程中。两次运行的java程序属于两个不同的jvm进程,数据 不会共享。因为当java程序运行结束时,jvm进程结束,进程在内存中的状态将会丢失。

类的加载

如果类还没有被加载到内存中,系统会通过加载,链接,初始化三个步骤。一般jvm连续完成,所以统称为加载或初始化

类加载是指将class文件读入内存,并为之创建一个java.lang.Class对象。类是java.lang.Class的实例。

类的加载由类加载器完成,类加载器有JVM提供。开发者可继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:

从本地文件系统来加载class文件,这是前面绝大部分示例程序的类加载方式。

从JAR包中加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就是放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。

通过网络在加载class文件。 把一个Java源文件动态编译、并执行加载。

加载器通常无须等到首次使用该类才加载,jvm运行预先加载某些类

类的连接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段将会负责把类的二进制数据合并到JRE中。类连接又可分为如下三个阶段:

验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。

准备:类准备阶段则负责为类的静态属性分配内存,并设置默认初始值。

解析:将类的二进制数据中的符号引用替换成直接引用。

类的初始化

类的初始化,主要是对类变量(静态变量)进行初始化

jvm会按照类初始化语句的顺序进行初始化。

初始化步骤

(1)假如这个类还没有被加载和连接,程序先加载并连接该类。

(2)假如该类的直接父类还没有被初始化,则先初始化其直接父类。

(3)假如类中有初始化语句,则系统依次执行这些初始化语句。

类初始化时机

创建类的实例。为某个类创建实例的方式包括使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。

调用某个类的静态方法。 访问某个类或接口的静态属性,或为该静态属性赋值。 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。例如代码:Class.forName("Person")。

初始化某个类的子类,当初始化某个类的子类时,该子类的所有父类都会被初始化。

直接使用java.exe命令来运行某个主类,当运行某个主类时,程序会先初始化该主类。

final型的静态属性,如果该属性可以在编译时就得到属性值,则可认为该属性可被当成编译时常量。当程序使用编译时常量时,系统会认为这是对该类的被动使用,所以不会导致该类的初始化。

类加载器

JVM的类加载机制主要有如下三种机制:

全盘负责:所谓全盘负责,就是说当一个类加载器负责加载某个Class的时候,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。

父类委托:所谓父类委托则是先让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

缓存机制:缓存机制将会保证所有被加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存中搜寻该Class,只有当缓存中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,并存入cache。这就是为什么我们修改了Class后,程序必须重新启动JVM,程序所作的修改才会生效的原因。

根类加载器

扩展类加载器

系统类加载器

用户类加载器

扩展类加载器的父类是根类加载器,但是根类加载器不是java实现的,所以扩展类加载器获取父类加载器时返回null

他们之间的父子关系并不是继承上的,是类加载器实例上的。

类加载器加载Class大概有8步

1.检查是否载入过(在内存中是否有此Class),如果有进入第8步,否则下一步

2.如果父类加载器不存在(幺妹父类一定是根类加载器,要么自己就是根类加载器)则调到第4步,否则如果父类加载器存在第3步

3.请求使用父类加载器载入目标类,成功则第8,失败第5

4.请求使用根类加载器区载入目标类,如果成功则第8步,否则第7

5当前类加载器尝试寻找Class文件,如果找到顺利执行第6,找不到第7

6从文件中载入Class, 成功到第8

7抛出ClassNotFoundException异常

8返回对应的java.lang.Class对象

其中第5,6步允许重写ClassLoader的findClass方法来实现自己的载入策略,甚至重写loadClass方法来实现自己的载入过程。

ClassLoader类有如下二个关键方法:

loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。

findClass(String name):根据二进制名称来查找类。 如果需要实现自定义的ClassLoader,可以通过重写以上两个方法来实现,当然我们推荐重写findClass()方法,而不是重写loadClass()方法。

执行代码前自动验证数字签名。 根据用户提供的密码解密代码,从而可以实现代码混淆器来避免反编译class文件。 根据用户需求来动态地加载类。 根据应用需求把其他数据以字节码的形式加载到应用中。

Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类(此处是父类,而不是父类加载器,这里是类与类之间的继承关系),URLClassLoader功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。 实际上应用程序中可以直接使用URLClassLoader来加载类,URLClassLoader类提供了如下两个构造器: URLClassLoader(URL[] urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询、并加载类。 URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父类加载器创建一个ClassLoader对象,其他功能前一个构造器相同。

java程序中,许多对象在运行时都会出现两种类型:编译时类型和运行时类型,比如多态。甚至在运行时接收到外部传入的一个对象,该对象编译时类型是obj,但程序需要调用该对象的运行时类型的方法

第一种做法是在编译时就明确知道运行时的类型信息。先用instanceof判断,在强制类型转换

第二种做法是无法预知运行时的类型,只有依靠运行时信息来发现该对象和类的真实信息,就必须使用反射

因为加载机制,我们可以获得Class对象,通过该对象可以访问到jvm中的这个类

使用class类的forName静态方法,传入某个类的全限定类名的字符串。

调用某个类的class属性,例如person.class

调用某个类的getClass方法,该方法是java.lang.Object类中的一个方法。所以所有对象都可以调用

一般使用第二种方法

因为代码安全,性能好

如果只有字符串信息就只能用第一种方法,会抛出一个异常。

一旦获得某个类对应的class对象后,程序可以从中获得信息。

获取构造器 访问Class对应的类所包含的方法 访问Class对应的类所包含的属性(Field) 访问Class对应的类上所包含的注释。 访问该Class对象对应类包含的内部类。 访问该Class对象对应类所在的外部类。 访问该Class对象所对应类所继承的父类、所实现的接口等。

Java 8在java.lang.reflect包下新增了一个Executable抽象基类,该对象代表可执行的类成员,该类派生了Constructor、Method两个子类。 Executable基类提供了大量方法来获取修饰该方法或构造器的注解信息;还提供了isVarArgs()方法用于判断该方法或构造器是否包含数量可变的形参,以及通过getModifiers()方法来获取该方法或构造器的修饰符。除此之外,Executable提供了如下两个方法来获取该方法或参数的形参个数及形参名。 int getParameterCount():获取该构造器或方法的形参个数。 Parameter[] getParameters():获取该构造器或方法的所有形参。 上面第二个方法返回了一个Parameter[]数组,Parameter也是Java 8新增的API,每个Parameter对象代表方法或构造器的一个参数。Parameter也提供了大量方法来获取声明该参数的泛型信息。

通过反射调用构造器创建对象。

class<?> clazz=class.forname(classname);

return clazz.getConstructor().newInstance();

一般需要从配置文件获得类名,然后反射获得类的构造器,然后创建类。

获取构造器可以指定获取带某个参数的。

一般需要动态生成类时,才会使用反射。

通过反射调用方法

利用getMethod获取方法,

利用invoke调用方法。

可以通过setAccessible方法,来取消访问权限,实现反射调用私有成员,包括方法,构造器,成员变量。

Spring框架就是通过这种反射调用方法的方式,将成员变量值以及依赖对象都放在配置文件里。Spring框架的Ioc。

通过反射来访问Field值

通过get方法可以获得成员变量的值,

通过set方法可以设置值

包括私有成员变量。

通过反射操作数组

java.lang.reflect包下还提供了一个Array类,Array对象可以代表所有数组,程序可以通过使用Array来动态创建数组,操作数组元素

java11新增的嵌套访问权限

在Java 11之前,外部类可以访问内部类的private成员,内部类之间也可相互访问对方的private成员;但通过反射来访问则不行。

Java 11之前存在的问题:通过反射访问和不通过反射访问时,Java的访问权限并不一致。

Java 11引入了嵌套上下文的概念。通过嵌套访问权限的支持,Java 11统一了反射访问和不通过反射访问时的权限不一致问题。

使用反射生成JDK动态代理

java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过使用这个类和接口可以生成JDK动态代理类或动态代理对象。 Proxy 提供用于创建动态代理类和代理对象的静态方法,它也是所有动态代理类的父类。如果我们在程序中为一个或多个接口动态地生成实现类,就可以使用Proxy来创建的动态代理类;如果需要为一个或多个接口动态地创建实例,也可以使用Proxy来创建动态代理实例。

Proxy提供了如下两个方法来创建动态代理类和动态代理实例: static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces):创建一个动态代理类所对应的Class对象,该代理类将实现interfaces所指定的多个接口。第一个ClassLoader指定生成动态代理类的类加载器。 static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h):直接创建一个动态代理对象,该代理对象的实现类实现了interfaces指定的系列接口,执行代理对象的每个方法时都会被替换执行InvocationHandler对象的invoke方法。

动态代理在AOP(Aspect Orient Program,即面向切面编程)里被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理。

从JDK1.5之后,Java的Class类增加了泛型功能,从而允许使用泛型来限制Class类,例如,String.class 的类型实际上是Class<String>。 使用Class<T>泛型可以避免强制类型转换。

获得了Field对象后,就可以很容易地获得该Field的数据类型,即使用如下代码即可获得指定Field的类型: //获取Field对象f的类型 Class<?> a = f.getType(); 通过这种方式只对普通类型的Field有效。但如果该Field的类型是有泛型限制的类型,如Map<String , Integer>类型,则不能准确的得到该Field的泛型参数。 为了获得指定Field的泛型类型,应先使用如下方法来获取指定Field的泛型类型: //获得Field实例f的泛型类型 Type gType = f.getGenericType(); 然后将Type对象强制类型转换为ParameterizedType对象,ParameterizedType代表被参数化的类型,也就是增加了泛型限制的类型。ParameterizedType类提供了两个方法: getRawType():返回被泛型限制的类型。 getActualTypeArguments():返回泛型参数类型。

原文地址:https://www.cnblogs.com/tomato190/p/12505696.html