GC是干啥的?
GC(garbage collection)---垃圾回收机制, 在C语言中有malloc和free, 相互配合工作, malloc() 函数在内存中动态申请空间, 使用完再使用free() 手动释放, 靠程序员来控制内存的释放回收是不可靠的, 在Java中内存的回收是靠JVM自动帮忙完成的不需要程序员干预
GC工作的主要区域
内存中划分为: 堆区, 栈区, 程序计数器, 方法区
程序计数器跟随线程一起创建销毁, 占用内存空间最小, 不需要依赖GC
同样, 栈区主要存放的是局部变量, 出了作用域就会被销毁, 也是跟随线程一起进行创建和销毁的, 不依赖GC
方法区: 存放的是类对象,参与的主要的活动是'类加载'很少涉及'类卸载',需要依赖GC但不迫切
堆区: 存放的是new出来的对象, 在内存中占用空间最多, 用完后就需要进行销毁, 是GC的主要工作区
垃圾回收是以对象为基本单位进行的, 而不是'字节' , 因为这样实现比较简单, 假设一个对象有两个成员变量: 其中一个被频繁使用, 另一个几乎不使用, 如果以字节为单位进行GC, 操作起来会很麻烦过程很复杂, 处理不好会造成对象的破坏, 而以对象为单位进行回收则不会出现这样情况, 当一个对象不被使用时则会直接销毁, 操作实现也比容易更合理
JVM如何判断哪种对象是垃圾需要回收呢?
在Java中主要使用的是'可达性分析'这种方法, 在其它语言(比如Python)使用的是'引用计数'这种方法
- 引用计数: 指的是在内存中额外引用计数器来记录某一个对象有多少个引用指向它, 若引用为为0, 表示这个对象没有再被使用可以被销毁, 这种方式原理非常简单, 但有三个明显的缺陷: 1) 当多线程中需要修改同一个引用计数,需要考虑线程安全 2) 如果一个比较大的对象引用一个计数器不会造成内存负担, 当一个比较小的对象引用时开销并不小 3) 循环引用, 假设两个对象中都存了对方的引用, 即使外部引用为0, 但由于对象里存放了相互的引用所以无法被销毁, 引用计数器始终为1, 因为外部并没有引用指向他们, 并没有被使用只是双方在相互引用, 而无法被销毁---->就好比去酒店开个两个房间A, B, 把A的房卡锁到B中, 把B的房卡锁到A中道理类似
- 可达性分析: 以代码中的特殊变量作为起点, 看哪些对象能被访问得到,可以访问到, 就标记为"可达", 当一圈标记完成后, 剩下的对象就是"不可达" 也就是垃圾
特殊变量指的是GCRoot:
- 1) 局部变量表中的引用(栈中的局部变量) 意为所有的线程的所有的栈的所有的栈桢中的局部 变量表中的所有变量
- 2) 常量池中的所有对象
- 3) 方法区中的静态引用类型成员
这些都可被称为GCRoot
可达性分析不存在额外空间占用也不会涉及到循环引用问题
JVM如何进行垃圾回收的呢?
垃圾回收算法: 标记清除, 复制算法, 标记整理, 分代回收
- 标记清除 ---> 当经过可达性分析后, 未被标记的对象就直接会被JVM清除 缺陷: 内存碎片化, 释放后的空间不连续(变相造成空间的浪费)
- 复制算法 ---> 把内存一分为二, 用一半留一半, 进行GC时把要保留的对象复制到另一半,然后剩下的全部释放; 缺陷: 可用空间少了一半
- 标记整理 ---> 类似于顺序表删除元素, 如图2,4,6,8, 被标记为可达则要删除1,3,5,7,9
此时需要将2放到1, 4放到2, 6放到3, 8放到4
缺陷: 针对内存进行搬运, 时间复杂度高, 比较耗时
4. 分代回收 ---> 给对象引入"年龄" (对象活过的GC轮次), 根据年龄分为新生代, 老年代放入不同 的区间, 使用不同的算法
橙色是新生代, 红色是老年代, 新生代中又分为伊甸区和两个幸存区, new出来的对象直接会放 入到伊甸区, 经过一轮GC后活下来的对象会通过复制算法放入到第一个幸存区, (绝大多数的新 生对象活不过第一轮), 又经过一轮GC后会活下来的对象会通过复制算法进入到下一个幸存区, 再经过几轮GC(多少轮应该看机器的判定没有明确的界限) 没有被销毁的对象会被放到老年代, 进入老年代后GC的频率降低, 使用标记整理的算法
分代回收是JVM中主要采取的回收算法
例外: 当一个对象比较大时会直接被放入老年代
JVM进行GC是通过垃圾回收器来实现上述算法的,不同的垃圾回收器针对上述的算法实现也会存有差异
以上