【JVM】虚拟机类加载机制

类加载的时机

类的生命周期:加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析三个部分被称为连接。
在这里插入图片描述
加载、验证、准备、初始化和卸载五个阶段的顺序是确定的,而解析阶段不一定,某种情况下它可以在初始化阶段之后开始。
六种必须对类进行初始化的情况:

  1. 遇到new、getstatic、putstatic、invokestatic四条字节码指令时,如果类型没有进行过初始化,则需要先初始化。
  2. 使用java.lang.reflect包的方法对类型进行反射调用时,如果类型没有进行过初始化,则需要先出发其初始化
  3. 初始化类时,如果其父类没有进行初始化,则需要先触发父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化那个主类
  5. 使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.methodhandle实例最后的解析结果为REF_getStatic、REF_PutStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
  6. 当一个接口定义了JDK8新加入的默认方法(被Default关键字修饰的接口方法),如果有这个接口的实现类发生了初始化,那接口要在之前进行·初始化

上述六种场景称为主动引用,除此之外,所有引用类型的方式都不会触发初始化,称为被动引用,例如:

  1. 通过子类引用父类的静态字段,不会触发子类的初始化。对于静态字段,只有直接定义此字段的类才会发生初始化。
  2. 通过数组定义来引用类,不会触发此类的初始化。
  3. 常量(被final修饰的成员变量)在编译阶段会直接存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会发生初始化

接口与类真正有区别的是上述六种有且仅有需要触发初始化场景中的第三种,当一个接口初始化时,并不要求其父类接口全部都完成了初始化,只有真正使用到父类接口时才会初始化。

类加载的全过程

加载

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

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将字节流所代表静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表此类的对象,作为方法区中这个类的数据的访问入口
    通过一个类的全限定名来获取定义此类的二进制字节流,没有指明要从哪里获取,如何获取,因此
  4. 从ZIP压缩包中读取,最终成为日后JAR,EAR,WAR格式的基础
  5. 从网络中获取,应用就是Web Applet
  6. 运行时计算生成,这种场景使用最多的就是动态代理技术
  7. 由其他文件生成,典型场景是JSP应用
  8. 从数据库中读取
    ….
    一个数组类的创建过程需要遵循以下规则
  9. 如果数组的组件类型是引用类型,那就递归采用上述的加载过程区加载这个组件类型,数组C将被标识在加载该类组件类型的类加载器的类名称空间上
  10. 如果数组不是引用类型,Java虚拟机将会把数组C标记为与引导类加载器相关连
  11. 数组类的可访问性与组件类型的可访问性一致,如果组件类型不是引用类型,那么默认为public

验证

验证阶段是确保Class文件中的字节流符合《Java虚拟机规范》,保证这些信息在被当作代码运行后不会危害虚拟机自身的安全
验证阶段分为四个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证

  1. 文件格式验证
    验证字节流是否符合Class文件格式的规范,需要包括以下验证点:

  2. 是否以魔数0xCAFEBABE开头

  3. 主、次版本号是否在当前Java虚拟机接受范围之内

  4. 常量池的常量中是否有不被支持的常量类型

  5. 元数据验证
    对元数据信息中的数据类型进行校验

  6. 这个类是否有父类,除Object类外都有父类

  7. 这个类的父类是否继承了不允许被继承的类(final)

  8. 如果这个类不是抽象类,是否实现了其父类或者接口中要求实现的方法

  9. 类中的字段、方法是否与父类产生矛盾,或者不符合规则的方法重载

  10. 字节码验证
    对类的方法体进行校验,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为

  11. 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作

  12. 保证任何跳转指令都不会跳转到方法体以外的字节码指令上

  13. 保证方法体中的类型转换总是有效的

    如果一个方法体通过了字节码验证,也仍然不能保证它一定就是安全的。
    为了避免过多的执行时间消耗在字节码验证阶段,JDK6之后向方法体Code属性的属性表中新增了一项名为“StackMapTable”的属性,使在字节码验证期间,Java虚拟机就不需要根据程序推导这些状态的合法性,只需要检查StackTable属性中的记录是否合法

  14. 符号引用验证

  15. 符号引用中通过字符串描述的全限定名是否能找到对应的类

  16. 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法字段

  17. 符号引用中的类、字段、方法的可访问性

