反射
?什么是反射?
反射是被视为动态语言的关键,反射机制允许程序在运行期间借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
- (类加载的四个方法)我们可以通过类的
class
属性、类实例的getClass()
方法、Class 类的静态方法forName()
、类加载器加载指定路径下的类型来实现类的加载。- 若已知具体的类,通过类的
class
属性获取,该方法最为安全可靠,程序性能最高:
java
复制代码Class clazz = String.class;
- 已知某个类的实例,调用该实例的
getClass()
方法获取Class对象:
java
复制代码TargetObject o = new TargetObject(); Class clazz = o.getClass();
- 已知一个类的全类名,且该类在类路径下,可通过 Class 类的静态方法
forName()
获取,可能抛出ClassNotFoundException
:
java
复制代码Class clazz = Class.forName("java.lang.String");
- 可以用系统类加载对象或自定义加载器对象加载指定路径下的类型:
java
复制代码ClassLoader cl = this.getClass().getClassLoader(); Class clazz4 = cl.loadClass("java.lang.String");
- 若已知具体的类,通过类的
加载完类之后,在堆内存的方法区中就产生了一个 Class 类型的对象(一个类只有一个 Class 对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16215.html
?反射的优缺点有哪些?
优点:文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16215.html
- 提高了 Java 程序的灵活性和扩展性,降低了耦合性;
- 允许程序创建和控制任何类的对象,无需提前硬编码目标类。
缺点:文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16215.html
- 性能较低,反射机制主要应用在对灵活性和扩展性要求很高的系统框架上;
- 可读性较差,反射会模糊程序内部逻辑。
?反射的应用场景了解么?
Spring/Spring Boot、MyBatis 等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16215.html
比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 Method
来调用指定的方法。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16215.html
java
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method " + method.getName());
return result;
}
}
Java 中注解的实现也用到了反射。可以基于反射分析类,然后获取到类、属性、方法、方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16215.html
类的加载和加载器
介绍一下类的生命周期
类在内存中完整的生命周期是加载、使用、卸载。其中加载过程又分为装载、链接、初始化三个阶段。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16215.html
文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16215.html
?介绍一下类的加载过程
文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16215.html
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过装载、链接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM 将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16215.html
- 装载(Loading)
将类的 class 文件读入内存,并为之创建一个
java.lang.Class
对象,此过程由类加载器完成。 - 链接(Linking)
- 验证(Verify):确保加载的类信息符合 JVM 规范,例如:以 cafebabe 开头,没有安全方面的问题。
- 准备(Prepare):正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
- 解析(Resolve):虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
- 初始化(Initialization)
- 执行类构造器
<clinit>()
方法的过程。类构造器<clinit>()
方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器) - 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
- 虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确加锁和同步。
- 执行类构造器
类加载器的作用是什么?
将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的 java.lang.Class
对象,作为方法区中类数据的访问入口。
类缓存:标准的 JavaSE 类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过 JVM 垃圾回收机制可以回收这些 Class 对象。
类加载器有哪些?(JDK 8)
JVM 支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是 Java 虚拟机规范却没有这么定义,而是将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:
- 启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用 C/C++ 语言实现的,嵌套在 JVM 内部,获取它的对象时往往返回 null。
- 它用来加载 Java 的核心库(
JAVA_HOME/jre/lib/rt.jar
或sun.boot.class.path
路径下的内容),用于提供JVM自身需要的类。 - 并不继承自
java.lang.ClassLoader
,没有父加载器。 - 出于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类。
- 加载扩展类加载器和应用程序类加载器,并指定为它们的父类加载器。
- 扩展类加载器(Extension ClassLoader)
- Java语言编写,由
sun.misc.Launcher$ExtClassLoader
实现。 - 继承于 ClassLoader 类。
- 父类加载器为启动类加载器。
- 从
java.ext.dirs
系统属性所指定的目录中加载类库,或从 JDK 的安装目录的jre/lib/ext
子目录下加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载。
- Java语言编写,由
- 应用程序类加载器(系统类加载器,AppClassLoader)
- Java语 言编写,由
sun.misc.Launcher$AppClassLoader
实现。 - 继承于 ClassLoader 类。
- 父类加载器为扩展类加载器。
- 它负责加载环境变量 classpath 或系统属性
java.class.path
指定路径下的类库。 - 应用程序中的类加载器默认是应用程序类加载器。
- 它是用户自定义类加载器的默认父加载器。
- 通过 ClassLoader 的
getSystemClassLoader()
方法可以获取到该类加载器。
- Java语 言编写,由
- 用户自定义类加载器
- 在 Java 的日常应用程序开发中,类的加载几乎是由上述 3 种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
- 体现 Java 语言强大生命力和巨大魅力的关键因素之一便是,Java 开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的 JAR 包,也可以是网络上的远程资源。
- 同时,自定义加载器能够实现应用隔离,例如 Tomcat、Spring 等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比 C/C++ 程序要好太多,想不修改 C/C++ 程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。
- 自定义类加载器通常需要继承于 ClassLoader。
?什么是双亲委派模型
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个 java.lang.Class
实例。一旦一个类被载入 JVM 中,同一个类就不会被再次载入。
在 JVM 中,一个类用其全限定类名和其类加载器作为唯一标识。换句话说,同一个类如果用两个类加载器分别加载,那么 JVM 会将它们视为不同的类,互不兼容。
那么,类加载器在执行类加载任务时,应如何确保一个类的全局唯一性呢?Java 虚拟机的设计者们通过一种被称为“双亲委托模型 (Parent Delegation Model)” 的委派机制来约定类加载器的加载机制。
按照双亲委托模型的规则,除引导类加载器,程序中的每个类加载器都应该拥有一个父类加载器,如 ExtClassLoader
的父类加载器是引导类加载器,AppClassLoader
的父类加载器是 ExtClassLoader
,自定义类加载器的父类加载器是 AppClassLoader
。类加载器执行类加载过程如图所示。这里需要说明的是,这里的父类加载器并不是继承关系,而是组合关系,即在下一级的类加载器中会用一个 ClassLoader
类的 parent
字段来记录它的父加载器对象,我们通过 getParent()
方法就可以获取它委托的父加载器对象。
当一个类加载器接收到一个类加载任务时,它并不会立即展开加载,而是先检测此类是否被加载过,即在方法区寻找该类对应的 Class
对象是否存在,如果存在就是已经加载过,直接返回该 Class
对象,否则会将加载任务委派给它的父类加载器执行。每一层的类加载器都采用相同的方式,直至委派给最顶层的启动类加载器为止。当父类加载器无法加载委派给它的类时,便会将类的加载任务退回给它的下一级类加载器执行加载。如果所有的类加载器都加载失败,那么就会报 java.lang.ClassNotFoundException
或 java.lang.NoClassDefFoundError
。
评论