GC的概念
- Garbage Collection 垃圾收集
- 1960年,List使用了GC;GC需要完成的3件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
- 为什么还要去了解GC和内存分配呢?
- 当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节
- Java中,GC的对象是堆空间和方法区(永久区)
- 只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的
GC算法
引用计数法(Reference Counting)
- 老牌垃圾回收算法
- 通过引用计算来回收垃圾
- 使用者:主流的Java虚拟机里面没有选用引用计数法
- COM
- ActionScript3:FlashPlayer
- Python
- Squirrel:游戏脚本领域
- 引用计数法的问题
- 引用和去引用伴随加法和减法,影响性能
- 很难处理循环引用
标记清除(Mark-Sweep)
- 最基础的收集算法,分为“标记”和“清除”两个阶段:
- 标记出所有需要回收的对象
-
在标记完成后统一回收所有被标记的对象
- 后续的收集算法都是基于这种思路并对其不足进行改进。它的主要不足有两个:
- 效率问题:标记和清除两个过程的效率都不高
- 空间问题:标记清除之后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾手机动作
复制算法(Copying)
- 为了解决标记清除算法的效率问题:
- 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块
- 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
- 不适用于存活对象较多的场合,如老年代;而采用这种算法来回收新生代
- IBM研究表明,新生代中的对象98%是“朝生夕死”的
- HotSpot虚拟机默认Eden和Survivor的大小比例是8:1
- 每次新生代中可用内存空间为整个新生代容量的90%(80% + 10%),只有10%的内存会被“浪费”
- 当Survivor空间不够用时,需要依赖老年代进行分配担保
- 分配担保(Handle Promotion)
- 如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代
标记压缩(Mark-Compact)
- 适合用于存活对象较多的场合,如老年代
- 标记过程仍然与“标记-清除”算法一样
- 让所有存活的对象都向一端移动
- 然后直接清理掉端边界以外的内存
分代思想
- 依据对象的存活周期将Java堆进行分类,短命对象归为新生代,长命对象归为老年代。
- 根据不同代的特点,选取合适的收集算法
- 少量对象存活,适合“复制算法”
- 大量对象存活,适合“标记清理”或者“标记压缩”
可触及性(可达性分析)
- 基本思路:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(不可达)时,则证明此对象是不可用的
- 可作为GC Roots的对象:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
Stop-The-World
- Java中一种全局暂停的现象
- 全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
- 多半由于GC引起
- 枚举GC Roots必须在一个能确保一致性的快照中进行:整个分析期间整个执行系统看起来就像被冻结在某个时间点上
- Dump线程
- 死锁检查
- 堆Dump