JVM基础知识

JVM简介
JVM是Java Virtual Machine 的简称,Java虚拟机
虚拟机是通过软件模拟硬件共功能,运行在完全隔离环境中完整计算机系统
常见的虚拟机又JVM、VMwave、Virtual Box
以前的C / C++是没有"虚拟机"的程序,会直接运行在真实的操作系统上,被编译成二进制,在CPU上运行
Java引入了"JVM",先把java代码编译成.class字节码文件 - >JVM上运行->JVM把这样的字节码转换成二进制指令
引入虚拟机性能会有折扣,也可能会占用更多资源
但是为什么Java还要引入 "JVM"虚拟机呢,为了更好的跨平台
像以前也说过C语言跨平台,但是这里跨平台是一次编写,到处编译 ,一个代码虽然在A机器上可以编译生成可执行程序,但是B机器如果使用相同代码编译生成可执行程序(但是这里切换平台代码大概率会进行调整)
因为不同的CPU有不同的指令集合,不同操作系统有不同的API
但是Java的"跨平台",是一次编写,到处运行
直接将写好的并编译好的.class文件放到不同的JVM平台执行,此时不需要修改任何内容就可以直接运行
JVM内存区域划分

此时Java应用程序中使用的内存其实是JVM中的内存,而JVM的内存是在其启动的时候从操作系统中内存分配的

1.程序计数器
一个很小的区域只保存一个数字 下一条执行的java字节码指令的地址
2.栈
本地方法栈给C++代码使用的,JVM里面也有一些方法是C++实现的,如果是这样这个方法前面就会有native修饰,这里面是无法直接看到源码,只可以通过JVM源码中查看

虚拟机栈
Java虚拟机栈描述的是Java方法执行的内存模型
并且每一个方法执行同时会创建一个栈帧(Stack Frame)并且每一个栈帧中都有操作数栈、局部变量表、动态链接、方法返回地址

局部变量表 :存放方法参数和局部变量,在编译期间进行分配好,执行期间不会修改其大小
操作数栈 :每个方法都会生成一个先入后出的操作数栈
动态链接 :指向运行时常量池的方法引用
方法返回地址:PC寄存器的地址
3 堆
程序中所创建的所有对象都保存在堆中,并且这个是最大的区域
4元数据区(方法区)
存储被虚拟机加载的类的信息、常量、静态变量
线程共享 :堆、元数据区,一个进程中只有一份
线程私有:栈和程序计数器,每个线程都有自己独立一份
内存溢出问题
这里会出现栈溢出或者堆溢出
栈溢出:就是方法调用栈帧太多了,像无线递归就会出现栈溢出
堆溢出:new 对象太多了
java
public class Test {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list =
new ArrayList<>();
while(true) {
list.add(new OOMObject());
}
}
}
这里不断创建OOMObject对象并且不释放,此时就会出现堆溢出

栈溢出
java
public class Test {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
Test test = new Test();
try {
test.stackLeak();
} catch (Throwable e) {
System.out.println("Stack Length: "+test.stackLength);
throw e;
}
}
}
如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverflowError异常
如果无法在栈上申请到足够空间会出现OOM异常


JVM类加载
类加载过程

1.加载
Loading加载阶段
1通过一个类全限定名获取定义类的二进制字节流(获取到.class文件)
2.将这个字节流代表的静态存储结构转化为方法区的运行时数据结构(打开.class文件,将数据读取到内存中)
3.内存中⽣成⼀个代表这个类的java.lang.Class对象, 根据全限定类名,作为整个类的入口
2.验证
根据其二进制(.class文件),验证其合法格式
验证其文件格式、字节码、符号引用等等
3.准备
给要创建的类对象,分配内存空间
此时这里未初始化的内存空间,全部置为0,如果此时访问static修饰的变量获取结果是0
public static int value = 123;此时value的结果仍然是0
4.解析
初始化常量,像,class中的字符串常量也放到内存中
5.初始化
针对类对象进行初始化
初始化类静态成员,执行静态代码块,对其父类进行加载
双亲委派模型
用来找.class文件
此时就会先不断向父亲找对应类,如果最上面没找到就会开始向下找,如果最终没找到此时就要抛出异常,此时没有这个类

