1960 年诞生与MIT的LISP 是第一门真正使用内存动态分配和垃圾收集技术的语言;
GC需要完成的3件事情:
1. 哪些内存需要回收(what)
2. 什么时候回收(when)
3. 如何回收(how)
程序技术器,虚拟机栈,本地方法栈随线程而生,随线程而灭, 不需要回收。
Java堆和方法区需要回收, GC主要的工作范围是Java堆;
判断对象存活与死亡:
引用计数算法(ReferenceCounting) : 简单,高效, 但不能解决对象间循环依赖问题
Java,C#,Lisp 等主流语言都通过可达性分析(Reachability Analysis )来判定对象是否存活的;
可达性分析:
通过一系列的GCRoots 对象为起点,往下搜索, 搜索走过的路径称为引用链。当一个对象到GCRoots 没有任何的引用链相连时, 则证明此对象是不可用的, 可以被回收。
GC Roots 对象包括以下几种:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
2. 方法区中类静态属性引用的变量
3. 方法区中常量引用的对象
4. 本地方法栈中JNI(即一般说的Native 方法)引用的对象
判断一个对象死亡, 一般要经过两次标记过程:
1. 可达性分析中判断不可达, 为第一次标记
2. 判断对象是否要执行finalize() 方法。 如果不需要, 则对象直接标记为回收;
如果需要执行finalize() 方法,对象被移至F-Queue 中; GC 对F-Queue中对象进行第二次标记。
Finalize() 方法是对象逃脱死亡命运的最后一次机会, 因为finalize() 方法只会被调用一次。
Finalize() 能做的事情, try-finally 都能做的更好, 不要使用finalize() 方法, 毕竟它是Java 刚诞生是为了c/C++程序员接受而模仿析构函数做的妥协。
垃圾收集算法:
标记-清除(Mark-Sweep): 基础的垃圾收集算法, 标记和清除两个过程的效率不高, 会产生大量不连续的内存碎片
复制算法(Coping): 内存分为两块, 每次只使用一块。 一块用完了, 将存活的对象复制到另一块上面去, 已使用的一块内存空间一次性清理掉。
实现简单, 运行高效, 没有碎片; 缺点是有一部分内存不能使用
新生代对象98% 是“朝生夕死”,采用复制算法回收: 新生代内存分为Eden, From Survivor,ToSurvivor(HotSpot 虚拟机默认Ecen,From,To 比例为8:1:1), 每次使用Eden和一块Survivor, 回收时将Eden 和Survivor 中还存活的对象复制到另一块Survivior 中, 最后清理掉Eden 和使用过的Survivor.
标记-整理(Mark_Compact):标记过程一样, 标记过后进行整理: 让所有存活的对象向一段移动, 然后清理掉端边界外的内存。
分代收集:
Java 堆分为新生代,老年代。
新生代每次GC 都有大量对象死去, 只有少量存活, 使用复制算法回收;
老年代中对象存活率高, 使用标记-清理或者标记-整理算法来回收
STW: 进行GC时必须停顿所有Java执行线程(STW:StopThe World), 以保障一致性: 不能在可达性分析过程中对象的引用关系还在不断的变化。
垃圾收集器:
收集算法是方法论, 收集器是算法的具体实现。
Young heneration:
Serial : 单线程, 复制算法, GC线程工作是会STW 直到收集结束。
简单高效, VM 在Client模式下的默认新手代收集器
ParNew: Serial 的多线程版本, 其余行为为Serial一样。 复制算法
在多CPU 环境下应该更高效。
Parallel Scavenge: 复制算法 , 多线程
控制吞吐量, 吞吐量优先收集器(其他收集器主要关注停顿时间), 适合后台运算而不需要太多交互的任务
Old generation:
Serial Old :Serail 的老年代版本, 单线程, 标记-整理算法
Parallel Old: Parallel Scavenge 的老年代版本, 多线程, 标记-整理算法
CMS(ConcurrentMark Sweep): 获取最短停顿时间为目的的收集器
标记-清除算法, 多线程
优点: 并发收集, 低停顿
缺点: 浮动垃圾, 内存碎片
其他的收集器在标记和清除/整理过程中都会STW, CMS 的清理线程和用户线程是并发的, 所以会产生浮动垃圾(Floating Garbage), 即并发的清理过程中新产生的垃圾。
CMS之所以使用标记-清除算法的原因也在这里, GC线程和用户线程都在工作, 整理对象是不合适的。
G1: 太复杂, 作者也不怎么了解
对象分配策略:
1. 大多数情况下, 对象在新生代Eden 区中分配, 当Eden 区中没有足够空间时, 虚拟机将发起一次MinocGC
2. 大对象直接进入老年代
大小由-XX:PretenureSizeThreshold 指定, 大于该值得对象直接在老年代中分配
3. 长期存活的对象进入老年代
对象年龄(Age): 在Eden 出生进过第一次Minoc GC, 进入Survivor, 年龄为1; 在Survivor 中每经历一次Minor GC , 年龄增加1;
对象年龄达到一定程度(默认15岁, 可以由参数 –XX:MaxTenuringThreshold 设置)将会被晋升到老年代。
重新总结下Java 内存布局:
1. 程序计数器
线程私有
2. 虚拟机栈
线程私有
3. 本地方法栈
线程私有
4. 方法区
线程共享,jdk 8 的hotSpot虚拟机由metaSpace实现方法区, 而不再是以前的“永久代”
5. Java 堆
1. 新生代
Eden:From Survivor : To Survivor 默认为8:1:1
2. 老年代
JDK8 下的一次GC日志可以明确证明这些内存划分:
[GC (System.gc())[PSYoungGen: 8033K->744K(57344K)] 8033K->752K(188416K), 0.0012865 secs][Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 744K->0K(57344K)][ParOldGen: 8K->617K(131072K)] 752K->617K(188416K), [Metaspace:3209K->3209K(1056768K)], 0.0064976 secs] [Times: user=0.00 sys=0.00,real=0.00 secs]
Heap
PSYoungGen total 57344K, used 1474K [0x0000000780380000,0x0000000784380000, 0x00000007c0000000)
eden space 49152K, 3% used[0x0000000780380000,0x00000007804f0bc8,0x0000000783380000)
from space 8192K, 0% used[0x0000000783380000,0x0000000783380000,0x0000000783b80000)
to space 8192K, 0% used[0x0000000783b80000,0x0000000783b80000,0x0000000784380000)
ParOldGen total 131072K, used 617K[0x0000000700a00000, 0x0000000708a00000, 0x0000000780380000)
object space131072K, 0% used [0x0000000700a00000,0x0000000700a9a450,0x0000000708a00000)
Metaspace used 3216K, capacity 4494K, committed4864K, reserved 1056768K
class space used 355K, capacity 386K, committed 512K,reserved 1048576K