JVM 总结
什么是 JVM
JVM 是 Java 虚拟机,是一个虚拟出来的计算机,仿真模拟计算机。
JVM 在运行程序的时候,这个程序在 JVM 里面是怎么样的?
JVM 的内存主要分为五块
- 程序计算器
程序计算器是用来记录当前线程的执行的位置,通过程序计数器的记录,程序就可以继续进行下去。
线程私有的,就是每个线程都不一样。
需要注意的是这里面没有 OutOfMemory
的异常,在执行 native
方法的时候,记录是空值,一些循环,比如 for
、 while
也是通过这个来实现的。
- 虚拟机栈
每一个线程都会有一个虚拟机栈,用于解释代码的执行过程。线程每调用一个方法的时候就会创建一个栈帧,并放到这个虚拟机栈中,方法执行完成后,这个栈帧出栈。
栈帧是什么呢?或者说里面是什么内容呢?栈帧主要包括以下 4 个内容:
- **局部变量表(LocalVariableTable)**,里面是这个方法里面的用到的变量,this 是下标为 0 的变量
- 操作数栈,JVM 是基于栈的,操作数可以理解为中间的计算结果,结果是保存在栈里面的。
- 动态链接,在加载类的时候,类的字段和方法对应的内存布局之间的连接。
- 返回值,方法的返回值
线程私有的,就是每个线程都不一样。
- 本地方法栈
和虚拟机栈类型,不过这里面具体就要看 native 层的了。
线程私有的,就是每个线程都不一样。
- 方法区
类被加载后,类的属性、方法等信息会保存在这里。
- 堆
一般 new 出来的实例都是放在这里的。其他的部分在方法区里面的常量池。
那,哪些不在堆里面呢???
那些不在堆里面分配的对象实例可能在栈里面,具体的就哟啊说到逃逸分析了,这里
https://juejin.im/post/6844904086010069006
https://zhuanlan.zhihu.com/p/94568794
一个 class 字节码文件里,类的结构是如何的?
类的结构可以粗略看出是一些无符号数和一些表组成的,表里面可以包含其他的表,大概的结构如下。
魔数
大小版本号
常量池(表结构)
访问flag
父类
接口列表(表结构)
字段表(表结构)
方法表(表结构)
属性表(表结构)
表结构由表的元素个数 count 和表的内容组成,表的内容可以是无符号数和表。不同的表,表的内容的每一项结构都是固定和相同的。
常量池里面的每一项都是表
JVM 是如何找到一个类的
JVM 是通过 ClassLoader 来加载类的。
ClassLoader 的 loadClass 方法可以让系统找到一个类。
ClassLoader loadClass 的时候用到了双亲委派模式来加载一个类。先判断是否加载过,如果没有加载过,就先交给父类来加载,如果父类也没有加载过,再回到自己,自己去加载。
Java 里面的双亲委派并不是很强制的,可以通过复写 ClassLoader 的 loadClass 方法来破坏这种双亲委派模式,从而达到加载自己的类过程。
ClassLoader 加载类一般是从指定的路径下加载类的,双亲委派模型能保证先优先从顶层的类加载器来加载类,而顶层的类加载器一般是系统提供的,系统提供的类加载器都是加载的指定位置的类库,这样能保证系统类不被篡改。
另外 ClassLoader 的 defindClass 可以通过一个 byte 数组来定义一个类。
JVM 加载类的时候,有哪些过程呢?
类加载可以分为三大步(分别是,载入、连接和初始化),也可以说是 5 个步骤。
类的加载过程:
- 加载 从二进制流加载类,主要做了三件事情
- a. 通过类的全限定名查找
.class
文件,并生成二进制流 - b. 解析
.class
文件,主要就是按照Class
类的结构解析,生成JVM
特定的数据结构并保存在方法区
。 - c. 生成一个 Class 类的实例,后续对这个类的访问可以通过这个 Class 的实例来完成。
- 链接,可细分为一下三小步
- 验证 校验版本号是否符合要求,高版本 class 不能在低版本
JVM
上运行
- 验证 校验版本号是否符合要求,高版本 class 不能在低版本
- 准备 给类的静态变量赋初始值,
final
类型赋值。
- 准备 给类的静态变量赋初始值,
- 解析 主要是把常量池里面的符号引用转为直接引用。
- 初始化
主要执行静态代码块
、调用构造器的构造方法
。
类的生命周期,除了加载过程中的 5 个阶段,还有使用
和卸载
2 种阶段。
Java 内存模型
之前说过,每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。每个线程都是不一样的,还说过 JVM 是虚拟的一个硬件平台。那么 JVM 具体在执行一个线程的时候内存的情况是如何的呢?
线程工作的时候,会给线程分配一个内存空间,我们称之为工作内存,然后还有一个主内存。
线程在工作的时候, JVM 会先从主内存把数据载入工作内存,计算完成以后,在把最后的结果“写回”到主内存。
为什么要这样先载入最后写回?可能是因为载入的指令比较耗时吧。
JVM GC
四种索引
- 强引用。默认就是强引用,即使内存不够也不会释放。
- 软引用。SoftRefrence,内存不够的时候,会断开链接,然后释放内存。
- 弱引用。WeakRefrence,gc 的时候,如果发现了弱引用,就会释放,而不管是否内存不足。需要注意的是,可能需要多次 gc 才有可能找到这个弱引用,因为不同的 gc 清理或者说计算的内存区域不一样。
- 虚引用。代码里面无法直接使用。无法通过 get 方法获取引用对象。
软引用和弱引用在创建的时候可同时传入一个ReferenceQueue,当引用的对象被回收的时候,ref 实例会被放到 Queue 里面。
GC 的类型
- GC_FOR_MALLOC 表示在堆上分配内存不足
- GC_CONCURRENT 表示堆内存达到一定量的值,系统触发
- GC_BEFOR_OOM 准备抛出 oom 异常
- GC_EXPLICIT 调用系统方法 system.gc
软引用在前三个 gc 的时候释放,弱引用在创建后,下一次 gc 的时候释放。
回收算法
DVM 和 JVM 区别,或者说是如何优化的
DVM 基于寄存器, JVM 基于栈
基于寄存器的优点是字节指令少,执行效率高,易优化,缺点就是实现复杂
基于栈的有点就是易移植,相对容易实现。缺点就是效率慢,指令多。
ART
虚拟机是在 DVM
上做了优化。但还是基于寄存器的。