在加载阶段主要用到的是方法区:
方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。
如果把方法的代码看作它的“静态”部分,而把一次方法调用需要记录的临时数据看做它的“动态”部分,那么每个方法的代码是只有一份的,存储于JVM的方法区中;每次某方法被调用,则在该调用所在的线程的的Java栈上新分配一个栈帧,用于存放临时数据,在方法返回时栈帧自动撤销。
在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成验证、准备、解析(可选的)。
装载:查找和导入类或接口的二进制数据;
链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;
验证:检查导入类或接口的二进制数据的正确性;
准备:给类分配所需存储空间;
解析:将常量池中符号引用转成直接引用,根据实际需要,可以推迟到初始化之后进行;
初始化:也就是为变量赋予正确的初始值,如激活类的静态变量,初始化Java代码和静态Java代码块
2.1 装载
装载阶段主要是将java字节码以二进制的方式读入到jvm内存中,然后将二进制数据流按照字节码规范解析成jvm内部的运行时数据结构。java只对字节码进行了规范,并没有对内部运行时数据结构进行规定,不同的jvm实现可以采用不同的数据结构,这些运行时数据结构是保存在jvm的方法区中(hotspot jvm的内部数据结构定义可以参见撒迦的博文借助HotSpot SA来一窥PermGen上的对象)。当一个类的二进制解析完毕后,jvm最终会在堆上生成一个java.lang.Class类型的实例对象,通过这个对象可以访问到该类在方法区的内容。
jvm规范并没有规定从二进制字节码数据应该如何产生,事实上,jvm为了支持二进制字节码数据来源的可扩展性,它提供了一个回调接口将通过一个类的全限定名来获取描述此类的二进制字节码的动作开放到jvm的外部实现,这就是我们后面要讲到的类加载器,如果有需要,我们完全可以自定义一些类加载器,达到一些特殊应用场景。由于有了jvm的支持,二进制流的产生的方式可以是:
(1) 从本地文件系统中读取
(2) 从网络上加载(典型应用:java Applet)
(3) 从jar,zip,war等压缩文件中加载
(4) 通过动态将java源文件动态编译产生(jsp的动态编译)
(5) 通过程序直接生成。
2.2 连接
连接阶段主要是做一些加载完成之后的验证工作,和初始化之前的准备一些工作,它细分为三个阶段。
2.2.1 验证
验证是连接阶段的第一步,它主要是用于保证加载的字节码符合java语言的规范,并且不会给虚拟机带来危害。比如验证这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。按照验证的内容不同又可以细分为4个阶段:文件格式验证(这一步会与装载阶段交叉进行),元数据验证,字节码验证,符号引用验证(这个阶段的验证往往会与解析阶段交叉进行)。
2.2.2 准备
准备阶段主要是为类的静态变量分配内存,并设置jvm默认的初始值。对于非静态的变量,则不会为它们分配内存。
在jvm中各类型的初始值如下:
int,byte,char,long,float,double 默认初始值为0
boolean 为false(在jvm内部用int表示boolean,因此初始值为0)
reference类型为null
对于final static基本类型或者String类型,则直接采用常量值(这实际上是在编译阶段就已经处理好了)。
2.2.3 解析
解析过程就是查找类的常量池中的类,字段,方法,接口的符号引用,将他们替换成直接引用的过程。
a.解析过程主要针对于常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四种常量。
b. jvm规范并没有规定解析阶段发生的时间,只是规定了在执行anewarray,checkcast,getfield,getstatic,instanceof,invokeinterface,invokespecial,invokespecial,invokestatic,invokevirtual,multinewaary,new,putfield,putstatic这13个指令应用于符号指令时,先对它们进行解析,获取它们的直接引用.
c. jvm对于每个加载的类都会有在内部创建一个运行时常量池(参考上面图示),在解析之前是以字符串的方式将符号引用保存在运行时常量池中,在程序运行过程中当需要使用某个符号引用时,就会促发解析的过程,解析过程就是通过符号引用查找对应的类实体,然后用直接引用替换符号引用。由于符号引用已经被替换成直接引用,因此后面再次访问时,无需再次解析,直接返回直接引用。
2.3 初始化
初始化阶段是根据用户程序中的初始化语句为类的静态变量赋予正确的初始值。这里初始化执行逻辑最终会体现在类构造器方法<clinit>()方中。该方法由编译器在编译阶段生成,它封装了两部分内容:静态变量的初始化语句和静态语句块。
2.3.1 初始化执行时机
jvm规范明确规定了初始化执行条件,只要满足以下四个条件之一,就会执行初始化工作
(1) 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法(对应new,getstatic,putstatic,invokespecial这四条字节码指令)。
(2) 通过反射方式执行以上行为时。
(3) 初始化子类的时候,会触发父类的初始化。
(4) 作为程序入口直接运行时的主类。
2.3.2 初始化过程
初始化过程包括两步:
(1) 如果类存在直接父类,并且父类没有被初始化则对直接父类进行初始化。
(2) 如果类当前存在<clinit>()方法,则执行<clinit>()方法。
需要注意的是接口(interface)的初始化并不要求先初始化它的父接口。(接口不能有static块)
2.3.3 <clinit>()方法存在的条件
并不是每个类都有<clinit>()方法,如下情况下不会有<clinit>()方法:
a. 类没有静态变量也没有静态语句块
b.类中虽然定义了静态变量,但是没有给出明确的初始化语句。
c.如果类中仅包含了final static 的静态变量的初始化语句,而且初始化语句采用编译时常量表达时,也不会有<clinit>()方法。
2.3.4 并发性
在同一个类加载器域下,每个类只会被初始化一次,当多个线程都需要初始化同一个类,这时只允许一个线程执行初始化工作,其他线程则等待。当初始化执行完后,该线程会通知其他等待的线程。
2.4 在使用过程中类,对象在方法区和堆上的分布状态
public class TestThread extends Thread implements Cloneable { public static void main(String[] args) { TestThread t = new TestThread(); t.start(); } }
上面这代码中TestThread及相关类在jvm运行的存储和引用情况如下图所示:
t在main线程VM Stack的局部变量表中
其中 t 作为TestThread对象的一个引用存储在线程的栈帧空间中,Thread对象及类型数据对应的Class对象实例都存储在堆上,类型数据存储在方法区,前面讲到了,TestThread的类型数据中的符号引用在解析过程中会被替换成直接引用,因此TestThread类型数据中会直接引用到它的父类Thread及它实现的接口Cloneable的类型数据。
在同一个类加载器空间中,对于全限定名相同的类,只会存在唯一的一份类的实例及类型数据。实际上类的实例数据和其对应的Class对象是相互引用的。
http://my.oschina.net/tryUcatchUfinallyU/blog/144133