⭐️写在前面
这里是温文艾尔の学习之路
- 👍如果对你有帮助,给博主一个免费的点赞以示鼓励把QAQ
- 👋博客主页🎉 温文艾尔の学习小屋
- ⭐️更多文章👨🎓请关注温文艾尔主页📝
- 🍅文章发布日期:2022.02.08
- 👋java学习之路!
- 欢迎各位🔎点赞👍评论收藏⭐️
- 🎄新年快乐朋友们🎄
- 👋jvm学习之路!
- ⭐️上一篇内容:【JVM】JVM05(从字节码角度分析i++和++i的执行流程))
文章目录
- ⭐️1.类加载阶段
- ⭐️1.1加载Loading
- ⭐️1.2连接Linking
- ⭐️1.2.1连接阶段-验证
- ⭐️1.2.2连接阶段-准备
- ⭐️1.2.3连接阶段-解析:
- ⭐️1.3初始化阶段Initialization
- 相关练习1
- 相关练习2
⭐️1.类加载阶段
⭐️1.1加载Loading
JVM在该阶段的主要目的是将字节码从不同的数据源(可能是class文件、也可能是jar包,甚至网络)转化为二进制字节流加载到内存中,生成一个代表该类的java.lang.Class对象
将类的字节码载入方法区中,内部采用C++的instanceKlass描述java类,它的重要field有:
_java_mirror即java的类镜像,例如对String来说,就是String.class,作用是吧klass暴露给java使用
_super即父类
_fields即成员变量
_methods即方法
_constants即常量池
_class_loader即类加载器
_vtable虚方法表
_itable接口方法表
如果这个类还有父类没有加载,先加载父类
加载和链接可能是交替运行的
注意
- nstanceKlass这样的【元数据】是存储在方法区(1.8后的元空间内),但_java_mirror是存储在堆中
比如现在有两个person的实例对象,每个实例对象都有自己的对象头(16字节)其中8个字节对应着class地址,如果想通过对象获取class信息,就会访问对象的对象头,通过class地址找到Person.class类对象,再通过类对象间接的去元空间(Metaspace)找到信息
⭐️1.2连接Linking
分为三个小的步骤
⭐️1.2.1连接阶段-验证
- 目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机中的要求,并且不会危害虚拟机自身的安全
- 包括:文件格式验证(是否以魔数oxcafebabe开头)、元数据校验、字节码验证和符号引用验证
- 可以考虑使用
-Xverify:none
参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间
例如使用UE等支持二进制的编辑器修改HelloWorld.class的魔数,在控制台运行
public class Demo01 {
public static void main(String[] args) {
System.out.println("hello world");
}
}
将.class用sublime打开,修改
修改之后运行,因修改之后格式规范错误,验证阶段没有通过
⭐️1.2.2连接阶段-准备
为static变量分配空间,设置默认值
- static变量在JDK7之前存储于
instanceKlass
末尾,从JDK7开始,存储于_java_mirror
末尾 - static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果static变量是final的基本类型,以及字符串常量,那么编译阶段就确定了,赋值再准备阶段完成
- 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
代码示例
class A{
//连接阶段-准备
//1.n1是实例属性,不是静态变量,因此在连接阶段-准备时不会被分配内存
//2.n2是静态变量,分配内存 n2 是默认在此阶段初始化,值为0,只有在连接阶段后的初始化阶段值才会被初始化为20
//3.n3是常量,他和静态变量不一样,因为一旦赋值就不会改变,所以n3此阶段值为30
//4.String准备阶段值为null,赋值操作在初始化阶段完成,如果加上final的话,赋值操作就会在准备阶段完成
public int n1=10;
public static int n2=20;
public static final int n3=30;
public final String n4 = "hello";
}
jdk1.8及以后,静态变量和类对象存储在一起,存储在堆中,在早期的jvm(jdk1.6以前)里,静态变量在存储在方法区,从1.7开始到1.8静态变量转移到堆中
⭐️1.2.3连接阶段-解析:
虚拟机将常量池内的符号引用解析为直接引用的过程
比如A类中有B类的引用,但只作为符号存在前,只有在解析阶段才会将这类的符号引用解析为直接引用
举例
public class Demo02 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader classLoader = Demo02.class.getClassLoader();
//loadClass方法不会导致类的解析和初始化
Class<?> c = classLoader.loadClass("com.wql.jvm.ClassStep.C");
System.in.read();
}
}
class C{
D d = new D();
}
class D{
}
loadClass("com.wql.jvm.ClassStep.C");不会使C类中D加载触发,而new C()则会触发D
⭐️1.3初始化阶段Initialization
< cinit>()v方法
初始化即调用< cinit>()v,虚拟机会保证这个类的【构造方法】的线程安全
发生的时机
概括的说,类初始化是【懒惰的】
main
方法所在的类,总会被首先初始化- 首次访问这个类的静态变量或静态方法时会触发
- 子类初始化,
如果父类还没初始化,会触发
- 子类访问父类的
静态变量
,只会触发父类的初始化 Class.forName
new 会导致初始化
不会导致类初始化的情况
- 访问类的
static final
静态常量(基本类型和字符串)不会触发初始化 - 类对象
.class
不会触发初始化
案例
public class Demo03 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws Exception {
//1.静态常量不会触发初始化
System.out.println(B.b);
//2.类对象.class 不会触发初始化
System.out.println(B.class);
//3.创建该类的数组不会触发初始化
System.out.println(new B[0]);
//4.不会初始化类B,但会加载B,A
ClassLoader c1 = Thread.currentThread().getContextClassLoader();
c1.loadClass("com.wql.jvm.ClassStep.B");
//5.不会初始化类B,但会加载B,A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("com.wql.jvm.ClassStep.B",false,c2);
// 1. 首次访问这个类的静态变量或静态方法时
System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,会引发
System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类初始化
System.out.println(B.a);
// 4. 会初始化类 B,并先初始化类 A
Class.forName("com.wql.jvm.ClassStep.B");
}
}
class A{
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
相关练习1
从字节码分析,使用abc者三个常量是否会导致E初始化
public class Demo04 {
public static void main(String[] args) {
System.out.println(E.a);
System.out.println(E.b);
System.out.println(E.c);
}
}
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;
}
答案:不会,不会,会
因为a是基本类型,而b是字符串常量,赋值操作在准备阶段就触发,不会触发初始化
而c是包装类型,触及装箱拆箱操作Integer c = Integer.valueOf(20),在初始化阶段完成
相关练习2
典型应用-完成懒惰初始化单例模式
public class Demo05 {
private Demo05(){}
//保存单例
private static class LazyHolder{
static final Demo05 INSTANCE = new Demo05();
}
//第一次调用getInstance方法,才会导致内部类加载和初始化其静态成员
public static Demo05 getInstance(){
return LazyHolder.INSTANCE;
}
}
以上的实现特点是:
- 懒惰实例化
- 初始化时的线程安全是有保障的