jvm之虚拟机类加载

类的生命周期:加载、验证、准备、解析、初始化、使用、卸载

只有加载、验证、准备、初始化和卸载这五个阶段的顺序确定。解析可以在初始化后,为了支持运行时绑定。

虚拟机只规定了5种情况必须立即对类进行初始化,

1.遇到new,getstatic,putstatic或invokestatic这四条字节码,如果类没有初始化,则先对类进行初始化。

2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,则需要先进行初始化。

3.当初始化一个类的时候,发现其父类没有进行初始化,需要先触发父类的初始化。

4.虚拟机启动时,用户需要指定一个主类,虚拟机需要先初始化这个主类。

5.使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法对应的类没有初始化,则先触发初始化

对于静态字段的访问,只有定义这个字段的类才会被初始化,因此通过子类访问父类的静态变量字段,只会加载父类,不会加载主类。

常亮的编译,会经过常量传播优化,将常量存储到引用他的类的常量池中。

接口与类初始化的区别在于:接口初始化时,不需要其父接口都完成了初始化,只有在真正使用到父接口的时候,才会初始化。


加载过程:

加载阶段虚拟机做三件事情:

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

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

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

数组不通过类加载器创建,由java虚拟机直接创建,但是数组的元素类型要通过类加载器创建,数组类的创建过程遵循以下规则:

1.如果数组的组件类型是引用类型,那就递归采用本节定义的加载过程加载这个组件类型,数组类 将在加载该组件类型的类加载器的类名称空间上被标识。

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

3.数组类的可见性与它的组件类型一致,如果不是引用类型,默认是public


加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,然后再内存中实例化一个java.lang.class对象,这个对象将作为程序访问中对这些类的外部接口。


验证阶段:

目的是保证Class文件的字节流中包含的信息符合虚拟机的要求。

1.文件格式验证:目的是保证输入的字节流能正确地解析并存于方法区内,该阶段验证是基于字节流,之后的阶段都是基于方法区的存储结构

2.元数据验证:验证类数据信息

3.字节码验证:

4.符号引用验证:


准备:正式的为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区分配。首先,这时候进行内存分配的都是类变量,static修饰的,不包括实例变量,实例变量需要等到对象实例化的时候随着对象一起分配在java堆中。其次,这里的初始值都是数据类型的零值。

特殊情况是,final修饰的会直接赋值。


解析:虚拟机将常量池内的符号引用替换为直接引用。

符号引用:以一组符号描述所引用的目标,符号可以是任意形式的字面量

直接引用:直接指向目标的指针、相对偏移量或者是一个能间接定位到目标额句柄。

类或接口的解析:

如果不是数组类型,把符号引用的全限定名传给这个类的类加载器加载。

如果是数组类性,加载数据元素类型。

验证访问权限。

字段解析:首先解析这个字段所属的类或者接口,用C表示,接下来搜索这个字段:

1如果C本身包含这个字段,直接返回这个字段的引用。

2否则,如果C中实现了接口,按照继承关系,从下往上递归搜索接口,如果找到了字段,返回这个字段的引用。

3否则,如果C不是java.lang.Object,按照继承关系从下到上递归搜索父类,找到返回字段的引用。

4.否则查找失败,java.lang.NoSuchFieldError

类方法解析:解析这个方法所属的类或者接口,用C表示:

1如果类方法中发现C是个接口,抛出异常

2在类C中查找是否有简单名称和描述符都与目标相同的,返回这个方法的引用

3在类的父类中查找

3在实现的接口中查找,如果找到,说明C是一个抽象类,抛出java.lang.AbstractMethodError异常

4没找到,抛出java.lang.NoSuchMethodError

5验证访问权限

接口方法解析:同样找出方法所属的类或者接口C

1发现C是一个类,抛出异常

2在C中查找

3.在C的父接口中查找

4抛出异常

接口的方法不会有访问权限问题,都是public


初始化:真正开始执行类中定义的java代码。初始化阶段执行类的构造器方法的过程<clinit>

1.<clinit>()方法室邮编一起自动收集类变量的赋值动作和静态代码块中的语句合并而成,编译器收集的顺序就是语句在源文件中的位置,静态语句块中只能访问到定义在静态语句块之前的变量,定义在其后的变量可以对其赋值,但是不能访问。

2.<clinit>()方法与实例的构造函数不同,虚拟机会保证子类的<clinit>()执行之前,父类的<clinit>()一定都被执行过。所以java中第一个执行<clinit>()方法的一定是java.lang.Object类。

3.因为父类先执行,所以父类的静态代码块先于子类初始化

4.<clinit>()方法不是必须的,如果一个类或者接口没有静态语句块和变量赋值,编译器不会生成这个方法

5.<clinit>()方法接口不需要先执行父接口的方法,只有等到父接口变量使用时,才初始化,同样类的<clinit>()方法执行时,同样不会先执行实现接口的方法

6.一个类的<clinit>()方法在多线程环境下正确的加锁、同步,一个类加载其中下,同一个类型只会初始化一次。


类加载器:虚拟机中任意一个类,都需要由加载他的类加载器和这个类本身一同确定唯一性。

从虚拟机角度来看只有两种类加载器,一种是启动类加载器,是虚拟机的一部分。一种是所有其他的类加载器,由java语言实现,独立于虚拟机外部,都继承java.lang.ClassLoader抽象类。

从java程序员角度分为三类:

启动类加载器:存放在JAVA_HOME/lib目录下或者被-Xbootclasspath执行的路径中的,并且被虚拟机识别的(仅按照名字识别)的类库加载到虚拟机内存中。

扩展类加载器:由sun.misc.Launcher$ExtClassLoader实现,负责加载JAVA_HOME/lib/ext目录下的或者被java.ext.dirs系统变量所指定的路径中的所有类

应用程序加载器:sun.misc.Launcher$AppClassLoader实现。负责加载用户类路径的类库。

加载器的加载模式叫做双亲委派模式,每个加载器(除了启动类加载器)都要求有父类加载器的引用,如果一个类加载器收到加载类的请求,不会自己尝试加载,而是把这个请求委派给父类加载器去完成,只有父类加载器无法加载时,子加载器才会尝试加载,所以所有类首先都会由启动类加载器尝试加载。



发布了45 篇原创文章 · 获赞 21 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/ly262173911/article/details/78587409