准备

准备阶段是正式为类中定义的变量(静态变量,也叫类变量,被static修饰)分配内存并设置类变量初始值的阶段,概念上讲,这些变量所使用的内存都应该在方法区中分配,JDK8之后,类变量则随着Class对象一起存放在Java堆中。
内存分配仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中

(java类的成员变量有两种:一种是被static关键字修饰的变量,叫类变量或者静态变量;另一种没有static修饰,为实例变量。
在语法定义上的区别:静态变量前要加static关键字,而实例变量前则不加。
在程序运行时的区别:实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以直接使用类名来引用。)

public static int value = 123;
变量value在准备阶段过后是0而不是123,因为尚未执行任何Java方法,把value赋值为123的putstatic指令是程序被编译后,存放于()方法之中,所以要初始化时赋值操作才会执行

public static final int value = 123;
特殊情况:类字段的字段属性表中存在ConstanrValue属性,拿在准备阶段就会被初始化为ConstanrValue属性所指定的初始值

解析

解析阶段是Java虚拟机将常量池的符号引用替换为直接引用的过程。
符号引用:符号引用是用一组符号来表示引用的目标,标识符可以是任何字面量,只要能定位到目标即可
直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
对一个符号引用进行多次解析是很常见的事。除invokedynamic外,虚拟机可以对第一次解析的结果进行缓存,避免重复操作。
Invokedynamic:invokedynamic用于动态语言支持,所以必须等到程序实际运行到这条指令时,解析动作才会进行。

  1. 类或接口的解析
    将设当前代码所处的类D,如果要把一个从未拆解过的符号引用N解析为一个类或接口C的直接引用,需以下步骤

  2. 如果C不是数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器加载这个类C,加载过程中,由于源数据验证,字节码验证的需要,又可能触发其他相关类的加载动作。以单这个加载过程出现任何异常,解析过程宣告失败

  3. 如果C是一个数组类型,数组中的元素为对象(Ljava/lang/Integer),则按照第一点操作。如果不是(Ljava.lang.Integer),由虚拟机创建一个代表该数组维度和元素的数组对象。

  4. 如果以上没有问题,则会在解析完成之前进行符号引用验证,确认D是否具备对C的访问权限,如果不具备,则抛出异常
    如果说D拥有C的访问权限,以下3条必须一条成立

  5. C是public的,并且与D处于同一模块

  6. C是public的,不与D在同一模块,但C蕴蓄D访问

  7. C不是public的,但与处于同一个包中

  8. 字段解析
    将字段所属的类或接口用C表示

  9. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。

  10. 否则,如果C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父类接口,如果接口中包含了简单名称和字段描述符与目标匹配的字段,则返回这个字段的直接引用,查找结束

  11. 否则,如果C不是java.lang.Object的话,会按照继承关系从下往上递归搜索其父类,如果父类中包含了简单名称和字段描述符与目标相匹配的字段,则返回这个字段的直接引用,查找结束。

  12. 否则,查找失败,抛出异常。

有一个同名字段同时出现在某个类的接口和父类当中,或者同时在自己或父类的多个接口中出现,按照解析规则仍是可以确定唯一的访问字段,但Javac编译器就可能直接拒绝其编译为Class文件。

  1. 方法解析
    需要先解析出方法所属的类或者接口的符号引用,用C表示

  2. 由于Class文件格式中类的方法和接口的方法符号引用的常量类型定义是分开的,如果在类的方法表中发现class_index中索引的C是个接口的话,那就直接抛出异常。

  3. 通过第一步,在类C中查找是否有简单名称和字段描述符与目标匹配的方法,如果有则返回这个方法的直接引用。

  4. 否则,在类C的父类中查找是否有简单名称和字段描述符与目标相匹配的方法,如果有则返回这个方法的直接引用

  5. 否则,在C实现的接口列标及它们的父接口中递归查找是否有简单名称和字段描述符与目标相匹配的方法,如果有,则说明类C是一个抽象类(方法在接口中有定义,自身却没有定义,则只可能是抽象类),查找结束,抛出异常。

  6. 查找失败,抛出异常

  7. 接口方法解析
    依然用C这个接口

  8. 如果接口方法中的class_index中的索引C是个类而不是接口,则抛出异常

  9. 否则,在接口C中查找是否有简单名称与字段描述符与目标相匹配的方法,如果有则返回这个方法的直接引用

  10. 否则,就在接口C父接口中递归查找直到java.lang.Object,是否有简单名称与字段描述符与目标相匹配的方法,如果有则返回这个方法的直接引用

  11. 对于规则3,由于java的接口允许多重继承,如果C的不同父接口中存在多个简单名称和字段描述符都与目标相匹配的方法,则会从中返回一个并结束查找

  12. 查找失败,抛出异常

