JVM
JVM详尽整理简介整体结构简图详图补充架构模型和生命周期架构模型栈的指令集架构寄存器指令架构生命周期虚拟机的启动虚拟机的执行虚拟机的退出类加载子系统图解角色角色图解说明加载过程图解Loading(加载)Linking(链接)Verification(验证)Preparation(准备)Resolution(解析)Initialization(初始化)类加载器分类结构图Bootstrap Class
简介
本文适合具有一定java基础和JVM基础的同学,相信认真阅读此篇文章后你一定能获取不小的进步(文章篇幅较长),在本文中将会结构化的形式尽可能细致的介绍JVM和常用的JVM相关的生态在使用JVM时的配置。若大佬有发现博客中的不恰当处,也请欢迎评论,让我们一起学习共同进步。
整体结构
简图
相信这张图片基本上同学们都见过,这个也是本次分享JVM的基础结构在后面的章程中,将会详细的介绍每个组件中的具体的内容。
详图
详图是在简图的基础上进行扩建的,比简图更加细化了具体的一些内容。
补充
在简图和详图的基础上,我也将本节课程的大部分的内容通过画图的方式,构建在porcesson上,提供给时间比较紧张的同学快速学习。
这里由于是截图很多的文字和细节可能看不清楚,详情可点击processonJVM结构说明图
架构模型和生命周期
架构模型
栈的指令集架构
- 设计和实现更简单,适用于资源受限制的系统
- 避开了寄存器分配的难题,使用零地址指令分配方式
- 指令中的指令大部分是零地址指令,其执行过程依赖于操作栈,指令集更小编译器容易实现
- 不需要硬件支持,可移植性更好,更好实现跨平台
寄存器指令架构
- 典型的应用是X86的二进制指令集
- 指令集架构则完全依赖硬件,可移植性差
- 性能优秀和执行更高效
- 花费更少的指令去完成一项操作
- 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主
生命周期
虚拟机的启动
- 通过引导类加载器创建一个初始类来完成
虚拟机的执行
- 程序开始时运行,程序结束停止
- 一个Java程序,其实是一个Java虚拟机的进程
虚拟机的退出
- 程序执行结束
- 程序执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
- 某线程调用了Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次类似的exit或halt操作
- 运用JNI Invocation API 来加载或卸载 Java 虚拟机时,JVM 退出的情况
类加载子系统
图解
友情提示不要只看中文版本,这样会忘记了名词,后期可能就认识了。
角色
- class file 存在本地硬盘上,相当于模版,执行时通过模版创建实例
- class file 加载到JVM 中被称为DNA元数据模版,放在方法区
- 在.class 文件 -> JVM -> 最终成为元数据模版,此过程就要一个运输工具(类装载器 Class Loader)
角色图解说明
加载过程
图解
Loading(加载)
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口
Linking(链接)
Verification(验证)
- 确保Class 文件的字节流中的信息符合虚拟机要求,保证加载类的正确性,不会危害虚拟机的自身安全
- 主要包含四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证
Preparation(准备)
- 为变量分配内存并且设置该变量的默认初始值,即零值
- 这里不包含final 修饰的 static,因为final 在编译的时候就会分配了,准备阶段会显示初始化
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中
Resolution(解析)
- 将常量池内的符号引用转换为直接引用的过程
1.1 符号引用就是一组符号来描述所引用的目标
1.2 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄 - 主要针对类或接口、字段、类方法、接口方法、方法类型等,常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等
Initialization(初始化)
- 类构造器方法 的过程
- 此方法不需要定义,是javac编译器自动收集类中的变量的赋值动作和静态代码块中的语句合并而来
- 构造器方法中指令按语句在源文件中出现的顺序执行
- () 不同于类的构造器(关联:构造器是虚拟机视角下的())
- 若该类具有父类,JVM 会保证子类的的)() 执行前,父类的() 已经执行完毕
- 虚拟机必须保证一个类的()方法在多线程下同步被加锁
类加载器
分类结构图
Bootstrap ClassLoader(启动类、引导类加载器)
- C / C++ 语言实现
- 加载核型库 (JAVA_HOME/jre/lib/rt.jar、resources.jar、sun.boot.class.path)
- 并不继承java.lang.ClassLoader,没有父类加载器
- 加载扩展类和应用类加载器,并指定为他们的父类加载器
- 出于安全考虑,Bootstrap 启动类加载器值加载名为java、javax、sun等开头的类
Extension ClassLoader (扩展类加载器)
- Java语言编写,由sun.misc.Launcher$ExtClassLoder实现
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK的安装目录jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录,也会自动由扩展类加载器加载
App ClassLoader (应用程序类加载器)
- Java语言编写,由sun.misc.Launcher$AppClassLoder实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
- 该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由他来完成加载
- 通过ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器
用户自定义类加载器
场景
- 隔离加载类
- 修改类的加载方式
- 扩展加载源
- 防止源码泄露
双亲委派机制
图解
过程
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,最终请求到达顶层的启动类加载器
- 如果父类记载其可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子类记载其才会尝试自己去加载,这就是双亲委派模式
优势
- 避免类的重复加载
- 保护程序安全
沙箱安全机制
对Java 核型类库的保护机制
问题
- JVM中表示两个class 对象是否为同一个类存在的必要条件?
1.1 类的完整类名必须一致,包括包名
1.2 加载这个类的ClassLoader(实例对象)必须相同 - 对类加载器的引用
JVM必须知道一个类型是由启动类加载器还是用户类加载器加载的,如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的引用作为类型信息保的一部分保存到方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的加载器是相同的 - 类的主动使用和被动使用
3.1 主动使用
创建类的实例
访问某个类的接口或接口的静态变量,或对该静态变量赋值
调用类的静态方法
反射
初始化一个类的子类
Java虚拟机启动时被标明为启动类的类
JDK7 开始提供的动态语言支持(java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic 句柄对应的类没有初始化,则初始化)
3.2 被动使用
除主动使用都是被动使用
运行时数据区
图解
线程
- JVM 中每个线程都与操作系统的本地线程直接映射
当一个Java线程准备好执行以后,此时操作系统的本地线程同时创建 - 操作系统负责所有线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功,它就会调用Java线程中的run方法
- 后台线程”
3.1 虚拟机线程
这种线程的操作是需要JVM到达安全点才会出现。这些操作必须在不同的线程中发生的原因是它们都需要JVM到达安全点,这样对才不会变化。这种线程的执行类型包括“stop-the-world” 的垃圾收集器,线程栈收集器,线程挂起以及偏向锁撤销
3.2 周期任务线程
这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行
3.3 GC线程
这种线程对在JVM里不同种类的垃圾收集行为提供了支持
3.4 编译线程
这种线程在运行是会将字节码编译成本地机器指令
3.5 信号调度线程
这种线程接受信号并发送给JVM,在它内存通过调用适当的方法进行处理
程序计数器(PC 寄存器)
这里在学习的时候大家可以在idea中安装一个jclasslib Bytecode viewer的插件或者可以通过javap -v <class文件> 命令查看详细的编译字节码文件,可以帮组更好的理解运行时数据区。
作用
用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
图解
介绍
很小的内存空间
每个线程都有它自己的程序计数器,是线程私有的
任何时间一个线程都有一个方法在执行,也就是所谓的当方法,程序计数器会存储当前线程在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,这未指定值
唯一一个没有OutOfMemoryError 区域
问题
- 为什么需要程序计数器
CPU 不断在线程中切换,PC寄存器中存放下一条指令的地址 - 为什么会被设定为线程私有
为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法就是为每个线程都分配一个PC寄存器
虚拟机栈
概述
- 每一个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧
- 线程私有
- 生命周期和线程一致
- 主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回
特点
- 栈是一种快速有效的分配方式,访问速度仅次于程序计数器
- JVM 直接对Java栈的操作只有两个
- 每个方法执行,伴随着进栈(入栈、压栈)
- 执行结束后出栈 - 对于栈来说不存在垃圾回收问题
异常
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈的容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError 异常 ,可通过-Xss 参数调整
- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError 异常
栈帧
概述
- 每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在
- 在线程上正在执行的每个方法都各自对应一个栈帧
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
原理
- 不同线程中所包含的栈帧是不允许在相互引用的,即不可能一个栈帧之中的引用另外一个线程的栈帧
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
- Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令,另外一种就是抛出异常,不管使用那种方式,都会导致栈帧被弹出
待续。。。。
更多推荐
所有评论(0)