摘要:
Java中类加载机制非常重要,通过分析类加载机制可以配合理解IOC等相关技术。主要内容为《深入理解JAVA虚拟机》的学习笔记。
章节如下:
1 JVM类加载器
2 类加载方式
3 类的加载全过程
资料:http://lavasoft.blog.51cto.com/62575/184547
http://www.infoq.com/cn/articles/cf-Java-class-loader
http://my.oschina.net/volador/blog/87194?p=3#comments
1 JVM类加载器
1)Bootstrap Loader(启动类加载器):加载System.getProperty("sun.boot.class.path")所指定的路径或jar。
2)Extended Loader(标准扩展类加载器ExtClassLoader):为bootstrap子类,负责加载System.getProperty("java.ext.dirs")所指定的路径或jar。在使用Java运行程序时,也可以指定其搜索路径,例如:java -Djava.ext.dirs=d:\projects\testproj\classes HelloWorld
3)AppClass Loader(系统类加载器AppClassLoader):为Extended子类,负责加载System.getProperty("java.class.path")所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如: java -cp ./lavasoft/classes HelloWorld
4)自定义class Loader
备注:ExtClassLoader和AppClassLoader在JVM启动后,会在JVM中保存一份,并且在程序运行中无法改变其搜索路径。如果想在运行时从其他搜索路径加载类,就要产生新的类加载器。
2 类加载方式
类加载有三种方式:
1)命令行启动应用时候由JVM初始化加载
2)通过Class.forName()方法动态加载,解析,初始化
3)通过ClassLoader.loadClass()方法动态加载,不解析,不初始化
4)自定义classloader
3 类的加载全过程
类加载共包括5个步骤:加载->连接(包括验证,准备,解析)->初始化
3.1 加载:
这一块虚拟机要完成3件事:
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
关于第一点,很灵活,很多技术都是在这里切入,因为它并没有限定二进制流从哪里来:
从class文件来->一般的文件加载
从zip包中来->加载jar中的类
从网络中来->Applet
. .........
相比与加载过程的其他几个阶段,加载阶段可控性最强,因为类的加载器可以用系统的,也可以用自己写的,程序猿可以用自己的方式写加载器来控制字节流的获取。获取二进制流获取完成后会按照jvm所需的方式保存在方法区中,同时会在java堆中实例化一个java.lang.Class对象与堆中的数据关联起来。加载完成后就要开始对那些字节流进行检验了。
3.2检验
检验确保class文件的字节流信息符合jvm的口味。假如class文件是由纯粹的java代码编译过来的,自然不会出现类似于数组越界、跳转到不存在的代码块等不健康的问题,因为一旦出现这种现象,编译器就会拒绝编译了。但是,跟之前说的一样,Class文件流不一定是从java源码编译过来的,也可能是从网络或者其他地方过来的,甚至你可以自己用16进制写,假如jvm不对这些数据进行校验的话,可能一些有害的字节流会让jvm完全崩溃。
检验主要经历几个步骤:文件格式验证->元数据验证->字节码验证->符号引用验证
1)文件格式验证:验证字节流是否符合Class文件格式的规范并验证其版本是否能被当前的jvm版本所处理。保证字节流能正确解析并存储于方法区内,后面的3个校验都是在方法区进行的。
2)元数据验证(语义验证):对字节码描述的信息进行语义化分析,保证其描述的内容符合java语言的语法规范。对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。
3)字节码检验:最复杂,分析数据流和控制流,检验分析方法体,保证其在运行时不会作出危害虚拟机安全的行为。
4)符号引用验证:对类自身以外信息(常量池中的符号引用)进行匹配性校验。(如:权限定名能否找到对应的类,指定类中是否存在符合方法的字段描述符及简单名称描述的方法和字段,引用的类是否能被当前类访问等)这一步将为后面的解析工作打下基础。
验证阶段很重要,但也不是必要的,假如说一些代码被反复使用并验证过可靠性了,实施阶段就可以尝试用-Xverify:none参数来关闭大部分的类验证措施,以简短类加载时间。
3.3准备
准备阶段为类变量(指那些静态变量)分配内存。这里要说明一下,这一步只会给那些静态变量设置一个初始的值,而那些实例变量是在实例化对象时进行分配的。这里给类变量设初始值跟类变量的赋值不同,比如下面:
public static int value=123;
在这一阶段,value的值将会是0,而不是123,因为这个时候还没开始执行任何java代码,123还是不可见的,而我们所看到的把123赋值给value的putstatic指令是程序被编译后存在于<clinit>(),所以,给value赋值为123是在初始化的时候才会执行的。
这里也有个例外:
public static final int value=123;
这里在准备阶段value的值就会初始化为123了。这个是说,在编译期,javac会为这个特殊的value生成一个ConstantValue属性,并在准备阶段jm就会根据这个ConstantValue的值来为value赋值了。
3.4解析
解析将常量池内符号引用替换为直接引用。
符号引用:以一组符号描述所引用的目标,与虚拟机实现的内存布局不相关,引用的目标不一定加载到内存中。
直接引用:直接指向目标的指针,相对偏移量或一个能间接定位目标的句柄。与虚拟机实现的内存布局相关,引用的目标一定加载到内存中。
1) 字段解析
2) 类方法解析
3) 接口解析
3.5初始化过程
在前面的类加载过程中,除了在加载阶段用户可以通过自定义类加载器参与之外,其他的动作完全有jvm主导,到了初始化这块,才开始真正执行java里面的代码。这一步将会执行一些预操作,注意区分在准备阶段,已经为类变量执行过一次系统赋值了。其实说白了,这一步就是执行类<clinit>()方法。
1)<clinit>()方法与类构造器方法不同,它可以自动调用父类<clinit>方法,因此虚拟机中第一个被执行的<clinit>方法的类一定是java.lang.object
2)<clinit>是由编译器自动收集类中的所有类变量的复制动作和静态语句块合并。编译器收集的顺序和源文件定义的顺序一致,也就是说,静态语句块中可以访问语句块之前的变量,定义在其之后的变量只能赋值,不能访问。
3)<clinit>对于类或接口不是必须,如果类没有静态语句块,变量赋值可以不生成<clinit>
4)接口中,执行子接口<clinit>方法不需要先执行父接口<clinit>方法,只有父接口中定义的变量被使用,父接口才会被初始化。
5)虚拟机保证<clinit>方法的同步。
例子:
1 public class Haha { 2 static { 3 if (true) { 4 System.out.println("init Test"); 5 while (true) { 6 } 7 } 8 } 9 10 public static void main(String[] args) { 11 Runnable r = new Runnable() { 12 @Override 13 public void run() { 14 System.out.println(Thread.currentThread() + "start"); 15 Haha t = new Haha(); 16 System.out.println(Thread.currentThread() + "over"); 17 } 18 }; 19 Thread t1 = new Thread(r); 20 Thread t2 = new Thread(r); 21 t1.start(); 22 t2.start(); 23 } 24 }
输出结果:
Init Test 线程阻塞