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

java类加载机制详解

2017-03-25 19:35 381 查看
今天和大家探讨java虚拟机加载类的机制,jvm想执行.class文件第一步就是把class文件装载进内存。

1.概述

        引子:编程过程中,大家肯定遇到过java.lang.ClassNotFoundException错误,特别是当我们自定义类加载器进行类的加载或者利用java的反射机制获取Class对象时。
       虚拟机整体架构的第一部分就是类装载器,Java类加载机制指的是将java编译后生成的.class文件中的二进制数据读入到内存中,将其放到jvm构造的运行时数据区内存的方法区中,在heap堆区创建对应的描述Class结构的原信息对象,通过该元数据对象可以获取Class结构的众多信息,用户可以通过此Class信息调用class对象的功能。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口,这就是虚拟机的类加载机制。

2类加载的过程

类的加载过程包括类加载的过程包括了装载、链接(验证、准备、解析)、初始化五个阶段。五个阶段中加载,验证,准备和初始化发生的顺序是确定的,解析过程则不确定,我们都知道java支持动态绑定(多态),运行时才知道最终对象的引用,所以解析阶段可能发生在初始化阶段之后。虽然这几个阶段是按顺序开始的,但他们不是按顺序执行的,也就是说他们的执行和完成的时间不分先后,通常都是相互交叉进行。
     JVM中Java类会在首次被使用时执行初始化,为类的(静态)变量赋予正确的初始值。但这不代表类加载器只有等到某个类被首次主动使用时才加载,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
 

(1)装载阶段:查找并加载类的二进制数据

类加载的第一个阶段,jvm虚拟机需要完成下面三个重要工作:
    1、通过一个类的全限定名来获取其定义的二进制字节流。
    2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
 
本阶段可以利用系统提供的类加载器,也可以利用自定义的加载器,加载阶段完成之后,.calss文件已经被加载到方法区中并在堆区中创建了对应的Class对象。

(2)连接阶段:进行验证、准备和解析

1)验证阶段:此阶段主要为保证加载类的正确性。
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
2)准备阶段:为类型分配内存并初始化为默认值
准备阶段为类的静态变量分配内存(在方法区内存中,此处只考虑方法区存在的情况,注:jdk1.7将方法区一部分移到了堆中,jdk1.8元数据区替代了原来的永久代),并将其初始化为默认值,而不是代码中赋予的初始值,初始值的赋予则是在java类初始化阶段完成。对于基本类型的静态变量一般被赋予0,对于引用的静态类型,一般被赋予null,但是如果被final修饰,准备阶段就会被赋予代码中的初始值,此时已经将变量值放入了方法区的常量池中。注:类的实例变量内的分配随着对象的实例化在堆中分配内存。
3)解析阶段:将字符引用解析为直接饮用
把类中的符号引用转化为直接引用,虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,在此之前的符号引用仅仅是一个标识。

(3)初始化阶段:

 初始化,为类的静态变量赋予正确的初始值(上个阶段已经赋值(默认值)),JVM负责对类进行初始化,主要对类变量进行初始化。

JVM初始化步骤

 1、假如这个类还没有被加载和连接,则程序先加载并连接该类
 2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
 3、假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化的时机:

1.创建类的实例
2.访问类的静态变量,或静态变量的赋值
3.调用类的静态方法
4.使用反射获取类的信息,强制创建类的实例对象
5.初始化某个类的子类时
6.直接使用java.exe运行某个类的主类
 

3类加载器:

JVM利用类加载器ClassLoader来完成类的加载,Java虚拟机中的类加载器分为两种:原始类加载器(primordial class loader)和类加载器对象(class loaderobjects)。原始类加载器是Java虚拟机实现的一部分,类加载器对象(java编写)是运行中的程序的一部分。
类加载器利用命名空间进行区分,子加载器的命名空间包含所有父加载器的命名空间。因此子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。由父加载器加载的类不能看见子加载器加载的类。如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。



     类加载器调用了许多Java虚拟机中其他的部分和java.lang包中的很多类。比如,类加载对象就是java.lang.ClassLoader子类的实例,ClassLoader类中的方法可以访问虚拟机中的类加载机制;每一个被Java虚拟机加载的类都会被表示为一个 java.lang.Class类的实例。像其他对象一样,类加载器对象和Class对象都保存在堆中,被加载的信息被保存在方法区中。
           如果从java开发人员的角度来说,类加载器可以分为启动类加载器:BootstrapClassLoader,扩展类加载器:ExtensionClassLoader,应用程序类加载器:Application ClassLoader。第一种属于前文的原始类加载器,后两种成为类加载器对象:

