JVM类加载过程详解

jvm将class文件加载到内存,并对其进行校验,转换和初始化,最终形成jvm可以直接操作的内容,这就是虚拟机的加载机制
类的生命周期:类从加载到内存,再到使用,再到被卸载,整个生命周期经历的步骤如下图:

类加载时机:
类在什么情况下会被加载呢?对此虚拟机并没有规定,虚拟机明确定义了类初始化的时机,而类在初始化之前必然已经进行了加载和连接,发生下面情况之一的,就会触发类的初始化:
1.使用new来创建类的实例
2.使用类的静态变量(注意是变量,如果是final修饰的常量,String字面量等会在编译期被放入class文件的常量池中的情况,不会触发初始化)
3.调用类的静态方法
4.使用java.lang.reflect包中的方法对类进行反射调用时
5.初始化一个类时,会先初始化其父类(接口不会)
6.虚拟机启动时,指定的main方法所在类

类加载步骤:
'使用'之前的步骤称为类加载,类加载分为 加载、连接、初始化 ,而连接阶段又被分为了 验证、准备、解析 三个阶段,注意:除了'解析'这一步外,其他步骤都是严格按照图上标示的先后顺序来启动的,虚拟机并没有规定解析要在初始化之前还是初始化之后进行
加载:在此阶段,虚拟机需要完成下面几个过程:
1.根据类的全限定名获取类的二进制字节流(获取来源并不局限于class文件,可以是从网络中,可以是从数据库中,可以从zip包中,还可以在运行时动态生成,大家熟知的动态代理框架javaProxy和CgLib都是在运行时以动态的方式生成类的二进制字节流),这个过程由类加载器完成,这也是我们程序员可以操作的地方,后面我会有文章介绍类加载器
2.将这个字节流代表的静态结构转换为方法区代表该类类型信息的动态结构
3.为该类生产一个java.lang.Class对象,作为访问类型信息的入口(class对象不同于一般的对象,在hotSpot虚拟机中,它被存放在方法区而不是堆区)
验证
由于类的二进制字节流并不仅仅来源于java编译器编译出的class文件,上面说了,还会有其他各种来源,因此为了保证虚拟机运行的都是正常代码,同时也是为了安全,虚拟机需要对字节流进行验证,以保证其中内容符合虚拟机规范,不会影响到虚拟机安全。
验证阶段大致包含这些内容:文件的格式内容,比如文件起始的魔数,主次版本号,常量池内容是否合规;这个类是否有父类,是否继承了不该被继承的类,类中的字段、方法是否符合规范;对类中方法的方法体进行校验,判断是否含有非法指令;由于在后面的解析阶段会将常量池中的符号引用解析为直接引用,所以这里需要提前验证下符号引用的有效性,比如代表类全限定名的符号,要判断是否有这样的类存在
准备:
该阶段会正式为类变量分配内存并赋初始值,初始值并不是程序中指定的值,比如对于int类型的变量就赋值为0,boolean类型就赋值为false,引用类型就赋值为null。
有两点需要注意:1.这里是对类变量也就是static变量而不是成员变量进行赋值,2.注意那个变字,如果是常量final static.int a=123,或者static String s="abc"这种会直接进入方法区常量池的内容,会直接赋值为程序中指定的值而不是初始值
解析:
该阶段会将该类中的符号引用解析为直接引用,在我的博文"java运行时数据区"中说了符号引用存在于class文件中,加载后会存放运行时常量池中,这部分数据无法直接定位到目标位置,而只是用于描述目标,将其解析为直接引用后就可以通过直接引用定位到目标的句柄(句柄这里可以简单的理解为在内存中的偏移量)
初始化:
初始化阶段就是执行类的cinit方法,cinit方法是编译期间虚拟机收集类中所有对类变量赋值的语句以及static代码块中的语句合并而成的,在准备阶段,虚拟机已经为类变量赋了初始值,而初始化阶段就正真的是按照程序员的意愿来对类变量进行初始化了。cinit方法对于类或者接口来说并不是必须的,如果没有定义类变量或者对其赋值,则不会生成cinit方法;虚拟机会对cinit方法加锁,防止多个线程同时对类进行初始化导致并发问题;
我们对比下init方法 :了解了cinit方法后,再来看init方法就很简单了,init方法是对成员变量进行初始化,相对应的也就是编译期间虚拟机会收集对成员变量的赋值语句以及代码块中的语句合并为init方法。与cinit方法不同的是,init方法中会显式调用父类的构造器,cinit方法中不用是因为虚拟机会保证父类的cinit方法一定会先执行,所以java.lang.Object一定是最先被执行cinit方法的类。
补充一点: 数组类本身不是被类加载器加载的,而是虚拟机直接加载的,但是数组中的元数据还是由类加载器来加载

猜你喜欢

转载自blog.csdn.net/wb_snail/article/details/80607878