java虚拟机学习(四)类的加载过程

类从虚拟机内存加载到从内存卸载,经历的生命周期是:加载,验证,准备,解析,初始化,使用,卸载这几个阶段, 其中验证,解析,初始化被称为 连接过程(Linking).

(打算这块和类加载原理后再看class文件结构那篇)

除了解析和使用,其他的过程基本顺序就是这样, 解析可以是在初始化完成之后,这是为了运行时动态绑定。

在虚拟机规范中定义了5中情况(有且只有)必须对类进行初始化(之前进行过,加载,验证,准备):

1.碰到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类没有进行初始化时,

2.在使用反射时,如果类没有初始化。

3.在这个类的父类没有初始化时。

4.虚拟机启动时指定的Main类,即主类。

5.使用动态语言支持时,特定的方法句柄对应的类没有初始化时(REF_getStatic,REF_putStatic,REF_invokeStatic)

类的加载

1.获取定义一个类的二进制流,二进制流可以是从网络获取,zip包,jar包都可以,也出现在jsp,动态代理(java.lang.reflect.Proxy)等。

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

3.内存中生成代表这个类的java.lang.class对象,作为方法区这个类的各种数据访问入口。

非数组类是开发者可控性最强的,它可以有系统提供的classLoader加载,也可以有用户自定义的classLoader加载,而数组类是由虚拟机直接创建,但数组类的类元还是由classLoader来加载。数组类的创建过程遵循一下原则:

1.如果数组的组件类型(类似与 Foo[] fooArray)为引用类型,数组将在该组件类型的类加载器的类名称空间上被标识。

2.如果数组的组件类型不是引用类型(如 int[]),java虚拟机将会把数组标记为与引导类加载器关联。

3.数组类的可见性与它的组件类型的可见性一致

加载完成后,在内存(并没有明确规定是在java堆中,class类对象比较特殊,在HotSpot虚拟机中这块内存指的是方法区内存)中实例化类的java.lang.class对象,这个对象作为程序访问方法区数据类型的外部接口,加载与连接过程是交叉进行的(因为连接过程中包含验证过程),其他连接过程与加载过程依然保持顺序执行。

验证:

java 的class文件并不一定是由java源码生成,它可以由十六进制编译器直接编写来产生,但java虚拟机不会去访问数组边界以外的数据,也不会将对象转换为没有实现的类,执行不存在的代码之类的事情。但这些都需要经过虚拟机的验证过程。也是防止虚拟机遭受恶意代码的攻击,如果要验证的流没有经过class文件格式的规范则抛出java.lang.VerifyError或其子类异常。详细的参考java虚拟机规范 . 大致的验证有以下4个阶段检验动作:

1.文件格式验证:

这个阶段验证字节码是否符合规范及该虚拟是否能处理,包括一下验证点:

是否以魔数0xCAFEBABEK开头。

主,次版本是否在当前虚拟机处理范围内。

常量池是否含有不被支持的常量类型等。

远不止这些,不过这个验证通过后字节码才能进入内存的方法区并转换为运行时的数据结构。

2.元数据验证:

主要是检测元数据是否符合java规范如:

这个类是否有父类。

类是否继承了不允许继承的类。

是否实现了接口的所有方法等。

3.字节码验证(应该可以理解为方法的合法性验证):

整个验证过程中最复杂的一步,主要是通过数据流和控制流分析,程序语法的合法性,确保符合逻辑的,在第二阶段对元数据验证结束后,这个阶段主要是对类的方法体进行验证,保证运行时不会危害虚拟机 ,如:

1.保证操作数栈的数据类型与指令代码序列配合工作且不会出现栈中是int类型,使用时却以long的方式载入到本地变量表,

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

3.保证类型转换是有效的。比如把子类对象赋值给父类对象这是安全的,但把父类对象赋值给子类的,甚至把对象赋值给与之不相干的类,前者是危险的后者是不合法的。

但这个过程之后也并不能完全保证它是安全的。 (关键字:StackMapTable, -XX:-UseSpliteVerifier, -XX:+FailOverToOldVerifier 以后用到的时候在看。先记下来:) )

4.符号引用验证(类,方法,字段是否可解析的验证):

目的是确保解析动作能正常运行,如果无法通过符号验证会抛出,java.lang.incompatibleClassChangeError的子类,如:java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchmethodError等,如果所运行的代码已经被反复使用和验证过,可以考虑使用-Xverify:none参数关闭大部分类验证,以缩短类加载时间。

通常需要校验的内容:

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

2.在指定的类中是否存在符合方法的字段描述符以及简单名称所描述的方法字段(应该是能否根据字段描述找到方法这个意思)。

3.符号引用中的类,字段,方法是否可被当前类访问(修饰符修饰的访问可能行)。

准备:

