【转载】JVM类加载机制小结

本文转载自http://shift-alt-ctrl.iteye.com/blog/1845137

 

一.类加载   

    虚拟机把class文件加载至内存之后,对字节码数据进行校验/解析/初始化等操作,最终形成可被VM直接使用的java类型,这就是虚拟机类加载机制.类的加载完全可以在运行时进行,这给VM提供了动态加载类提供了可行性.

    类生命周期过程大概分为:加载-->校验-->解析-->初始化-->使用-->卸载;类在被"使用"之前必须进行前4个阶段,其中初始化工作可以被延迟进行,直到类需要创建实例/static方法或属性被调用(非编译常量)/反射机制中使用类的成员(Member)时被触发.

  1. 加载:通过类的全限定名的方式获取class文件的字节流.事实上,加载器可以从任何类型文件(zip,jar,txt等)或者网络中获取class字节流.参见ClassLoader类
  2. 校验:检查加载至内存的字节流是否符合当前JVM的规范要求,字节流信息需要被当前JVM(考虑到JVM的版本问题)正确的识别,包括字节流的格式/字节码的语义(语法)/类的继承关系/变量的声明合法性等进行全面校验,如果校验成功,则表明此class字节流是安全的/没有被修改的/可以被正常使用的.
  3. 准备:准备阶段是一个过渡阶段,在类文件校验合法之后,将会得出类的结构图,此时有必要对类的属性(static)进行默认值设定,比如static int默认为0,即使指定了值.不过对于final类型的,将会以"常量"特例被处理,直接持有原始值.
  4. 解析:解析阶段是JVM将常量池中的符号引用转换成直接引用的过程.
  5. 初始化:到此为止,一个类的"前戏"工作基本结束,接下来就可以"服务"了,此时类的属性(static)将会被设定为用户指定的值(区别于准备阶段),而且还会执行类的static区块.同时也会连带执行父类的static区块和静态属性的值初始化.对于类的static区块在多线程环境中被同步执行,如果有多个线程同事执行static区块,事实上最终只会在有一个线程执行.

二.类加载器

    对于任意一个类,它的唯一性将有它的类加载器和其类型来决定;如果一个类被多个classLoader加载(最终执行load操作的classLoader必须不同),那么它们最终获得的class引用将不相等(即"!="),即使用==,equals(),instanceof,isInstance(),isAssignableFrom()都得不到预期的值,事实上可以简单的认为,他们是不同的"类"(类型).

  1. 引导类加载器:Bootstrap-classLoader,此类主要负责加载JAVA API,即处于$JAVA_HOME$\lib下的class文件,这些lib位置可以在外部通过-Xbootclasspath指定,但是此目录下的jar文件不能被重命名或者人为增加,否则将不能被正常加载;引导类加载器不能被java程序获得,尝试获得引导类加载器将返回null.
  2. 扩展类加载器:负责加载$JAVA_HOME$\lib\ext目录下的class,开发者可以人为的在此目录下添加jar文件,以便被此JVM之上的所有应用所收益.此加载器可以被程序获取,一般为classpath类加载器的父类.(sun.misc.Launcher$ExtClassLoader)
  3. 应用类加载器:Application ClassLoader,负责加载应用的classpatch下的所有class文件,其中ClassLoader.getSystemClassLoader()获得的就是此加载器.(sun.misc,Launcher$AppClassLoader);自定义类加载器的父加载器一般设定为它.

JVM并没有使用继承关系来组织这三种类加载器,而是采取了组合关系(即classLoader.setParent(...)).

    JVM类加载过程和类关系维护采取了"双亲委派模型":对于任何类加载器(包括自定义类加载器)对于指定类的加载,首先交付给"父-类加载器"查找或加载,如果父-类加载器已经加载则直接获取class引用,否则继续传递当前类加载器的父-类加载器,直到引导类加载器,如果此时引导类加载器也没有持有class信息,则抛出异常(异常信息只作为中断信号),此时当前类加载器(直接接收加载请求的类加载器,比如自定义类加载器)将会尝试加载类信息...通过这种"委派关系",能够巧妙的(当然不是最佳的手段)规避类被重复加载的可能.

 

三.编译与泛型

    java中的泛型,是伪泛型,只是简单的在API级别做了"模样",但是对于类的编译过程,则会导致泛型类型的擦除,即编译过程或者编译之后的文件中,最终泛型仍然以"原生类型"表示.即在运行时无法直接还原"泛型",java还支持了反射机制,为了让反射机制能够和"泛型"配合,那么java最终提供了Generic(例如GenericArrayType)和ParameterizedType来配合获得泛型(和参数化类--类型)的信息,这些信息虽然在编译时被擦除,但是元数据(metadata)仍然被有效的class文件中.

    例如:List<Integer>在编译之后,API级别上就成了List,"丢失"了参数化的信息.那么方法 void invoke(List<Integer> list) 和void invoke(List<String> list),那么将不能共存在一个类中,因为编译之后它们是一样的.(识别方法的特性,有方法签名 + 参数列表,而定);但是还有一中特例,如果方法的参数列表中或者返回值中或者当前类是参数化的,那么对于方法的返回值类型也参与了"方法标识";例如上述两个方法,如果第二个方法改成Integer invoke(List<String> list)或者<T> T invoke(List<T> list)将被成功编译.

上一篇:【转载】JVM内存分配与调优参数列表


下一篇:BlockingQueue