分析Java类加载全过程
2012-11-06 10:19 OSCHINA 字号:T | T
一个Java文件从被加载到被卸载这个生命过程,总共要经历4个阶段,那么是哪4个阶段呢?下面作者将详细的给网友解答……
AD:WOT2015 互联网运维与开发者大会热销抢票今天去涉猎了一下类的加载的过程,现在也总结一下:
一个java文件从被加载到被卸载这个生命过程,总共要经历4个阶段:
加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载
其中加载(除了自定义加载)+链接的过程是完全由jvm负责的,什么时候要对类进行初始化工作(加载+链接在此之前已经完成了),jvm有严格的规定(四种情况):
1.遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,加入类还没进行初始化,则马上对其进行初始化工作。其实就是3种情况:用new实例化一个类时、读取或者设置类的静态字段时(不包括被final修饰的静态字段,因为他们已经被塞进常量池了)、以及执行静态方法的时候。
2.使用https://www.wendangku.net/doc/ee14254296.html,ng.reflect.*的方法对类进行反射调用的时候,如果类还没有进行过初始化,马上对其进行。
3.初始化一个类的时候,如果他的父亲还没有被初始化,则先去初始化其父亲。
4.当jvm启动时,用户需要指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm 会先去初始化这个类。
以上4种预处理称为对一个类进行主动的引用,其余的其他情况,称为被动引用,都不会触发类的初始化。下面也举了些被动引用的例子:
1./**
2. * 被动引用情景1
3. * 通过子类引用父类的静态字段,不会导致子类的初始化
4. * @author volador
5. *
6. */
7.class SuperClass{
8.static{
9. System.out.println("super class init.");
10. }
11.public static int value=123;
12.}
13.
14.class SubClass extends SuperClass{
15.static{
16. System.out.println("sub class init.");
17. }
18.}
19.
20.public class test{
21.public static void main(String[]args){
22. System.out.println(SubClass.value);
23. }
24.
25.}
输出结果是:super class init。
1./**
2. * 被动引用情景2
3. * 通过数组引用来引用类,不会触发此类的初始化
4. * @author volador
5. *
6. */
7.public class test{
8.public static void main(String[] args){
9. SuperClass s_list=new SuperClass[10];
10. }
11.}
输出结果:没输出
1./**
2. * 被动引用情景3
3. * 常量在编译阶段会被存入调用类的常量池中,本质上并没有引用到定义常量类类,所以自然不会触发定义常量的类的初始
化
4. * @author root
5. *
6. */
7.class ConstClass{
8.static{
9. System.out.println("ConstClass init.");
10. }
11.public final static String value="hello";
12.}
13.
14.public class test{
15.public static void main(String[] args){
16. System.out.println(ConstClass.value);
17. }
18.}
输出结果:hello(tip:在编译的时候,ConstClass.value已经被转变成hello常量放进test类的常量池里面了)
以上是针对类的初始化,接口也要初始化,接口的初始化跟类的初始化有点不同:
上面的代码都是用static{}来输出初始化信息的,接口没法做到,但接口初始化的时候编译器仍然会给接口生成一个
下面分解一下一个类的加载全过程:加载->验证->准备->解析->初始化
首先是加载:
这一块虚拟机要完成3件事:
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在java堆中生成一个代表这个类的https://www.wendangku.net/doc/ee14254296.html,ng.Class对象,作为方法区这些数据的访问入口。
关于第一点,很灵活,很多技术都是在这里切入,因为它并没有限定二进制流从哪里来:
从class文件来->一般的文件加载
从zip包中来->加载jar中的类
从网络中来->Applet
..........
相比与加载过程的其他几个阶段,加载阶段可控性最强,因为类的加载器可以用系统的,也可以用自己写的,程序猿可以用自己的方式写加载器来控制字节流的获取。
获取二进制流获取完成后会按照jvm所需的方式保存在方法区中,同时会在java堆中实例化一个https://www.wendangku.net/doc/ee14254296.html,ng.Class 对象与堆中的数据关联起来。
加载完成后就要开始对那些字节流进行检验了(其实很多步骤是跟上面交叉进行的,比如文件格式验证):检验的目的:确保class文件的字节流信息符合jvm的口味,不会让jvm感到不舒服。假如class文件是由纯粹的java代码编译过来的,自然不会出现类似于数组越界、跳转到不存在的代码块等不健康的问题,因为一旦出现这种现象,编译器就会拒绝编译了。但是,跟之前说的一样,Class文件流不一定是从java源码编译过来的,也可能是从网络或者其他地方过来的,甚至你可以自己用16进制写,假如jvm不对这些数据进行校验的话,可能一些有害的字节流会让jvm 完全崩溃。
检验主要经历几个步骤:文件格式验证->元数据验证->字节码验证->符号引用验证
文件格式验证:验证字节流是否符合Class文件格式的规范并验证其版本是否能被当前的jvm版本所处理。ok没问题后,字节流就可以进入内存的方法区进行保存了。后面的3个校验都是在方法区进行的。
元数据验证:对字节码描述的信息进行语义化分析,保证其描述的内容符合java语言的语法规范。
字节码检验:最复杂,对方法体的内容进行检验,保证其在运行时不会作出什么出格的事来。
符号引用验证:来验证一些引用的真实性与可行性,比如代码里面引了其他类,这里就要去检测一下那些来究竟是否存在;或者说代码中访问了其他类的一些属性,这里就对那些属性的可以访问行进行了检验。(这一步将为后面的解析工作打下基础)
验证阶段很重要,但也不是必要的,假如说一些代码被反复使用并验证过可靠性了,实施阶段就可以尝试用
-Xverify:none参数来关闭大部分的类验证措施,以简短类加载时间。
接着就上面步骤完成后,就会进入准备阶段了:
这阶段会为类变量(指那些静态变量)分配内存并设置类比那辆初始值的阶段,这些内存在方法区中进行分配。这里要说明一下,这一步只会给那些静态变量设置一个初始的值,而那些实例变量是在实例化对象时进行分配的。这里的给类变量设初始值跟类变量的赋值有点不同,比如下面:
1.public static int value=123;
在这一阶段,value的值将会是0,而不是123,因为这个时候还没开始执行任何java代码,123还是不可见的,而我们所看到的把123赋值给value的putstatic指令是程序被编译后存在于
这里也有个例外:
1.public static final int value=123;
这里在准备阶段value的值就会初始化为123了。这个是说,在编译期,javac会为这个特殊的value生成一个ConstantValue属性,并在准备阶段jm就会根据这个ConstantValue的值来为value赋值了。
完成上步后,就要进行解析了。解析好像是对类的字段,方法等东西进行转换,具体涉及到Class文件的格式内容,并没深入去了解。
初始化过程是类加载过程的最后一步:
在前面的类加载过程中,除了在加载阶段用户可以通过自定义类加载器参与之外,其他的动作完全有jvm主导,到了初始化这块,才开始真正执行java里面的代码。
这一步将会执行一些预操作,注意区分在准备阶段,已经为类变量执行过一次系统赋值了。
其实说白了,这一步就是执行程序的
下面来个例子说明一下:
1.static class Parent{
2.public static int A=1;
3.static{
4. A=2;
5. }
6.}
7.static class Sub extends Parent{
8.public static int B=A;
9.}
10.public static void main(String[] args){
11. System.out.println(Sub.B);
12.}
首先Sub.B中对静态数据进行了引用,Sub类要进行初始化了。同时,其父类Parent要先进行初始化动作。Parent 初始化后,A=2,所以B=2;上个过程相当于:
1.static class Parent{
2.
3.public static int A=1;
4.static{
5. A=2;
6. }
7. }
8.}
9.static class Sub extends Parent{
10.
11.public static int B=A;
12. }
13.}
14.public static void main(String[] args){
15. System.out.println(Sub.B);
16.}
由于接口里面不能存在static{}这种静态代码块,但仍然可能存在变量初始化时的变量赋值操作,所以接口里面也会生成
另外,接口的实现类在初始化的时候也一样不会执行接口的
另外,jvm会保证一个类的
下面用个例子说明一下:
1.public class DeadLoopClass {
2.
3.static{
4.if(true){
5. System.out.println("要被 ["+Thread.currentThread()+"] 初始化了,下面来一个无限循环");
6.while(treu){}
7. }
8. }
9.
10./**
11. * @param args
12. */
13.public static void main(String[] args) {
14.// TODO Auto-generated method stub
15. System.out.println("toplaile");
16. Runnable run=new Runnable(){
17.
18.@Override
19.public void run() {
20.// TODO Auto-generated method stub
21. System.out.println("["+Thread.currentThread()+"] 要去实例化那个类了");
22. DeadLoopClass d=new DeadLoopClass();
23. System.out.println("["+Thread.currentThread()+"] 完成了那个类的初始化工作");
24.
25. }};
26.
27.new Thread(run).start();
28.new Thread(run).start();
29. }
30.
31.}
这里面,运行的时候将会看到阻塞现象。
北大青鸟推荐:Java精选笔试题(含答案解析)如果你是计算机专业出生,但是还没有找到工作的话,你就得补补技术了,一些关于面试、笔试的题要多刷一刷。有可能你知道答案,但是由于语言组织能力有所欠缺,所以面试官的印象不是很好,下面分享一些Java精选的鄙视题,希望对面试这者有帮助。 1,volatile关键字是否能保证线程安全?() 答案:否 volatile关键字用在多线程同步中,可保证读取的可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的读取值,而非cache中。但多个线程对volatile的写操作,无法保证线程安全。 假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值,在线程1对count进行修改之后,会write到主内存中,主内存中的count变量就会变为6;线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6;导致两个线程及时volatile关键字修改之后,还是会存在并发的情况。 2,下面哪个流类属于面向字符的输入流( ) A、BufferedWriter B、FileInputStream C、ObjectInputStream D、InputStreamReader 答案:D Java的IO操作中有面向字节(Byte)和面向字符(Character)两种方式。
面向字节的操作为以8位为单位对二进制的数据进行操作,对数据不进行转换,这些类都是InputStream和OutputStream的子类。 面向字符的操作为以字符为单位对数据进行操作,在读的时候将二进制数据转为字符,在写的时候将字符转为二进制数据,这些类都是Reader和Writer的子类。 3,Java能不能不通过构造函数创建对象() A、能 B、不能 答案:A Java创建对象的几种方式: (1) 用new语句创建对象,这是最常见的创建对象的方法。 (2) 运用反射手段,调用https://www.wendangku.net/doc/ee14254296.html,ng.Class或者https://www.wendangku.net/doc/ee14254296.html,ng.reflect.Constructor类的newInstance()实例方法。 (3) 调用对象的clone()方法。 (4) 运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。 (1)和(2)都会明确的显式的调用构造函数;(3)是在内存上对已有对象的影印,所以不会调用构造函数;(4)是从文件中还原类的对象,也不会调用构造函数。 4,下列哪个叙述是正确的() A.子类继承父类的构造方法。 B.abstract类的子类必须是非abstract类。 C.子类继承的方法只能操作子类继承和隐藏的成员变量。 D.子类重写或新增的方法也能直接操作被子类隐藏的成员变量。 答案:C 子类是不继承父类的构造方法的,而是必须调用其父类的构造方法。
永隆 JAVA笔试题 一、选择题 1、关于Java 类的加载过程,下面哪些描述是正确的() A、在 Java 中,有四种类型的类加载器:BootStrapClassLoader、ExtClassLoader、AppClassLoader 以及用户自定义的ClassLoader。//Extension ClassLoader, System ClassLoader+用户自定义的classloader B、使用 new 关键字创建类实例时,其实就显示地包含了类的加载过程 C、在 Java 中,类的实例化流程分为两个部分:类的加载和类的实例化。类的加载又分为显式加载和隐式加载。 D、Class.forName 来加载类时,是通过 ExtClassLoader进行加载的。 //system classLoader 加载 2、关于HashMap的实现机制,下面哪些描述是正确的() A、HashMap中key-value 当成一个整体进行处理,系统总是根据数组的坐标来获得key-value 的存储位置。//没有存储顺序,无下标之说! B、HashMap基于哈希表的 Map 接口的实现,允许使用 null 值和 null 键。 C、如果HashMap中,如果Key的hash相同的话,HashMap将会出错。//会替换相应的value D、HashMap每次容量的扩增都是以2的倍数来增加。//大约获得2倍的桶数! 3、下面的代码执行输出正确的是() 1. public class test( 2. public int aMethod()[ 3. static int i=0; 4. i++; 5. return I; 6. ) 7. public static void main (String args[]){ 8. test test = new test(); 9. test.aMethod(); 10.int j = test.aMethod(); 11.System.out.printIn(j); 12.] 13.} A. 编译错误 B. 编译成功,打印出是“0” C. 编译成功,打印出是“1” D. 编译成功,打印出是“2” A 4、如何获取下面表单 select