JAVA之类加载机制与反射(一)

来源:互联网 发布:知乎嗜血法医 编辑:程序博客网 时间:2024/05/01 11:41

类的加载、连接和初始化


系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类。

JVM和类

当调用java命令运行某个Java程序时,该命令会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动多少个线程,它们都处于该Java虚拟机进程里。
同一个JVM中的所有线程、所有变量都处于同一个进程,它们都使用该JVM进程的内存区。

当遇到以下几种情况,JVM线程将被终止:
  • 程序运行到最后正常结束
  • 程序运行到System.exit()或Runtime.getRuntime().exit()代码处结束程序
  • 程序执行过程中遇到未捕获的异常或错误而结束
  • 程序所在的平台强制结束了JVM进程

当Java程序运行结束时,JVM结束,该进程在内存中的状态会丢失。
两个JVM之间不会共享数据。(P820)

类的加载

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

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

系统中所有类其实也是实例——它们都是java.lang.Class的实例

使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有以下几种来源:
从本地文件系统加载class文件,这是最常见的方式
从JAR包加载class文件
通过网络加载class文件
把一个Java源文件动态编译,并执行加载

类加载器通常无需等到首次使用该类时才加载该类,JAVA虚拟机规范允许系统预先加载某些类

类的连接

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

类连接又可分为以下三个阶段:
  1. 验证:验证阶段用于检验被夹在的类是否有正确的内部结构
  2. 准备:类准备夹断则负责为类的类变量分配内存,并设置默认初始值
  3. 解析:将类的二进制数据中的符号引用替换成直接引用

类的初始化

在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对类变量进行初始化
在Java类中对类变量指定初始值有两种方式——声明类变量时指定初始值;使用静态初始化块为类变量指定初始值

声明变量时指定初始值,静态初始化块都将被当成类的初始化语句,JVM会按这些语句在程序中的排列顺序来一次执行它们

public class Test {static{b=6;System.out.println("-------------------");}static int a=5;static int b=9;static int c;public static void main(String []args){System.out.println(b);                                                                                                                                                   }}
输出结果为:

如果调整一下静态初始化块和静态赋值语句的顺序

public class Test {static int a=5;static int b=9;static int c;static{b=6;System.out.println("-------------------");}public static void main(String []args){System.out.println(b);}}
输出结果为:

可以看到,初始化的语句确实是以在程序中的排列顺序依次执行的

JVM初始化一个类包含以下几个步骤:
假如这个类还没有被加载和连接,则先加载并连接该类
假如该类的直接父类还没有被初始化,则先初始化其直接父类
假如类中有初始化语句,则系统依次执行这些初始化语句


可以看出,当程序使用任意一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会被初始化。

类的初始化时机

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

  • 创建类的实例。(为某个类创建实例的方式包括:使用new操作符类创建实例,通过反射来创建实例,通过反序列化的方式来创建实例)
  • 调用某个类的类方法(静态方法)
  • 访问某个类或接口的类变量,或为该类变量赋值
  • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
  • 初始化某个类的子类
  • 直接使用java.exe命令来运行某个类的主类。当运行某个主类是,程序会先初始化该主类
对于一个final型的类变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于“宏变量”。Java编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量,也不会导致该类的初始化。
反之,如果final修饰的类变量的值不能在编译时却定下来,则会导致该类被初始化。

当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用class的forName()静态方法才会导致强制初始化该类

package 类的加载连接和初始化;class Tester{static{System.out.println("Tester类的静态初始化块...");}}public class ClassLoaderTest {public static void main(String []args) throws ClassNotFoundException{ClassLoader cl=ClassLoader.getSystemClassLoader();cl.loadClass("类的加载连接和初始化.Tester");System.out.println("系统加载Tester类");Class.forName("类的加载连接和初始化.Tester");}}
运行结果为:

类加载器

类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象

类加载器简介


类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。

在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为唯一标识
这就意味着两个类加载器加载的同名类(Person、pg、kl)和(Person、pg、kl2)是不同的,它们所加载的类也是完全不同、互不兼容的。

当JVM启动时,会形成有三个类加载器组成的初始类加载器层次结构
  • Bootstrap ClassLoader :根类加载器
  • Extension ClassLoader:扩展类加载器
  • System ClassLoader:系统类加载器
Bootstrap ClassLoader被称为引导(也称为原始或根)类加载器,它负责加载Java的核心类。

在Sun的JVM中,当执行java.exe命令时,使用-Xbootclasspath选项或使用-D选项指定sun.boot.class.path系统属性值可以指定加载附加的类。

根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的
以下代码可获得的根类加载器所加载的核心类库
import java.net.URL;public class BootstrapTest {public static void main(String []args){URL [] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();for(int i=0;i<urls.length;i++)System.out.println(urls[i].toExternalForm());}}
输出结果为:

