其实在这篇之前,笔者已经大致研究过虚拟机的类加载机制了,但是不是纯粹读书做的笔记,而是研究的是java的静态变量时读过一次,这一次读书,就是为了再次回顾知识点,加深印象。
首先说到类加载,就需要立即知道三个大步骤:加载,链接,初始化。不过细分之下有以下几点。
1.加载,
2.链接,链接阶段有细分为:验证,准备,解析
3.初始化,
4.使用,
5.卸载,
类加载时机
什么时候需要进行的类的加载,这一点需要明确说明 java虚拟机规范中没有明确规定,这一点由具体的虚拟机自己去把握,但是,但是,但是,这里强调以下初始化操作,虚拟机规范明确指出,5种情况下必须立即对类进行初始化操作(这时候其实之前的操作,比如,加载,链接,等操作自然需要执行)
首先要知道初始化操作分为两大类:
1.主动引用
2.被动引用
1.主动引用
1.遇到new,getstatic,pustatic,invokestatic,这四条字节码时,需要进行类的初始化(如果类没有进行过初始化操作)
new就是实例化一个对象。
getstatic,pustatic 就是读取或者设置一个类的静态字段时(这边需要提前明确下,不包括final修饰的静态字段)。
invokestatic就是调用类的静态方法。
2.反射调用时。
3.当初始化一个类时,如果其父类没有初始化过,则需要进行父类的初始化操作。
4.当执行一个main方法时,需要先进行初始化操作,其实平时说的类的加载,这样说不准确,加载的动作其实是由于初始化操作而引发的。
5.jdk 1.7动态语言支持(不明确)
2.被动引用
主动引用是上面5种场景,其余都是被动引用,其实我有点钻牛角尖,什么样子才叫被动引用,这个名词哪儿来的,书中也没有说,只是说除了上面5种情况,其余的都叫做被动引用。
被动引用例子1,子类访问父类静态字段
/**
* @author: Wayne
* @desc: 父类
* @date: 2017/11/15 11:22
* @version: 1.0
*/
public class Father {
public static int num = 1;
static {
System.out.println("father init");
}
}
/**
* @author: wayne
* @desc: 子类
* @date: 2017/11/15 11:23
* @version: 1.0
*/
public class Son extends Father{
static {
System.out.println("Son init");
}
}
/**
* @author: wayne
* @desc: 被动引用-子类直接访问父类静态字段
* @date: 2017/11/15 11:01
* @version: 1.0
*/
public class Test {
public static void main(String[] args) {
System.out.println(Son.num);
}
}
father init
1
可以看见输出结果只有父类进行了初始化操作,但是子类没有进行,这种看起来子类应该也会进行初始化操作啊,但是并没有。同理,访问静态方法也是如此。
对于静态字段,只有定义这个字段的类才会被初始化,通过子类引用父类,只会触发父类的初始化,而不会触发子类的初始化。 被动引用例子2,通过数组定义来引用类
/**
* @author: Wayne
* @desc: 父类
* @date: 2017/11/15 11:22
* @version: 1.0
*/
public class Father {
public static int num = 1;
public static int get(){
System.out.println("father get");
return 1;
}
static {
System.out.println("father init");
}
}
public class Test {
public static void main(String[] args) {
Father []father = new Father[10];
}
}
没有输出,但是我们发现可以使用father.lethg() 和 father.clone().
被动引用例子3,final修饰的变量fangwen
/**
* @author: Wayne
* @desc: 父类
* @date: 2017/11/15 11:22
* @version: 1.0
*/
public class Father {
public final static int NUM = 1;
public static int get(){
System.out.println("father get");
return 1;
}
static {
System.out.println("father init");
}
}
/**
* @author: wayne
* @desc: 被动引用-直接访问final变量
* @date: 2017/11/15 11:01
* @version: 1.0
*/
public class Test {
public static void main(String[] args) {
System.out.println(Father.NUM);
}
}
1
只是输出了打印的值1,没有输出Father初始化的过程。这是因为被final修饰的变量在编译期时已经写入了常量池内,所以在Test类去访问时,相当于从常量池内直接去取,不会触发初始化操作了。另外书中介绍存在的常量池优化,会将Father.NUM的值存储到Test的常量池内,后续的继续访问都只是在Test内,与Father无关,Test内并没有Father的符号引用。
另外看见下面两图,上面一个是final类型的,下面一个是普通static变量,通过.访问时,final就能直接看见值,而static并不能。
类加载过程
本文最开始就列出5个过程,现在一一看下。
加载
加载阶段,虚拟机需要完成3个动作,注意,这只是虚拟机规范要求的,具体的实现可以多种多样。
1.通过一个类的全限定名获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转换为方法区运行时数据结构。
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区内这个类的各种数据的访问入口。(终于看到这句话了,其实我这一次回头看主要就是看这句话的)。
在此,不讨论数组的加载过程,暂时没有兴趣。只讨论一般类的加载阶段。
注意加载阶段,对于一般类来说,静态的东西都放在了方法区内,生成的Class的对象也放在了这里面,作为数据入口。
验证
验证阶段不是我本次读书所关心的,大致列一下吧。以后跟着问题去看。
1.文件格式是否正确,是否符合Class文件规范。
2.元数据验证,对字节码的语义进行分析,是否满足java规范,
3.字节码验证,很复杂,不明。
4.符号引用验证,此验证发生于解析阶段将符号引用转换为直接引用时,主要检查是否能够找到对应的类,并且访问权限等等,常见的异常,比如NoClassFound,NoSuchMethod异常都是由它产生。
准备
准备阶段以前就看过了,最上面java静态的研究时,就知道了,这是一个重要的一块,不知道这个,很多关于静态变量的值的问题,你可能完全懵逼....
这个阶段最重要的就是为类变量分配初始值,这些变量所需要使用的内存都在方法区内进行分配,这边需要注意一个概念,静态变量和实例变量,不知道这两个概念的,先去自行百度,不再叙述,实例变量是随着对象实例化时分配在堆中。这边的初始化值是一个默认初始值的概念,类似于0值,如果感兴趣,可以回到开头去看下那一偏文章内的题目,很有意思。
这边需要注意一下常量的值,也就是final 类型的变量的值,final类型的变量的初始值就在准备阶段赋予了它所指定的值,没有所谓的初始值的概念。
解析
解析阶段不是关注重点,暂时不明和验证一样,知道即可
初始化
在初始化之前的加载阶段,可以允许用户自定义类加载器执行加载,之后的过程都由虚拟机主导,这边我说一说对于初始化阶段的认知吧,就不直接copy书中的观点了。
1.初始化阶段是执行的类变量以及静态快的初始化操作,顺序是你类中定义的顺序
2.初始化操作只执行一次,以后不会再次执行
3.<clinit> 方法与类的构造函数<init>不同的点在于,它不会显示的调用父类的构造方法,想一想这是为什么?还记得类的加载时机不?在子类进行初始化时,必须执行父类的初始化操作,这是虚拟机层面规定的。