您的位置:首页 > 职场人生

黑马程序员_java基础之类加载器解析

2014-10-11 10:08 507 查看
-------android培训java培训、期待与您交流!
----------

JVM简介及其运行的原理

1、JVM

JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。从Java平台的逻辑结构上来看,我们可以从下图来了解JVM:


从上图能清晰看到Java平台包含的各个逻辑模块,也能了解到JDK与JRE的区别。
对于JVM自身的物理结构,我们可以参考下图:
                                                                       



2、Java语言程序运行的过程

Java语言写的源程序通过Java编译器,编译成与平台无关的‘字节码程序’(.class文件,也就是0,1二进制程序),然后在OS之上的Java解释器中解释执行。



也相当与



注:JVM(java虚拟机)包括解释器,不同的JDK虚拟机是相同的,解释器不同。

3、Java虚拟机与程序的生命周期

在如下几种情况下,Java虚拟机将结束生命周期:

            执行了System.exit()方法;

            程序正常执行结束;

            程序在执行过程中遇到了异常或错误而异常终止;

            由于操作系统出现错误而导致Java虚拟机进程终止;

4、类的加载、连接、初始化

1)、加载:查找并加载类的二进制数据;

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构;

加载.class文件的方式:

            从本地系统中直接加载;

            通过网络下载.class文件;

            从zip,jar等归档文件中加载.class文件;

            从专有数据库中提取.class文件;

            将Java源文件动态编译为.class文件;

类的加载的最终产品是位于堆区中的Class对象;

Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

2)、连接:

           验证:确保被加载的类的正确性确;

                      类文件的结构检查:保类文件遵从java类文件的固定格式;

                      语义检查:确保类本身符合java语言的语法规定,比如验证final类型的类没有子类以及final类型的方法没有被覆盖;

                      字节码验证:确保字节码可以被java虚拟机安全地执行;

                      二进制兼容性的验证:确保相互引用的类之间协调一致;

                      其他方面的验证以确保被加载类的正确性。

          准备:为类的静态变量分配内存,并将其初始化为默认值;

           解析:把类中的符号引用转换为直接引用;

3)、初始化:为类的静态变量赋予正确的初始值;

Java程序对类的使用方式可分为两种:主动使用、被动使用。

所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们;

主动使用(六种)

              创建类的实例;

              访问某个类或接口的静态变量,或者对该静态变量赋值;

               调用类的静态方法;

               反射(如Class.forName(“com.shengsiyuan.Test”));

              初始化一个类的子类;

             Java虚拟机启动时被标明为启动类的类(Java Test);

除了以上六种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化;

经历上述步骤后就可以使用内存中生成的Class类的实例对象去创建类的对象去使用。

其中类的加载需要类的加载器,下面就介绍java中的类加载机制。

java类加载器

类加载器(class loader)用来加载 Java类到 Java
虚拟机中。一般来说,Java
虚拟机使用 Java 类的方式如下:Java源程序(.java
文件)在经过 Java
编译器编译之后就被转换成 Java字节代码(.class
文件)。类加载器负责读取 Java
字节代码,并转换成java.lang.Class类的一个实例。

类加载器的树状组织结构

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java应用开发人员编写的。系统提供的类加载器主要有下面三个:

1、引导类加载器(bootstrap class loader):它用来加载 Java的核心库,是用原生代码来实现的,并不继承自
java.lang.ClassLoader。

2、扩展类加载器(extensions class loader):它用来加载 Java的扩展库。Java
虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类。

3、系统类加载器(system class loader):它根据 Java应用的类路径(CLASSPATH)来加载
Java类。一般来说,Java
应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

除了引导类加载器之外,所有的类加载器都有一个父类加载器。通过上表中给出的 getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器
Java 类的类加载器。因为类加载器 Java类如同其它的 Java
类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器,下图中给出了一个典型的类加载器树状组织结构示意图,其中的箭头指向的是父类加载器。

类加载器树状组织结构示意图



java.lang.ClassLoader类介绍

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载
Java应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,比较重要的方法如下表所示。关于这些方法的细节会在下面进行介绍。

