JVM类加载机制

参考《深入理解Java虚拟机》,周志明著。

个人理解,如有错误,还望大家指出,不胜感激。

在介绍类加载之前,我们可以先看一下Java程序的执行过程:

我们编写的.java代码,首先经javac编译后变为.class文件存放在磁盘上,然后经过类加载器进入运行时数据区(当然实际上不拘泥于从磁盘上拿.class文件进行加载),最后被传入执行引擎进行执行。

虚拟机的类加载过程包括 加载、验证、准备、解析、初始化 5个阶段。

类加载的时机

那么,什么时候进行类加载?虚拟机规范严格规定了有且只有下述五种情况必须立即对类进行“初始化”,而加载、验证、准备需在此之前开始(解析阶段则不一定,为支持Java语言的运行时绑定,它在某些情况下可以在初始化阶段之后再开始)。

1.遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行初始化,则先要触发其初始化。生成这四条指令的常见Java场景有:

  • 使用new关键字实例化对象的时候(new);
  • 读取或设置一个类的静态字段(getstatic、putstatic),final修饰的除外,因为编译期会把常量放入常量池;
  • 调用一个类的静态方法的时候(invokestatic)

这里注意一点,对于静态字段,只有直接定义这个字段的类才会被初始化,如果通过子类引用父类中的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类还没有进行过初始化,则需要先触发其初始化,如Class.forName("className");

3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类进行初始化;

4.我们在执行一个main方法的时候,包含main方法的那个类会先被初始化;

5.用JDK1.7启的动态语言支持时,如果一个MethodHandle实例最后解析的结果是REF_getStaticREF_putStaticRef_invokeStatic的方法句柄时,并且这个方法句柄所对应的类没有进行初始化,则要先触发其初始化。

好了,下面我们来看一下类加载的具体过程:

1.加载

在加载阶段,虚拟机需要完成以下三件事情:

1.通过一个类的全限定名来获取定义此类的二进制字节流(也就是我们的.class。同样地,不拘泥于磁盘上的.class文件,还可以从ZIP包中读取、从网络中获取、运行时生成等);

2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(也就是将类的静态属性存放在方法区,同样地,final修饰的除外);

3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。

从Java开发人员的角度看,类加载器包括启动类加载器、扩展类加载器、以及应用程序类加载器。

  • 启动类加载器:负责加载存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所制定的路径中且被虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使在lib目录中也不会被加载)类库加载到虚拟机内存中。
  • 扩展类加载器:它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
  • 应用程序类加载器:它负责加载用户类路径(ClassPath)上所指定的类库。

我们可以加入自己定义的类加载器。这些类加载器之间的关系如图:

VM基于上述类加载器,通过双亲委派模型进行类的加载,其工作过程为:   当一个类加载器收到类加载任务,优先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

双亲委派模型有什么好处? 比如位于rt.jar包中的类java.lang.Object,无论哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,确保了Object类在各种加载器环境中都是同一个类。这样可以避免类的重复加载。

2.验证

验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的安全(我们知道,字节码并不一定由Java代码编译生成)。

验证阶段大致包括四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。

3.准备

在这个阶段,JVM正式为类变量(静态变量)分配内存,并设置类变量初始值(通常情况为零值)。

既然是为静态变量分配内存,那么当然是分配在方法区中了。

变量初始值(零值):

数据类型零值
int0
long0L
short(short)0
char'\u0000'
byte(byte)0
booleanfalse
float0.0f
double0.0d
referencenull

这里要注意一点:类常量(由static和final修饰)在该阶段会被初始化为所指定的值,如:

public static final int value = 123;

javac编译时会为value生成constantValue属性,在准备阶段虚拟机将value值设置为123;。

4.解析

在该阶段,虚拟机将常量池内的符号引用替换为直接引用。

符号引用:可以是任何形式的字面量,只要能无歧义地定位到目标即可,符号引用所引用的目标不一定已经加载到内存中。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。有了直接引用,那该引用所引用的目标必然在内存中已存在。

‘5.初始化

在该阶段,执行类的构造器(<clinit>()方法)。

构造器(<clinit>()方法):<clinit>()方法是编译器自动产生的,由类变量的赋值动作和static{}块组成,顺序与Java代码中的顺序一致。

虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕(接口除外,执行接口的<clinit>()方法并不需要先执行父接口的<clinit>()方法)。

<clinit>()方法对类或接口来说并不是必需的,如果类或接口没有static字段以及static{}块,那么该类或接口便没有<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步。如果多个线程同事去初始化一个类,那么只有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程的<clinit>()方法执行完毕。同一个类加载器下,一个类型只会被初始化一次,因此执行<clinit>()方法的那个线程退出<clinit>()方法后,其他线程被唤醒之后不会进入<clinit>()方法。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值