但其中最主要的Class文件装载过程是加载、连接、初始化这三步。
加载类加载过程的第一个过程毫无疑问是加载,如果虚拟机还没有加载过此类,会通过类加载器将字节码文件加载到内存中。当然也可以是从ZIP压缩包,或者JAR、WAR等格式,最终也不过是从中取出类文件而已。
连接虽然任何二进制都可以是Class类型,但是只有JVM能够装载的Class文件类型才能运行在JVM之上。(也就是要符合虚拟机的规范的字节码文件才能通过)加载后是连接的过程(连接通俗的说就是将类文件与虚拟机建立关联),从上图可以看到连接的包括了三个过程:验证、准备、解析。 连接的第一步是 验证,验证会从四个阶段的检验依次进行:
验证验证的四个阶段:
- 文件格式验证,其实就是检查是否符合Class文件规范,主要有:魔数检查、版本检查等等,例如:魔数检查就是看Class文件开头是否是 0xCAFEBABE 开头(魔数通俗的说就是打一个让JVM认识的标签)。版本检查就是看Class文件主次版本号是否能在当前版本虚拟机处理等等。
- 元数据验证,(元数据可能不好理解,其实元数据从字面意思上就是 描述数据的数据,所以元数据验证就是语义的检查)检查类中是否被final修饰、是否有继承了父类等等。
- 字节码验证,要做的就是确定程序中的语义是否合法且符合逻辑的(通过分析数据流和控制流),确保跳转指令指向正确位置,操作数类型合理等等。
- 符号引用验证,先说什么是符号引用?符号引用是以一组符号来描述所引用的目标,可以是任何字面量。(听到这你可能还不能Get到的话,暖男一颗剽悍的种子,下面举个例子,你就悟了)
在上面我们知道 .class 文件是经过编译器编译后的文件,而 .class 文件里面的内容就是字节码,通过字节码中记录着自己将要使用的其他类或方法等。
例如下面这段代码,我们定义一个字符串类型变量的 userName,通过 System.out.println() 打印。
String userName
System.out.println(userName);
我们看上面代码编译成字节码后是什么样子的,如下所示:
0 ldc #2
2 astore_1
3 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
6 aload_1
7 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
10 return
可以看到在代码中最常见的表达式:
System.out.println(userName)
而转换后的字节码中是使用符号引用来“代替”表达:
invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
所以符号引用验证就是通过检查符号引用所“代替”的类、属性、方法是否存在且有权限被访问。
上面的验证东西很多,但是不管什么样的验证,都是为了 确保类符合 Java 规范与符合 JVM 规范,同时 避免危害到虚拟机的安全。
准备连接的第二步是 准备,这个阶段主要做两件事,为通过验证了的类来分配内存空间并设置初始化。
如对于 final static 修饰的变量,会直接赋值我们的定义值。可以看下面这段代码,会在准备阶段分配内存,并初始化值。
private final static String value = "一颗剽悍的种子"
各数据类型默认初始值,如下图所示:
注意:上图中并没有 boolean 类型,Java中的 boolean 类型的底层实现实际上就是 int 类型,int 类型默认值 0,对应的就是 boolean 类型默认值 false。)
解析连接的第三步是 解析,解析阶段的工作就是将 符号引用转为直接引用。因为在编译时类、方法等都是用符号引用来代替(所以为什么叫符号引用,符号只是个标识),而符号引用是并不知道这些数据所引用的 实际地址。
所以如果仅仅用符号引用就面临一个问题,就是 不能确定一定存在该对象。所以通过解析将符号引用转化为 JVM可直接获取的内存地址或指针,也就是 直接引用。
当解析将符号引用转成直接引用时,也就是目标必定已经在虚拟机的内存中存在(说白了用直接引用就是确定了存在该类、方法或属性)。
初始化类装载过程中最后阶段是初始化。而这个阶段将会执行构造器<clinit>方法,它是在通过我们前面提到过的Javac将 .java 文件编译成 .class 字节码文件时,所有类初始化代码,也就是包括静态变量赋值语句、静态代码块、静态方法,收集在一起后成为 <clinit>() 方法。
简单的概括初始化目的就是 初始化给类静态变量或静态代码块为程序员自己所定义的值。
到此,类的加载过程就像冯·诺依曼计算机结构中的输入设备,负责将数据丢进了入口后就是真正到JVM内部(JVM运行时数据区)去操纵数据,直至将我们的想法通过代码最后交给机器来完成。
JVM运行时数据区JVM运行时数据区主要分为 堆、程序计数器、方法区、虚拟机栈和本地方法栈 这五个分区。其中按 线程共享 和 线程私有 两类:
- 线程共享:堆、方法区。
- 线程私有:程序计数器、虚拟机栈、本地方法栈。
在JVM 内存中最大的一块内存空间就是堆,而且堆也被所有线程共享,所以堆也几乎存储着所有的对象。堆被按年代进行划分为 新时代、 老年代 以及 持久代,新生代又接着被分为 Eden (伊甸园区) 和 Surivor (幸存区)。而 Surivor 进一步由 From Survivor 和 To Survivor 进行划分。
JDK 8之前以及之后堆按年代划分的变化,如下图所示: