JVM 简介
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。 虚拟机是指通过软件模拟的具有完整硬
件功能的、运行在一个完全隔离的环境中的完整计算机系统。
JVM三个主要方面
1.JVM内存区域划分
为什么要进行区域划分?
Java 虚拟机是仿照真实的机器和真实的操作系统进行设计,真实的操作系统中, 对于进程的地址空
间是进行了分区域的设计,JVM 也就仿照了操作系统的情况,也进行了分区域的设计。

JVM具体是怎样划分的?
核心区域四个
(1)程序计数器
这是一个很小的区域,只是用来记录当前指令执行到哪个地址了
(2)元数据区
保存当前类被加载好的数据
.java = > .class = > 加载到内存中去
要想运行代码,就需要把.class加载到内存里面(类加载)
(3)栈
用来保存方法的调用关系。每次调用方法就会进入方法内部执行,当方法执行完毕,返回到调用位
置继续往后走

当调用test1方法,进入test1的方法栈
栈这个空间不算大,一般是几MB几十MB这样的情况,大部分情况是够用的,少数情况下可能会出
现"栈溢出"的情况
(4)堆
用来保存new的对象的
java
Test test=new Test();
对于t这样的一个对象
如果t是一个局部变量,t就是在栈上
如果t是一个成员变量,t就是在堆上
如果t是一个静态成员变量,t就是在元数据区
堆是JVM中最大的空间区域,往集合类里面添加元素,如果对上的对象不再使用的话就需要释放掉
(垃圾回收)
补充

在Java 8之前,元数据区也叫做"方法区"。
这里的元信息指的就是一些属性,比如,这类叫啥名字,是不是public,继承自哪些类,实现哪些
接口,方法叫啥名字,参数相关信息...
2.JVM类加载
类加载本身是一个复杂的事情
类加载主要关心两个方面
类加载的步骤
五个阶段
a)加载:找到 .class 文件
根据类的全限定名(包名+类名,形如java.lang.String)
打开文件,读取文件内容到内存里(类似于类加载的过程,.class = > 类对象)
b)验证:解析,校验 .class文件读到的内容是否合法的,并且把这里的内容转换成结构化的数据
c)准备:给类对象申请内存空间,此处申请的内存空间相当于是"全0"的空间
d)解析:针对字符串常量进行初始化
字符串常量本身就包含在 .class文件中,就需要 .class文件里解析出来的字符串常量放到内存空间
里(元数据区,常量池中)
d)初始化:针对刚才提到的类对象进行最终的初始化
针对类对象的各种属性进行填充,包括类中的静态成员,如果这个类还有父类,并且父类还没加
载,此环节也会触发父类的加载
类加载的触发时机
并不是Java程序一启动就会加载到所有的类,而是懒汉模式,当Java代码用到哪个类就会触发哪
个类的加载
- 构造这个类的实例
- 调用/使用类静态属性/静态方法
- 使用某个类的时候,如果父类还没有加载也会触发父类的加载
类加载器
类加载器:JVM 中有专门的模块,专门负责类加载,JVM 默认提供了三种类加载器
- BootstrapClassLoader 爷 -> Java 标准库的目录
- ExtensionClassLoader 爹 -> Java 扩展库的目录
- ApplicationClassLoader 子 -> Java 的第三方库 / 当前项目
这是个类加载器首当其冲的就是要进行"找 .class文件"环节,这里的类加载器是通过parent这样的
引用来向上引用
双亲委派模型
双亲委派模型描述了类加载中根据全限定名找到 .class文件的过程,双亲委派模型的实现离不开类
加载器
双亲委派模型工作的过程
1.进行类加载时,通过全限定类名找 .class 的时候,就会从ApplicationClassLoader作为入口开始,然后把加载类这样的任务委托给父亲ExtensionClassLoader来进行
2.ExtensionClassLoader也不会立即进行查找,而是也委托给父亲BootstrapClassLoader来进行,BootstrapClassLoader也想委托给父亲,由于没有父亲只能自己进行类加载。
3.根据类名,找标准库范围是否存在匹配的 .class 文件,如果BootstrapClassLoader没有找到,再把任务还给孩子ExtensionClassLoader
4.接下来 ExtensionClassLoader来负责进行找 .class 文件的过程,找到就加载,没找到也就把任务还给孩子 ApplicationClassLoader
5.接下来 ApplicationClassLoader 负责找 .class. 找到就加载,没找到就抛出异常
垃圾回收
Java中释放内存的手段。在C语言中,申请内存要使用malloc,申请之后一定要手动调用free进行
释放,否侧会出现内存泄漏。但可能因为一些原因导致free没有执行到。手动释放内存太麻烦太容
易出错了,Java 引入垃圾回收进行自动释放,JVM 会自动识别出某个内存是不是后续不再使用了
并且自动释放。
GC的工作区域
GC主要回收JVM中堆的内存区域
|-------|--------------------|
| 程序计数器 | 线程销毁,自然就释放 |
| 栈 | 方法执行结束,栈帧就结束,随之释放了 |
| 元数据区 | 类对象,一般不会释放 |
| 堆 | 创建很多新对象,也会有旧对象消亡 |
说是"内存回收",本质上是"回收对象",不会出现把一个对象是放到一半的情况
GC的工作过程
1.找到垃圾(不再使用的对象)
有两个方法
(1)引用计数(Python,PHP采用了这个方案)
每个对象在new的时候都搭配一个小的内存空间,保存一个整数

