7.2 类加载的时机
加载 -》 验证 -》 准备 -》 解析 -》 初始化 -》 使用 -》 卸载
其中验证,准备,解析属于linking阶段。为了支持java语言的动态绑定,解析可能在初始化之后执行
有5种情况必须立即对类进行初始化:
- 遇到new,getstatic,putstatic或者invokestatic这四条字节码时,如果类没有被初始化则会出发初始化。常见命令是,使用new实例化一个对象,读取或者设置一个类的静态字段,以及调用一个类的静态方法
- 使用java.lang.reflect包对类进行反射是,如果类没有进行过初始化,则需要先出法初始化
- 初始化一个类,如果父类没有初始化,则父类也需要初始化
- 虚拟机启动时被指定的主类会初始化
- jdk 1.7动态语言支持中。java.lange.invoke.MethodHandle实例最后解析出的方法句柄对应的类没有初始化时会出发初始化
这五种被叫做主动引用,其他的叫被动引用,被动引用不会触发初始化,比如
- 通过子类引用父类的静态字段,子类不会初始化
- 通过数组定义来引用类:
MyClass [] array = new MyClass[10]
- 常量字段编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发该定义类的初始化
对于接口,与类唯一的不同在于3:不要求接口的所有父接口完成初始化,而只有在真正用到父接口的时候才会初始化
7.3 类加载的过程
- 加载
- 步骤:
- 通过一个类的全限定名来获得定义此类的二进制字节流
- 将字节流所代表的静态储存结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.class对象,最为方法区这个类的各种数据的访问入口
- 对于数组的加载,如果数组组件类型(去掉一个维度的类型)是引用类型,则递归的去加载这个组件类型;如果不是引用类型,比如int[]。java虚拟机会将数组标记为与引导类加载器关联。数组类的可见性与他的组件类型的可见性一致。
- 步骤:
- 验证:虽然编译器会拒绝编译不合法的java文件,但是由于加载的二进制码可以由多种格式组成,因此荏苒需要验证。
- 准备:准备阶段是正式为类在方法区中分配内存及设置初始这。这里分配只包括类变量(static)。这里的初始值指的是0值,比如
public static int val=123
会被赋0,因为这里只是处理内存,真正的赋123是在初始化阶段,由<clinit>()
执行。 - 解析:对类,接口,字段,类方法,接口方法进行解析。将常量池内的符号引用替换为直接引用。
- 初始化: 执行类构造器
<clinit>()
的过程。<clinit>()
方法是有编译器自动收集类中所有类变量的复制动作和静态语句块中的语句构成的。收集顺序是有语句在源文件中出现的顺序决定的。静态语句块只能访问静态语句块之前的变量,定义在他后面的变量只能赋值不能访问。<clinit>()
不需要显示调用父类构造器,虚拟机会保证父类clinit已经被调用。而类构造函数(或者说实例构造器<init>()
则需要调用。)- 父类的
<clinit>()
先调用 <clinit>()
对于类和接口不必须,如果没有static语句和变量,编译器可以不生成<clinit>()
- 接口中不一定先执行父类
<clinit>()
,只有在父接口定义的变量被使用时才执行父类的<clinit>()
- 虚拟机会保证一个类的
<clinit>()
在多线程中被正确加锁同步,多线程同时初始化时只有一个线程执行<clinit>()
,其他线程会阻塞等待。
类加载器
类加载器可以自己定义,每个类加载器都拥有一个独立的类名称空间;比较两个类相同的前提是他们被同一个类加载器加载。
类加载器的层次关系为启动类加载器,扩展类加载器,应用程序类加载器,自定义类加载器。启动类加载器是最顶层的。
双亲委派模型是说:一个类加载器收到了类加载的请求,他首先不会自己去尝试加载,而是把这个请求委派给父类加载器去完成,因此最终都是传给顶层的启动类加载器。
双亲委派模型有两个特例(破坏):
- 父类加载器不认识某些类,需要调用子类加载器。比如JNDI服务
- 程序动态性:代码热替换,模块热部署。比如OSGi
本文采用创作共用保留署名-非商业-禁止演绎4.0国际许可证,欢迎转载,但转载请注明来自http://thousandhu.github.io,并保持转载后文章内容的完整。本人保留所有版权相关权利。
本文链接:http://thousandhu.github.io/2016/06/18/深入理解java虚拟机-虚拟机类加载机制/