深入理解JVM(9)——类加载的过程
加载是类加载的第一步。
一、加载
a)加载的过程
1)通过一个类的全限定名获取这个类的二进制字节流,也就是class文件
2)将二进制字节流的存储结构转换为特定的数据结构,存储在方法区
3)在内存中创建一个Java.lang.Class的对象,接下来在程序运行的过程中所有对这个类的访问都是通过这个Class类型的对象进行访问的。
b)从那里加载二进制字节流
1) 一般的二进制字节流都是通过已经编译好的本地class文件中进行加载
2) 从压缩文件中进行加载
3)从网络中进行加载
4)从数据库中进行加载
5) 通过其他文件动态生成
c)普通类的加载和数组类加载过程的区别
数组也是一种数据类型,成为数组类型。String[] str = new String[10];这是一个数组类型,只不过是该数组的数据元素是String类型,在程序运行过程中当遇到new需要创建一个数组的时候,JVM会首先创建一个数组类,然后再由类加载器创建数组中的元素类;但是普通类的创建过程就是直接由类加载器创建。
d) 加载过程的注意点
1) JVM并没有给出类在方法区存放的数据结构
2) JVM并没有给出Class对象的存放位置
3) 加载阶段和连接阶段是交叉进行的,类加载过程中每个步骤的开始顺序有要求限制,但是并没有规定每个步骤的结束顺序
二、验证
a)验证的目的:保证二进制字节流中的二进制信息符合JVM的规范,不会出现安全问题。
b)为什么需要验证:
Java是一门安全语言,他能确保程序员不会访问数组之外的内存、不会把一个对象转换成任意类型、能避免代码跳转到一个不存在的地方,如果出现这样的情况,编译过程就会报错,这就是说Java语言的安全性是通过编译器实现的。
我们知道编译器和虚拟机是两个相互独立的部分,虚拟机只认二进制字节码,但是它不关心二进制字节码的来源,如果是Java编译器生成的二进制字节码,那么就会相对的安全,但是还能通过其他的途径获取到二进制字节码,这样的二进制字节码是没办法保证符合JVM规范的,为了防止二进制字节流中出现安全问题,因此需要验证。
c) 验证的过程:
1)文件格式的验证
2)元数据的验证
3)字节码的验证
4) 符号引用的验证
三、准备:
a) 为已经在方法区中的类的静态成员变量分配内存
b)为静态成员变量设置初始值(默认初始化)
四、解析:是虚拟机将常量池中的符号引用变为直接引用的过程
五、 初始化:就是执行类构造器Clinit()方法的过程。Clinit()方法是编译器自动产生的,收集类中静态代码块中的类成员变量赋值语句和类的静态成员变量赋值语句。在准备阶段,类的静态成员变量已经被默认初始化,但是在初始化阶段需要进行显示的初始化。
六、初始化过程中需要注意的点:
a)clinit()方法对类中的静态成员变量赋值的顺序是根据代码中成员变量出现的顺序进行赋值的;
b)静态代码块能够访问在静态代码块之前出现的类的静态成员变量,在其后的静态成员变量不能被访问
c)静态代码块能够为出现在其后的静态成员变量赋值
d)构造函数init()需要显示的调用父类的构造器,但是类的clinit()方法不用调用父类的这个方法,因为虚拟机会保证在执行这个类的clinit()方法之前已经执行了父类的这个方法。
e)如果在接口/类中没有静态代码块也没有类的静态成员变量的话,那么编译器就不会生成clinit()方法
f)接口中只会出现静态成员变量,不会出现静态代码块,如果出现静态成员变量的话,编译器就会自动生成一个clinit()方法
g)接口在执行clinit()方法的时候不会确保其父接口的clinit()方法已经被执行,只有当父接口中的静态成员变量被执行到的时候才会执行父接口的clinit()方法。
h) 虚拟机会给clinit()方法上锁,当多个线程同时执行某一个类的clinit()方法的时候,只能有一个线程能够正常执行,其他的线程会被阻塞,并且只要这个类的clinit()方法已经被执行过之后,其他的线程就不能在执行,这样也就保证了在同一个类加载器下一个类只能被初始化一次。