JVM学习之Java类的加载机制

平常我们使用java的多,深入到jvm层的机会却很少,平时若不关注,也不会清楚java文件编译后的class文件是如何被jvm加载到内存,如何进行初始化,如何进行运行的

因此这里主要学习的目标就是class文件的加载,会包含以下内容:

  • 什么是类加载
  • 类加载的过程
  • 什么时候触发类加载
  • 类加载器
  • 双亲委托机制

I. 什么是类的加载

简单来讲,类加载就是将class文件中的二进制,读取到内存中,解析其中定义的数据结构,然后在运行时方法区创建对应的数据结构,在堆内创建对应的class对象,而这个class对象,就是封装了对应的数据结构,和相关数据的访问操作方法;

上面的这一段简述中,却包含以下几个点:

1. 加载哪里的class文件?

第一步就是要明确的获取到对应的class文件了,jvm支持以下几个case中获取

  • 本地系统
  • 从网络上获取
  • 从数据库(or缓存等第三方存储)中获取
  • 从jar,zip包获取(比如我们依赖的第三方jar,大部分都是这种方式了)
  • 源码编译获取(如我们常用的Groovy脚本,源码方式存在,由GroovyEngine加载时就是源码编译成class文件之后由jvm加载的)

2. 数据结构

将class文件加载到内存后,一是在堆内创建class对象,一是在运行时方法区内创建对应的数据结构,具体的数据结构主要应该是类型信息

  • 类的方法代码,变量名,方法名,访问权限,返回值等

  • 类(静态)变量也存储在方法区

这一块有必要在jvm的内存分配中详细的研究下,每个存储区间到底干嘛用的,内部存写啥,先留一个坑位

3. class对象

class对象是在堆内创建,反射机制就是主要利用它来实现,通过class对象基本可以完全的操作这个类(包括创建对象,访问成员,调用方法)

II. 类加载过程

类加载过程主要包括: 加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

用一张图来表示整个过程,且会带上每个过程主要干嘛用的

类加载过程

1. 加载

加载作为类加载的第一个过程,主要就是将class文件代表的二进制,加载到内存中

  • 获取class对应的二进制流(可以从任何能获取到的地方读取对应的二进制流)
  • 将二进制流的静态存储结构转换为方法区的运行时数据结构
  • 在堆内创建class对象

上面的三个过程中,最灵活的就是获取二进制的过程,可以按照你的实际场景,从各种地方捞出数据

2. 验证

主要是验证class文件是否合法,有没有被篡改等,属于连接的一个过程

  • 文件格式验证:魔数校验,jdk版本校验
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行。

3. 准备

简单来说就是准备好静态变量的存储空间,并设置默认值,属于连接的一个过程

  • 正式为类分配内存
  • 为类变量设置默认的初始化值(不执行实际的赋值语句,这里专指基本类型的零值,对象的null)
  • 对static final 变量赋与代码中实际的值

4. 解析

简单来讲就是将常量池内的符号引用替换成实际引用,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,同样属于连接的一个过程

  • 符号引用:是一组符号来描述目标,可以是任何字面量
  • 直接引用:是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄

5. 初始化

为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化

准备阶段为类变量赋上了默认值,这里则主要是初始化代码中的赋值,一般而言根据实际定义的顺序进行初始化

a. 初始化步骤

  • 若类没有被加载连接,则优先加载
  • 若父类没有被初始化,则优先初始化父类
  • 执行类的初始化语句(直接赋值,静态代码块)

b. 初始化的时机

  • new创建一个对象时
  • 访问或修改类的静态变量,执行静态方法
  • 反射调用
  • 子类被使用
  • jvm启动时指定

6. 卸载

简单来说就是用完了,收拾线程的过程

  • 程序执行完成
  • 异常
  • 系统层面错误
  • System.exit()

III. 类加载器

可以理解为类加载器就是用来加载类的工具,同一个类被不同的类加载器加载之后,也认为他们是不同的

四种类加载器:自定义类加载器,应用类加载器,扩展类加载器,启动类加载器

1. 启动类加载器(BootStrap ClassLoader)

源头,根,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的

2. 扩展类加载器(Extension ClassLoader)

该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器

3. 应用类加载器(Application ClassLoader)

该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

4. 自定义类加载器(User ClassLoader)

自己实现的继承ClassLoader的加载器,可以按照自己的意愿,从某些地方加载类

5.类加载机制

  • 全盘负责
    • 当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托
    • 先尝试让父类加载器来加载,当父类做不到时,再自己来做
  • 缓存机制
    • 缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

6.类的加载

类加载有三种方式:

  • 1、命令行启动应用时候由JVM初始化加载

  • 2、通过Class.forName()方法动态加载

    • 加载class到内存,并执行static块
  • 3、通过ClassLoader.loadClass()方法动态加载
    • 只加载class文件到jvm,在class.newInstance()时,执行static块

IV. 双亲委托

双亲委托,就是来了一个类加载,先扔给上面去处理,层层上传,只有上面处理不了时,才自己来决定

有啥好处?

  • 系统类防止内存中出现多份同样的字节码
  • 保证Java程序安全稳定运行

说明:双亲委托机制是可以被破坏的

V. 其他

参考:

个人博客: Z+|blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正

扫描关注

QrCode