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块
- 只加载class文件到jvm,在
IV. 双亲委托
双亲委托,就是来了一个类加载,先扔给上面去处理,层层上传,只有上面处理不了时,才自己来决定
有啥好处?
- 系统类防止内存中出现多份同样的字节码
- 保证Java程序安全稳定运行
说明:双亲委托机制是可以被破坏的
V. 其他
参考:
- java类的加载机制
- 《深入理解Java虚拟机-JVM高级特性与最佳实践》
个人博客: Z+|blog
基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
声明
尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正
- 微博地址: 小灰灰Blog
- QQ: 一灰灰/3302797840