第一章 了解
1、JVM的位置
从最底层到应用层:硬件 -->操作系统(如:Linux)–> JVM --> 字节码文件 --> user,JVM是运行在操作系统上的,与硬件没有直接互交。
2、Java代码执行流程
Java源码 —> Java编译器(词法分析–>语法分析–>语法抽象语法树–>语义分析–>注解抽象语法树–>字节码生成器) --> 字节码文件 -->Java虚拟机( 类加载器 --> 字节码校验器 --> 翻译字节码和JIT编译器)–> 操作系统
3、JVM发展历程
- 1996年Java1.0版本的时候,sun公司发布一款名为sun classicVM的Java虚拟机,这款虚拟机只提供解释器。
- hotspot 从服务器、桌面到移动端、嵌入式都有应用
- BEA的JRockit,该虚拟机专注服务器端应用
- IBM的J9,该虚拟机与hotspot接近,服务器端、桌面应用、嵌入式等多用途VM
第二章 类加载子系统
1、类加载过程
一个类从被加载到虚拟机内存,再到卸载出内存这样的整个生命周期需要经历5个阶段:
加载(loading) --> 连接(Linking) --> 初始化(Initialization) --> 使用(Using) --> 卸载(Unloading) ,这就是类的生命周期。其中,连接又分为三个阶段:验证(Verification) --> 准备(Preparation) --> 解析(Resolution),下面我们依次展开每个过程。
加载
在加载阶段,Java虚拟机需要完成以下三个事情:
(1)通过一个类的全限定名获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在内存生成一个代表这个类的java.lang.class对象,作为方法区这个类的各个数据访问入口
连接
(1)验证(Verify)
目的就是确定Class文件的字节流中包含的信息是否符合《Java虚拟机规范》,保证运行后不会危害虚拟机自身安全。验证大致完成四个阶段的验证动作:文件格式验证、元数据验证、字节验证和符号引用验证。
(2)准备(Prepare)
- 准备阶段是仅为类变量分配内存,并且设置该类变量的默认初始值,初始值为零,比如我们代码定义一个类变量:
public static int value = 1;
意思是指在准备阶段,value值是为零的,要到初始化阶段才被赋值为1。
- 但不包括final修饰static及不包括实例变量,final在编译时就分配,实例变量会随着对象一起分配到Java堆中。
(3)解析(Resolve)
将常量池内的符号引用转换为直接引用的过程,这个阶段通常在执行完初始化之后再执行的。
符号引用就是一组符号来描述引用的目标;直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
初始化
2、你知道双亲委派机制吗?
类加载器用来把类加载到Java虚拟机中,从JDK1.2版本开始,类加载过程采用双亲委派机制,这样机制能更好地保证Java平台的安全。
为什么使用双亲委派机制?
为了避免类的重复加载以及保护程序安全,防止核心API被随意更改。
缺点:顶层的classLoader无法访问底层的classLoader所加载的类
原理
就是一个类加载器收到请求时,它不会自己去尝试加载类,而是把请求委托给父类加载器去完成,依次递归,如果父类加载可以完成类加载任务就返回成功,如果父类加载器无法加载时才自己去加载。
沙箱安全机制:就是在加载自定义类的时候会率先使用引导类加载器加载,它是起到保证对Java 核心源代码的保护功能。
打破双亲委派机制是什么意思?
只要加载类时候,不是从应用程序类加载–>扩展类加载器–>启动类加载器这样的顺序就算打破了。因为加载class核心方法是在loaderclass类的loadclass方法上的,只要自定义classloader,重写loadclass方法,那就算打破双亲委派机制了吧。
双亲委派模型主要出现过三次较大规模的“被破坏”情况
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前–即JDK1.2发布之前。
双亲委派模型的第二次“被破坏”是这个模型自身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。
双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的,例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。
第三章 运行时数据区
一、程序计数器(PC寄存器)
PC寄存器用来存储将要执行的下一条指令的地址,然后由执行引擎读取下一条指令。PC寄存器也是用来记录当前线程执行的地址。是唯一没有规定任何OOM情况。
方法区和堆是有GC的,而栈和PC寄存器及本地方法没有GC,但栈可能出现内存溢出问题。
例子:
javap -verbose xxx
面试
使用PC寄存器存储字节码指令地址有什么用?
JVM的字节码解释器需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
为什么使用PC寄存器记录当前线程的执行地址?
因为CPU需要不停切换各个线程,这时候切换回来以后,就得知道接着从哪里开始继续执行
二、虚拟机栈
1、概念
每个线程在创建时都会创建一个虚拟机栈,也称为栈帧 (Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法返回地址以及一些附加信息。是线程私有的,生命周期和线程一致。
值得注意的是:栈是不存在GC问题的,但是出现栈溢出问题。
面试题:开发中遇到的异常有哪些?内存溢出;OOM异常
该区两类异常:如果线程请求的栈深度大于虚拟机所允许的深度(抛出stackOverFlowError异常);如果Java虚拟机栈容量是动态扩展的,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
2、静态变量和局部变量
变量的分类:
- 按照数据类型分:基本数据类型和应用数据类型
- 按照在类中声明位置分:
- 成员变量:
- 类变量:在连接和准备阶段给类变量默认赋值
- 实例变量:随着对象创建,会在堆空间中分配实例变量空,并进行默认赋值
- 局部变量:在使用前,必须进行赋值否则编译不通过如下:
3、局部变量表
局部变量表(local variable table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量是以变量槽为最小单位。
4、操作数栈(operand stack)
操作数栈主要是用来保存过程中间结果,同时作为计算过程中变量临时的存储空间;操作数栈是JVM执行引擎的一个工作区。
Load_.存放在局部变量表中
5、动态链接
每个栈桢内部都包含一个指向运行时常量池中该栈桢所属方法的应用。在Java源文件被编译到字节码中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。
常量池的目的就是为了提供一些符号和常量,便于指令的识别。
6、方法的调用
JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
符号引用转换直接引用的转换过程是在编译期间确定还是在运行期间确定下来?
- 静态链接:如果被调用的目标方法在编译期可知,则是在静态链接确定的
- 动态链接:如果被调用的方法在编译期无法被确定下来,只能在运行期确定的则称为动态链接
7、方法返回值
存放调用该方法的PC寄存器的值,一个方法的结束有两种方式:正在执行完成和出现未处理的异常也就是非正常退出。
8、一些附加信息
9、面试题
(1)栈溢出错误有哪些?
stackoverflowerror;当整个空间不足了,栈再扩容时就出现OOM
(2)调整栈大小就能保证不出现溢出吗?
不能,只能暂时保证但后来就不一定啦
(3)分配的栈内存越大越好吗?
理论上越大的话就避免出现溢出问题,但是可能某时候就出现溢出,空间是有限的并不能越大越好。
(4)垃圾回收是否会涉及到虚拟机栈?
PC计数器和虚拟机栈都不出现GC问题,只有方法区和堆存在GC和error
(5)方法定义的局部变量是否线程安全?
这个得具体问题具体分析。比如定义StringBuffer本身就是安全那就是安全啦,但是StringBuilder就不安全。
三、本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用,也是线程私有的。
如果线程请求份哦的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出stackoverflowError异常
如果本地方法栈可以动态扩展,且在尝试扩展的时候无法申请到足够的内存,或者在创建新线程时没有足够的内存去创建对应的本地方法栈,此时Java虚拟机抛出OutOfMemoryError异常