一、jvm内存模型

要理解gc必须理解jvm,jvm主要管理两种内存:堆非堆
堆(Heap Memory):类实例和数组的内存
非堆(No-heap Memory):方法区,类结构(运行时常数池,字段和方法),类加载信息

GC主要回收的内存是堆内存

二、堆内存模型

1. 堆内存分两块,新生代和老年代,比例(1:2)
    * 老年代主要存放应用程序中生命周期较长的的存活对象
2. 新生代分三个部分,一个eden区和两个survivor区,比例8:1:1
    * eden区存放新生的对象
    * survivor区存放每次垃圾回收后存活的对象

三、可回收对象的判定

什么样的对象是垃圾? 1. 引用计数算法

原理就是给对象添加一个引用计数器,每当有一个地方引用它,计数器就加1, 当引用失效时,计数值就减1,计数器为0的对象就是不在被使用的。
    * 简单、高效(objectiveC用的该算法)
    * 很难处理循环引用(相互引用两个对象)
  1. 可达性分析算法
    为了解决循环引用,java采用新的算法
从GC的ROOT作为起点,向下搜索它们引用的对象,可生成一棵引用树,树的节点可视为可达对象,反之视为不可达
如何定义GC root,java语言定义如下GC-ROOT对象
    * 虚拟机栈中引用的对象
    * 方法区中静态属性引用的对象
    * 方法区中常量引用的对象
    * 本地方法栈中jni引用的对象
## stop the world   
垃圾回收时需要整个引用状态保持不变,否则判定是垃圾,等会就不是了,GC的时候所有的程序执行处于暂停的状态,卡住,这种卡顿非常短,对程序的影响微乎其微,GC的卡顿由此而来。

四、几种垃圾回收算法

如何回收?Java虚拟机采用的是分代回收算法,想要理解分代回收算法,有必要先了解其它几种简单算法 1. 标记清楚算法(Mark-Sweep)

标记阶段:标记所有需要被回收的对象
清楚阶段:回收被标记对象占用的空间
    * 简单,容易实现
    * 容易产生内存碎片,碎片太多可能导致后续需要为大对象分配空间无法找到足够的空间,提前触发新的一次垃圾收集动作。
  1. 复制算法(Copying)
将内存分为大小相同的两块,每次只使用其中的一块,当这一块用完了,将还活着的对象复制到另外一块,再把已使用的一块一次清理掉,这样就不会出现内存碎片问题,
    * 简单,高效
    * 对内存空间的使用付出了高昂的代价,可 使用内存缩少到了原来的一半
Copying算法的效率取决于活着的对象,活着的对象越多copying越多,效率越慢
  1. 标记整理算法(Mark-compact)
标记阶段和Mark-sweep一样,标记出需要被回收的对象,完成标记后,将存活的对象向一端移动,清理掉端边界以外的内存
    * 适用于存活对象多,回收对象少的情况
  1. 分代回收算法
    不是一种新的算法而是复制算法和标记整理算法的整合
复制算法:适用于存活对象少,回收对象多
标记整理算法:适用于存活对象多,回收对象少

堆内存分为老年代(Old generation)和新生代(young Generation),老年代的特点是每次垃圾收集时只有少量的对象需要被回收,新生代的特点是每次垃圾回收时都有大量的对象需要被回收,根据不同代的特点采用不同的算法,这就是分代算法。
详细分析

1. 对于新生代采用Copying算法,因为新生代每次垃圾回收的时候都要回收大部分对象,也就是说需要复制的存活的对象较少,所以采用Copying算法的效率最高,实际中并不是按照上面Copying算法中说的1:1分成相等的两块,而是将新生代分为eden空间和survivor空间8:1:1
2. 由于老年代每次只回收少量对象,一般使用Mark-compact算法

QA1.为什么需要两块Survivor空间?

这里涉及到一个新生代和老年代的存活周期的问题,比如一个对象在新生代经历15次(仅供参考)GC,就可以移到老年代了。问题来了,当我们第一次GC的时候,我们可以把Eden区的存活对象放到Survivor A空间,但是第二次GC的时候,Survivor A空间的存活对象也需要再次用Copying算法,放到Survivor B空间上,而把刚刚的Survivor A空间和Eden空间清除。第三次GC时,又把Survivor B空间的存活对象复制到Survivor A空间,如此反复。
所以,这里就需要两块Survivor空间来回倒腾。

QA2.为什么EDEN空间这么大而Survivor空间这么小?

新创建的对象都是放在Eden空间,这是很频繁的,尤其是大量的局部变量产生的临时对象,这些对象绝大部分都应该马上被回收,能存活下来被转移到survivor空间的往往不多。所以,设置较大的Eden空间和较小的Survivor空间是合理的,大大提高了内存的使用率,缓解了Copying算法的缺点。
我看8:1:1就挺好的,当然这个比例是可以调整的,包括上面的新生代和老年代的1:2的比例也是可以调整的。
新的问题又来了,从Eden空间往Survivor空间转移的时候Survivor空间不够了怎么办?直接放到老年代去。

Eden空间和两块Survivor空间的工作流程?
现在假定有新生代Eden,Survivor A, Survivor B三块空间和老生代Old一块空间。

// 分配了一个又一个对象
放到Eden区
// 不好,Eden区满了,只能GC(新生代GC:Minor GC)了
把Eden区的存活对象copy到Survivor A区,然后清空Eden区(本来Survivor B区也需要清空的,不过本来就是空的)
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor A区的存活对象copy到Survivor B区,然后清空Eden区和Survivor A区
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor B区的存活对象copy到Survivor A区,然后清空Eden区和Survivor B区
// ...
// 有的对象来回在Survivor A区或者B区呆了比如15次,就被分配到老年代Old区
// 有的对象太大,超过了Eden区,直接被分配在Old区
// 有的存活对象,放不下Survivor区,也被分配到Old区
// ...
// 在某次Minor GC的过程中突然发现:
// 不好,老年代Old区也满了,这是一次大GC(老年代GC:Major GC)
Old区慢慢的整理一番,空间又够了
// 继续Minor GC
// ...
// ...

五、java 的四种引用及区别

强:引用关系在就不会被回收,即使发生oom
软:内存不足会被回收,不会受到gc影响
弱:只要扫描到就会被回收 
虚:GC回收的时候会收到一个通知,形同虚设不会决定对象的生命周期,任何时候都可能被回收,回收的时候会向reference队列放一条消息,用于堆内存管理

六、通过日志分析GC类型

GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。
GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发GC操作来释放内存。
GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。
GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。