
文章目录
- 一、初始
- 二、Java内存区域划分
-
- [1. 运行时数据区划分](#1. 运行时数据区划分)
- [2. 内存溢出](#2. 内存溢出)
- [3. 补充](#3. 补充)
- 三、Java类加载机制
-
- [1. 类加载流程](#1. 类加载流程)
-
- [1. 加载](#1. 加载)
- [2. 验证](#2. 验证)
- [3. 准备](#3. 准备)
- [4. 针对字符串常量进行初始化](#4. 针对字符串常量进行初始化)
- [5. 初始化](#5. 初始化)
- [2. 类加载时机](#2. 类加载时机)
- [3. 双亲委派模型](#3. 双亲委派模型)
- [4. GC垃圾回收------重点](#4. GC垃圾回收——重点)
-
- [1. GC步骤第一步------找垃圾](#1. GC步骤第一步——找垃圾)
-
- [1. 方案壹------引入计数机制(非Java)](#1. 方案壹——引入计数机制(非Java))
- [2. 方案贰------引入周期性可达性分析](#2. 方案贰——引入周期性可达性分析)
- [2. GC步骤第二步------回收垃圾](#2. GC步骤第二步——回收垃圾)
-
- [1. 方案壹------标记清除法](#1. 方案壹——标记清除法)
- [2. 方案贰------复制算法](#2. 方案贰——复制算法)
- [3. 方案叁------标记整理法](#3. 方案叁——标记整理法)
- [4. 综合方案------分代回收](#4. 综合方案——分代回收)
- [5. 其他方案------其他垃圾回收器](#5. 其他方案——其他垃圾回收器)
一、初始
为什么我们Java要引入Java虚拟机JVM呢,Java虚拟机又称为Java解释器/Java执行引擎
因为它充当着翻译官,可以很好的实现跨平台功能,并且更好地兼容操作系统和CPU
二、Java内存区域划分
在每一个Java的进程 中,都包含了一个JVM
JVM在启动的时候,就会向操作下申请一块内存空间,因此我们Java程序就可以利用这块内存空间去执行代码逻辑了
1. 运行时数据区划分
-
程序计数器:它是一片很小的内存区域,用来存放下一个Java字节码指令的地址
-
虚拟机栈:主要服务于Java程序,明确方法之间的调用关系、明确方法内部的局部变量,明确方法结束后返回上一层方法的位置、明确方法返回值,比如
|fuc3|
|fuc2|
|fuc1|
|main|<----每个方法就是一个栈帧 -
本地方法栈:给C++代码使用的,因为JVM底层是C++实现的,并且在有些方法底层调用的就是C++代码
-
堆:存储
new出来的对象 -
元数据区/方法区:存储一些类的对象或者是静态成员和方法
2. 内存溢出
- 栈溢出:栈帧(方法)太多导致的,比如死递归
- 堆溢出:
new的对象太多了
3. 补充
一个进程中只存在一份堆和元数据区,这就说明一个线程中new的对象可以被另一个线程引用
但是每个线程都有自己的程序计数器、本地方法栈、虚拟机栈
三、Java类加载机制
本质上就是把class文件读取到内存中并且构建的过程
1. 类加载流程
1. 加载
把class文件根据"全限定类名"找到对应的class文件,并且把文件数据读取到内存中
2. 验证
根据读取到的二进制内容判断其是否是一个正确的格式,我们打开Java官方文档中的虚拟机规范
文档链接

好,我们来逐个参数简单看下
U4 maginc指的就是开头四个字节为魔幻数字,就是一种二进制文件的格式U2 minor_version&major_version指的就是主版本号和副版本号,描述class文件是经过哪个版本的Java编译器生成的U2 constant_poll_count&cp_info constant_pool指的就是常量池数量以及内部的数据格式cp_infoU2 access_flags指的就是类访问权限U2 this_class指的就是当前类的编号U2 super_class指的就是父类的编号U2 interfaces_count&U2 interfaces[interfaces_count]指的就是描述实现类的接口以及其编号U2 fields_count&field_info fields[fields_count]指的就是描述类的属性、属性访问权限等等U2 methods_count&method_info methods[methods_count]指的就是方法的属性、方法的访问权限等等U2 attributes_count指的就是关于类的一些注解
3. 准备
给要去创建类的对象在JVM的元数据去分配一块内存空间,且默认把新申请的未初始化的设置为0
4. 针对字符串常量进行初始化
要把class文件中的字符串常量池加载到内存,此时我们的字符串就有了起始地址,后续在调用的时候就可以把字符串取出来
5. 初始化
初始化类静态成员、执行静态代码块、加载类等等
2. 类加载时机
本质上是一个懒汉模式,仅需加载一次并且在JVM中是单例存在的,那么什么时候会触发
new实例的时候、调用这个类的静态方法或者是静态成员、针对子类的父类加载
3. 双亲委派模型
在我们类加载流程第一步的时候,对于寻找class文件,涉及到三个模块的类加载器,之间存在父子关系
BootStrapClassLoader(爷):加载Java标准库的类
ExtensionClassLoader(父):加载扩展库类,一般是JDK厂商内置的,但现在少用了
ApplicaitonClassLoader(子):加载第三方库和项目中的类,也是整体的入口
它们之间的关系就是,对于一个类,先直接丢给爷爷去处理,处理不过就向下委派,如果最后还是没找到这个类,就会抛出类加载异常
4. GC垃圾回收------重点
在C语言中,内存泄露是一个非常严重的问题
如果我们进行malloc操作不及时进行释放,就会导致后续我们再进行内存分配的时候产生无内存可用的情况
而在我们Java中,通常会指派一些线程,周期性地对堆上的对象进行扫描,自动判定这个内存空间(对象)是不是不再使用
并且我们回收是针对一整个对象回收,并不存在回收半个的情况
如果不再使用就会自动释放,但是就是因为GC这种机制会有额外的内存和时间的开销,像C/C++这种追求效率的语言就不会采用
下面我们就来谈谈整体的回收流程
1. GC步骤第一步------找垃圾
GC是如何去判断这个对象不再进行使用的呢,答案就是看这个对象是否被强引用
那么如何去判断有没有被强引用呢,这里有几种方案,我们一一阐述
1. 方案壹------引入计数机制(非Java)
给每个对象安排一个空间,存放一个整数,围绕对象引用个数进行计数器更新
但是这会暴露出两个很大的问题
- 消耗更多哦内存空间
- 循环引用出现误判
我们针对循环引用的误判来做下解读
java
class Test{
Test t;
}
main{
Test a = new Test();
Test b = new Test();
a.t = b;
b.t = a;
a = null;
b = null;
}
如果我们画个图,就是这样子

当我想使用 0x100对象,发现是 0x200 引用的,那我去找0x200
但是我想使用0x200对象,发现是0x100引用的,那我去找0x100
嗯?不是死循环了吗,我这两个对象明明都使用不了
并且由于其计数器不为0,就不能被释放掉,就会持续占用越来越多的内存资源
2. 方案贰------引入周期性可达性分析
在Java的代码中,一系列的对象都存在类似于树形结构的关系
java
class Test{A a = new A();B b = new B();}
也就是可能类似于这种结构
Test
/ \
A B
/ \
C D
我们所谓的周期性可达性分析,就是从一个或者多个根节点出发尽可能地遍历这棵树,凡事能经过的对象都标记为可达
结合JVM知道对象数量,减去可达到对象数量,剩下的就是不可达的对象了,并且这种扫描会周期性的进行
对于根节点GCRoots可能有一下几种
- 栈的局部变量,因为栈有很多个,每个栈的栈帧也有很多个,每个栈帧中局部变量也有很多个
- 常量池引用所指向的对象,可能也有很多个
- 所有引用类型的静态成员所指向的对象,也可能有很多个
2. GC步骤第二步------回收垃圾
1. 方案壹------标记清除法
如果采用直接释放,会导致内存碎片问题,当我们想申请一大片连续的内存空间时候
明明总的空闲内存加起来够,但是由于是碎片化的,并没有集中在一起,因此就申请失败

2. 方案贰------复制算法
把内存区域分成两份,同一时刻只使用一份
如果是那种无效对象(垃圾)就会留在原地,其余有效对象就被拷贝到内存的另一部分区域,最后再把原来的那一部分区域整体释放
下一次就会从右边开始,看哪些是"垃圾",把"垃圾"留下,有效对象就拷贝到内存的另一部分区域,最后再把原来的那一部分区域整体释放
我们画个图就是这样的

我们这两部分内存就会被反复使用,但是这样会导致空间利用效率很低,并且对象复制开销也很大
3. 方案叁------标记整理法
这就类似于我们顺序表中的删除中间元素的方法,即搬运

但是这么搞对象搬运的开销也可能会很大
4. 综合方案------分代回收
JVM会根据对象的情况/特点结合存活时间进行有针对性的回收
我们普遍认为,根据GC扫描轮数,如果某个对象年龄(存活时间)比较久,有很多概率会继续存活下去
因此我们把内存划分为了以下区域

- 新
new的对象首先会在伊甸区中,大多数对象经过第一轮GC都会被淘汰 - 在伊甸区未淘汰的对象通过复制算法进入幸存区,同一时刻只使用一个幸存区
- 在幸存区会进行第二轮GC扫描,进一步淘汰一大批对象
- 如果在第二轮未被淘汰,则会进入另一个幸存区,重复GC扫描
- 当在两个幸存区都没有被GC扫描所淘汰,达到一定阈值,就会进入老年代
- 在老年代同样会进行GC扫描,只不过频次慢了些
- 这里说个例外情况:如果对象内存非常大则可能会直接进行老年代
5. 其他方案------其他垃圾回收器
- CMS------在多线程中尽可能扫描回收
- G1(G ONE)------针对内存空间特别大的情况,会把内存划分出更多区域,一次GC只针对一部分区域
- 2GC------目前处在实验性阶段,它会使得垃圾回收对于业务逻辑的响应时间更短,即开销更小,据说能达到0.1ms!
本篇文章属于是纯纯的八股文了
面试爱考,实际开发作用不大
END QAQ