这个整数就表示当前对象有多少个引用指向它,每次进行引用赋值的时候都会自动触发引用计数的
修改,通过引用计数记录有多少个引用.,在 Java 中,要想使用某个对象一定是通过引用来完成
的,如果引用计数为 0 了,就说明没有引用指向这个对象了,这个对象就是垃圾
这种方法存在以下几个问题:
1)内存消耗更多
尤其是对象本身比较小,引用计数消耗的空间的比例就更大,假设引用计数是4个字节,对象本身
是8个字节,引用计数相当于提高了50%的空间占用率
2)可能会出现"循环引用"的问题

此时这两对象的引用不为0,虽然不为0但是这两对象都无法使用
(2)可达性分析(Java采取了这个方案)
工作流程
-
以代码中的一些特定对象,作为遍历的"起点" => GCRoots
- 栈上的局部变量(引用类型)
- 常量池引用指向的对象
- 静态成员(引用类型)
这三个对象,程序运行到任何一个时刻JVM 都是容易获取到
-
尽可能的进行遍历,为了判定某个对象是否能访问到
-
每次访问到一个对象,都会把这个对象标记成"可达",当完成所有的对象的遍历之后,未被标记成"可达"的对象就是"不可达"
JVM 中一共有多少个对象,JVM 自身是知道的,通过可达性分析,知道了哪些是"可达"的剩下的
就是"不可达",不可达接下来要回收的垃圾
可达性分析这个过程是"周期性",每隔一定的时间触发一次这样的可达性分析的遍历
引用计数,是有空间开销;可达性分析,用时间换空间。
2.回收垃圾
(1)标记-清除
把垃圾对象的内存直接释放,但这样会产生内存碎片问题。

如果当内存碎片非常多时,即使总的空闲时间很大,但申请一个稍微大一点的内存都会失败
(2)复制算法

一次只使用其中的一半,把不是垃圾的对象拷贝到另外一侧,然后在把这一侧给整体的释放掉.,
此时可以确保空闲的内存就都是连续的了
缺点:
- 内存的空间利用率是很低的
- 一旦不是垃圾的对象较多, 复制的成本就会很高 (尤其是这样的对象中包含大的对象的时候)
(3)标记-整理
类似于顺序表的"搬运"
优点:解决内存碎片,同时也能保证内存利用率
缺点:还是存在内存搬运数据的操作,开销挺大的,复制成本的问题仍然存在
(4)分代回收
这里的"代"表示对象的年龄,针对不同的年龄的对象采取不同的策略,不同年龄的对象特点是不同
的。不同年龄的对象特点是不同的,根据经验而言:如果某个对象已经是一个年龄大的对象了,此
时大概率还会继续存在很久;如果某个对象的年龄很小,这个对象可能会很快挂掉
对于这种情况,老年代的GC频次就可以降低;新生代的GC频次就会比较高
具体分配区如下

新创建的对象就放到"伊甸区",绝大部分的伊甸区的对象活不过第一轮 GC 所以幸存区比伊甸区小
伊甸区 => 幸存区:复制算法,复制的对象规模是很少并且复制的开销可控的
幸存区中的对象也要经历 GC 的扫描,每一轮GC都会消灭一大部分对象,剩余的对象再次通过复
制算法复制到另外一个幸存区,如果这个对象在幸存区中经历了多次复制都存活下来了, 对象的年
龄就大了
总体流程图如下
