JVM系列-类加载机制

 

转载:https://blog.csdn.net/chayangdz/article/details/101102245

之前在看《深入理解Java虚拟机》这本书的时候,感觉看过之后就忘,而且看完整本书之后,总是感觉没有融会贯通,单独聊一个知识点还行,如果想站在一个全局的角度就说这些内容,就说不出来了。所以后来又读了一些JVM的专栏,看了一些其他的书,才慢慢的梳理出一些思路。

《深入理解Java虚拟机》讲的很好了,而且网上也有无数的JVM的帖子,所以在这篇文章中,就不再详细说那些内容了。

在说类加载之前,先说下整体的一个流程吧。

我们平时写代码的时候,写出来的都是.java文件,这就是我们平时说的源文件。这些文件会先经过编译,编译成.class文件,这就是字节码文件。Java虚拟机会加载这些字节码文件,经过一系列流程之后,就可以运行起来。

接下来再简单说下JVM,JVM可以提供一个托管的环境,在上面运行我们的Java代码。
Java是一门高级程序语言,语法比较复杂,没法直接在硬件上运行。所以就需要JVM。我们通过编译器,把写的.java格式的代码编译成.class文件,这个文件就是虚拟机可以识别的文件了,.class文件是二进制文件,如果用普通的编辑器打开这个文件,就可以看到里面的内容。这里我贴下书中的一个截图:
在这里插入图片描述
.class字节码就是长这个样子的,不过是按照16进制的方式呈现出来的。如果想学习这些内容的含义的话,就可以看JVM这本书的第六章,类文件结构了。不过之所以把.class文件叫做字节码,是因为Java字节码指令的操作码都是固定为一个字节。
还有一点,其实JVM是和语言无关的,设计者在当初设计JVM的时候,就考虑了让其他语言也运行在JVM上,现在也有很多语言可以在JVM上运行,比如JRuby,Jython,Scala,Groovy等。
在这里插入图片描述
好了,言归正传,接下来继续说类加载的事情,虚拟机把.class字节码文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,就是虚拟机的类加载机制。
类从被加载到虚拟机内存开始,到卸载出内存,一共分为如下7个阶段:
在这里插入图片描述
其中,验证、准备、解析这三个阶段,可以统称为连接阶段。

加载阶段:查找字节流,创建类。
先通过类的全限定名来获取此类的二进制字节流,然后把这个字节流转换为方法区的运行时数据结构,最后在内存中生成Class对象。(是类的Class对象,不是具体的实例对象)
其中,根据类名获取类的二进制字节流这一步,就需要通过类加载器完成。

连接阶段:将创建的类合并至JVM中,使之能够执行。
验证:确保class文件的字节流符合JVM要求。
准备:为类变量(static修饰的变量)分配内存,设置初始值(0值)
(不包括实例变量,实例变量会在对象实例化时随着对象一起分配到堆中)
解析:将常量池内的符号引用替换为直接引用。(只会替换一部分符号引用)

初始化阶段:执行clinit方法,执行Java代码的初始化逻辑。
<clinit>()方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。JVM会保证一个类的()方法在多线程环境中被正确的加锁和同步。如果多线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法。

其中,加载阶段和连接阶段交叉进行,加载尚未完成,连接可能就开始了。所以连接的验证阶段,是对.class文件进行的校验。

上面提到了符号引用和直接引用这两个概念,下面说下这两个概念的定义:
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

直接看两个定义还是有点迷糊,还是得结合具体的流程来理解:

Java代码在进行Javac编译的时候,会把.java编译成.class文件,这个class文件是有一定结构的,(就是JVM的第六章,类文件结构),这个类结构的第二部分,是常量池,主要存放编译期间生成的两大类常量,字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
1.类和接口的全限定名(Fully Qualified Name)
2.字段的名称和描述符(Descriptor)
3.方法的名称和描述符
那为什么会是这样的流程呢?
因为Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载class文件的时候进行动态连接。所以在class文件被加载到虚拟机之前,这个类无法知道其他类及其方法和字段所对应的具体地址,甚至不知道自己的方法和字段的地址。 在编译的时候,当需要引用这些成员时,java编译器会生成一个符号引用,这些符号引用放到类的常量池中。之后在类加载的解析阶段,会把常量池中的符号引用替换为直接引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化)。

在JVM的书中,提到了在解析阶段,会把符号引用替换为直接引用,但其实在虚拟机规范中,并没有规定解析阶段发生的具体时间,只要求了如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。

关于解析的阶段,还有一个点需要说下,我上面写了是把一部分符号引用替换为直接引用,那为啥不是所有的呢?这里就还有另外一些知识点:
class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

关于.class文件第二部分常量池,再说一点,在JVM的内存划分中,有个运行时常量池,属于方法区的一部分。在类加载后,.class类文件的常量池部分,将会进入JVM方法区的运行时常量池中存放。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值