1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?

第一步 判断对象是否要存活

引用计数算法

首先给对象添加一个引用计数器 如果有人引用了这个对象 计数器加1 ,
当引用失效时 计数器减一;任何时刻计数器都为0的对象就是不能被使用的。

优点:实现简单 效率高
缺点:很难解决对象循环引用的问题 A引用B B的计数器加一 B也引用A A的计数器加一 AB的就都不为1 但实际上两个对象都没用

根搜索算法 JAVA C#

通过一系列名为 “GC Roots”的对象作为起点 从这些节点向下搜索,搜索走过的路径称为引用链,
当一个对象对到GC Roots没有任何引用链相连时 (GC Roots 到这个对象不可达),证明对象不可用

Java中 GC Roots对象包括

  1. 虚拟机栈(本地变量表)中的引用对象
  2. 方法区中静态类属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI (Native方法)的引用对象

何为引用

如果reference类型的数据中存储的数值是另一块内存的起始地址,就称这块内存代表这一个引用。

  1. 强引用 类似 Object obj=new Object() 只要强引用还存在,垃圾回收永远不会回收被引用的对象。
  2. 软引用 还有用 并非必须 在系统将要发生内存溢出时,将会把这些对象列进回收范围并进行二次回收。
  3. 弱引用 非必须对象 被弱引用的对象只能生存到下一次垃圾收集发生之前。
  4. 虚引用 一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用来获取一个对象实例。

生存还是死亡?

真正需要回收到对象需要经过两次标记

方法区 垃圾回收性价比低 两个内容 废弃的常量,无用的类。

  1. 废弃的常量 没有引用。
  2. 无用的类 该类所有实例都被回收 加载该类的ClassLoader已经被回收 没有任何地方引用 无法通过反射访问该类方法。

大量使用反射 动态代理 CGLib 等 bytecode框架的场景 以及频繁自定义ClassLoader的场景都需要具备类卸载功能。

垃圾回收算法

标记-清除算法

标记清除法是最基础的算法 分为两个阶段

  1. 首先标记处所需要回收的对象
  2. 在完成标记后统一回收掉所有被标记的对象

缺点 1. 效率问题 标记和清除效率不高 2. 空间问题 清除后会产生大量不了连续的空间碎片。

复制算法

复制算法将可用的内存按容量分为大小相等的两块, 只使用其中一块,当使用完了以后把活着的对象复制到另外一块上,把已使用的内存空间全部清掉。

优点 实现简单 运行高效
缺点 把内存缩小为原来的一半

复制算法用来回收新生代。

新生代中的对象98%朝生夕死 所有并不需要有按1:1 比例分配空间

新生代分为Eden 空间和两块较小的Survivor空间 每次用Eden 和一个Survivor 。
回收时把Eden 与Survivor的活着的对象一次性拷贝到另一款Survivor上 最后清理掉Eden 与Survivor空间。

标记-整理算法

标记整理算法是把存活的对象都向一段移动,然后清理掉边界以外的内存。

分代收集算法

根据对象的存活周期不同将内存分为几块,一般分为新生代和老年代,这样可以根据各个代的特点采取最适当的手机算法。

新生代采取复制算法

老年代采取标记清理或标记整理算法。

垃圾回收器

收集算法是内存回收的方法论,垃圾回收器就是内存回收的具体实现。

Serial收集器 (stop the world)

Serial收集器是新生代收集器,是单线程收集器,在垃圾回收的同时会暂停掉所有工作线程。

虽然会停掉其他所有工作线程 但是有显著的有点 简单高效,目前在client模式下的虚拟机里还有应用。

ParNew收集器

ParNew收集器是serial收集器的多线程版本,除了多线程外和secrial完全一样。

是server模式下虚拟机中首选新生代收集器 因为除了serial收集器 只有它能和CMS收集器配合。

Parallel Scavenge 收集器

Parallel Scavenge 收集器是也新生代收集器,也使用复制算法,是并行多线程收集器。

Parallel Scavenge收集器目的但是达到一个可控吞吐量,代码运行时间/(gc时间+代码运行时间)。

主要是为了给虚拟机设定一个可控目标,会自适应调节吞吐量。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,是 单线程收集器,使用标记整理算法。

主要意义是用在Client模式下。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。

CMS收集器

CMS收集器是一种以获取最短收集停用时间为目标的收集器。基于标记清楚算法实现。

CMS收集器需要四个步骤

  1. 初始标记 只是标记一下CG Roots直接关联到的对象,速度快。
  2. 并发标记 跟搜索算法(CG Roots Tracing)确定存活和死亡的对象。
  3. 重新标记 修正并发标记时间内程序导致标记变动的记录,需要时间但是比重新标记快。
  4. 并发清除 清除标记内容。

初始标记,重新标记需要stop the world

优点:并发收集,低停顿

缺点:

  1. 对CPU资源相对敏感 默认启动线程数为(CPU+3)/4。在GC期间降低工作效率。
  2. 无法处理浮动垃圾,初始标记和重新标记后会有浮动垃圾无法回收。
  3. 标记清楚算法会有大量碎片空间。

G1收集器

我理解的G1收集器是分区域、分先级,在规定时间内完成优先级最高的GC。

G1收集器是基于标记-整理算法实现的,不会产生碎片空间。

G1收集器能精确控制停顿时间,规定总时间M,GC时间N,毫秒级,无停顿。

G1收集器将整个java堆分成多个大小固定的独立区域(Region),根据垃圾堆积数量,后台维护一个优先级列表,每次根据允许时间 优先回收垃圾最多的区域。

内存分配策略

java技术中自动化内存管理可以归结为自动化的解决了两个问题,
1. 给对象分配内存
2. 回收给对象分配的内存

对象优先在Eden分配

对象在新生代Eden中分配,没有足够空间时虚拟机将发起Minor GC。

Minor GC 新生代的垃圾回收动作,java对象大多生命周期短,Minor GC非常频繁,速度也比较快。

Full GC 老年代的垃圾回收,经常会伴随至少一次的MinorGC ,速度比MinorGC慢10倍以上。

大对象之间进入老年代

大对象是指需要大量连续内存空间的JAVA对象,放入老年代的目的是为了避免Eden和两个Survivor之间发生大内存拷贝。

所以要避免生命周期短的大对象,FullGC 慢。

长期存活的对象进入老年代

虚拟机采用分代收集思想,内存回收时就需要明确哪些对象放在新生代,哪些对象放在老年代。

对象年龄计数器 对象在Eden中经历Minor GC存活后转移到Survivor中,对象年龄加一,没过一次Minor GC 对象不死亡年龄加一,当年龄加到一定程度(默认15,可以设置)时移到老年代。

动态对象年龄判断

15次是默认标准,但是如果Survivor区中的对象年龄普遍偏大(相同年龄的对象大小大于Survivor空间的一半),那就都移到老年代,给Survivor区域腾地方。

空间分配担保

发生minorGC时 会检查晋升到老年代的大小平均值是否大于老年代的剩余空间,如果直接进行一次Full GC,小于就看是否允许担保失败,如果允许就进行Minor GC 否则就进行Full GC。