准备阶段正式为类的变量分配内存,这里的内存指的是方法区,变量是静态变量,非静态变量和类对象实例都是在java堆中分配内存,如:

static int value=123

在准备阶段value的值为0,这个时候方法还没有执行,只有经历初始化阶段value的值才会是123(putstatic是编译后存放于类构造器<clinit>()方法中),基本数据类型为 数字的都是0, 0f,0l,0d等,char为\u0000,String 不是基本类型,reference是null, 如果以上int 被final修饰过,则准备阶段虚拟机会根据ConstantValue的设置将value设置为123

解析:

解析阶段是把符号引用转换为直接引用的过程。

符号引用:

以一组符号来描述所引用目标(java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替),与虚拟机的内存布局无关,引用的目标不一定已经加载到内存,各种虚拟机实现的内存布局可以不相同,但是他们能接受的符号引用必须相同,符号引用的字面量形式明确定义在java虚拟机规范class文件中。

直接引用:

直接引用可以是指向目标的指针,偏移量,或是能间接定位的目标句柄。与内存布局相关,能直接引用说明对象已经存在与内存中,不同的虚拟机实例上翻译出来的直接引用可能不一样。

虚拟机根据需要判断,是在类加载时对常量池中符号做解析 ,解析字段,方法,接口的过程中会去class_index中索引所属的类 ,并解析类符号,在此过程中如果出错,都会导致以上解析无法继续进行。

1.类或接口解析:

如果代码所处的类为A, 要将为解析过的符号引用N解析为一个类或接口C的直接引用(也就是类A 里有类C的引用)。需要一下步骤。

1.如果这个类C不是数组类型,那虚拟机会把符号引用的全限名传递给类D加载器去加载,可能会触发其他相关类的加载动作,如:该类的父类或实现接口。如果加载过程中出现任何异常,解析会失败。

2.如果C是数组,且元素为对象类型,则会去加载该对象,之后虚拟机生成一个代表此数组纬度和元素的数组对象。

3.上面的步骤完成后要进行符号引用验证,确定D是否有对C的访问全限,如果不具有则抛出java.lang.illgalAcessError。

2.字段解析:

解析一个为被解析过的字段引用,首先会对字段表内class_index中索引的CONSTANT_CLASS_info符号引用做解析,字段所属的类或接口符号引用的过程出现异常,会导致字段符号引用的失败。若成功则有以下步骤:

1.如果一个类本身包含了简单名称和字段描述都与目标字段匹配,则返回这个字段引用,查找结束。

2.否则去查找C是否实现了接口,如果是会往上递归查询,如果存在名称和字段描述都匹配,返回引用查找结束。

3.否则去查找父类当中是否存在,如果存在名称和字段描述都匹配,返回引用查找结束。

4.如果父类,接口都没有查到,抛出java.lang.NoSuchFieldError异常。

若找到了但是没有访问全限,则抛出java.lang.IllegalAccessError.实际上可能会更严格,若字段在父类和接口中多次出现,可能会拒绝编译。

3.方法解析;

方法解析中, 类方法解析和接口方法解析是有区别的,在常量池中这俩符号引用不同。

1.一旦一个类被解析为类而非接口,在类方法表中如果这个方法所属的类是接口,那么直接会抛出java.lang.incompatibleClassChangeError,换句话说是尝试引用一个纯接口而非实现类的方法时,会抛出此异常。

2.如果简单名称与描述符号都与目标匹配的方法,则返回这个方法的引用,查找结束。

3.如果第二部没有找到会往上递归去父类中寻找。

4.如果父类中未找到,则会去实现的接口或接口的父类中去查找,如果找到则会抛出,java.lang.AbstractMethodError,说明该类为抽象类。

5.如果以上都没有找到,则会抛出java.lang.NoSuchMethodError。

查找成功会去做权限验证,如果无访问全限则报:java.lang.IllegalAccessError异常

4:接口解析:

1.第一条与方法解析相反,查找是在接口方发表中查询, 如果这个方法包含的接口是类,则会报java.lang.incompatibleClassChangeError。

2.之后去判断简单名称和描述符号都与目标匹配,如果查到了就返回这个方法的直接引用。

3.否则一直会递归到object类,如果找到则返回直接引用,如果没找到则报java.lang.NoSuchMethodError异常。

在成功的情况下跟类方法解析成功一样都会去做全限检查

 初始化:

首先会执行<cinit>方法,cinit方法并不是一定产生 只有当类中有static和变量初始赋值时产生的,在子方法执行<cinit>之前保证执行完父类的<cinit>方法,接口中虽然没有静态变量块但是,仍然会产生<cinit>方法,接口执行cinit方法前不需要执行父类的cinit方法,接口的实现类初始化也不会执行接口的cinit方法,在多线程环境中,一个类的cinit执行会阻塞其他类执行这个类的cinit方法,而且只执行一次。

