搞懂JVM类加载过程,其实很简单
Posted 格子衫111
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了搞懂JVM类加载过程,其实很简单相关的知识,希望对你有一定的参考价值。
我们知道我们写的程序经过编译后成为了.class文件,.class文件中描述了类的各种信息,最终都需要加载到虚拟机之后才能运行和使用。而虚拟机如何加载这些.class文件?.class文件的信息进入到虚拟机后会发生什么变化?
先看一个整体图,红框部分就是需要讲解的类加载部分
一、类加载器ClassLoader角色(快递员)
- class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
- class file 加载到JVM中,被称为DNA元数据模板。
- 在 .class文件 --> JVM --> 最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
二、类加载的执行过程
类的生命周期——7个阶段
类从被加载到虚拟机内存中开始,到卸载出内存,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiallization)、使用(Using)和卸载(Unloading)这7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这七个阶段的发生顺序如下图:
图中,加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段不一定:它在某些情况下可以初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。接下来讲解加载、验证、准备、解析、初始化五个步骤,这五个步骤组成了一个完整的类加载过程。使用没什么好说的,卸载属于GC的工作 。
1、加载
加载是类加载的第一个阶段。有两种时机会触发类加载:
1)预加载
虚拟机启动时加载,加载的是JAVA_HOME/lib/下的rt.jar下的.class文件,这个jar包里面的内容是程序运行时非常常用到的,像java.lang.*、java.util.、java.io. 等等,因此随着虚拟机一起加载。要证明这一点很简单,写一个空的main函数,设置虚拟机参数为"-XX:+TraceClassLoading"来获取类加载信息,运行一下:
2)运行时加载
虚拟机在用到一个A.class文件的时候,会先去内存中查看一下这个A.class文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。
加载阶段做三件事:
- 通过全类名获取.class文件的二进制流
- 将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
ps:一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的
虚拟机规范对这三点的要求并不具体,因此虚拟机实现与具体应用的灵活度都是相当大的。例如第一条,根本没有指明二进制字节流要从哪里来、怎么来,因此单单就这一条,就能变出许多花样来:
- 从zip包中获取,这就是以后jar、ear、war格式的基础
- 从网络中获取,典型应用就是Applet
- 运行时计算生成,典型应用就是动态代理技术
- 由其他文件生成,典型应用就是JSP,即由JSP生成对应的.class文件
- 从数据库中读取,这种场景比较少见
总而言之,在类加载整个过程中,这部分是对于开发者来说可控性最强的一个阶段。
2、链接
1)验证Verification
为什么要做验证?因为class文件未必要从Java源码编译而来,可以使用任何途径产生。
虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃。
验证阶段所做4个工作:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
解释:
2)准备Preparation
为类变量(静态变量)分配内存并设置其初始值(即零值),这些变量所使用的内存都将在方法区中分配
tips:
从概念上讲,类变量所使用的内存都应当在 方法区 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中
各个数据类型的零值如下表:
举个例子:
下面两段代码,code-snippet 1 将会输出 0,而 code-snippet 2 将无法通过编译。
注意:
这是因为局部变量不像类变量那样存在准备阶段。类变量有两次赋初始值的过程,一次在准备阶段,赋予初始值(也可以是指定值);另外一次在初始化阶段,赋予程序员定义的值。
因此,即使程序员没有为类变量赋值也没有关系,它仍然有一个默认的初始值。但局部变量就不一样了,如果没有给它赋初始值,是不能使用的。
3)解析Resolution
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用 ==》 直接引用
a、啥叫符号引用?
符号引用包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
不好理解?那看个例子,
写一段很简单的代码:
public class TestMain
private static int i;
private double d;
public static void print()
private boolean trueOrFalse()
return false;
用 javap把这段代码的.class反编译一下
- 看到Constant Pool也就是常量池中有22项内容,其中带"Utf8"的就是符号引用。比如#2,它的值是"com/xrq/test6/TestMain",表示的是这个类的全限定名;
- 又比如#5为i,#6为I,它们是一对的,表示变量时Integer(int)类型的,名字叫做i;
- #7为d、#8为D也是一样,表示一个Double(double)类型的变量,名字为d;
- #18、#19表示的都是方法的名字。
那其实总而言之,符号引用和我们上面讲的是一样的,是对于类、变量、方法的描述。
tips:符号引用和虚拟机的内存布局是没有关系的,引用的目标未必已经加载到内存中了
b、啥叫直接引用?
直接引用可以是直接指向目标的指针;
直接引用是和虚拟机实现的内存布局相关的;
如果有了直接引用,那引用的目标必定已经存在在内存中了。
tips:同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。
解析阶段负责把整个类激活,串成一个可以找到彼此的网,过程不可谓不重要。那这个阶段都做了哪些工作呢?大体可以分为:
3、初始化
初始化阶段是执行初始化方法 <cinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
cinit<>方法作用:执行静态变量赋值和静态语句块
说明:
<cinit>()方法是编译后自动生成的。
<cinit>方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块) 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的。
怎么理解这个顺序?看下面这个例子:
另外,如果有父类和子类,那么父类的 <cinit>()方法先执行, 也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,
如下代码, 字段B的值将会是2而不是1
class TestClinit02
static class Parent
public static int A = 1;
static
A = 2;
static class Sub extends Parent
public static int B = A;
public static void main(String[] args)
System.out.println(Sub.B);
注意:接口与类不同,接口的实现类在初始化时不会执行接口的 <cinit>方法, 因为只有当父接口中定义的变量被使用时,父接口才会被初始化。
Java虚拟机必须保证一个类的<cinit>方法在多线程环境中被正确地加锁同步, 如果多个线程同 时去初始化一个类, 那么只会有其中一个线程去执行这个类的<cinit>方法,其他线程都需要阻塞等待。
<cinit> 方法和 <init> 方法有什么区别?
可以简单地把<cinit> 方法看作是static静态代码块,把<init>方法看成是构造函数,看下面这个例子,就知道执行的过程了
输出:
其中 static 字段和 static 代码块,是属于类的,在类的加载的初始化阶段就已经被执行。类信息会被存放在方法区,在同一个类加载器下,这些信息有一份就够了,所以上面的 static 代码块只会执行一次,它对应的是<cinit>方法。而构造函数每创建一次对象就会执行。
结论:
- <cinit>方法 的执行时期: 类初始化阶段(该方法只能被jvm调用, 专门承担类变量的初始化工作) ,只执行一次
- <init>方法的执行时期: 对象的初始化阶段
以上是关于搞懂JVM类加载过程,其实很简单的主要内容,如果未能解决你的问题,请参考以下文章