ClassLoader 中与加载类相关的方法

方法

说明

getParent()

返回该类加载器的父类加载器。

loadClass(String name)

加载名称为 name的类,返回的结果是 java.lang.Class类的实例。

findClass(String name)

查找名称为 name的类,返回的结果是 java.lang.Class类的实例。

findLoadedClass(String name)

查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。

defineClass(String name, byte[] b, int off, int len)

把字节数组 b中的内容转换成 Java类,返回的结果是 java.lang.Class类的实例。这个方法被声明为
final的。

resolveClass(Class<?> c)

链接指定的 Java 类。

对于上表中给出的方法,表示类名称的 name参数的值是类的二进制名称。需要注意的是内部类的表示,如 com.example.Sample$1和
com.example.Sample$Inner等表示方式。这些方法会在下面介绍类加载器的工作机制时,做进一步的说明。下面介绍类加载器的树状组织结构。
//演示类加载器的树状组织结构
public class ClassLoaderTree {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTree.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader()方法就可以获取到此引用。上面的代码中通过递归调用 getParent()方法来输出全部的父类加载器。其的运行结果如下所示。

sun.misc.Launcher$AppClassLoader@9304b1
sun.misc.Launcher$ExtClassLoader@190d11


如结果所示,第一个输出的是 ClassLoaderTree类的类加载器,即系统类加载器。它是 sun.misc.Launcher$AppClassLoader类的实例;第二个输出的是扩展类加载器,是
sun.misc.Launcher$ExtClassLoader类的实例。需要注意的是这里并没有输出引导类加载器,这是由于有些 JDK的实现对于父类加载器是引导类加载器的情况,getParent()方法返回
null。

类加载器的代理模式(父委托机制)

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下 Java虚拟机是如何判定两个 Java类是相同的。Java
虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java类 com.example.Sample,编译之后生成了字节代码文件
Sample.class。两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个
java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java
虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。

加载类的过程

在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用
loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在
Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类
com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是
java.lang.NoClassDefFoundError异常。

类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

下面讨论另外一种类加载器:线程上下文类加载器。

线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2开始引入的。类 java.lang.Thread中的方法
getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

前面提到的类加载器的代理模式并不能解决 Java应用开发中会遇到的类加载器的全部问题。Java提供了很多服务提供者接口(Service
Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI
有 JDBC、JCE、JNDI、JAXP和
JBI 等。这些 SPI的接口由 Java
核心库来提供,如 JAXP
的 SPI接口定义包含在 javax.xml.parsers包中。这些 SPI的实现代码很可能是作为
Java应用所依赖的 jar
包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI的
Apache Xerces所包含的 jar包。SPI
接口中的代码经常需要加载具体的实现类。如 JAXP中的 javax.xml.parsers.DocumentBuilderFactory类中的
newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由
SPI的实现所提供的。如在 Apache Xerces中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI的接口是
Java 核心库的一部分,是由引导类加载器来加载的;SPI实现的 Java
类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI的实现类的,因为它只加载 Java的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI接口的代码中使用线程上下文类加载器,就可以成功的加载到
SPI实现的类。线程上下文类加载器在很多 SPI的实现中都会用到。

下面介绍另外一种加载类的方法:Class.forName。

Class.forName

Class.forName是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)和
Class.forName(String className)。第一种形式的参数 name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。第二种形式则相当于设置了参数
initialize的值为 true,loader的值为当前类的类加载器。Class.forName的一个很常见的用法是在加载数据库驱动的时候。如
Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用来加载 Apache Derby数据库的驱动。

开发自己的类加载器

虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输 Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在
Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。

文件系统类加载器

第一个类加载器用来加载存储在文件系统上的 Java字节代码。完整的实现如下所示。

//文件系统类加载器
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}

如上所示,类 FileSystemClassLoader继承自类 java.lang.ClassLoader。在表中列出的
java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法
loadClass()封装了前面提到的代理模式的实现。该方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用
findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。

类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class文件),然后读取该文件内容,最后通过
defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