这就是程序中可以直接使用String、Stystem这些核心类库的原因(因为都在rt.jar文件中)


Extension ClassLoader被称为扩展类加载器,它负责加载JRE的扩展目录(%JAVA HOME%jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中JAR包的类

通过这种方式,就可以为Java扩展核心类以外的新功能,只要把自己开发的类打包成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径即可

System ClassLoader被称为系统(应用)类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。

程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器,如果没有特别的指定,则用户自定义的类加载器都以系统类加载器作为父类。

类加载机制


JVM的类加载机制主要有以下三种:
  • 全盘负责。所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其它Class也将由该类加载器负责载入,除非显式使用了另一个类加载器来载入。
  • 父类委托。先让parent(父)类加载器试图加载该Class,只哟偶在父类加载器无法加载该类时才常识从自己的类路径中加载该类
  • 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序需要使用某个Class时,类加载器先从缓冲区搜索该CLass,只有当缓存区中不存在该Class对象,系统才会读取带类对应的二进制数据,并将其转换成Class对象,存入缓冲区。(这就是为什么修改Class后,必须重新启动JVM,程序所做的修改才会生效的原因)
类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。
根类加载器<-扩展类加载器<-系统类加载器<-用户类加载器

package 类加载器;import java.io.IOException;import java.net.URL;import java.util.Enumeration;public class ClassLoaderPropTest {public static void main(String []args) throws IOException{ClassLoader systemLoader=ClassLoader.getSystemClassLoader();System.out.println("系统类加载器 "+systemLoader);Enumeration<URL> eml=systemLoader.getResources("");while(eml.hasMoreElements()){System.out.println(eml.nextElement());}ClassLoader extensionLader=systemLoader.getParent();System.out.println("扩展类加载器 "+extensionLader);System.out.println("扩展类加载器的加载路径 "+System.getProperty("java.ext.dirs"));System.out.println("扩展类加载器的parent: "+extensionLader.getParent());}}
输出结果为:

可以看到,扩展类的类加载器是null,而不是根类加载器,这是因为根类加载器并没有继承ClassLoader抽象类。
实际上,根类加载器是扩展类加载器的父类加载器,只是根类加载器并不是Java实现的!!!


该程序的系统类加载器是AppClassLoader的实例,扩展类加载器是ExtClassLoader的实例。实际上,这两个类都是URLClassLoader类的实例。

JVM的根类加载器并不是Java实现的,而且由于程序通常无须访问根类加载器,一次你访问扩展类加载器的父类加载器时返回null。


类加载器加载Class大致需要以下8个步骤
  1. 检测此Clss是否载入过(即缓冲区是否有此Class),如果有则直接进入第8步,否则接着执行第二步。
  2. 如果父类加载器不存在(要么parent是根类加载器,要么本身就是根类加载器),则跳到第四步执行;如果父类加载器存在,则接着执行第三步
  3. 请求使用父类加载器去载入目标类,如果成功载入则跳到第八步,否则接着执行第五步
  4. 请求使用根类加载器来载入目标类,如果成功载入则跳到第八步,否则跳到第七步
  5. 当前类加载器尝试寻找Class文件(从与该ClassLoader相关的类路径中寻找),如果找到则执行第六步,如果找不到则跳到第七步
  6. 从文件载入Class,成功载入后跳到第八步
  7. 抛出ClassNotFoundException异常
  8. 返回对应的java.lang.Class对象
第5、6步允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写loadClass()方法来实现自己的载入过程


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

JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。

ClassLoader有两个关键方法:
loadClass(String name,boolean resolve):该方法为ClassLoader的入口点,根据指定名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象
findClass(String name):根据指定名称来查找类,一般推荐重写该方法来实现自定义类加载器

loadClass()方法的执行步骤:
  1. 用findLoadedClass(String) 来检查是否已经加载类,如果已经加载则直接返回
  2. 在父类加载器上调用loadClass()方法。若父类加载器为null,则使用根类加载器来加载
  3. 调用findClass(String)方法查找类
重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更复杂

实例 P827

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

URLClassLoader类

Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类(此处的父类,就是指类与类直接的继承关系)。URLClassLoader功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。

在应用程序中可以直接使用URLClassLoader加载类。

实例 P830

创建URLClassLoader时传入了一个URL数组参数,该ClassLoader就可以从这系列URL制度的资源中加载指定类,这里的URL可以以file:为前缀,表明从本地文件系统加载;可以以http:为前缀,表明从互联网通过HTTP访问来加载;也可以以ftp:为前缀,表明从互联网通过FTP访问来加载

1 0
原创粉丝点击