(1)启动类加载器:Bootstrap ClassLoader

主要负责加载存放在\jre\lib目录下,或被-Xbootclasspath参数指定的路径中的(sun.boot.class.path),通常指的是rt.jar里的类。相对于另外的加载器,启动类加载器比较特殊,HotSpot虚拟机中它利用C++ 实现,作为虚拟机自身的一部分,它的实现设计JVM底层的实现细节,所以开发者无法直接获取它的引用,所以从他的子类获取它返回的为null,生成java虚拟机的同时就会生成启动类加载器。Bootstrap
ClassLoader之后自动加载Extension ClassLoader,,将其父Loader设为Bootstrap Loader(获取时为null),之后java实现的类加载器才能够工作。

(2)扩展类加载器:Extension ClassLoader

该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\jre\lib\ext目录中的类库。Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。

(3)应用程序类加载器:Application ClassLoader

该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,作为应用程序中类默认的类加载器。
实例:
 
   
publicstaticvoid main(String[] args) {

AppClass hello = new AppClass();

Classc = hello.getClass();

ClassLoader loader = c.getClassLoader();

System.out.println(loader); //sun.misc.Launcher$AppClassLoader@4e0e2f2a

System.out.println(loader.getParent()); //sun.misc.Launcher$ExtClassLoader@2a139a55

System.out.println(loader.getParent().getParent());//null

}


可以看到打印结果中,没有找到ExtClassLoader的父类,因为Bootstrap ClassLoader利用C++实现,无法找到确切的表示形式。

类加载有三种方式:

1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
   
ClassLoader loader = AppClass.class.getClassLoader();

System.out.println(loader);

//使用ClassLoader.loadClass()来加载类,不会执行初始化块

try {

loader.loadClass("com.ts.exercise2.AppClass2");

} catch (ClassNotFoundExceptione) {

e.printStackTrace();

}

//使用Class.forName()来加载类,默认会执行初始化块

try {

Class.forName("com.ts.exercise2.AppClass2");

} catch(ClassNotFoundException e) {

e.printStackTrace();

}

//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块

try {

Class.forName("com.ts.exercise2.AppClass2", false, loader);

} catch(ClassNotFoundException e) {

e.printStackTrace();

}


 

4双亲委派机制:

JVM在类加载时默认采取双亲委派机制,如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

Java程序中区分一个类通过其全类名进行匹配,如果一个类的全类名相同就被认为是一个类,但是在JVM中识别一个类则是利用全类名+类加载器,“全类名+类加载器”被作为一个唯一的标识识别不同的类,不同的类加载器会把类放在不同的命名空间中。

来看看源码中类加载的过程:

 
protectedClass<?> loadClass(Stringname,booleanresolve)

throws ClassNotFoundException

{

synchronized (getClassLoadingLock(name)) {

// First, check if the class has already been loaded

Class c = findLoadedClass(name);//首先检查class是否已被加载过

if (c == null){//如果没有被加载过,循环查找父类加载器

longt0 = System.nanoTime();

try {

//父类加载器不为空,且不为bootstrap加载器,执行父类的loadClass(),

//把类加载请求一直向上抛,直到父加载器为null(是Bootstrap ClassLoader)为止

if (parent != null){

c = parent.loadClass(name, false);

} else {

c =findBootstrapClassOrNull(name);

}

} catch (ClassNotFoundException e) {

// ClassNotFoundException thrown if class not found

// from the non-null parent class loader

}

if (c == null){

// If still not found, then invoke findClass in order

// to find the class.

//父加载器开始尝试加载.class文件,加载成功就返回一个java.lang.Class,

//加载不成功就抛出一个ClassNotFoundException,给子加载器去加载

longt1 = System.nanoTime();

c = findClass(name);

// this is the defining class loader; record the stats

sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);

sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);

sun.misc.PerfCounter.getFindClasses().increment();

}

}

if (resolve) {

//解析这个.class文件,主要就是将符号引用替换为直接引用的过程

resolveClass(c);

}

returnc;

}

}


整个loadClass源代码体现了双亲委派机制的过程。具体的加载过程在findClass()中。

采取双亲委派机制,只要是放在特定目录的类,无论哪个类开始加载他,最终都只能是能够加载它的那个类加载器加载本类,此时用户可以定义和这个被加载的类相同全类名的类,放在ClassPath目录下,则最终加载之后他们是放在不同的命名空间中的,如果不采取双亲委派,此时某个类被加载时就会被第一次调用它的加载器加载,这时在定义不同目录下相同全类名的类,就会导致混乱。

