赞
踩
众所周知Java 源文件最终会被编译成为字节码文件(class),而运行前需要先通过类加载器(ClassLoader)把class文件加载,因为在字节码文件中存储的各种关于类的信息,最终都是需要加载到虚拟机(JVM)之后才会运行和使用的。
JVM把描述类的信息从class文件加载到内存,并对数据完整性进行校验、转换解析和初始化,最终形成可以在JVM中直接使用的Java类型。Java 采用的是动态方案,类型的加载、链接和初始化都是在程序运行期间完成的,虽然在类加载时会增加一些额外的开销,但是正是因为运行期动态加载和动态链接使得Java语言具有优秀的扩展性,执行加载class文件工作时采用的是双亲委派机制。对于同一个类(package路径及类名都是一样的)在同一个ClassLoader中只会被加载一次
JVM对于类的生命周期定义了四大阶段,七个步骤
如上图所示,加载、验证、准备、使用、卸载这几个步骤的相对顺序是固定的,类加载都必须按照这种固定顺序进行,为了支持动态绑定,会先进行初始化然后才进行解析。
加载阶段和链接阶段的部分工作是交叉进行的,比如一部分的字节码文件验证动作,加载阶段尚未完成,链接阶段可能给已经开始。
加载是类加载全流程中的起点阶段,在加载阶段时,JVM一般需要完成以下3件事:
因为.class文件是以“全类名+ClassLoader名称”作为唯一标识,加载于方法区内部
Java虚拟机规范并没有严格限定字节流的来源,所以可以从:
总之,在加载时获取类的字节流时,是扩展性最强的,可以使用系统提供的引导类加载器来完成,也可以由用户自定义类加载器(继承ClassLoader并重写loadClass方法,再调用父类的loadClass方法进行加载)去完成。但对于数组类的加载情况比较特殊,虽然数组类本身是由JVM直接创建的,但是其元素最终还是需要依靠类加载器去加载的。
验证是链接阶段的第一个步骤,主要是为了确保class文件中的字节流包含的信息符合规范。因为class文件可以使用任何途径生成,甚至是直接用十六进制的编辑器直接编写创建Class文件都可以,若class文件不规范则会抛出一个java.lang.VerfifyErro的异常。验证阶段大致完成以下四方面的检验工作:
验证字节流书否符合class文件格式的规范,主要有以下验证点(部分):
经过文件格式验证之后,才会把字节流保存到内存中的方法区(相当于是把字节流缓存为对应的存储结构,为后续的操作提供数据源)
对方法区中对应的存储结构所描述的信息进行语义解析,主要是一些语法规范验证(部分):
字节码验证主要是通过数据流和控制流分析,确保程序语义是合法且符合逻辑的,主要是对方法体进行检验解析,确保方法运行时不会危害到JVM。
在JVM将符号引用转化为直接引用时(这个转化在解析阶段时触发),可以看成是对类自身以外(常量池的各种符号引用)的信息进行匹配性校验(部分):
虽然验证阶段是一个非常重要的阶段,但并非是必要的,若能确保所运行的代码不已被反复使用和验证过,则在实施时就可以给JVM配置**- Xverify:none**参数来关闭大部分验证措施,以缩短JVM的加载时间。
准备阶段时在方法区正式为类的静态变量分配内存并设置初始值,注意是默认的初始值,而非定义时的赋值且仅针对静态变量,因为实例变量是在对象实例化时随着对象一起分配到Java堆中的。
public static int v=666;//准备阶段后v的值依然为默认值0,而非666,因为此时尚未执行任何Java方法,而把v赋值为666的指令putstatic 是在编译后,存放在类构造器< clinit >方法中的,因此在初始化之后才会执行。
在解析阶段,JVM将常量池的符号引用(Symbolic References)替换为直接引用(Direct References)。
符号引用——符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量,只要使用时可以无歧义地精确定位到目标即可,在class文件中以CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。
直接引用——直接引用是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。
总之解析就是转为直接引用,比如对类或接口、字段、方法(包含类的方法和接口方法)的解析,由于篇幅问题不再详述。
初始化是类加载的最后一步(后面就是使用和卸载了),前面的类加载过程,除了在加载阶段用户可以通过自定义的类加载器参与之外,其余的动作都是完全由JVM主导和控制的(这也很好理解嘛,如果你的系统随意开放安全性相关的步骤是及其危险的),即到了初始化阶段时,才真正开始执行类中定义的Java程序代码(字节码)。
Java虚拟机规范中并没有强制约束何时开始进行类加载,具体细节交由JVM自己把握,但是对于类的初始化时机严格进行约束,以下五种情况JVM必须进行初始化(当然加载、验证、准备自然是先于此开始):
Java虚拟机规范对以上五种场景的行为称为对类的主动引用,有且仅有以上五种情况才会触发初始化。其他方式的引用称为被动引用,常见的有以下情况:
从另一个角度说初始化就是执行类构造器方法 < clinit > 方法的过程。
编译器根据语句在源文件中出现的顺序开始收集,当类中出现了静态代码块或者对静态变量的赋值操作时,编译器会自动为这个类生成< clinit >方法且执行< clinit >方法,执行时会先执行父类的< clinit >方法;不过,接口中虽然不能使用静态代码块,但编译器始终都会为接口生成 < clinit > 方法,但是接口中执行< clinit >方法时不需要先执行父接口的 < clinit >方法且接口的实现类在初始化时不一定会先执行接口的< clinit>方法,只有在父接口定了变量使用时,父接口才会被初始化。< clinit >类构造器方法主要是将静态代码块和静态变量的初始化,按照固定的顺序存放到< clinit > 方法下:
< clinit >实例构造器方法是Java源文件在编译之后会在字节码文件中自动生成的,用于对象的实例化,实例构造器会将语句块,变量初始化,调用父类的实例构造器等操作,按照固定的顺序存放到< init > 方法下:
总之,< clinit >方法是在类初始化中执行的,而< init >则是在对象实例化执行的,所以< clinit >一定比< init >先执行。
类加载器虽然只用于实现类的加载,但在java程序中起到的作用远远不止于此,在同一个JVM中有且只有被同一个类加载器加载的类才“相等”,否则即使来源于同一个class文件,被同一个JVM中的不同加载器加载也必定不相等。
相等:指的是对应的Class对象的equals、isAssignableFrom、isInstance、instanceof方法返回的结果相等,也可以认为是同一对象。
前文中描述,在类的加载流程中的类加载阶段,JVM规范中并没有限定字节码的来源,而是允许把这个动作扩展开放到允许应用程序自己决定如何去获取所需的字节码,而实现这个功能的代码模块统称为类加载器,通俗来说,类加载器通过全限定类名把对应的class文件加载到JVM内存并转为对应的Class对象。而从JVM的角度上来看,只存在两种不同的类加载器:引导类加载器Bootstrap ClassLoader和所有其他的类加载器
在Hotpot中 引导类加载器由C++实现,作为HotSpot的一部分,而其他的类加载器则由Java 语言实现且均继承自java.lang.ClassLoader,独立于HotSpot之外。
而从开发者角度上来看,可以分为四种类加载器:
public ClassLoader getClassLoader(){
ClassLoader loader==getClassLoader();
if(loader==null){
return null;
}
SecurityManager manager=System.getSecurityManager();
if(manager!=null){
ClassLoader loader2=ClassLoader.getCallerLoader();
if(loader2!=null && loader2!=loader && !loader.isAncestor(loader2)){
manager.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
return loader;
}
扩展类加载器和引用程序类加载器都是由SUN 实现,分别对应着sun.mic.Launcher E x t C l a s s L o a d e r 、 s u n . m i c . L a u n c h e r ExtClassLoader、sun.mic.Launcher ExtClassLoader、sun.mic.LauncherAppClassLoader。
应用程序就是通过上面的三种类加载器(或许还有自定义的类加载器)互相配合完成加载工作的。(所谓双亲委派指的是每一次的应用程序类加载器加载时,都至少需要依次访问他上面两层的扩展类加载器和引导类加载器)因为Java中的类加载机制是层级委托的关系,除了顶层的根加载器之外,其余的类加载器都应有接收自己委托请求的加载器(即所谓的“父”类加载器,两者并不存在直接的继承关系,而是组合关系),本质上来说双亲委派关系就是层级优先关系。
如上图所示,默认情况下当一个类加载器收到加载请求时,直接委托给他的“父”类加载器,“父”类加载器再委托给其“父”类,如此循环,因此所有请求都传递到引导类加载器中,有且仅当“父”类加载器反馈自己无法完成时,“子”类加载器才会尝试自己去加载。简而言之,就是每次都先委托给“父”类加载,当“父”类无法加载时,自己再去加载。
在源码中的注释中,有一段说明,在自定义类加载器时,已经不建议去重写loadClass方法,而是应该把自己的类加载逻辑写到findClass方法中,因为在loadClass方法里,“父”类加载器无法加载时就会调用自己的findClass方法,当然这样是为了确保不破坏双亲委派模型,ClassLoader的默认实现就是双亲委派模型。
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)) { // TODO 第1步,检测name对应的类是否已经加载过了 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime();// try { if (parent != null) { //TODO 第2,如果“父”ClassLoader不为空则委托给“父”ClassLoader {@link ClassLoader parent 是ClassLoader的成员} c = parent.loadClass(name, false); } else { //TODO 第2,“父”ClassLoader为空则直接返回由根加载器加载的类对象,为null则说明没有找到 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 抛出异常,则说明“父”无法完成加载,再走到第3步 } if (c == null) { long t1 = System.nanoTime(); //TODO 第3、 若依然还没有找到,也说明“父”类不为null但也无法加载,则调用类加载器自身的findClass方法(自身指的是“子”类加载器) c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { //通过jni 链接加载的类c resolveClass(c); } return c; } }
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
字节数组一般是class文件读取后的最终的字节数组,若是经过加密的,则需要解密后再传入
protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)
throws ClassFormatError{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
//通过JNI 调用
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
package com.example.david.gifdavid; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; /** * @author : Crazy.Mo */ public class MyLoader extends ClassLoader { private static final char CHAR_SEPARATOR = '/'; private String rootDir; public MyLoader(String rootDir) { this.rootDir = rootDir; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); //查找已经被加载的类,返回对应name的Class类的实例 //不为空则返回已经加载过的类 if (null != c) { return c; } else { ClassLoader parent = this.getParent(); try { c = parent.loadClass(name); //委派父类加载 } catch (Exception e) { e.printStackTrace(); } if (c != null) { return c; } else{ //如果还没获取,则读取d:/myjava/cn/sxt/in/User.class下的文件,转换成字节数组 byte[] classData = loadBytes(name); if (classData == null) { throw new ClassNotFoundException(); //如果没加载到,手动抛出异常 } else { c = defineClass(name, classData, 0, classData.length); } } } return c; } /** * * @param classname class文件的全类名,不包含.class * @return */ private byte[] loadBytes(String classname) { String path = rootDir + File.separator + classname.replace('.', CHAR_SEPARATOR) + ".class"; ByteArrayOutputStream bytesOut = null; InputStream input = null; try { input = new FileInputStream(path); bytesOut = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = -1; while ((len = input.read(buffer)) != -1) { bytesOut.write(buffer, 0, len); } bytesOut.flush(); return bytesOut.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (null != input) { input.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (null != bytesOut) { bytesOut.close(); } } catch (IOException e) { e.printStackTrace(); } } return null; } }
文章绝大部分内容整理摘自周志明《深入理解Java虚拟机 JVM高级特性与最佳实战》。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。