深入了解Java虚拟机

基本概述

1、class文件中描述的各种信息都要 加载到虚拟机中才能运行和使用
2、Class文件是一串二进制字符流,可以以任何形式存在,可以是磁盘文件 网络 数据库 内存 或动态产生等

虚拟机的类加载机制

1、java虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这个过程称为虚拟机的类加载机制
2、Java语言中的类型的加载、连接、初始化过程都是在程序运行期间完成的
1)提供了极高的灵活性和拓展性
2)运行期动态加载和动态连接

接口的加载过程与类加载的过程的区别

类的初始化

1、当一个类进行初始化时,要求其父类全部都已经进行初始化了(类和接口加载过程中的真正区别)
2、类用static{}静态代码块来输出 初始化信息

接口的初始化

1、当一个接口在初始化时,不要求其父类接口全部完成初始化,只有真正用到父接口时候(如引用接口中定义的常量)才会初始化 (类和接口加载过程中真正的区别)
2、接口中不能使用static{}静态代码块 但编译器 仍然会为接口生成() 类构造器 (见第十章前端编译与优化)用于初始化接口中所定义的成员变量

什么时候进行类加载过程中的加载

java虚拟机规范中并没有强制要求,java虚拟机具体实现自由把握

类加载生命周期

1、加载
a、什么时候进行类加载过程中的加载?

1)java虚拟机规范中并没有强制要求,java虚拟机具体实现自由把握

b、加载阶段Java虚拟机要做什么(Java虚拟机规范对这没有严格的要求)?
1 )通过一个类的全限定名来获取定义此类的二进制字节流
2) 将这个字节流所代表的静态存储结构 转换为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据的访问入口

2、连接
验证
准备
解析

3、初始化
a、 java虚拟机规范 严格规定了有且只有6种情况必须对类进行初始化情况

加载、验证、准备在此(初始化)之前开始
1)遇到new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类型没有进行过初始化,则需要先触发器初始化阶段
1.1)使用new关键字实例化对象的时候
1.2)读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
1.3)对于一个静态字段 只有直接定义这个字段的类才会被初始化,故通过子类来引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类的初始化
1.4)调用一个类型的静态方法的时候
2)java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化
3)当初始化类、如果发现父类还未初始化,则需要先对父类进行初始化
4)当虚拟机启动时,用户需要指定一个要执行的类(包含main() 方法的那个类),虚拟机会先初始化这个主类
理解 这个主类
5)当使用JDK7 新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic REF_putStatic REF_invokeStatic REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发器初始化
6)当一个接口中定义了jdk8新加入的默认方法(被default关键字修饰的接口方法)时 如果有这个接口的实现类发生了初始化,那么这个接口要在之前被初始化

这6种行为称为对一个类型的主动引用,除此之外,所有引用类型的方式都不会触发初始化 被称为被动引用-----思考:为什么说除此之外的所有引用类型的方式 不会触发初始化 而不是说任何方式,难道还有其他方式会触发初始化 还是触发初始化方式只有靠引用类型才会

1 子类引用父类的静态变量,不会导致子类初始化
2 通过数组定义来引用类 不会触发此类的初始化
3 常量在编译期间就存入常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

b 接口也有初始化过程

4、使用
5、卸载
注意
类型的加载、验证、准备、初始化和卸载 这五个阶段的顺序是确定的
解析阶段 可能在初始化阶段之后开始
因为java语言的运行时动态绑定
类型的加载按照这种顺序按部就班的开始(这些步骤可以交叉混合进行)

类加载过程

1、加载

在什么时候进行类加载的过程的加载
Java虚拟机规范中没有进行强制约束,由Java虚拟机的实现自由把握
加载阶段和连接阶段的部分动作是交叉进行的
加载动作可以未做完 连接动作开始
加载阶段和连接阶段的开始时间按固定的先后顺序 先加载后连接

加载阶段Java虚拟机要做什么(Java虚拟机规范对这没有严格的要求)
1 通过一个类的全限定名来获取定义此类的二进制字节流
2 将这个字节流所代表的静态存储结构 转换为方法区的运行时数据结构
3 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区中
方法区的数据存储格式由虚拟机实现自行定义
Java虚拟机规范中没有规定方法区的具体数据结构
会在堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区总的数据类型的外部接口
2、连接
2.1验证
连接阶段的第一步
确保class文件的字节流包含的信息是否符合java虚拟机规范的约束要求
保护自身 防止载入错误、有恶意的字节流,导致系统受攻击
若不符合规范 抛出 java.lang.VerifyError异常
通过验证阶段的字节流 进入java虚拟机内存的方法区进行存储

2.1.1文件格式验证
验证字节流是否符合文件格式的规范
主要目的:保证输入的字节流能正确的解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求
只有通过文件格式验证,该段字节流才被允许进入Java虚拟机内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构上进行的,不再直接读取、操作字符流了
2.1.2元数据验证
对字节码描述的信息(类的元数据信息)进行语义分析,保证其描述的信息符合java语言规范的要求
2.1.3字节码验证
最复杂的一个阶段
主要目的:通过数据流分析和控制流分析,确定程序语义是合法的 符合逻辑的
这个阶段会对类的方法体(class文件中的code属性) 进行校验分析,保证方法不会在运行时对虚拟机做出危害行为、
2.1.4符号引用验证
发生虚拟机将符号引用转换为直接引用的时候
这个转换动作将在连接的第三个阶段—解析阶段中发生
符号引用验证是对类自身以外的各种信息进行匹配校验
目的:确保解析行为正常执行