初始化

初始化阶段就是执行类构造器()方法

1.()方法是由编译器总动收集类中的所有类变量的赋值动作和静态语句块(static{ })中的语句合并产生的,编译器收集的顺序是语句在源文件中出现的顺序。静态语句块只能访问到在它之间声明的变量。在它之后声明的变量只能赋值,不能访问。
2.java虚拟机会保证在执行子类的()方法前,父类的()方法执行完毕
3. 父类()方法先执行,则代表父类的静态语句块在子类的赋值操作之前执行
4. ()方法对于类或者接口来说非必须,没有赋值操作和静态语句块,就不会生成()方法
5.接口中不能使用静态语句块,但仍然有变量赋值操作,所以也会有()方法。与class不同的是,接口执行()方法不需要先执行父类的()方法,只有用到父类接口定义的变量时,才会初始化、
6.如果有多个线程同时初始化一个类,那只有一个线程会去执行这个类的()方法,其他线程需要阻塞等待,直到活动线程执行完()方法(同一个类加载器下,一个类型只会初始化一次)

类加载器

类与类加载器

即使两个类来源于同一个class文件,被同一个Java虚拟机加载,只要加载他们的类加载器不同,那么这两个类就不相等

双亲委派模型

启动类加载器:加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的能被Java虚拟机识别的类库

拓展类加载器:加载存放在<JAVA_HOME>\lib\ext目录,或者被java.ext.dirs系统变量所指定的路径中的类库

应用类加载器:加载用户路径(ClassPath)上所有的类库
在这里插入图片描述
类加载器之间的父子关系不是继承来实现的,而是组合关系来复用父类加载器的代码

双亲委派模型的工作过程:子类加载器会把类加载请求委派给父类加载器,一层层最终委派到启动类加载器,只有父类加载器在其搜索范围内找不到这个类,子类加载器才会自己去加载。

优点:保证在各种类加载器环境中都能够保证是同一个类,如果都由各个类加载器自己去加载,则系统中就会出现多个同名的类,Java类体系中最基础的行为无从保证,应用程序将会变得一片混乱。

破坏双亲委派模型

第一次:在双亲委派模型出现之前,类加载器的概念和抽象类地java.lang.ClassLoader就已经出现,Java设计者无法从技术手段避免classloader()被覆盖的可能,只能添加了一个findclass()方法,并引导用户编写类加载逻辑是去重写此方法。

第二次:父类加载器去请求子类加载器完成类加载的行为。例如,JNDI服务,它是由启动类加载器来完成加载,但JNDI存在的目的是对资源进行查找和集中管理,需要调用由其他厂商实现并部署在应用程序ClassPath吓得JNDI服务提供者接口。
因此设计了线程上下文类加载器,打通了双亲委派模型的层次结构来逆向使用类加载器。

第三次:热部署。OSGi实现模块化热部署的关键是它自定义的类加载机制的实现,每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi下,类加载器不再是双亲委派模型推荐的树状结构,发展成为了网状结构,在收到类加载请求时,OSGi将按照下面的顺序进行类搜索

  1. 将以java.*开头的类,委派给父类加载器加载
  2. 否则,将委派列表名单内的类,委派给父类加载器加载
  3. 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
  5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
  6. 否则,查找Dynamic Import列表地Bundle,委派给对应Bundle的类加载器加载
  7. 否则,加载失败
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值