1. Java类的生命周期
到目前为止,相信同学们已经掌握了一套Java开发的最基本套路,那就是︰先定义一个Java类,然后定义它内部的属性/行为,最后在需要用的时候产生该类的对象,调用该对象的方法完成功能或操作属性值。 由此可见“类-class”这个概念在整个Java的语法和运行机制中都是非常核心的。那么一个Java类从被加载到虚拟机内存开始,到卸载出内存为止,它经过了哪些步骤呢?那这些步骤又被我们称作类的生命周期。
1.1 类的生命周期图
一个Java类从开始到结束,它的整个生命周期一共会经历7个阶段:加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备、解析三个部分又统称为连接。如下图:
在这七个步骤中,我们把前5个步骤统称为“类加载”,包括:加载、连接(验证、准备、解析)、初始化。
1.2 类加载的任务
那我们所谓的类加载,它的任务是啥呢?
-
加载:由类加载器完成,类的class文件读入内存后,就会创建一个java.lang.Class对象。一旦某个类被载入JVM中,同一个类就不会再次被载入。
-
连接:把类的二进制数据合并到JRE中。
-
初始化:JVM负责对类进行初始化,也就是对静态属性进行初始化。
在Java类中,对静态属性指定初始值的方式有两种:
-
声明静态属性时指定初始值;
-
使用静态初始化块为静态属性指定初始值。
-
1.3 双亲委派机制
在说明类加载任务时,有提到一个点:一旦某个类被载入JVM中,同一个类就不会被再次载入。那这一要点是如何实现的呢?答案就是--双亲委派机制。
在Java中默认提供了三种类加载器:
-
启动类加载器 ,或者叫根加载器。这个类加载器主要是去加载你在本机配置的环境变量Java_Home/jre/lib目录下的核心API,如rt.jar,其底层采用C++代码加载;
-
扩展类加载器。这个加载器负责加Java_Home/jre/lib/ext目录下的所有jar包;
-
应用程序类加载器。这个加载器加载的是你的项目工程的 ClassPath目录下的类库。另外,用户也可以根据自身需要实现自己的类加载器,继承ClassLoader即可。如果用户没有自定义自己的类加载器,这个就是程序默认的类加载器。
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class 文件时,Java虚拟机采用的是双亲委派模式,即把请求交由上级加载器处理,是—种任务委派模式。双亲委派机制模型图如下:
其工作原理如下:
-
如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行;
-
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的根类加载器;
-
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去 加载,这就是双亲委派机制;
-
父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至最底层类加载器也无法加载此类,则抛出异常。
由于启动类加载器是用C++开发实现的,所以打印出来的Java对象是null。
思考:为什么要设计双亲委派机制呢?
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。 相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object 的类,并放在程序的Class Path中,那系统中将会出现多个不同的 Object类,Java类型体系中最基础的行为也就无法保 证,应用程序也将会变得一片混乱。
2. 反射
前面讲了那么久,那到底什么是反射呢?
Java的反射技术是java程序的特征之一,它允许运行中的Java程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性。
使用反射可以获得Java类中各个成员的名称并显示出来。简单的说,反射就是让你可以通过名称来得到对象(类,属性,方法)的技术。
总结为一句话:运行时探究和使用编译时未知的类。
在这一句话中,有两个时间节点需要大家特别注意:运行时和编译时。
在正常情况下,我们的开发实现都是在运行时使用编译时已知的类,就我们Java开发的最基本套路:要先定义好一个类,然后再用它。在定义过程中,我们已经确定了这个类有哪些属性方法,然后在运行中产生该类对象、操作该对象的属性和调用该对象的方法。
但是呀,反射让我们把这个过程给反过来了,即在编译时我们可能并不能确定要使用的到底是哪个类的对象,要调用它的哪个方法,而是在“运行期”的时候给予确定。这就是所谓的编译时未知,运行时探究和使用。
那么,这样的反射在Java中又是如何实现的呢?
在讲解类加载时,我们讲过,JVM会把程序中所要用到的类的class文件加载到内存当中,而这个类的所有基本信息就会被封装到一个java.lang.Class类型的对象里面;因此,我们要用到的每个类都会在内存中有一个Class对象,该对象就被称之为这个类的“类模板对象”。
这个类模板对象--- Class对象,本意是交给JVM自己使用的,用于记录和查找这个类的信息的。但是反射机制允许我们在我们的程序运行过程中得到这个对象,从而实现“运行时探究和使用编译时未知的类”;例如︰我们用的Eclipse或IDEA这些IDE工具。它们在当初开发的时候,并不知道程序员要写的类叫什么,有什么。但是,当它们在运行起来以后通过它的内部的反射实现可以探究到我们书写的类的信息。所以,当程序员在这些IDE中用“.”操作符的时候,会弹出这个类的方法和属性,这就是反射的效果。