到此类的加载过程结束,这个时候的类才能真正去使用。

时间: 2024-10-11 15:13:01

java虚拟机学习(四)类的加载过程的相关文章

java 类的加载过程

ClassLoader的主要职责就是负责各种class文件到jvm中,ClassLoader是一个抽象的class,给定一个class文件的二进制名,ClassLoader会尝试加载并且在jvm中生成构建这个类的各个数据结构,然后使其分布在对应的内存区域中. 1类的加载过程简介 类的记载过程一般分为三个比较大的阶段,分别是加载阶段,连接阶段和初始化阶段,如下图所示 加载阶段:主要负责查找并且加载类的二进制数据文件,其实就是class文件. 连接阶段:连接阶段所做的工作比较多,细分的话还可以分为如

java 反射,类的加载过程以及Classloader类加载器

首先自定义一个类Person package reflection; public class Person { private String name; public int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int a

JAVA类的加载过程

周末闲来无事,做个小例子去看一下JAVA代码初始化的过程 JAVA代码初始化分为两个步骤:类初始化,对象初始化. 类初始化 1 类初始化是指类加载器将类加载到内存时,对类成员的初始化过程(其中包括static修饰的变量). 2 对于加载完的类,它的类变量都会赋一个默认值,即使你定义时就赋值了. 3 例如int类型就是0,引用类型就是null. 对象初始化 1 其实和类初始化差不多,但是他通过构造函数对对象进行了赋值 其实简单来说 类初始化做了2件事情:1 把所有属性全部赋值为0或者NULL,2

java中带继承类的加载顺序详解及实战

一.背景: 在面试中,在java基础方面,类的加载顺序经常被问及,很多时候我们是搞不清楚到底类的加载顺序是怎么样的,那么今天我们就来看看带有继承的类的加载顺序到底是怎么一回事?在此记下也方便以后复习巩固! 二.测试步骤: 1.父类代码 1 package com.hafiz.zhang; 2 3 public class Fu 4 { 5 private int i = print("this is father common variable"); 6 private static

[jvm解析系列][九]类的加载过程和类的初始化。你的类该怎么执行?为什么需要ClassLoader?

通过前面好几章的或详细或不详细的介绍,我们终于把字节码的结构分析的差不多了.现在我们面临这样一个问题,如何运行一个字节码文件呢? 首先,java语言不同于其他的编译时需要进行链接工作的语言不通,java语言有一个很明显的特性,那就是动态加载,一个字节码的加载往往都是在程序运行的时候加载进来的,很多时候这种方式给我们带来了便利.虽然从某种意义上来说他可能消耗了一定的资源降低了性能. 类的生命周期? 没错,一个类的生命周期,在很多人眼里可能类天生都摆在那里了,随着程序生,随着程序死.但是事实情况并不

spring启动component-scan类扫描加载过程---源码分析

有朋友最近问到了 spring 加载类的过程,尤其是基于 annotation 注解的加载过程,有些时候如果由于某些系统部署的问题,加载不到,很是不解!就针对这个问题,我这篇博客说说spring启动过程,用源码来说明,这部分内容也会在书中出现,只是表达方式会稍微有些区别,我将使用spring 3.0的版本来说明(虽然版本有所区别,但是变化并不是特别大),另外,这里会从WEB中使用spring开始,中途会穿插自己通过newClassPathXmlApplicationContext 的区别和联系.

Dubbo源码解析之SPI(一):扩展类的加载过程

Dubbo是一款开源的.高性能且轻量级的Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用.智能容错和负载均衡,以及服务自动注册和发现. Dubbo最早是阿里公司内部的RPC框架,于 2011 年开源,之后迅速成为国内该类开源项目的佼佼者,2018年2月,通过投票正式成为 Apache基金会孵化项目.目前宜信公司内部也有不少项目在使用Dubbo. 本系列文章通过拆解Dubbo源码,帮助大家了解Dubbo,做到知其然,并且知其所以然. 一.JDK SPI 1.1 什么是SPI? S

类本质的是? 类的加载过程?

类本质的东西 类也是一个对象: Person对象的类型是Class类型,Class里面包含了* 类本身也是对象,是个Class类型的对象:简称类对象: 利用Person类对象 创建Person类型的对象: 利用Class类型创建Person类对象: //获取内存中的类对象 Class c = [p class]; Class c2 = [p2 class]; 也可以用Person类来获取类对象: Class c3 = [Person Class]; 我们要查看p和p2的类对象,就是利用类方法cl

【Spring源码分析系列】启动component-scan类扫描加载过程

原文地址:http://blog.csdn.net/xieyuooo/article/details/9089441/ 在spring 3.0以上大家都一般会配置一个Servelet,如下所示: 1 <servlet> 2 <servlet-name>spring</servlet-name> 3 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-clas