深入理解Java虚拟机——类加载子系统——第二章

本文详细介绍了Java虚拟机的类加载子系统,包括内存结构概述、类加载器的角色与加载过程、类加载的时机、加载过程的各个阶段以及类加载器的分类。重点讨论了双亲委派机制及其优势,以及沙箱安全机制,确保了程序的安全性和核心API不受篡改。此外,还探讨了自定义类加载器的使用和实现,强调了类加载器在确定类唯一性中的关键作用。
摘要由CSDN通过智能技术生成

深入理解Java虚拟机——Java虚拟机介绍——第一章
深入理解Java虚拟机——类加载子系统——第二章
深入理解Java虚拟机——运行时数据区和本地方法接口——详细篇——第三章
深入理解Java虚拟机——对象的实例化内存布局与访问定位——超级详细篇——第四章
深入理解Java虚拟机——直接内存——超级详细篇——第五章
深入理解Java虚拟机——执行引擎——超级详细篇——第六章
深入理解Java虚拟机——StringTable——超级详细篇——第七章
深入理解Java虚拟机——Java垃圾回收器——史上最烂的图文并茂结合——第八章
深入理解Java虚拟机——Class文件结构——第九章——中篇
深入理解Java虚拟机——字节码指令集与解析指令——第十章——中篇
深入理解Java虚拟机——类的加载过程(类的生命周期)——第十一章——中篇
深入理解Java虚拟机——再谈类的加载器——第十二章——中篇

1:内存结构概述

Java内存区域结构
在这里插入图片描述
我们如果自己手写一个java虚拟机,主要考虑那些结构?
类加载器和执行引擎
我们今天要说的就是Java内存区域结构中的类加载子系统部分

2:类加载器与类的加载过程

2.1 类加载子系统作用

  • 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转化解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制
  • 与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类型的加载,连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期间动态加载和动态连接这个特点实现。例如,如果编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类:用户可以通过java预定义的和自定义类加载器,让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分,这种组装应用程序的方式目前已广泛应用于Java程序之中。从最基础的Applet,JSP到相对复杂的OSGI技术,都使用了Java语言运行期类加载的特性
  • 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件(二进制字节流)在文件开头有特定的文件标识
  • ClassLoader只负贵class文件的加载,至于它是否可以运行,则由ExecutionEngine决定。
  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字常量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

在这里插入图片描述

2.2 类加载器ClassLoader角色

在这里插入图片描述
上图概述
1:class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
2. class file 加载到JVM中,被称为DNA元数据模板,放在方法区。

3.在 .class文件-> JVM ->最终成为元数据模板,此过程就要一个运输工具(类装载 Class Loader) ,扮演一个快递员的角色。

2.3 类加载时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:如下图的7个阶段
在这里插入图片描述
加载,验证,准备,初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定),这些阶段通常都是相互交叉地混合式进行的,通常会在一个阶段执行的过程中调用,激活另外一个阶段。
什么情况下需要开始类加载过程的第一个阶段:加载?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载,验证,准备自然需要在此之前开始)
1)遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
3)当初始化一个类的时候如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先进行初始化
如上5种场景的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用
被动使用类字段演示一:通过子类引用父类的静态字段,不会导致子类初始化
被动使用类字段演示二:通过数组定义来引用类,不会触发此类初始化
被动使用类字段演示三:常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量类的初始化

  • 接口的加载过程与类的加载过程稍微有一些不同,针对接口需要做一些特殊说明:接口也有初始化过程,这点与类是一致的,接口不可以使用静态语句块“static{}”,但是编译器仍然会为接口生成“< clinit >()”类的构造器,用于初始化接口中所定义的成员变量。接口与类真正有所区别的是:当一个接口在初始化时,并不需要其父接口全部都完成了初始化,只有真正使用到父接口的时候(如引用接口中定义的常量)才会初始化

2.4 类加载过程

在这里插入图片描述
在这里插入图片描述
以下是对加载,链接,初始化阐述
在这里插入图片描述
对加载阶段使用代码解读
在这里插入图片描述
对链接阶段使用代码解读
1:如何理解链接中的验证
我们把两个字节码文件(.class)放入BinaryViewer,观察他们开头的二进制字节码
在这里插入图片描述
在这里插入图片描述
发现都是CA。。。。。BE,说明在进行验证时,如果验证成功会出现开头如上的二进制码

所有可以被java字节码所识别的文件的开头都是CA FE BA BE

验证是链接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身安全(如果验证到输入的字节流不符合Class文件格式的约束,虚拟机就应该抛出一个java.lang.VerifyError异常或其子类异常)验证阶段大致会完成下面4个阶段的检验动作:1:文件格式验证(验证字节流是否符合Class文件格式规范,并且可以被当前版本的虚拟机处理),2:元数据验证(对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范的要求),3:字节码验证(最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的),4:符号引用验证(最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在链接的第三个阶段——解析阶段中发生),符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常检验以下内容:1:符号引用中通过字符串描述的全限定名是否可以找到对应的类 2:在指定类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段 3:符号引用中的类,字段,方法的访问性(private,public)是否可以被当前类访问

符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类。
2:如何理解链接中的准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值,通常情况下是数据类型的0值,假设一个类变量的定义为

public static int value=1234

那变量value在准备阶段过后的初始值为0而不是1234,因为这时候尚未开始执行任何Java方法,而把value赋值为1234的putstatic指令是程序被编译后,存放于类构造器< clinit >()方法之中,所以把value赋值为1234的动作将在初始化阶段才会执行。这里不包含final修饰的static,如下

public static final int value=1234

因为final在编译阶段就会分配,准备阶段就会进行赋值把1234赋值给value
如下图是Java中所有基本数据类型的零值
在这里插入图片描述

3:如何理解链接中的解析
我们对如下代码进行反编译javap -v HelloApp.class

public class HelloApp {
   
    private static int a = 1;
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值