JVM Java虚拟机
jdk java开发工具包
jre java 运行是环境
jvm java虚拟机(解释执行java字节码)
JVM 中的内存区域划分
JVM 其实也是一个进程
进程运行过程中,要从操作系统这里申请一些资源
这些内存空间,就支撑了后续 java 程序的执行
比如 在 java 中定义变量(就会申请内存),内存其实就是 jvm 从系统这边申请到的内存
jvm 从系统申请了一大块内存,这一大块内存给 java 程序使用的时候,又会根据实际的使用用途,来换分出不同的空间,这个就是"区域划分"
这就像是买了个房,然后里面的区域布置就是根据自己需求来划分

1.堆(只有一份)
代码中 new 出来的对象,就都是在堆里
对象中持有的非静态成员变量,也就是在堆里
2.栈(可能有 N 份)
本地方法栈/虚拟机栈
包含了方法调用关系和局部变量
3.程序计数器(可能有 N 份)
这个区域比较小的空间,专门用来存储下一条要执行的 java 指令的地址
每个线程都有自己的程序计数器和栈(每个线程自己的执行流)
4.元数据区(只有一份)
"元数据" 是计算机中的一个常见术语
往往指的是一些辅助性质的,描述性质的属性
类的信息,方法的信息
一个程序,有哪些类,每个类都有哪些方法,每个方法里面都要包含哪些指令,都会记录在元数据区中
我们写的 java 代码,if,while,for 各种逻辑运算,这些操作最终都会被转换成 java 字节码
(javac 就会完成完成上述代码 => 字节码)
此时这些字节码在程序运行的时候就会被 jvm 加载到内存中,放到元数据区(方法区)中
此时,当前程序要如何执行,要做哪些事情,就会按照上述元数据区里记录的字节码依次执行了

硬盘上不仅仅要存文件数据本体,还需要存储一些辅助信息
比如,文件的大小,文件的位置,文件的拥有者,文件的修改时间,文件的权限信息
统称为"元数据"
static 修饰的变量,称为"类属性"
static 修饰的方法,称为"类方法"
非 static 的变量,称为"实例属性"
非 static 的方法,称为"实例方法"
上述带有 static 修饰的变量,就是在类对象中,也就是在 元数据区中

区分一个变量在哪个内存区域中,最主要就是看变量的形态(局部变量,成员变量,静态成员变量)
JVM 的类加载机制
类加载指的是 java 进程运行的时候,需要把 .class 文件从硬盘,读取到内存,并进行一系列的检验解析的过程
.class 文件 => 类对象
硬盘 => 内存
类加载过程在 Java 官方文档中给出说明

类加载大体的过程可以分成 5 个步骤
1.加载
在硬盘上的 .class 文件找到打开文件,读取到文件内容
2.验证
当前需要确保读到的文件的内容,是合法的 .class 文件格式

3.准备 给类对象,申请空间
此时申请到的内存空间,里面的默认值,都是全 0 的
(这个阶段中,类对象里的静态成员变量的值也就相当于是 0 了)
4.解析 主要是针对类中的字符串常量进行处理


5.初始化 针对类对象完成后续的初始化
还要执行静态代码块的逻辑,还可能会触发父类的加载
初始化:把类对象的各个部分的属性进行赋值填充 => 触发对父类的加载,初始化静态成员,执行静态代码块
双亲委派模型
描述了如何查找 .class 文件的策略
JVM 中进行类加载的操作,是有一个专门的模块,称为"类加载器"
JVM 中的类加载器默认是有三个的(也可以自定义)

上述的三个类加载器,存在"父子关系"

3.垃圾回收机制(GC)
引入这样的机制之后,就不需要靠手动来进行释放了,程序会自己判定,某个内存是否会继续使用
如果内存后续不用了,就会自己释放掉
垃圾回收中的一个很重要的问题:STW(stop the world)问题
触发垃圾回收的时候很可能会使当前程序的其他业务逻辑被暂停
垃圾回收,是回收内存,JVM 中的内存有好几块
1.程序计数器(不需要GC)
2.栈(不需要GC)
局部变量都是在代码块执行结束之后自动销毁,(这是栈自己的特点和垃圾回收没啥关系)
生命周期都非常明确
3.元数据区/方法区(一般不需要GC)
一般都是涉及到"类加载"很少涉及到"类加载"
4.堆 是GC 主要的战场
这里的回收内存,更准确的说是"回收对象"
每次垃圾回收的时候,释放的若干个对象(实际的单位都是对象)

垃圾回收,具体是怎样进行展开的
1.识别出垃圾,那些对象是垃圾(不再使用),那些对象不是垃圾
2.把标记为垃圾的对象的内存空间进行释放
1.识别出垃圾
半段这个对象后续是否要继续使用
在 Java 中,使用对象,一定需要通过引用的方式来使用(有一个例外,匿名对象)
new MyThread().start();
这行代码执行完,对行的 My Thread 对象就会被当做垃圾
如果一个对象没有任何引用指向它,就视为无法被代码中使用,就可以作为垃圾了
2.可达性分析
本质上是用 "时间" 换 "空间",相比与引用计数,需要消耗更多的额外的时间,但是总体来说,还是可控的,不会产生类似于"循环引用"这样的问题
在写代码的过程中,会定义很多的变量
比如,栈上的局部变量 / 方法区 中的静态类型的变量 / 常量池中引用的对象
就可以从这些变量作为起点,出发,尝试去进行"遍历"
所谓的遍历就是会沿着这些变量中持有的引用类型的成员,再进一步的往下进行访问
所有能被访问到的对象,自然就不是垃圾了,剩下的遍历一圈也访问不到的对象,自然就是垃圾
已二叉树举例

主要释放的方式有三种
1.标记 - 清除
把标记为垃圾的对象,直接释放掉

此时就是把标记为垃圾的对象对应的内存空间直接释放
上述释放方式,就可能会产生很多小的,但是离散的 空闲内存空间
就可能会导致后续申请内存失败
内存申请,都是一次申请一个连续的内存空间
申请 1M 内存空间,此时 1M字节都是连续的
如果存在很多内存碎片,就可能导致,总的空闲空间,远远超过 1MB,但是并不存在比 1MB 大的连续的空间,此时去申请空间就会失败
2.复制算法

复制算法,核心就是不直接释放内存,而是把不是垃圾的对象,复制到内存的另一半里
接下来把左侧空间整体释放掉
这确实能规避内存碎片问题,但是也是有缺点的
1.总的可用内存变少了
2.如果每次要复制的对象比较多,此时复制开销也就很大了,要是当前这一轮 GC 的过程中,大部分对象都释放,少数对象存活,这个时候适用使用复制
3.标记 - 整理 也能解决内存碎片问题

JVM 中没有直接使用上述的方案,而是结合上述思想,搞出了一个"综合性"方案,取长补短
分代回收(依据不同种类的对象,采取不同的方式)
引入概念,对象的年龄
JVM 中有专门的线程负责周期性扫描/释放
一个对象,如果被线程扫描了一次,可达了,年龄就 + 1(初始年龄相当于是 0)
JVM 中就会根据对象年龄的差异,把整个堆内存分成两个大的部分
新生代(年龄小的对象) / 老年代(年龄大的对象)