加载class文件主要三步是:加载->连接->初始化,而连接过程又可分为三步:验证->准备->解析
加载是类加载的一个子阶段,两者不要混淆。
《Java虚拟机规范》严格规定了有且只有六种情况必须对类进行“初始化”(加载和连接自然要在之前开始)。类加载时机
主动引用:
- 遇到 new、 getstatic、 putstatic 或 invokestatic 这四条字节码指令时,如果类型没有 进行过初始化,则需要先触发其初始化阶段。
- 对类进行反射时。
- 初始化类时,发现父类还没有初始化。
- 虚拟机启动,加载父类。
- JDK7动态语言
当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法) 时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
被动引用
除了以上场景需要对类进行初始化外,其他所有引用类的方式都不会触发初始化,故称为被动引用子类引用父类的静态字段
- 通过数组来定义引用类,不会触发此类的初始化。该过程会对数组类进行初始化。
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
类加载的过程
-
加载
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的 Class 对象,作为在方法区中关于此类的各种数据的访问入口。
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 **loadClass()**
方法)。
数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
-
验证
为类的变量分配内存并设置初始值
另外此时进行的仅包括类的变量,而不包括实例的变量。
在经过准备阶段后数据类型为默认的零值(如0、0L、null、false等)准备
- 比如我们定义了
public static int value=111
,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。 - 特殊情况:比如给 value 变量加上了 fianl 关键字
public static final int value=111
,那么准备阶段 value 的值就被赋值为 111。
- 比如我们定义了
解析即将常量池内的符号引用替换为直接引用。
符号引用:以一组符号来描述所引用的目标,要求能够无歧义的定位得到目标,所引用的内容不一定已经加载到虚拟机内存。
直接引用:直接引用是可以直接指向目标的指针、 相对偏移量 或者是一个能间接定位到目标的句柄。 所引用的内容必然已经加载在在虚拟机内。
例如,Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方发表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。解析
为类加载的最后一步,虚拟机真正开始执行类中编写的java代码。之前变量已经赋过一次初始零值,初始化则会根据代码指定的内容去初始化类变量和其他内容,初始化阶段就是执行类构造器<clinit>()方法的过程。<br />
() 方法为编译器收集类中所有变量的赋值动作和静态语句块中的语句合并产生。<br />对于
() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为
()` 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。 初始化
卸载类即该类的Class对象被GC。
卸载类需要满足3个要求:卸载
- 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被GC
所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。