您的位置:首页 > 编程语言 > Java开发

李刚老师著疯狂Java部分章节-Java类加载和类反射

2013-08-29 20:37 239 查看
本文将会深入介绍Java类的加载、连接和初始化的深入知识,并重点介绍Java反射相关的内容。读者在阅读本章的类加载、连接及初始化知识时,可能会感觉这些知识比较底层,但掌握这些底层的运行原理会让读者对Java程序的运行有更好的把我。而且Java类加载器除了根类加载器之外,其他类加载器都是使用Java语言编写的,所以程序员完全可以开发自己的类加载器,通过使用自定义类加载器,可以完成一些特定的功能。

本文重点将深入介绍java.lang.reflect包下的接口和类,包括Class、Method、Field、Constructor和Array等,这些类分别代表类、方法、属性、构造器和数组,Java程序可以使用这些类动态地获取某个对象、某个类的运行时信息,并可以动态地创建Java对象,动态地调用Java方法,访问并修改指定对象的属性值。本章还将介绍该包下的Type和ParameterizedType两个接口,其中Type是Class类所实现的接口,而ParameterizedType则代表一个带泛型参数的类型。

除此之外,本章还将介绍使用Proxy和InvocationHandler来创建JDK动态代理,并会通过JDK动态代理向读者介绍高层次解耦的方法,还会讲解JDK动态代理和AOP(Aspect Orient Programming,面向切面编程)之间的内在关系。

JVM和类

当我们调用Java命令运行某个Java程序时,该命令将会启动一条Java虚拟机进程,不管该Java程序有多么复杂,改程序启动了多少个线程,它们都出于该Java虚拟机进程里。正如前面介绍的,同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。当系统出现以下几种情况时,JVM进程将被终止:

程序运行到最后正常结束。

程序使用System.exit()或Runtime.getRuntime().exit()代码结束程序。

程序执行过程中遇到未捕获的异常或错误而结束。

程序所在平台强制结束了JVM进程。

类的加载

当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载和类初始化。

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是我们前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

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

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

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

通过网络加载class文件。

把一个Java源文件动态编译、并执行加载。

类加载器通常无须等到“首次使用”该类时加载该类,Java虚拟机规范允许系统预先加载某些类。

类的连接

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

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

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

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

类的初始化

在类的初始化阶段,虚拟机负责对类进行初始化,主要是对静态属性进行初始化。在Java类中对静态属性指定初始值有两种方式:(1)声明静态属性时指定初始值;(2)使用静态初始化块为静态属性指定初始值。

JVM初始化一个类包含如下几个步骤:

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

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

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

类初始化的时机

当Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口;

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

调用某个类的静态方法。

2.访问某个类或接口的静态属性,或为该静态属性赋值。

3.使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。例如代码:Class.forName("Person"),如果系统还未初始化Person类,则这行代码将会导致该Person类被初始化,并返回Person类对应的java.lang.Class对象。关于Class的forName方法请参看18.3节。

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

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

类加载器

类加载器负责将.class文件加载到内存中,并为之生成对应的java.lang.Class实例。尽管Java开发中无须过分关心类加载机制,但所有的编程人员都应该了解其工作机制,明白如何做才能让其更好地满足我们的需要。

类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。现在的问题是怎么样才算“同一个类”?正如一个对象有一个唯一的表示一样,一个载入JVM的类也有一个唯一的标识。

同理,载入JVM的类也有一个唯一的标识,在Java中,一个类用其全限定类名(包括报名和类名)作为标识。但在JVM中,一个类用其全限类名(包括包名和类名)作为标识。但在JVM中,一个类用其权限定名和其类加载器作为唯一标识。

Bootstrap ClassLoader:根类加载器。

Extension ClassLoader:扩展类加载器。

System ClassLoader:系统类加载器。

Bootstrap ClassLoader,被称为引导(也成为原始或根)类加载器。它负责加载Java的核心类。在Sun的JVM中,当执行java.exe的命令时使用-Xbootclasspath选项或使用-D选项指定sun.boot.class.path系统属性值可以指定加载附加的类。

根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。

类加载机制

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

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

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

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

类加载器加载Class大致要经过如下8个步骤:

1.检测此Class是否载入过(即在缓存中是否有此Class),如果有则直接进入第8步,否则接着执行第2步。

2.如果父类加载器不存在(如果没有父加载器,则要么parent一定是根加载器,要么本身就是根据加载器),则跳到第4步执行。如果父加载器存在,则接着执行第3步。

3.请求父加载器载入目标类,如果成功载入则跳入到第8步,不成功跳到第5步。

4.请求使用根加载器来载入目标类,如果成功载入则跳到第8步。如果不成功跳到7步。

5.寻找Class文件(从与此ClassLoader相关的类路径中寻找)。如果找到则执行第6步,如果找不到则跳到第7步。

6.从文件中载入Class,成功载入后跳到第8步。

7.抛出ClassNotFoundException。

8.返回Class。

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

创建并使用自定义的类加载器

JVM中除根加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写ClassLoader所包含的方法来实现自定义的类加载器。查询API文档中关于ClassLoader的方法不难发现,ClassLoader中包含了大量protected方法——这些方法都可以被子类重写。

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

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

findClass(String name):根据二进制名称来查找类。

如果需要实现自定义的ClassLoader,可以通过重写以上两个方法来实现,当然我们推荐重写findClass()方法,而不是重写loadClass()方法。因为loadClass()方法的执行步骤如下:

1.用findLoaderClass(String)来检查是否已经加载类,如果已经加载则直接返回。

2.在父类加载器上调用loadClass方法。如果父类加载器为null,则使用根类加载器加载。

3.调用findClass(String)方法查找类。

从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略。如果重写loadClass()方法,则实现逻辑更为复杂。

在ClassLoader里还有一个核心方法:Class defineClass(String nane, byte[] b, int off, int len),该方法负责将指定类的字节码文件(即class文件,如Hello.class)读入字节数组:byte[] b内,并把它转化为Class对象,该字节码文件可以来源于文件、网络等。

defineClass管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等等。不过不用担心,程序员无须重写该方法。事实上,该方法是final型,即使我们想重写也没有机会。

除此之外,ClassLoader里还包含如下一些普通方法:

findSystemClass(String name):从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass将原始字节转换成Class对象,以将文件转换成类。

static getSystemClassLoader():这是一个静态方法,用于返回系统类加载器。

getParent():获取该类加载器的父类加载器。

removeClass(Class<?> c):链接指定的类。类加载器可以使用此方法来链接类c。读者无须理会关于此方法太多细节,如需要获取该方法更多细节可查看《Java Language Specification》。

findLoaderClass(String name):如果此Java虚拟机已撞在了名name的类,则直接返回该类对应的Class实例;否则,返回null。该方法是Java类加载里缓存机制的体现。

下面程序开发了一个自定义的ClassLoader,该ClassLoader通过重写findClass方法来实现自定义的类加载机制,这个ClassLoader可以在加载类之前先编译该类的源文件,从而允许使用该ClassLoader来直接运行Java源文件。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: