jvm类加载

一. 问题背景

遇到一道面试题“简述java类加载机制”。今天了解一下类加载。

此笔记仅供自己参考,如有错误请指正

参考:java中级程序员必会的教程,解密JVM【黑马程序员出品】

二. 类加载

我们编译的java代码都是以.java格式文件保存的,而它是不能被jvm执行。jvm需要将.java文件编译成.class文件(即字节码技术),jvm才能真正地执行我们编写的代码。

2.1 类加载有哪些阶段

类加载分为3个大阶段:加载阶段链接阶段初始化阶段。而链接阶段分为:验证阶段准备阶段解析阶段。如下所示:

jvm类加载

2.1.1 加载阶段

加载阶段是将类的字节码载入方法区中(如果该类的父类还没加载,则先加载父类),其内部采用C++的instanceKlass描述java类,该instanceKlass的重要fields有如下:

  1. _java_mirror即java类的镜像,如String类,就是String.class,作用是把klass暴露给java使用
  2. _super即父类
  3. _fields即成员变量
  4. _methods即成员方法
  5. _constants即常量池
  6. _class_loader即类加载器
  7. _vtable虚方发表
  8. _itable接口方法表

因为klass是采用C++写的,所以java不能直接访问klass,所以就有了java_mirror,其作用是充当了java与c++的桥梁,使得java通过java_mirror能访问到klass。

如下图:
jvm类加载
注:instanceKlass的元数据是存储在方法区内(即jdk1.8的元空间),而java_mirror是存储在堆中的。

2.1.2 链接阶段

链接阶段分为3个:验证阶段准备阶段解析阶段

2.1.2.1 验证阶段

验证加载进来的类(实际上指字节码)是否符合jvm规范,这是一个安全性检查。

加入修改了类的字节码,比如它的魔数,运行的时候将会报出ClassFormatError,如下:
jvm类加载

2.1.2.2 准备阶段

准备阶段是为static变量分配空间,设置默认值。

内容有:

  • static变量jdk1.7以前存储与instanceKlass的末尾,而jdk1.7及以后则存于java_mirror末尾(因为java_mirror在jdk1.7及以后是存储在堆中的,所以static变量也是存于堆中

  • static变量分配空间和赋值是2个步骤:分配空间是在准备阶段完成的;赋值是在初始化阶段完成的

  • 如果static变量是final的基本数据类型 以及 字符串常量,那么编译阶段值就确定了,所以赋值在准备阶段完成

  • 如果static变量是final的引用类型,那么赋值在初始化阶段完成

如下:
jvm类加载
jvm类加载

2.1.2.3 解析阶段

将常量池的符号引用解析为直接引用(即将符号引用改为内存地址,直接去内存地址查找)

如下:
jvm类加载
jvm类加载

2.1.3 初始化阶段

初始化即调用<cinit>()V,虚拟机会保证这个类的构造方法的线程安全。

类初始化是懒惰的,即当jvm用的某个类,才会去加载那个类。

2.1.3.1 导致类初始化的情况

  1. main方法所在的类,总会被首先初始化

  2. new会导致初始化

  3. 首次访问类的静态变量或静态方法时

  4. 子类初始化,如果父类还未初始化,则先初始化父类

  5. 子类访问父类的静态变量,只初始化父类

  6. Class.forName触发初始化

2.1.3.2 不导致类初始化的情况

  1. 访问该类的static final静态常量(即基本数据类型 以及 字符串常量),因为它们的值在编译阶段就确定,然后static final变量在准备阶段会完成分配空间 以及 赋值 ,所以不会触发初始化

  2. 类对象.class不会触发初始化(因为类对象.class是在类加载阶段完成的,即字节码)

  3. 类加载器的loadClass方法

  4. Class.forName的参数2为false时