类加载机制(class loading)
在java代码中,类的加载、连接与初始化的过程都是在程序的运行期间完成的
在程序运行期间完成类的加载、连接和初始化为Java代码提供了更大的灵活性,增加了更多的可能性。比如Java的动态代理,在类加载之后才会生成动态代理类。
-
加载 :查找并加载类的二进制数据(存放在方法区 jdk1.8之后叫元空间matespace)
类加载指的是将类的.class文件中的二进制数据读入到内存当中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang,Class对象,用来封装类在方法去内的数据结构。(规范并没有说明Class对象位于哪里,Hot Spot虚拟机将其放在了方法区中)
加载.class文件的方式
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip、jar等归档文件中加载.class文件
- 将java源文件动态编译为.class文件
可以用jvm参数 -XX:+TraceClassLoading追踪类的加载信息并打印出来
-
连接 :
-
验证:确保被加载的类的正确性
-
准备:为类的静态变量分配内存,并将其初始化为默认值
-
解析:把类中的符号引用转换为直接引用
-
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
-
直接引用:直接引用可以是
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
-
-
-
初始化:为类的静态变量赋予正确的初始值,执行static{}静态代码块中的语句,发生在方法区。类的初始化步骤:
- 假如这个类还没有被加载和连接,那么久先进行加载和连接
- 假如类存在直接父类,并且这个父类还没有被初始化,那就初始化直接父类
- 假如类中存在初始化语句,那么就一次执行这些初始化语句。静态变量、静态代码块、构造方法
-
使用
-
卸载
[外链图片转存失败(img-NCzHYXcT-1566177755394)(…\img\jvm类加载.png)]
加载、验证、准备、初始化和卸载这5个顺序是确定的,类的加载的过程必须按照这个顺序按部就班地开始,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。
class Test{
public static int i = 1;
}
1、加载Test类的二进制文件到内存
2、验证正确 为i分配内存,赋值为0,//初始为默认值
3、为i赋值 解析
- Java程序对类的加载可以分为两种
- 主动使用
- 被动使用
- 所有的Java虚拟机的实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。初始化只会执行一次
什么情况下需要开始类加载过程的第一个阶段:加载?Java虚拟机规范中并没有进行强制的约束,由虚拟机的具体实现自由把握。但是对于初始化阶段,虚拟机规范则严格规定了有且只有5种情况必须立即对类进行初始化,(而加载、验证、准备自然需要在此之前开始)
- 遇到new(实例化对象)、getstatic(读取类的静态字段)、putstatic(设置类的静态字段)(被final修饰已在编译期把结果放入常量池的静态字段除外)或invokestatic(调用静态方法)这4条指令时,如果没有进行初始化,则需要先触发其初始化。
- 使用java.lang.reflect包下的方法对类进行反射调用的时候
- 初始化一个类的时候如果发现其父类还没有初始化,需要先出发其父类的初始化。当java虚拟机初始化一个类的时候要求它的所有父类都已经被初始化,这条规则不适用于接口;
在初始化一个类的时候,并不会先初始化它所实现的接口
在初始化一个接口的时候,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有程序首次使用接口的静态变量时,才会导致该接口的初始化。
// interface2初始化了 但是interface1并没有初始化 public class MyTest2 { public static void main(String[] args) { System.out.println(interface2.b); } } interface intterface1{ public static int a = 5; } interface interface2 extends intterface1{ public static int b = 6; }
4.当虚拟机启动时,执行包含mian()方法的类,或者比如junit 包含 test的类
5.当是jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
这5种场景中的行为称为对一个类进行主动使用,除此之外,所有引用类的方式都不会触发初始化,就是被动使用。
package com.fjh.jvm;
// 因为str是被final修饰的常量,在编译期把str存放在了Myparent的常量池中了,所以不会初始化Myparent类
public class MyTest {
public static void main(String[] args) {
System.out.println(MyParent.str);
}
}
class MyParent{
public static final String str = "hello word";
static{
System.out.println("my parent static block");
}
}
=====输出结果============
hello word
==========================
// 当一个常量值并非编译期间可以确定,那么就不会放到调用类的常量池中,这时在程序运行时会主动的使用
// 这个常量所在的类,导致这个类被加载
public class MyTest {
public static void main(String[] args) {
System.out.println(MyParent.str);
}
}
class MyParent{
public static final String str = UUID.randomUUID().toString();
static{
System.out.println("my parent static block");
}
}
=====输出结果============
my parent static block
fdb1074f-8803-442b-b293-7e891e25e1d6
==========================
// 程序不会输出静态代码块,
// 对于数组类型实例来说,其类型是由jvm在运行期间动态生成的,表示为[com.fjh.Test 这种形似。动态生成的类型父类型就是Object
//
public class MyTest {
public static void main(String[] args) {
MyParent[] myParents = new MyParent[1];
}
}
class MyParent{
public static final String str = UUID.randomUUID().toString();
static{
System.out.println("my parent static block");
}
}
Java编译器为它编译的每个类都至少生成一个实例初始化方法,即()方法。源代码中的每一个类的构造方法都有一个相 对应的()方法。如果类没有明确地声明任何构造方法,编译器则为该类生成一个默认的无参构造方法,这个默认的构造器仅仅调用父类 的无参构造器。
一个()方法内包括的代码内容可能有三种:调用另一个() 方法;对实例变量初始化;构造方法体的代码。
如果构造方法是明确地从调用同一个类中的另一个构造方法开始,那它对应的 () 方法体内包括的内容为:
1、一个对本类的()方法的调用;
2、实现了对应构造方法的方法体的字节码。
如果构造方法不是通过调用自身类的其它构造方法开始,并且该对象不是 Object 对象,那 () 法内则包括的内容为:
1、一个父类的()方法的调用;
2、任意实例变量初始化方法的字节码;
3、实现了对应构造方法的方法体的字节码。
Java虚拟机与程序的生命周期
- 在如下几种情况下,Java虚拟机将结束生命周期
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到异常或者错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止