JVM模型与基础知识入门
JVM模型与基础知识入门
JAVA 概念图
java 8 官网文档 https://docs.oracle.com/javase/8/docs/index.html
java 概念图
Write Once Run Anywhere 一次构建,处处运行
我们知道,对于用一个java程序,可以同时跑在windows、linux、mac等不同操作系统中,那java又是如何实现程序的跨平台操作,解决不同操作系统不同命令的处理问题?JVM(Java Vittual Machine)由此应运而生。
java 文件的运行过程首先是需要编译为Class文件交由JVM处理运行,对于不同的平台,JDK版本也略有差异,不同版本的JDK,JVM也是不一样的,但一个符合规格的Class 文件可以在不同的JVM中运行。java之所以能够实现跨平台运行也是因为有各种版本的Java 虚拟机支持
源码到类文件(Java to Class)
java 8 虚拟机规范 https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
通过 javac
命令,可以将一个java文件编译成一个Class文件,编译过程会包含几个步骤
xxx.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 —> 注解抽象语法树 -> 字节码生成器 -> xxx.class文件
编译过程会根据Java 中的Class文件规范生成一定格式的Class文件,Class文件的相关数据结构可参照官网 https://docs.oracle.com/javase/specs/jvms/se8/html/index.html,这里不过多展示,一个Class字节码中的常用数据结构
MAGIC魔数与class文件版本
常量池
访问标志
类索引、父类索引、接口索引
字段表集合
方法表集合
属性表集合
类加载机制(类文件到虚拟机)
类加载机制 装载、链接、初始化
- 虚拟机把Class文件加载到内存
- 对加载的数据进行校验、转化解析和初始化
- 形成虚拟机可以直接使用的java类型,即
java.lang.Class
装载(Load)
装载即查找和导入class文件
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的java.lang.Class 对象,作为对方法区中这个数据的访问入口
链接(Link)
-
验证(Verify)
保证被加载类的正确性
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
-
准备 (Prepare)
为类的静态变量分配内存,并将其初始化为默认值
-
解析(Resolve)
把类中的符号引用转换为直接引用
初始化(Initialize)
对类的静态变量,静态代码块执行初始化操作
类加载机制图解
注意: 使用与卸载属于类生命周期的一部分,不算是在类加载过程中的阶段。
类装载器 ClassLoader
在装载(load)阶段,其中第一步:通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载Class文件的。
加载原则
检查某个类是否已经加载:顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader 逐层检查,只要某个ClassLoader已加载,就视为已加载此类,保证此类被所有ClassLoader只加载一次.
加载顺序:加载的顺序是自顶向下的,也就是从上层来逐层尝试加载此类。
双亲委派机制:
定义:如果一个类加载器在接收到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委派给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成次加载时,才自己去加载。
优势:Java类随着加载它的类加载器一起具备一种带有优先级的层级关系,比如,Java中Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各个类加载环境中都是同一个类,如果不采用双亲委派模型,那么由各个类加载器器自己去加载的话,那么系统中会存在多种不同Object类
运行时数据区
在装载阶段阶段,将这个字节流代表的静态存储结构转化为方法区的运行时数据结构,在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口,说白了就是类文件被类加载器装载进来之后,类中的内容(比如变量,常量,方法,对象等这些数据得要有个去处,也就是要存储起来,存储的位置肯定是在JVM中有对应的空间)
图解
方法区
方法区是各个线程共享的内存区域,在虚拟机启动时创建。用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据
虽然Java虚拟机规范把方法去描述为堆的一个逻辑部分,但是它却有一个别名叫做**Non-Heap(非堆),**目的是与Java堆区分开来,当方法区无法满足内存分配时,将抛出OutOfMemoryError异常
此时回看装载阶段的第2步,将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构,如果这时候把从Class文件到装载的第一步和第二步合并起来的话即
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c1YRMQno-1664380543249)(C:\Users\Alienware\AppData\Roaming\Typora\typora-user-images\image-20220918173012835.png)]
- 方法区在JDK8 中就是Metaspace,在JDK6或7中就是Perm Space
- Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存储编译时期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。
堆
Java堆是Java虚拟机管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享,Java对象实例以及数组都在堆上分配
Java Virtual Machine Stacks(虚拟机栈)
类加载机制的装载、链接、初始化完成后,后续过程肯定是要使用加载的数据,那怎么才能被使用到?换句话说里面内容怎样才能被执行?比如通过主函数main调用其他方法,这种方式实际上是main线程执行后调用的方法,即要想使用里面的各种内容,得要已线程为单位,执行相应的方法才行。那一个线程执行的状态如何维护,一个线程可以执行多少个方法,这样的关系又如何去维护呢?
虚拟机栈是一个线程执行的区域,保护着一个线程中方法的调用状态,换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出
栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间
每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
1
- 局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中,局部变量表中的变量不可以直接使用,如需要使用的话,必须通过相关指定将其加载至操作数栈中作为操作数使用。
- **操作数栈:**以压栈和出栈方式存储操作数的
- **动态链接:**每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。
- **方法返回地址:**当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法内得到处理
程序计数器
我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据cpu调度来的。
假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置
程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令,因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空。
本地方法栈
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
结合类加载机制理解运行时数据区
装载
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的Java.lang.Class对象,作为对方法区中这些数据的访问入口
值得探讨的两个方向:(1)类的装载方式有哪些?(2)类装载到底做了什么?
-
类的装载方式有哪些?
- 本地系统加载
- 网络下载 .class 文件
- 从zip,jar等归档文件中加载 .class文件
- 从数据库中提取 .class 文件
- 由 java源文件动态编译成 .class文件
- 由Class.forName()加载
- ClassLoader.loadClass()加载
-
类装载到底做了什么?
-
通过一个类的全限定名获取定义此类的二进制字节流
这个阶段事可控性比较强的阶段,既可以用系统提供的类加载器进行加载,又可以自定义类加载器进行加载。
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
方法区用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。其中类信息包括类版本、字段、方法、构造方法、接口定义等
-
类加载的最终产品是位于堆区中的Class对象。
Class对象封装了类在方法区内的数据结构,并且向java程序员提供了访问方法区内的数据结构的接口。在java堆中生成一个代表这个类的Java.lang.Class对象,作为对方法区中这些数据的访问入口
Java对象实例以及数组都在堆上分配
-
链接
验证
保证被加载类的正确性
-
文件格式验证
验证字节流是否符合Class文件格式规范,比如是否已0xCAFEBABE开头,主次版本号是否在当前虚拟机的处理范围之内,常量池中的常量是否有不被支持的类型。
-
元数据验证
对字节码描述的信息进行语义分析,保证其符合Java语言规范的要求。
-
字节码验证
通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。
-
符号引用验证
确保解析动作能正确执行
验证阶段是重要的一环,但不是必须的,若所应用的类经过反复验证没有问题,可以使用 -Xverifynone
参数关闭大部分类验证措施,从而缩短虚拟机类加载的时间
准备
在方法区中,为类的静态变量分配内存,并设置其初始值
- 内存分配仅仅是类变量,也就是static类型的变量,不包含实例变量,实例变量会在对象实例化时随对象分配在堆中。
- 这里的默认值时根据类型赋值,不是在代码中显示赋予的值
解析
把类中的符号引用转换为直接引用
Run-Time Constant Pool
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
解析阶段是虚拟将将常量池内的符号引用转换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化
执行类构造器,为类的静态变量赋予正确的初始值,有两种方式
- 直接给类变量指定初始值
- 通过静态代码块为类变量指定初始值
类的初始化步骤
- 如果这个类还没有被加载和链接,那先进行加载和链接
- 假如这个类存在直接父类,并且这个类还没有被初始化(在一个类加载器中,类只能初始化一次),那就初始化直接父类
- 假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
类什么时候才会被初始化?
-
创建类的实例
-
访问某个类或接口的静态变量,或者对该静态变量进行赋值
-
调用类的静态方法
-
反射【Class.forName(“com.XXX”)】
-
初始化一个类的子类(因为会先初始化父类)
-
JVM启动时表明的启动类
步骤 -
如果这个类还没有被加载和链接,那先进行加载和链接
-
假如这个类存在直接父类,并且这个类还没有被初始化(在一个类加载器中,类只能初始化一次),那就初始化直接父类
-
假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
类什么时候才会被初始化?
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量进行赋值
- 调用类的静态方法
- 反射【Class.forName(“com.XXX”)】
- 初始化一个类的子类(因为会先初始化父类)
- JVM启动时表明的启动类