1. JVM
JVM 的出现是为了实现跨平台
一个 JVM 中包含这几个主要部分
- 类加载器
- 执行引擎(解释执行字节码)
- 动态内存管理
1.1 JVM 中的内存区域划分
JVM 中内存来自于操作系统,JVM启动后就会从操作系统申请一大块内存,再针对这个内存划分出一些区域
2.2 GC 要回收哪些内存
堆 主要回收这里
方法区 GC 需要回收方法区的内容!但是方法区空间小,数据失去作用的概率低
栈 不需要回收,栈上的内存需要核实释放时机是明确的(线程结束,栈上的内存就全部被释放了额,某个栈帧销毁,对应的)局部变量被释放
程序计数器 只是存了个地址,不需要回收
2.3 回收的基本单位
内存的单位是“字节”,回收内存就是按照字节来回收的吗?其实不是,而是按照对象的方式来回收的,每个对象都持有一定的内存,释放对应的对象,也就回收了对应的内存。
2.4 回收对象的基本思路
- 标记:找出这个对象到底是否需要回收(判断对象的生死)
- 回收:把死了的对象回收回去
2.4.1 标记:
- 1.1 引用计数法【Java 中没有使用,而是在 Python、PHP】
- 1.2 可达性分析【Java 中使用的方式】
1.1引用计数法:记录当前这个对象,是否有引用指向(有几个引用)
每个对象都专门分配一个计数器变量,有新的引用指向该对象,引用计数 +1,有旧的引用指向别的对象,就引用计数 -1,
引用计数法,有一个致命缺陷,无法解决循环引用的问题
1.2 可达性分析
代码中的对象之间,其实是有一定的关联关系的,这样的关联关系错综复杂,构成了一个“有向图”【图是一种比树还要复杂的数据结构】
从哪开始遍历:
- 针对每个线程的每个栈帧的局部变量表
- 常量池中引用的对象
- 方法区中静态变量引用的对象
遍历的起点不是一个,而是很多起点,把每个起点都要进行往下遍历
1.3 回收方法区对象的规则
- 该类的所有实例都已经被回收了
- 加载该类的 ClassLoader 也已经被回收了
- 该类对象没有在代码中被使用了(包括各种静态成员,包括反射等)【通过可达性分析分析出来的】
同时具备这三个条件,就认为该类对象是可以被回收的
一点补充内容:
1)各种不同的引用
引用本质上就是一个低配指针,初心就是为了找对象,引用不光能够找对象,还能决定对象的生死(副作用)
a)强引用,平时用的引用,技能找到对象,也能决定对象的生死
b)软引用,既能找到对象,也能一定程度的决定对象生死——只有软引用指向,如果内存充裕,此时对象是被不会被回收的,但是如果内存不够了,软引用的对象也会被回收
c)弱引用,只能找到对象,但是不能决定对象的生死
d)虚引用,不能找到对象,也不能决定对象的生死,当死了的时候,弱引用可以收到通知,进而做一些善后工作
2)finalize 方法
Object 类的方法
这个方法会在对象被销毁之前调用【不建议使用!调用时机不确定】
2.4.2 回收
- 2.1 标记-清除
- 2.2 标记-复制
- 2.3 标记-整理
2.1 标记消除
将垃圾对象直接删除。
缺点:会制造出许多内存碎片。
2.2 标记复制
把内存区域一分为二,把不是垃圾的对象拷贝到内存区域的另一边,然后整体释放左边的内存区域
优点:能够解决内存碎片问题,保证内存回收互殴,并不存在碎片(已经使用的对象之间是连续的,空余内存也是连续的)
缺点:需要一块额外的内存空间,如果内存的对象比较多,此时就比较低效
2.3 标记整理
当保留的对象多,回收的对象少此时复制就不合适了,此处采取的策略,类似于顺序表删除。当出现内存碎片的时候,就把后面对象往前搬运,把碎片挤掉即可
优点:不再复制一样依赖更大的内存空间,也没有内存碎片
缺点:低效
3.一个对象的一生
1.对象诞生于新生代——伊甸区,新对象得到生存就是新生代中的内存
2.第一轮GC 扫描伊甸区后,就会把大量的对象干掉(统计发现,绝大部分对象都是“朝生夕死”),少数没有被干掉的对象就会被拷贝进生存区(标记复制算法,例如先拷贝到生存区1)
3.进入生存区的对象,也会被 GC 进行扫描(可达性分析),如果发现对象已经不可达,也就要被销毁,没被销毁的对象,就通过复制算法,拷贝到另外一个生存区(生存区2)
搞两个生存区就是为了更好的施展复制算法,由于生存区对象存活概率也很低,施展复制算法成本不高
生存区的对象,来源于两个地方:伊甸区和生存区1
4.对象在生存区中经历了若干轮次的拷贝之后,也没被回收,此时说明,这个对象
5.老年代的对象也是要经历 GC 扫描的,但是由于老年代的对象,存活时间都比较长,所以扫描老年代的周期就比扫描新生代的周期长