jvm 类加载器详解

类加载器

想说明白什么是类加载器需要先明白几个关键问题:

第一个问题:
如何确定Java虚拟机中一个类的唯一性?虚拟机如何避免一个类被重复加载?
虚拟机中有多个类加载器,只有这个类和类加载器关联才可以确定其唯一性。也就是说一个类如果被不同类加载器加载了,就会生成多个逻辑上完全没有关系的类。

第二个问题:
什么是类加载器?他的作用是什么?
负责动态将Java类加载到Java虚拟机内存空间中。
我认为,是它将硬盘中编译好的.class文件加载导入到内存中变为一个class对象(堆中)和相关class信息(元空间)。我们创建对象全靠这些信息。

第三个问题:	
为什么要有很多不一样的类加载器?
区分同名的类,这里的同名类,来自不同的jar包,他们不止类名相同,包名也都一样。可通过自己定义自己的类加载器加载,防止冲突的发生。其实主要还是防止jdk自带的类和用户自己定义的类冲突。

第四个问题:
类加载器的加载机制是什么?为什么要使用这个机制?如何避免同一个类被重复加载?
使用双亲委派机制,类加载器通过双亲委派机制防止一个类被多个类加载器加载。
  1. 加粗样式双亲委派机制:

    类加载器会先将类传递给更高级别的类加载器加载;若没有更高的类加载器,或者更高级别的类加载器无法加载,才会自己加载。

    这种方法成功的避免了同一个类被不同的类加载器重复加载。

  2. jvm自带类加载器

    • 启动/引导类加载器(bootstrap classLoader)
      最高级别的类加载器,使用C++实现,加载javahome\lib中的jar包时使用,只能加载特定的jar包,如:rt.jar。引用方式很特别,指定双亲为null时自动引用。
    • 扩展类加载器(extension classLoader)
      加载javahome\lib\ext中的文件,可被开发者直接使用。
    • 应用程序/系统类加载器(Extension classloader)
      加载用户路径(classpath)下的所有类。也是默认的类加载器

这三种类加载器的传递方式为:

类加载过程

类加载有一个先对固定的顺序:

加载、验证、准备、初始化和卸载这5个阶段顺序是确定的,类的加载过程必须按照这种顺序开始, 而解析阶段则不一定: 它在某些情况下可以在初始化阶段之后再开始, 这是了支持Java语言的运行时绑定。

加载

三个操作:
1 通过类的全选定名获取这个类的字节流。
2 将这个字节流(字节流代表某种静态结构)转化为方法区运行时数据结构。
3 生成一个这个类的class对象,方法去数据的入口。

数组类情况比较特殊,它不是由一个类加载器加载的,它被虚拟机直接加载。
数组的组件类型(String[] 的组件类型就是String)是引用类型(一个类),则先加载组件类型,之后在加载这个组件类型的类加载器上关联这个数组。用于确定数组的唯一性。
数组的组件类型不是引用类型,数组会直接与引导类加载器关联

验证

确保虚拟机引入的class文件字节流是安全的。防止恶意代码攻击的发生。
四个操作:
1 文件格式验证
class文件符合规范,且能这个版本的虚拟机运行。
已特定魔数开头,版本号再虚拟机处理范围之内,常量池中的常量存在,以及常量池中各个索引类型正常,文件有没有被附加或删除信息,等等。。。
2 元数据验证
验证文件是否符合java规范。
类是否有父类,父类是否被final修饰,若这个类非抽象是否实现了继承类的抽象方法,是否与父类方法矛盾
3 字节码验证
验证程序方法体和程序块的语法符合逻辑,不会做出危害虚拟机的事情。
确保类型转换正常,引用赋值类型正常。
4 符号引用验证
在第四阶段解析阶段发生,对类和常量池的引用进行验证。将符号替换成实际对象的引用
校验是否能找到引用的对象,对象中是否有调用的方法和字段,对象和方法是否可以为当前代码所引用(private 就不可以)。

准备

对一切类的静态变量(被static修饰的变量)进行赋值,若次变量被final修饰则直接赋值为其等于的值或者引用的对象,若未被final修饰则赋值为0(int、byte、short)、null(对象)、0.0f(float)、\u0000(char)、false(boolean)、0.0d(double)、0L(int)。

解析

将常量池中的符号引用变为直接引用的过程。在变换之前会发生符号引用验证。对同一个目标的解析结果会被缓存,防止多次对同一目标进行解析。

  • 符号引用:使用一组符号来描述引用的目标。jvm规范对描述方式未明确定义,只要求准确即可。
  • 直接引用:指向目标的指针,偏移量或者句柄。

解析种类:
1 类与接口解析:设被解析者为N,N的引用者为D,N代表的类或为C
a、若C不是一个数组,虚拟通过把N全限定名传递给D的类加载器来加载C。
b、若C是一个数组类型,数组的元素类型为对象,那么会优先加载数组的元素类型,并生成数组对象。
c、若以上步骤正常进行,则C加载完成成为一个有效类或接口。此时进行符号引用解析,若无法引用抛出IllegalAccessError异常,若成功则进行赋值。

2 字段解析:设这个字段的拥有者为C(类),需要的字段为B
a、首先解析C。
b、若C中存在与B描述一致的字段,则直接饮用这个字段。
c、否则,若C中实现了接口,会递归查找他的父接口中有无符合B描述的字段,若存在则直接引用。
d、否则,若C不是Object类,递归其父类,查找符合B描述的字段,若存在则直接引用,不存在或者C就是Object,直接抛出NoSuchMethodError异常。
注意:若父类和父接口同时存在,会发生编译错误。
注意:最后会进行字段权限的验证,若不具备方法引用条件会抛出IllegalAccessError异常

3 类方法解析:设这个字段的拥有者为C(类),需要的字段为B
a、首先解析C。若发现C是接口直接抛出IncompatibleClassChangeError异常。
b、若C中有对应的方法,直接引用。
c、否则递归查找其父类,直到结束。若存在则引用。
d、否则查找父接口,若存在抛出AbstractMethodError 异常,否则抛出NoSuchMethodError 异常。
注意:最后会进行方法权限的验证,若不具备方法引用条件会抛出IllegalAccessError异常

4 接口方法解析
a、如果发现这是个类不是接口,则抛出IncompatibleClassChangeError异常。
b、否则在此接口中匹配方法。找到则直接引用。
c、否则递归查找其父接口,直到object类,若有匹配的则引用。
d、否则抛出NoSuchMethodError异常。

初始化

执行jvm的构造器(clinit()方法)。将类中所有变量赋值动作、静态语句块的动作合并成构造器,执行。
jvm的类构造器会先执行父类构造器。
jvm的接口构造器不会先执行父接口构造器,除非父类定义的变量被显式调用。
jvm的构造器都是线程安全的。
简而言之,类初始化时代码块和初始化方法执行顺序如下图:

注意:这个顺序和代码块初始化方法的位置无关

声明:部分文字图片来自 周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值