5自定义类加载器

为什么要自定义类加载器:
简单的原因就是因为系统提供的类加载器无法满足实际某些场景的应用,
类似javaWeb服务器,可能在一个服务器上部署了多个网站,每个网站会用到一些相同的类库,如果只利用系统的类加载器则只能存在一种类库,对于不同版本的类库则不能同时存在;jsp热替换的支持, JSP最终要编译成.class文件才能由虚拟机执行,但JSP运行时修改的概率远远大于第三方类库或自身.class文件,而且JSP这种网页应用也把修改后无须重启作为一个很大的优势看待,这些系统提供的加载器都无法完全满足,需要自己定义加载器。
ClassNotFoundException。

@since  1.2

protectedClass<?> findClass(String name) throwsClassNotFoundException {

thrownew ClassNotFoundException(name);

}


双亲委派机制的讲解说明了真正的加载过程在findClass()函数中,可以看到没有具体实现,默认抛出一个  
要创建用户自己的类加载器,只需要扩展java.lang.ClassLoader类,然后覆盖它的findClass(Stringname)方法即可,该方法根据参数指定类的名字(二进制名字),返回对应的Class对象的引用。此方法将被 loadClass 方法调用,默认实现了双亲委派机制。
代码示例:
定义Student类
定义自定义类加载器
publicclassClassLoaderDemo extends ClassLoader {

public ClassLoaderDemo() {

super();

// TODO Auto-generated constructor stub

}

publicClassLoaderDemo(ClassLoader parent) {

super(parent);

// TODO Auto-generated constructor stub

}

@Override

public Class<?>loadClass(String name)throwsClassNotFoundException {

returnsuper.loadClass(name);

}

/*

* (non-Javadoc)

*

* @seejava.lang.ClassLoader#findClass(java.lang.String) 重写findClass

*/

protected ClassfindClass(String name)throwsClassNotFoundException {

Filefile= newFile("C:/Student.class");//把生成的class文件复制到c盘根目录

try {

byte[] bytes = getClassBytes(file);

// 利用字节数组转换成Class对象

Classc = this.defineClass(name, bytes, 0, bytes.length);

returnc;

}catch(Exception e) {

e.printStackTrace();

}

returnsuper.findClass(name);

}

/*

* // 利用字节流读入.class的字节

*/

privatebyte[] getClassBytes(File file) throws Exception {

FileInputStreamfis= newFileInputStream(file);

FileChannelfc = fis.getChannel();

ByteArrayOutputStreambaos= newByteArrayOutputStream();

WritableByteChannelwbc= Channels.newChannel(baos);

ByteBufferby = ByteBuffer.allocate(1024);

while (true) {

inti = fc.read(by);

if (i == 0 || i == -1)

break;

by.flip();

wbc.write(by);

by.clear();

}

fis.close();

returnbaos.toByteArray();

}

}


 
测试程序
publicstaticvoidmain(String[] args)throws Exception {

ClassLoaderDemocld=newClassLoaderDemo(ClassLoader.getSystemClassLoader().getParent());

Class<?>cc=Class.forName("cn.zzu.xingong.demo.Student", true,cld);

//ClassLoaderDemocld=new ClassLoaderDemo();

Objectobject=cc.newInstance();

System.out.println(object);

System.out.println(object.getClass().getClassLoader());

}


结果:
cn.zzu.xingong.demo.Student@1cf536e8
cn.zzu.xingong.demo.ClassLoaderDemo@590a8143

测试代码中,构建ClassLoaderDemo时添加了ClassLoader.getSystemClassLoader().getParent()参数即把自定义ClassLoader的父加载器设置为Extension ClassLoader,,父类有两个构造函数,其实也可以不带参数,如果不带参数其父类默认为Application ClassLoader,此时结果为:
cn.zzu.xingong.demo.Student@72ffb35e

sun.misc.Launcher$AppClassLoader@456999c8
因为利用eclipse开发,自动编译文件,在CLASSPATH下有Studeng.class,那么自然是由Application ClassLoader来加载这个.class文件了,可以删除生成的class文件,AppClassLoader无法加载,就会交给自己的子类加载,或者给自定义类加载器指定父类为Extension ClassLoader,加载不到Student.class文件,就会交给子类加载器加载。

待续!后面还会继续补充。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息