JAVA类加载详细整理——《深入理解JAVA虚拟机》读书笔记

时间:2022-12-29 09:23:47

最近抽了些时间准备面试,整理了一下《深入理解JAVA虚拟机》里有关类加载的知识点,拿出来和大家分享下


1.java类的生命周期

加载->(连接阶段:验证->准备->解析)->初始化->使用->卸载

虚拟机规范中规定的有且只有下列5中情况需要对类进行初始化:

1.遇到new、getstatic(访问static字段)、putstatic(设置static字段)、invokestatic(执行static方法)字节码指令时

2.使用java.lang.reflect包的方法对类进行反射调用时

3.初始化子类时,如果父类还没有进行初始化时,需要初始化父类

4.虚拟机启动时,需要初始化主类(包含main()方法的那个类)

5.使用jdk1.7的动态语言支持时,当MethodHandler的解析结果为REF_getStatic、REF_putStatc、REF_invokeStatic方法句柄时,如果对应的类没有初始化过,则需要进行初始化

接口虽然不能使用static语句块,但编译器仍然会为接口生成<cinit>()类结构,用于初始化接口中定义的常量字段,不过接口在初始化时不要求父接口全部都完成了初始化

 

2. 类加载的阶段:

1.加载:

         1.通过一个类的全限定名来获取定义此类的二进制字节流

         2.将此二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构

         3.在内存中生成一个表示此类的Class对象(注意并没有规定这个Class对象是存在于堆中,在HotSpot中这个对象是存在于方法区中),作为方法区这个类的各种数据的访问入口

在1上衍生出的JAVA技术:

         1.从ZIP包中获取,成为日后JAR、WAR、EAR等的基础

         2.从网络中获取,例如Applet

         3.运行时计算生成,如java.lang.reflect.Proxy中就利用了ProxyGenerator.generateProxyClass来为特定接口生成形式为*$Proxy的代理类二进制字节流

         4.从其他文件中生成,如JSP文件

         5.从数据库中读取,这种场景较为少见,如SAP Netweaver中可以选择将程序安装到数据库中来完成程序代码在集群中的分发

加载阶段和连接阶段可以是交叉进行的,但是加载阶段总是先开始于连接阶段

 

2.验证:

         保证Class字节流的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

         从安全上讲,验证阶段是重要的,直接涉及到虚拟机自身承受恶意代码攻击的能力

         从执行性能上将,验证阶段的工作量在虚拟机的类加载子系统中占了相当大的一部分

         JVM规范2对这部分的定义较为笼统,定义了Class文件格式中的静态和结构化约束,如果不合跑出java.lang.VerifyError异常或其子类异常,但并没有规定检查哪些方面,怎么检查,什么时候检查,直到规范7才增加了对这部分的规定

         大致会完成以下四个阶段的检验动作:

1.文件格式验证

         保证格式符合JAVA类型信息的要求以能正确地解析并存储于方法区中

         1.是否以魔数开头:0xCAFEBABE

         2.主、次版本号是否在当前虚拟机的处理范围中

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

         4.指向常量中的各种索引值中是否有指向不存在的常量或不符合类型的常量

         5.CONSTANT_Utf8_infoz型的常量中是否有不符合UTF8编码的数据

         6.Class文件中各个部分以及文件本身是否有被删除的或附加的其他信息

         …(还有很多)

 

2.元数据验证

         进行语义分析验证

         1.是否有父类(JAVA中除了java.lang.Object外其他的类都应该有父类)

         2.是否继承了不允许被继承的类(被final修饰的类)

         3.如果不是抽象类,是否实现了父类中所有的抽象方法和接口中的所有方法

         4.是否覆盖了父类的final字段或方法重载不符合规则

         …

 

3.字节码验证

         进行数据流和控制流分析

         1.保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作(比如不会出现在操作数栈防了一个int型的数据却按long类型来加载到本地变量表中)

         2.保证跳转指令不会跳到方法体外的字节码指令上

         3.保证类型转化是有效的

         但即使进行了大量的分析也不能保证字节码就是安全的,涉及到了著名的停机问题

 

4.符号引用验证

         保证解析(下下个阶段)能正常执行

         1.通过全限定名是否能找到对应的类

         2.是否存在相应的方法或字段

         3.符号引用中的类、字段、方法(private、protected、public、default)的是否能被当前类访问

         …

        

3.准备阶段

         在方法区中分配类变量(static修饰的变量)内存并设置初始值,注这里的初始值指的是数据类型的零值而不是“通常情况下”我们在程序中自己设定的值,如public static int value = 123在准备阶段后的初始值是0而不是123,putstatic指令是放到cinit方法中在初始化阶段才会执行。但public static final int value = 123在准备阶段后的初始值是123因为在编译时javac会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123

 

4.解析阶段

         将符号引用转化为直接引用

         符号引用:Class文件中定义的用于引用的字面量

         直接引用:直接指向目标的指针、相对偏移量或能简介定位到目标的句柄

       虚拟机并为规定解析发生的具体时间,只要求了在执行某些字节码指令前(如newarray、invokestatic、invokevirtual)需要先对他们使用的符号引用进行解析

1.类或接口的解析

         假设当前代码所处的类为D,需要将未解析过的符号引用N解析成类或接口C的直接引用,需要经过以下步骤:

         1.如果C不是一个数组类型,需要将代表N的全限定名传给D的类加载器加载类C,一旦加载过程出现异常即宣告解析失败

         2.如果C是数组类型,且元素类型为对象,即N的描述符会是类似于[Ljava/lang/Integer的形式,则按1加载元素类型,接着又虚拟机生成一个代表此数组维度和元素的数组对象

         3.确保D对C具备访问权限

2.字段解析

         首先会对字段表内的classindex项中索引的CONSTANT_Class_info符号引用进行解析(//todo ?),再按如下步骤对C进行后续字段的搜索

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

         2.如果C中实现了接口,则按照继承关系从下往上递归搜索各个接口和它的父接口

         3.如果C不是java.lang.Object,则按继承关系从下往上递归搜索父类

         4.查找失败

         如果成功解析到了引用,将会进行访问权限验证,如果不合跑出java.lang.IllegalAccessError异常

3.类方法的解析

         1.如果在发现C是个接口,抛出java.lang.IncompatibleClassChangeError

         2.在C中查找

         3.在C父类中查找

         4.在C实现的接口以及其父接口中查找,如果存在匹配,抛出java.lang.AbstractMethodError

         5.否则,查找失败,抛出java.lang.NoSuchMethodError

4.接口方法解析

         1.如果C是个类,抛出java.lang.IncompatibleClassChangeError

         2.在C中查找

         3.在C父接口中查找

         4.否则,查找失败,抛出java.lang.NoSuchMethodError

         接口中不存在访问权限的问题(书上这么说,但是接口本身可以不是public的然后其他包就不能访问了啊)

 

4.初始化阶段

         执行类构造器<clinit>(),一些tips

         1.<clinit>()方法是编译器自动收集变量赋值动作和static语句块整合而来的,static语句中可以给定义位置在static语句块后的static变量赋值,但不能进行其他访问操作,不然编译会不通过

         2.虚拟机保证父类的<clinit>()方法会在子类的之前执行

         3.<clinit>()方法不是必须的,如果没有任何赋值动作或static语句块(可以由多个),编译器可以不生成<clinit>()方法

         4.接口<clinit>()方法的执行不需要先执行父接口的<clinit>()方法,除非使用到了父接口中定义的变量

         5.虚拟机保证<clinit>()方法在多线程环境能正确的加锁、同步