介绍
JVM类加载过程分为五个部分:加载-连接-初始化-使用-卸载,其中“连接”阶段包括:“校验”、“准备”、“解析”。
加载
加载过程负责查找和导入class文件
1、通过一个类的全限定名来获取定义此类的二进制字节流
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang. Class对象,作为方法区这个类的各种数据的访问入口。
问题:ClassLoader.load() 与Class.forName()区别?
相同点:
- 都可以动态加载类
- 传递的参数都是类的全路径:包名+类名
- 加载成功后都会返回对应类型的Class对象
- 加载失败会抛出“ClassNotFountException”
不同点:
- ClassLoader需要指定class文件存储路径而forName不需要(使用的默认的classpath路径)
- ClassLoader 需要实例化并调用load方法而forName不需要实例化,调用的是静态方法
连接
连接阶段全都是JVM内部做的事,外部不好插手,程序员一般无法进行扩展,所以此阶段了解即可。
验证
确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
确保被导入类的正确性
: 文件格式验证
: 元数据验证
: 字节码验证
: 符号引用验证
准备
为类变量分配内存,并将其初始化为默认值
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
//准备阶段ab变量都会被分配内存并设置初始值
//final修饰的常量此阶段会设置初始值1
public static final int a = 1;
//未被final修饰的变量会设置初始值,int默认值为0
public static int b = 2;
解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能简介定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
初始化
初始化过程即为执行类中的静态初始化代码、构造器代码以及静态属性的初始化,虚拟机规范则是严格规定了有且只有六种情况必须立即对类进行“初始化”:
1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2)使用java.lang .reflect 包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先
初始化这个主类。
5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
6)当一个接口中定义了JDK 8新加入的默认方法(被defaul t 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
总结下:
主动使用案例:
- 创建类的新实例
- 调用类中声明的静态方法
- 操作类或者接口中声明的非常量静态字段
- 调用Java API中特定的反射方法
- 初始化一个类的子类
- 指定一个类作为Java虚拟机启动的初始化类
被动使用案例:
- 通过子类引用父类的静态字段,不会导致子类初始化
- 通过数组定义来引用类,不会触发此类的初始化
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
clinit方法
类变量初始化和静态初始化都会由java 编译器放到一个没有方法名的方法()中,这个方法只能被虚拟机调用,通过javap -c 命令可查看。类的初始化方法()只会被执行一次,这点由虚拟机保证,虚拟负责多线程并发情况下处理。()方法中的代码顺序与类代码中定义的保持一致。
()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。如下面代码,变量b和static{}中内容会被放入到()中,而常量A不会,A在准备阶段已执行过。
//准备阶段执行
private static final int A = 1;
//类变量初始化和静态初始化
private static int b = 0;
static {
System.out.println("静态代码块");
}
tip:()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法。
使用
一旦一个类被装载、连接和初始化,它就随时可以使用了。程序可以访问它的静态字段,调用它的静态方法,或者创建它的实例。
卸载
启动类加载器,扩展类加载器和系统类加载器加载的类在JVM整个生命周期中都不会被卸载,只有自动加载器加载的类可以被卸载。
类可以被卸载,需要满足三个条件:
1、类的对象为null
2、类的Class为null
3、类对应的类加载器为null
由此看来卸载一个类的条件很苛刻,元空间一般存放类的信息,一般元空间发生oom,只能选择调大此区域,没有别的好办法。
面试题
public class ClinitDemo {
private static ClinitDemo clinitDemo = new ClinitDemo();
private static int count1;
private static int count2 = 0;
private static final int count3 = 1;
private ClinitDemo(){
count1++;
count2++;
}
public static void main(String[] args) {
System.out.println(count1);
System.out.println(count2);
System.out.println(count3);
}
}
直接看答案
1
0
1
我们来分析下执行过程:
1、count3为静态常量,准备阶段则已赋值,准备阶段会为别的类变量赋默认值
2、clinitDemo和count2会被按照代码顺序放入()方法中,初始化阶段先执行()方法
3、()方法中,先执行count1++、count2++,则count1 count2都为1,后会执行private static int count2 = 0,则count2为0