理解Java虚拟机(2)之.class文件加载过程
读《深入理解Java虚拟机》-周志明 读书笔记
虚拟机只能执行.class文件,在.class文件加载过程中,生命周期包括:加载,验证,准备,解析,初始化,使用,卸载
加载将.clss文件加载进虚拟机,加载来源有
- 1.常见jar包;
- 2.网络获取(dubbo的RPC是典型,底层通过java的RMI方法,通过固定协 议,将远端生成者接口,对象序列化,消费者调用);
- 3.运行时动态生成,典型代表反射技术;
- 4.从其他文件,如jsp;
- 5.通过数据库读取,(没见到过,书里说中间件服务器)
加载完成后,虚拟机就会按照.class文件里面的内容相应的存储到内存的方法区(方法区是虚拟机加载类信息,常量,静态变量,即使编译后的代码,是线程共享的,简单讲就是加载的东西放在这里面),加载是通过ClassLoader这个加载,这个加载机制是通过(双亲委派模型,后面详细讲双亲委派模型,简单讲就是JVM有一个系列的继承加载,自己也可以自定义ClassLoader,先通过自己的ClassLoader,在通过JVM的ClassLoader),平常使用Tomcat,Jetty,这些容器都是有自己的ClassLoader,用过maven配置jetty启动时,都会在pom.xml配置标签,这个就是去配置jetty的ClassLoader。
验证
验证是连接阶段的第一步,并不是加载完了才验证(这样的太傻瓜式了,要是很多.class文件,在最开始加载就有错误,等加载完了再验证那就不用工作了),验证和加载是相辅相成的。在.java编译成.class文件里讲过.class打头就是个”cafebabe”的魔数,首先验证这个,不是这个打头的必然报错,接着看看版本号,版本号是不是java没有的(比方搞个java6.0,必然错了),然后看常量类型啊,验证相当于看这个.class文件是否符合JVM规范,用过Eclipse的就知道,每次”ctrl+s”就能看到自己写的代码有没有错误,因为eclipse就是有自己的CLassLoader,即时编译,你一保存就给你编译了.class,打开文件夹就能看到class目录的文件夹,然后通过去验证编译后的.class是不是符合JVM规范,然后错了就各种红叉叉,iead就没有即时编译,”ctrl+s”就找不到对应的class文件夹,eclipse的验证不是虚拟机的验证,这只是eclipse自己根据jVM规范验证的,编译后的.class文件JVM在加载的过程中,还是要自己验证的,不过虚拟机有个参数可以设置不验证(-Xverify:none),这样可以加快加载速度,当然前提是你相信.class符合规范,验证是很重要的一件事,不然怎么承受恶意的攻击,使得代码更安全,当然验证也会使得加载时间长,在安全和时间上,还是选择更安全些,毕竟java大都都是用来做企业级应用的,安全是最重要的。准备
准备阶段是正式给类变量(static修饰的变量)分配内存并设置类变量的初始值,实例变量(不是static修饰的类层面的变量)会在对象实例化时随着对象一起分配在java堆中,要注意的是初始值的含义,和final修饰的实例变量,初始值指数据类型的0值,下面a在这个时候初始值为0;final修饰的变量这个时候会给值,c此时的值就是为3;(感觉final修饰符有些破坏了面向对象的设计,也不知道这个设计的初衷)
public Class Test(){
private static int a = 1;//类变量
private int b = 2 ;//实例变量
private final int c = 3 ;
}
- 解析
解析是虚拟机将常量池(常量池位于方法区)的符号引用替换成直接引用的过程,先解释下符号引用,直接引用
符号引用:
public class Test {
public static int sa = new Random().nextInt();
}
public class TestClass {
//这里就是符号引用
public static int sa = Test.sa;
public static int sb = 1;
}
编译下上上面的,再反编译过来如下(javap -verbose TestClass.class):
D:\learn\myLearn\readBook\src\main\java\JVM>javap -verbose TestClass.class
Classfile /D:/learn/myLearn/readBook/src/main/java/JVM/TestClass.class
Last modified 2016-8-12; size 330 bytes
MD5 checksum 96ed1ae561b16a0cfecbdf7518c947cb
Compiled from "TestClass.java"
public class JVM.TestClass
SourceFile: "TestClass.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()
#2 = Fieldref #18.#19 // JVM/Test.sa:I
#3 = Fieldref #5.#19 // JVM/TestClass.sa:I
#4 = Fieldref #5.#20 // JVM/TestClass.sb:I
#5 = Class #21 // JVM/TestClass
#6 = Class #22 // java/lang/Object
#7 = Utf8 sa
#8 = Utf8 I
#9 = Utf8 sb
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 <clinit>
#15 = Utf8 SourceFile
#16 = Utf8 TestClass.java
#17 = NameAndType #10:#11 // "<init>":()V
#18 = Class #23 // JVM/Test
#19 = NameAndType #7:#8 // sa:I
#20 = NameAndType #9:#8 // sb:I
#21 = Utf8 JVM/TestClass
#22 = Utf8 java/lang/Object
#23 = Utf8 JVM/Test
{
public static int sa;
flags: ACC_PUBLIC, ACC_STATIC
public static int sb;
flags: ACC_PUBLIC, ACC_STATIC
public JVM.TestClass();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
":()V
4: return
LineNumberTable:
line 8: 0
static {};
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #2 // Field JVM/Test.sa:I
3: putstatic #3 // Field sa:I
6: bipush 114
8: putstatic #4 // Field sb:I
11: return
LineNumberTable:
line 9: 0
line 11: 6
}
截部分看看
java/lang/Object."<init>":()
#2 = Fieldref #18.#19 // JVM/Test.sa:I
#3 = Fieldref #5.#19 // JVM/TestClass.sa:I
static {};
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #2 // Field JVM/Test.sa:I
3: putstatic #3 // Field sa:I
6: bipush 114
8: putstatic #4 // Field sb:I
最后satatic{}可以看到Test.java中sa(#3 = Fieldref #5.#19 // JVM/TestClass.sa:I)变量指向的TestClass(JVM/Test.sa:I 3: putstatic #3 // Field sa:I ),初始化就是将这种指向变成指向new Random().nextInt(),的具体内存地址,这样才能真正的调用直接引用就是直接new 对象了,将句柄指向对象的指针地址;在解析的过程中,遇到引用其他的类,就去触发对应类的加载,最后都是直接引用
- 初始化
初始化是类加载的最后一步,这个阶段才是真正执行类中定义的代码,在准备阶段,变量已经赋值过系统要求的值,初始化阶段根据我们写代码的主观习惯去初始化变量和其他资源。相当于程序真正运行起来。