文章目录
虚拟机概念
系统虚拟机
完全是对物理计算机的仿真。提供了一个可运行完整操作系统的软件平台
【Virtual Box\VMware】
程序虚拟机
专门为执行单个计算机程序而设计
【Java虚拟机】
Java虚拟机
JVM介绍
- Java虚拟机是一台执行Java字节码的虚拟计算机,它拥有独立的运行机制,其运行的Java字节码也未必由Java语言编译而成。
- VM平台的各种语言可以共享Java虚拟机带来的跨平台性、优秀的垃圾回器,以及可靠的即时编译器。
- Java技术的核心就是Java虚拟机(JVM,JavaVirtual Machine),因为所有的Java程序都运行在Java虚拟机内部。
JVM 虚拟机作用
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
JVM 虚拟机特点
- 一次编译,到处运行
- 自动内存管理
- 自动垃圾回收功能
JVM位置
JVM是运行在操作系统之上的,它与硬件没有直接的交互。
JVM加载类
类文件数据类型
- Class文件是一组以8位字节基础单位的二进制流,各数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。
- 根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。
- 无符号数:
- 它属于基本数据类型。
- 它以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数。
- 它可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表:
- 它是由多个无符号数或者其他表作为数据项构成的复合数据类型。
- 它习惯性地以“info”结尾。
- 它用于描述有层次关系的复合结构的数据,整个Class文件本质上可被视作一张表。
类文件结构
- 魔数(magic)
- 每个Class文件的头4个字节成为魔数。
- 其唯一的作用:确定该文件是否为一个能被虚拟机接受的Class文件。
- 值为OxCAFEBABE(咖啡宝贝)。
- 使用反解析工具javap -v Hello.class
- 第5、6字节为次版本号,第7、8字节为主版本号。
- Java的版本号从45开始,JDK1.1之后的每个大版本,版本号加1(DK1.0~1.1使用了45.0~45.3的版本号)
- 常量池
- 常量池中常量的数量不是固定的,所以会有一个常量池容量的计数器(注意该计数器从1开始)。
- 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
- 两大类常量:
- 字面量:如文本字符串、被声明为fina的常量值、基本数据类型的值等。
- 符号引用:类和接口的完全限定名、属性的名称和描述符、方法的名称和描述符
- 常量池项目类型
- 访问标志
- 访问标志用于识别一些类或者接口层次的访问信息
- 包括:
- 其是类还是接口。
- 是否为public类型。
- 是否为abstract。
- 类索引、父类索引、接口计数器、接口索引集合
- 类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
- 除java.lang.Object类外,所有类的父类索引均不为0。
- 接口索引集合就用来描述这个类实现了哪些接口。
- 以上几项主要用来确定该类型的继承关系。
- 字段计数器、字段表集合
- 这2项主要描述接口或类中声明的变量个数及变量。主要包括:类级变量、实例级变量;不包括局部变量。
- 可以包括的信息有:字段的作用域(public、private、protected修饰符)、实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型(基本类型,对象,数组)、字段名称。
- 方法计数器、方法表集合
- 这2项主要描述接口或类中声明的方法个数及方法。主要包括:访问标志、名称索引、描述符索引、属性表集合。
- 属性计数器、属性表集合
- 这2项主要描述接口或类中声明的属性个数及属性。主要包括:访问标志、名称索引、描述符索引、属性表集合。
- 这2项主要描述接口或类中声明的属性个数及属性。主要包括:访问标志、名称索引、描述符索引、属性表集合。
JVM类加载机制
类加载过程
一个Java类型从被加载到虚拟机内存,直到卸载出内存,就是它的个生命周期。
这一过程包括:加载(Loading)、验证(Verification)准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段。
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
- 加载class文件的方式。
- 从本地系统中直接加载
- 通过网络获取,典型场景:WebApplet
- 从zip压缩包中读取,例如jar、war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景:JSP应用
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
链接-验证
- 目的是确保Class文件的字节流中包含的信息,符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包含四种验证:文件格式验证、元数据验证、字节码验证和符号引用验证。
链接-准备
- 给类变量分配内存,并设置初始值(零值)
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
链接-解析
- 将常量池内(类、接口、字段和方法)的符号引用转换为
直接引用的过程。 - 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
初始化
- 初始化阶段的重要工作是执行类构造器方法的过程。
- 初始化的特点:
- 该方法由编译器自动生成,由javac编译器自动收集类中的所有类静态成员的赋值语句和静态代码块共同产生;而若没有类变量和静态代码块,则没有
- 类构造器方法中指令按语句在源文件中出现的顺序执行
- ()方法与实例的构造器方法不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的(方法执行前,父类的0方法已经执行完毕
- 该方法一个类的在多线程下被加同步锁
类加载器
- 类加载器负责加载所有的Class,在标准Java程序中,
- JVM四种类加载器:
- BootstrapClassLoader(启动类加载器)
- ExtensionClassLoader(扩展类加载器)
- AppClassLoader(应用类加载器)
- 自定义类加载器
BootstrapClassLoader(启动类加载器)
- 使用C/C++语言实现,嵌套在JVM内部。
- 加载Ext加载器和App加载器,并指定成为他们的父(上层)加载器。
- 不继承于java.lang.ClassLoader,没有父加载器。
- 用于加载Java的核心库(jre/lib/rtjar、resourcesjar或环境变量sun.boot.class.path路径下的内容)
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
ExtensionClassLoader(扩展类加载器)
- 使用Java语言实现,派生于ClassLoader,由sun.misc.Launcher$ExtClassLoader实现
- 父加载器是BootstrapClassLoader
- 用于加载jre/lib/ext子目录(扩展目录)jar包,并java.ext.dirs系统属性所指定的目录中加载类库
AppClassLoader(应用/系统类加载器)
- 使用Java语言实现,派生于ClassLoader,由sun.misc.Launcher$AppClassLoader实现
- 父加载器是ExtClassLoader
- 该加载器是程序中默认的类加载器,一般,Java应用的类由它来加载
- 用于加载环境变量classpath或系统属性是“jaya.class.path”指定路径下的类库
自定义类加载器
- 出于安全,避免用户自己编写的类动态替换Java的一些核心类,比如String。
- 避免类的重复加载。VM中区分不同的类,是靠类加载器+类的全名
- 继承ClassLoader,并实现findClass方法。
双亲委托模式
Java虚拟机对class文件采用的是按需加载的方式,采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
工作原理
1.如果一个类加载器收到了类加载请求,它并不会自己先去加载而是把这个请求委托给父类加载器去执行
2.如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
3.如果父类加载器能完成类加载任务,则返回成功;倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制
优点
- 避免类的重复加载
- 保护程序,防止核心API被篡改
运行时数据区
概述
PC寄存器
PC寄存器(程序计数器)可看作是当前线程所执行的字节码的行号指示器。它来存储指向下一条指令的地址,也即将执行的指令代码。由执行引擎读取下一条指令。
特点:
- 在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理等都依赖于它完成
- 每个线程有一个独立的程序计数器,线程之间互不影响
- 如果线程执行的Java方法,则计数器记录正在执行的虚拟机字节码的指令的地址;如果正在执行的是本地方法,这个计数器值则应为空
- 运行时数据区中唯一不会出现OOM(OutOf Memory)的区域,没有垃圾回收
为什么使用PC寄存器记录当前线程的执行地址呢?
- 因为CPU需要不停的切换各个线程,当切换回来的时候,CPU就需要知道从哪接着继续执行。
- JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么会被设定为线程私有?
所谓的多线程在一个特定的时间段内只会执行某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复。为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个pc寄存器,这样各个线程之间便可以进行独立计算,从而不会出现互相干扰的情况。
虚拟机栈
每个线程在创建时都会创建一个虚拟机栈,其内部保存的一个个栈帧(StackFrame),每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。是线程私有的。
- 生命周期与线程一致。
- 主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
- Java虚拟机栈不存在垃圾回收,但是会有OOM。
- Java虚拟机规范允许Java栈的大小是动态的或者固定不变的。
- 如果是可动态扩展,当无法申请到足够内存时会出现OutofMemoryError。
- 如果是固定大小,当线程请求的栈容量超过固定值,则出现StackOverflowError。
- -Xss设置栈内存的大小,设置的栈的大小决定了函数调用的最大深度。
- Linux(64bit)默认值:1M。
- Windows默认值:The default value depends onvirtual memory。(DK5之后,默认1M)
栈运行原理
- JVM对栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出"原则;如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另一个线程的栈帧
- 在一个线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧
- Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另一种是抛出异常。不管使用哪种方法,都会导致栈帧被单出
栈内部结构
- 栈帧:支持虚拟机进行方法调用和方法执行背后的数据结构
- 局部变量表是一个数组,在编译时其长度就被定义好。主要用于存储方法参数、定义在方法内的局部变量。
- 局部变量的数据类型可以包括:基本数据类型、引用数据类型和返回值地址。其中一个局部变量的数组元素可以存入boolean,byte,char,short,int,float,reference或者是returnAddress;两个数组元素可以存入long或者double类型。
- Java虚拟机通过索引定位的方式使用局部变量。
- 局部变量表,最基本的存储单元是Slot(变量槽),32位以内64位的的类型只占用一个slot(包括returnAddress类型),类型占用两个slot(1ong和double)。
- 如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。
- 操作数栈(OperandStack)也常被称为操作栈,后入先出(Last In First Out,LIFO)。
- 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 操作数栈中,32bit类型占用一个栈单位深度,64bit类型占用两个栈单位深度。
- Java虚拟机的解释引擎是基于栈的执行引擎,其中栈指的就是操作数栈。
- 操作数栈并非采用访问索引方式进行数据访问,而是只能通过标准的入栈、出栈操作完成一次数据访问。在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但在大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,以支持方法代码的动态链接。
- 在Java源文件被编译成字节码文件时,所有的变量、方法引用都作为符号引用,保存在class文件的常量池中。
- 描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的。动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
- 用于存放调用该方法的pc寄存器的值当一个方法开始执行后,只有两种方式可以退出这个方法:正
常退出、异常退出。无论采用何种退出方式,在方法退出之后都需要返回到方法被调用的位置,程序才能继续执行。- 正常退出:调用者的PC计数器作为返回地址,栈帧中一般
会保存这个计数器值。 - 异常退出:返回地址是通过异常处理表来确定的,栈帧中
一般不会保存这部分信息。
- 正常退出:调用者的PC计数器作为返回地址,栈帧中一般