详解Java反射机制(一)
在学习Java反射相关的知识前,我们需要对Java类的加载、连接和初始化的知识进行学习了解。这些知识可能比较枯燥,但却是底层的知识。掌握了这些底层运行原理,对于后续的学习的有更好的帮助。
1.类的加载、连接和初始化
当程序主动使用某个类时,如果该类没有加载到内存中,系统会通过加载、连接、初始化三个步骤对该类进行初始化。
一般情况下,JVM会连续完成这三个步骤,所以有时也将类的加载、连接与初始化三个步骤统称为类加载或者类初始化。
下面将主要对类的加载、连接和初始化三个步骤进行解析。
1.1 类的加载
系统可能在第一次使用某个类时加载该类,但也可能采用预先加载机制来预加载某个类,总之类的加载必须由类加载器完成。
类加载器通常由JVM提供,由JVM提供的类加载器通常称为系统类加载器。此外,开发过程中也可以通过继承ClassLoader基类创建自己的类加载器。
通常使用不同的类加载器,可以从不同来源加载类二进制数据,通常有如下几种来源:
从不同来源加载类的二进制数据 |
---|
1.从本地系统直接读取.class文件,这是绝大多数类的加载方法; |
2.从zip或者jar包等文件中加载.class文件,这种方式也比较常见; |
3.网络下载.class文件; |
4.从专有数据库中提取.class数据; |
5.将Java源文件数据上传到服务器中动态编译为.class文件,并执行加载。 |
- 需要注意的是:不管类的字节码内容从哪里加载,加载的结果是一样的。这些字节码内容加载到内存后,都将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象。所有需要访问和使用类数据只能通过这个Class对象。
1.2 类的连接
当类被加载后,生成一个代表这个类的java.lang.Class对象,接着会进入连接阶段。类连接又可以分为三个阶段:验证、准备以及解析。
类的连接的三个阶段 |
---|
(1)验证:确保加载的类信息符合JVM规范。例如:以cafe开头,表示没有安全的问题。 |
(2)准备:正式为类变量(被static修饰的变量)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。 |
(3)解析:虚拟机常量池内的符号引用(变量名)替换为直接引用(地址)的过程。 |
1.3 类的初始化
类的初始化主要就是对静态的类变量进行初始化:
- (1)执行类构造器clint()方法的过程。
- (2)当初始化一个类时,如果发现这个类的父类还没有进行初始化,则需要先完成对父类的初始化。
- (3)虚拟机会保证一个类的clint()方法在多线程环境下能够被正常的加锁和同步。
//类的初始化
class Base{
private static int a=getNum();
//静态代码块
static {
++a;
System.out.println("(2)a="+a);
}
static {
++a;
System.out.println("(3)a="+a);
}
public static int getNum(){
System.out.println("(1)a="+a);
return 1;
}
}
class TestClint extends Base{
private static int b=getNum();
static {
++b;
System.out.println("(5)b="+b);
}
static {
++b;
System.out.println("(6)b="+b);
}
public static int getNum(){
System.out.println("(4)b="+b);
return 1;
}
}
public class MyTest {
public static void main(String[] args) {
TestClint testClint = new TestClint();
}
}
运行后的结果为:
1.3.1 会触发类的初始化操作
虽然类的加载和初始化在一起执行的,但实质上类的加载不一定触发类的初始化。
当Java程序首次通过下面这6种方式来使用某个类时,系统就会初始化该类。
会发生类的初始化操作 |
---|
(1)当虚拟机启动时,先初始化main()方法所在的类。 |
(2)当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类。 |
(3)new一个类的对象时。 |
(4)使用一个类的静态的成员(包含静态变量与静态方法),但这个静态变量不能是final的。 |
(5)使用java.lang.reflect包的方法对类进行反射调用。 |
/***
*类的加载包含类的初始化
* (1)当虚拟机启动时,先初始化main()方法所在的类
* (2)当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类。
* (3)new一个类的对象时
* (4)使用一个类的静态的成员(包含静态变量和静态的方法),但是这个静态变量不能是final的。
* (5)使用了java.lang.reflect包的方法对类进行反射调用。
*/
class Father{
static {
//当初始化一个类时,如果父类没有被初始化,则先会初始化它的父类
System.out.println("main方法所在的父类(1)");
}
}
public class MyTest1 extends Father {
static {
//当虚拟机启动时,先初始化main()方法所在的类
System.out.println("main()方法所在的类(2)");
}
public static void main(String[] args) throws ClassNotFoundException {
new A();//第一次使用A就是在创建它的对象
//使用一个类的静态的成员,但是这个静态变量不能是fianl的
B.test();
//使用java.lang.reflect包的方法对类进行反射调用
Class<?> aClass = Class.forName("org.westos.demo.C");
}
}
class A{
static {
System.out.println("A类初始化");
}
}
class B{
static {
System.out.println("B类初始化");
}
public static void test(){
System.out.println("B类的静态方法");
}
}
class C{
static {
System.out.println("C类初始化");
}
}
运行后的结果为:
1.3.2 不会触发类的初始化操作
说完了会触发类的初始化操作,我们接下来聊聊不会触发类的初始化的操作。
不会触发类的初始化操作 |
---|
1.引用静态常量(被fianl修饰)不会触发类的初始化 |
2.当访问一个静态域时,只有真正声明这个域的类才会被初始化。例如:当通过子类引用父类的静态变量,不会导致子类初始化 |
3.通过数组定义类引用,不会触发此类的初始化 |
/***
* 类的加载过程中,没有带上类的初始化
* (1)引用静态常量不会触发此类的初始化
* (2)当访问一个静态域(静态变量,静态方法),只有声明这个域的类才会被初始化。
* 即当通过子类引用父类的静态变量,静态方法时,不会导致子类初始化。
*
* (3)通过数组定义类引用时,不会触发此类的初始化。
*
*
*
*/
public class MyTest2 {
public static void main(String[] args) {
System.out.println(D.num);//D类不会初始化,因为num是final的
System.out.println(F.num);
F.test(); //F类不会初始化,E类会初始化,因为num和test()是在E类中声明的
//G类会初始化,此时还没有正式用的G类
G [] arr=new G[5];//没有创建G的对象,创建的是准备用来装G对象的数组对象
}
}
class D{
public static final int num=10;
static{
System.out.println("D类的初始化");
}
}
class E{
static int num=10;
static {
System.out.println("E父类的初始化");
}
public static void test(){
System.out.println("E父类的静态方法");
}
}
class F extends E{
static {
System.out.println("F子类的初始化");
}
}
class G{
static {
System.out.println("G类的初始化");
}
}
2.详解类加载器(面试常问)
前面我们所提到的类加载的过程是通过类加载器来完成的。所以这里很有必要介绍下Java类加载器。
2.1 Java类加载器
Java类加载的分类 | 具体解释 |
---|---|
1.引导类加载器(Bootstrap ClassLoader),又称为根类加载器 | 它负责加载Java的核心库(JAVA_HOME/jre/lib/rt.jar等或sun.boot.class.path路径下的内容),是用原生代码(C/C++)来实现的,并不继承自java.lang.ClassLoder,所以通过Java代码获取引导类加载器对象将会得到null。 |
2.扩展类加载器(Extension ClassLoader) | 它由sun.misc.Launcher$ExtClassLoader实现,是java.lang.ClassLoader的子类,负责加载Java的扩展库(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路径下的内容)。 |
3.应用程序类加载器(Application Classloader) | 它由sun.misc.Launcher$AppClassLoader实现,是java.lang.ClassLoader的子类,负责加载Java应用程序类路径(classpath、java.class.path)下的内容。通俗的讲:项目路径bin文件夹下的字节码,以及如果你配置环境变量classpath。 |
4.自定义类加载器 | 一般什么情况下需要定义自定义类加载器:(1)字节码文件需要加密与解密操作;(2)字节码的路径不在常规路径,有自定特定的路径。例如:tomcat |
2.2 Java中类加载器的双亲委托模式
类加载器负责加载所有的类,系统会为所载入内存中的类生成一个java.lang.Class实例。
一旦一个类被载入JVM中,同一个类就不能再次被载入。但问题出现了,我们如何辨识"同一个类"呢?在JVM中,一个类使用其全限定类名和其类加载器作为唯一标识。换句话说,同一个类如果使用两个类加载器分别进行类加载,JVM将视为"不同的类",它们是互不兼容的。
类加载器在执行任务时,如何确保一个类的全局唯一性?
在设计Java虚拟机时,设计者们通过一种称为"双亲委派模型"的委派机制来约定类加载器的加载机制。
按照双亲委派模型的规则,除了引导类加载器(根类加载器)之外,程序中的每一个类加载器都应该有一个超类加载器。比如应用程序类加载器的超类加载器是引导类加载器,而应用程序类加载器的超类加载器是扩展类加载器,而自定义加载器的超类就是应用程序类加载器。
Java类加载器双亲委托模式具体处理详解:
- 当一个类加载器接收到一个类加载任务时,它不会立即展开加载,会先检测这个类是否加载过,在方法区寻找该类的Class对象是否存在。如果存在就加载过了,直接返回Class对象。否则会将加载任务委托给它的超类加载器来执行,一层的类加载器都采用相同的方式,直至委派给最顶层的启动类加载器为止,如果超类加载器无法加载委派给它的类时,便会将类的加载任务退回给它的下一级类加载器去执行加载,如果所有的类加载器都加载失败,就会报java.lang.ClassNotFoundException或java.lang.NoClassDefFoundError。
具体的处理流程也可以见下图:
需要注意的是:
- 由于Java虚拟机规范并没有要求类加载器的加载机制一定要使用双亲委托模式,只是建议采用这种方式而已。比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器就接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。
/**Java中类加载器的双亲委托模式
* 1.类加载器设计时,这四种类加载器是有层次结构的,但是这种层次结构不是通过继承关系来实现的。
* 是通过组合的方式,来实现“双亲”的认亲过程。
* 例如:应用程序类加载器把扩展类加载器称为“父加载器”,在应用程序类加载器中保留应用程序类加载器的一个引用(成员变量),
* 把变量名称设计为parent。所有的类加载器有一个getParent(),即获取父加载器的方法。
*
*2.有什么用?
* 目的是为了安全,而且各司其职的作用
*
* 当应用程序类加载器接到加载某个类的任务时,例如:java.lang.String
* (1)会先在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载了。
* (2)如果没有找到,即没有加载过。会把这个任务先提交给“父加载器”
*
*
*当扩展类加载器接到加载某个类的任务时,例如:java.lang.String
* (1)会先在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载了。
*(2)如果没有找到,即没有加载过。会把这个任务先提交给“父加载器”
*
*当引导类加载器接到加载某个类的任务时,例如:java.lang.String
* (1)会先在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载了。
* (2)如果没有找到,即没有加载过。会在它负责的范围内尝试加载。
* 如果找到了,那么就返回这个类的Class对象,就结束了。
* 如果没有找到,那麽就把这个任务往回传,让“子加载器”扩展类加载器去加载。
*
*“子加载器”扩展类加载器接到“父加载器”返回的任务,去它负责的范围内进行加载。
* 如果找到了,那么就返回这个类的Class对象,就结束了。
* 如果没有找到,那麽就把这个任务往回传,让“子加载器”应用程序类加载器去加载。
*
*
* “子加载器”应用程序类加载器接到“父加载器”返回的任务,去它负责的范围内进行加载。
* 如果找到了,那么就返回这个类的Class对象,就结束了。
* 如果没有找到,那麽就报错ClassNotFoundException或java.lang.NoClassDefError。
*
*
*
* Java中是认为不同的类加载器,加载的类名相同的类,识别为不同的类。
*
*
*/
public class MyTest {
public static void main(String[] args) {
//1.先获取这个类的Class对象
Class<MyTest> myClass = MyTest.class;
//Class<String> myClass = String.class;
//2.获取它的类加载器对象
ClassLoader classLoader = myClass.getClassLoader();
System.out.println("当前类的加载器:"+classLoader);
ClassLoader parentloader = classLoader.getParent();
System.out.println("父加载器:"+parentloader);
ClassLoader grand = parentloader.getParent();
System.out.println("爷爷加载器:"+grand);
}
}
运行后的结果为:
总结
本节主要介绍了反射前的预备性知识,例如类的加载过程,Java类加载器以及双亲委托模式等介绍,值得注意的是Java类加载器的双亲委托模式是面试中的高频问点,要做到详细了解。