2.2准备
为类中定义的变量(静态变量 被static修饰的变量)分配内存空间并设置类变量初始值的阶段–概念上 在方法区(逻辑上的概念)对变量进行分配
进行内存分配的仅包括静态变量分配内存,不包括实例变量
实例变量在对象实例化时随对象一起分配内存在java堆中
类变量的初始值 通常是数据类型的零值
若类变量有final修饰 初始值为指定的属性值
概念上 变量使用的内存在方法区(逻辑概念)进行分配 逻辑概述
jdk7 之前hotspot 使用永久代实现方法区
jdk8之后类变量随class文件存放在java堆中

2.3解析
将java常量池中的符号引用替换为直接引用的过程
符号引用
以一组符号来描述所引用的目标
直接引用
可以直接指向目标的指针、相对偏移量、或者能够直接定位到目标的句柄
解析动作
类或接口的解析
字段解析
方法解析
接口方法解析
初始化
3初始化
概述
类加载过程的最后一个阶段
除加载阶段用户程序可以通过自定义加载器的方式局部参与外,其余动作完全由Java虚拟机主导控制
初始化阶段 Java虚拟机真正开始执行类中编写的Java程序代码,主导权由Java虚拟机移交给应用程序

关于赋值:

准备阶段,变量已经赋过依次系统要求的初始零值

初始化阶段,根据程序员制定的值去初始化变量值 和其他资源
初始化阶段就是执行类构造器()方法的过程
类构造器不是程序员在Java代码中编写的,而是Javac编译器自动生成物
()方法是如何产生的
()由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的
1 编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态静态语句块可以赋值,但不能访问
public class Test{
static {
i = 0;//给变量赋值可以正常编译通过 可以赋值
System.out.println(i)//报错 不能通过 提示非法前向引用 不能访问
}
static int i = 1;
}
2 ()方法与类的构造函数实例构造器()方法不同
不需要显式的调用父类构造器 Java虚拟机会保证子类的()方法执行前,父类的()方法执行完毕 因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object
父类的()方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作
public class parent {
public static int A = 1;
static {
A = 2;
}
}
class Sub extends parent{
public static int B = A;
}
class Test{
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
结果 : 2 因为: 父类中定义的静态语句块要优先于子类的变量赋值操作

3 ()方法对类和接口来说并不是必须的 如果一个类没有静态代码块也没有对变量的赋值操作,那么编译器 不会为这个类生成一个()方法
4 接口中不能使用静态代码块,但仍然有变量初始化的赋值操作 会生成()方法
不同的是: ()方法不会先执行父接口的()方法,只有当父接口中定义的变量被使用时,父接口才会初始化
5 Java虚拟机必须保证一个类的()方法 在多线程环境中被正确地加锁同步

类加载器

实现类的加载动作

通过一个类的全限定名来获取描述该类的二进制字节流(动作在java虚拟机的外部实现) 实现这个动作的代码被称为类加载器(Class Loader)

类加载器动作在java虚拟机的外部实现,以便于让应用程序自己决定如何去获取所需的类

对于一个任意的类,都必须由加载它的类加载器和这个类本身一起共同确立 其在java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间

比较两个类是否相等
只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使两个类源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等

类加载器种类

1、java虚拟机角度:两种类加载器
1)启动类加载器
c++语言实现 虚拟机的一部分
2)其他所有的类加载器
java语言实现 独立于虚拟机外部,全部继承自抽象类 java.lang.ClassLoader

2、java 开发人员 角度: 三层类加载器(JDK 9之前的Java应用都是由这三种类加载器相互配合完成加载的)

1)启动类加载器(Bootstrap Class Loader)
这个类加载器 负责加载 存放在<JAVA_HOME>\lib 目录或者被-Xbootclasspath 参数所指定的路径中存放的而且是java虚拟机能够识别的类库 加载到虚拟机的内存中
启动类加载器 无法被java程序直接引用

2)扩展类加载器(Extension Class Loader)
扩展类加载器在类sun.misc.Launcher$ExtClassLoader 中以java代码的形式实现的
负责加载<JAVA_HOME>\lib\ext目录中 或者被java.ext.dirs 系统变量所指定的路径中所有的类库
由java代码实现,开发者可以直接在程序中使用扩展类加载器来加载class文件
应用程序类加载器(Application Class Loader)

应用程序类加载器 由sun.misc.Launcher$APPClassLoader 实现
负责加载用户类路径上的所有类库,可以在代码中直接使用这个类加载器
也称系统类加载器
因为是ClassLoader类中的getSystem-ClassLoade() 方法的返回值
应用程序中如果没有自定义过自己的类加载器,一般情况下应用程序加载器就是程序中默认的类加载器

双亲委派模型

各种类加载器之间的层次关系被称为类加载器的”双亲委派模型“,除了顶层的启动类加载器没有父类加载器外,其余的类加载器都有自己的父类加载器
定义
除了顶层的启动类加载器外,其余的类加载器,都应该有自己的父类加载器
类加载器父子关系 一般不是继承实现,而是组合关系来复用父加载器的代码

工作过程
一个类加载器收到类加载的请求,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,所以所有的类加载请求都传到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载

使用双亲委派模型 来组织类加载器之间的关系 好处:

1 类随着类加载器一起具备了一种带有优先级的层次关系,保证Java程序的稳定运行
2、没有双亲委派模型 都由各个类加载器自行去加载的话,如果用户自己写一个java.lang.Object类 ,程序会变得混乱。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值