虚拟机类加载机制
转载请注明原帖地址:http://www.cnblogs.com/dongxiao-yang/p/5369195.html
java代码编译后产生的文件是各种Class字节码文件,这些文件都需要被jvm虚拟机加载到内存中才可以运行。从类被加载到jvm内存开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段。如下图所示:
一 类加载的时机
类加载过程的第一个阶段:加载(Loading),什么时候开始,jvm规范并没有强制约束。但是对于阶段初始化(Initialization),虚拟机严格规定了有且只有5种情况必须对类进行初始化,显然在初始化前加载(Loading)阶段一定会先执行,所以这几种情况一定会调用类加载。
1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)时、以及调用一个类的静态方法的时候。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要触发父类的初始化。
4.当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.当使用JDK1.7的动态语言支持时,如果一个java.lang.invokeMethodHandle实例的最后解析结果是REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
二 类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的模块被称为"类加载器"。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话说的更通俗一些:比较两个类是否“相等”,只有在这两个类由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个class文件,同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不同。
测试代码
1 package com.youku.data.mr.too.jvm; 2 3 import java.io.IOException; 4 import java.io.InputStream; 5 6 public class ClassLoaderTestLL { 7 8 public static void main(String[] args) { 9 10 ClassLoader mycl = new ClassLoader() { 11 @Override 12 public Class<?> loadClass(String name) 13 throws ClassNotFoundException { 14 // TODO Auto-generated method stub 15 //return super.loadClass(name); 16 String filename = name.substring(name.lastIndexOf(".")+1)+".class"; 17 //System.out.println(filename); 18 InputStream is =getClass().getResourceAsStream(filename); 19 if (is == null) { 20 System.out.println("parent :"+filename); 21 return super.loadClass(name); 22 } 23 byte[] b = null; 24 System.out.println("son :"+filename); 25 try { 26 b = new byte[is.available()]; 27 is.read(b); 28 } catch (IOException e) { 29 // TODO Auto-generated catch block 30 e.printStackTrace(); 31 } 32 33 return defineClass(name, b, 0, b.length); 34 } 35 }; 36 37 Object obi; 38 try { 39 obi = mycl.loadClass("com.youku.data.mr.too.jvm.ClassLoaderTestLL").newInstance(); 40 System.out.println(obi.getClass()); 41 42 System.out.println(obi instanceof com.youku.data.mr.too.jvm.ClassLoaderTestLL); 43 44 } catch (InstantiationException e) { 45 // TODO Auto-generated catch block 46 e.printStackTrace(); 47 } catch (IllegalAccessException e) { 48 // TODO Auto-generated catch block 49 e.printStackTrace(); 50 } catch (ClassNotFoundException e) { 51 // TODO Auto-generated catch block 52 e.printStackTrace(); 53 } 54 55 56 } 57 58 }
这段代码主要是参考了深入jvm虚拟机里面的代码,不同的是在一些地方增添了输出日志,结果如下。
son :ClassLoaderTestLL.class
parent :Object.class
parent :Throwable.class
parent :InstantiationException.class
parent :IllegalAccessException.class
parent :ClassNotFoundException.class
parent :ClassLoader.class
son :ClassLoaderTestLL$1.class
class com.youku.data.mr.too.jvm.ClassLoaderTestLL
false
不出意料的
System.out.println(obi.getClass());
System.out.println(obi instanceof com.youku.data.mr.too.jvm.ClassLoaderTestLL);
这两句输出的结果是类不相等。
加了
System.out.println("parent :"+filename);
System.out.println("son :"+filename);
这两段输出很清晰的显示了哪些类调用的新loadclass里的inputstream过程。
三 双亲委派模型
从java虚拟机的角度讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用c++实现,是虚拟机自身的一部分;另一种就是所有其它的类加载器,这些加载器都是有java语言实现,独立于虚拟机外部,并且全部都继承自抽象类java.lang.ClassLoader。也可以细化为3种类加载器。
-
启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所制定的路径中,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机中。启动类加载器无法被java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null替代即可。
-
扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
-
应用程序类加载器(Application ClassLoader):这个类加载器有sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以一般也称为系统类加载器。它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序没有自定义过加载器,一般情况下这个就是默认的加载器。
上图加载器之间的这种层次关系称为双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承的关系来实现,而都使用组合关系老复用父加载器的代码。
这个可以从ClassLoader这个类的源代码里看出来
1 */ 2 public abstract class ClassLoader { 3 4 private static native void registerNatives(); 5 static { 6 registerNatives(); 7 } 8 9 // The parent class loader for delegation 10 // Note: VM hardcoded the offset of this field, thus all new fields 11 // must be added *after* it. 12 private final ClassLoader parent;
其中的parent是以一个类内部对象的形式出现的
双亲委派模型的工作过程是:如果一个类加载器收到类类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器自己无法完成这个加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去加载。
为了维持双亲委派模型的原则,目前ClassLoader的loadclass方法代码为
1 protected Class<?> loadClass(String name, boolean resolve) 2 throws ClassNotFoundException 3 { 4 synchronized (getClassLoadingLock(name)) { 5 // First, check if the class has already been loaded 6 Class c = findLoadedClass(name); 7 if (c == null) { 8 long t0 = System.nanoTime(); 9 try { 10 if (parent != null) { 11 c = parent.loadClass(name, false); 12 } else { 13 c = findBootstrapClassOrNull(name); 14 } 15 } catch (ClassNotFoundException e) { 16 // ClassNotFoundException thrown if class not found 17 // from the non-null parent class loader 18 } 19 20 if (c == null) { 21 // If still not found, then invoke findClass in order 22 // to find the class. 23 long t1 = System.nanoTime(); 24 c = findClass(name); 25 26 // this is the defining class loader; record the stats 27 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); 28 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 29 sun.misc.PerfCounter.getFindClasses().increment(); 30 } 31 } 32 if (resolve) { 33 resolveClass(c); 34 } 35 return c; 36 } 37 }
所以jdk1.2以后已经不提倡用户自定义classloader的时候自己重写loadClass()方法,应当把自己的类加载器逻辑写到findClass()方法中去,在loadClass()如果父类加载失败,则会调用自己的findClass()来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。
参考资料:深入理解java虚拟机 第二版