写在前面
本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!
本专栏目录结构和文献引用请见100个问题搞定Java虚拟机
解答
JVM加载java类就是将字节流(如.class文件,网络传输的字节流)文件加入到内存中的过程,分为以下三步:加载、链接、初始化
1. 加载是指查找字节流,并且据此创建类的过程。
加载需要借助类加载器,在 Java 虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。
2. 链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。
链接还分验证、准备和解析三个阶段。其中,解析阶段为非必须的。
3. 初始化,则是为标记为常量值的字段赋值,以及执行 <clinit> 方法的过程。
类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。
补充
加载
Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象)。
这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;
如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。
加载规则
双亲委派机制
详情请见我的另一篇博客——双亲委派模型是什么?
类的唯一性
类加载器名称+类全限定名称
链接
这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。
验证
这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
准备
创建类或接口中的静态变量,并初始化静态变量的初始值。
但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
解析
将符号引用解析为实际引用, 符号引用是在编译阶段由编译器生成,包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型
初始化
这一步真正去执行类初始化的代码逻辑,包括
- 静态字段赋值的动作
- 执行类定义中的静态初始化块内的逻辑
编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
具体来说就是为标记为常量值的字段(基本类型或字符串且被修饰为final)赋值,以及执行方法(其他赋值操作和静态代码块)。
类的初始化过程是线程安全的,并且只能被初始化一次。jvm会通过加锁来保证方法仅被执行一次。
初始化的时机
对一个类的主动引用
被动引用并不会引发类的初始化,如引用类的静态常量,引用父类的静态字段不会初始化子类,数组定义来引用类不会导致初始化。
JDK9
在 JDK 9 中,由于 Jigsaw 项目引入了 Java 平台模块化系统(JPMS),Java SE 的源代码被划分为一系列模块。
平台类加载器(Platform Class-Loader)
-
扩展类加载器被重命名为平台类加载器(Platform Class-Loader),而且 extension 机制则被移除。
也就意味着,如果我们指定 java.ext.dirs 环境变量,或者 lib/ext 目录存在,JVM 将直接返回错误!
建议解决办法就是将其放入 classpath 里。 -
部分不需要 AllPermission 的 Java 基础模块,被降级到平台类加载器中,相应的权限也被更精细粒度地限制起来。
-
rt.jar 和 tools.jar 同样是被移除了! JDK 的核心类库以及相关资源,被存储在 jimage 文件中,并通过新的 JRT 文件系统访问,而不是原有的 JAR 文件系统。
虽然看起来很惊人,但幸好对于大部分软件的兼容性影响,其实是有限的,更直接地影响是 IDE 等软件,通常只要升级到新版本就可以了。 -
增加了 Layer 的抽象, JVM 启动默认创建 BootLayer,开发者也可以自己去定义和实例化 Layer,可以更加方便的实现类似容器一般的逻辑抽象。
结合了 Layer,目前的 JVM 内部结构就变成了下面的层次,内建类加载器都在 BootLayer 中,其他 Layer 内部有自定义的类加载器,不同版本模块可以同时工作在不同的 Layer。