优点:
避免重复加载类 :先进行加载父类C ,这样如果有两个子类A、B 有相同的父类,这样直接把其父类加载进来即可,A启动时候就会加载C类,加载B类时候,此时就不需要重新加载类了
保证一定安全:就是如果自己项目中有和Java标准库中的类相同,此时肯定为了保证Java核心API不被篡改的问题,此时优先加载标准库中的,其次是Java扩展中的,最后才是第三方库以及自己代码中的
垃圾回收机制
上面介绍了Java内存的各个区域,但是并不是所有都需要进行垃圾回收
像程序计数器、虚拟机栈、本地方法栈这三个部分区域而言,生命是与线程相关的,随线程生,当然也随着线程亡 ,但是其元数据区类对象、静态成员等等只会加载一次 ,所以说Java中内存分配和回收关注的为Java堆与方法区 这两个区域
此时的垃圾回收是以对象为单位,并不是以字节为单位,因此一个对象要么释放,要么不释放,不存在只释放一半的情况
引入计数器算法
给对象增加一个计数器,每当一个地方引用它时,计数器+1;当引用失效时候,计数器-1;当没有任何地方引用这个对象此时的计数器就为0,此时可能是不在被使用
这样计数的确简单,效率也比较高

此时这个引用计数有两个缺点
1.可能会消耗更多空间(次要) ,如果对象较大可以忽略这里的计数空间,但是如果对象所占空间较小,此时计数空间可能占用这个对象的大部分空间,这样就浪费了
2.循环引用问题(主要)

此时这里虽然a和b空间释放了,这里的对象出现了互相引用的问题,导致无法使用,使用的话需要访问对方引用,使用对方引用又需要自己的引用
可达性分析
Java中使用的并不是引用计数的方式,使用的是可达性分析
可达性分析,这里的关系类似是二叉树这种结构,但是会有多个根节点,这个树中各个对象之间有关系,此时会从根节点遍历所有对象,此时遍历过的都会有标记为可达 ,出去可达的,剩下的都是不可达的,会被当作垃圾回收

此时这里对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots 是不可达的,因此他们会被判定为、可回收对象
常被作为GC Roots的对象 有
1.栈上的局部变量
2.方法区常量池引用的对象
3.方法区引用的静态属性引用的对象
这里的引用指的是强引用,强引⽤指的是在程序代码之中普遍存在的,并且只要强引用还在,其引用的对象就不会回收
如何识别那些对象是垃圾
引用识别,可达性分析,所有的GC Root出发,遍历,访问到的对象会进行标记为可达,未访问的标记为不可达,并且这个可达性分析会有多次
标记 - 清除算法
就是先标记再清除
标记出所有需要回收的对象,再将这些标记的对象进行删除

问题
1.效率问题 :这里标记和清除的效率都不高
2.空间问题 :这里虽然把这些标记的地方删除了,但是此时剩余的空间是零碎的,但是我们开辟空间都是连续的,此时如果我们开辟一个较大的空间,可能不够,这里只有较多的空间碎片,此时无法创建,还需要继续进行标记删除
复制算法
上面标记清除算法会有很多空间碎片会导致这里可能创建一个较大空间导致创建不了

这里是先使用一半的空间,另一半预留,此时对这些不使用对象回收的时候,回收后,把这些左边使用的空间回收后的 保留区域 复制到另一半没使用空间里面,并且此时是排好序的,把左边原本使用的区域全部释放,这样就可以解决上面标记清除的问题了
但是此时又有新的问题
1.空间利用率低,此时只可以使用一部分空间(最多50%)
2.复制开销较大,如果每次存活对象较多的话,这里复制开销是非常大的
标记 - 整理算法
当每次存活对象比较多的时候上面的复制算法,此时效率就会变低

此时就会有一个这里的标记-整理算法和标记清除类似,但是这里会将一些未被回收的对象向前搬运,解决了空间不够用的问题,但是此时如果存活对象较多,此时效率也比较低
分代算法
虽然上面的可以进行垃圾回收,但是各有优缺点
分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从⽽实现更好的垃圾回收

1.新创建的对象会放到伊甸区
2.伊甸区对象经过第一轮GC其实就会把大部分对象给回收(根据经验),没有被淘汰的就会通过复制算法幸存区 ,此时幸存比较少,这里的幸存区每次只是用一半
3.下一轮GC会针对这里幸存区对象进行扫描,此时还会淘汰一大部分对象,没有被淘汰的就会通过复制算法进入另一个没使用的幸存区
(这里幸存区的对象较少通过复制算法并不会出现拷贝导致开销较大问题)
4.这里会有一定的周期对其幸存区进行扫描,随着来回拷贝,每经过一次拷贝其对象年龄+1
5.经过一段时间,这个对象的年龄达到一定阈值,此时就会将这个对象放入老年代
6.进入老年代 之后,针对老生代GC频率较低,减少开销,此时老年代中如果发现对象是垃圾,此时采用标记清理方式处理 ,这里虽然比较开销较大,但是这里老年代的GC扫描频率较低
7.此时如果创建对象比较大,此时会直接放入老年代