垃圾回收
如何判断对象为垃圾对象
引用计数法
在对象中添加一个引用计数 器,当有地方引用这个对象的时候, 计数器 1,当失效的时候,计数器-1
引用计数法无法解决循环引用问题
证明没有用引用计数法:
代码语言:javascript复制/**
* 循环引用
*/
public class RefCountGC {
public Object instance = null;
private byte[] bigSize = new byte[2*1024*1024];
public static void main(String[] args) {
RefCountGC obj1 = new RefCountGC();
RefCountGC obj2 = new RefCountGC();
obj1.instance = obj2;
obj2.instance = obj1;
//设置为空
obj1 = null;
obj2 = null;
//垃圾回收
System.gc();
}
}
运行程序前设置参数:-verbose:gc -XX: PrintGCDetails
表示开启gc 打印gc信息
输出结果:
从输出日志空可以看出程序进行了垃圾回收 而引用计数法堆循环引用的对象是不能回收的 所以虚拟机并不是用引用计数法来判断对象是否存活的
可达性分析
作为GCRoot的对象 • 虚拟机栈(局部变量表中的) • 方法区的类属性所引用的对象 • 方法区的常量所引用的对象 • 本地方法栈所引用的对象
如何回收
回收策略(方法论)
标记清除
分成标记和清除两个阶段
缺点 • 效率问题 • 内存碎片
复制算法
Minor GC会把Eden中的所有活的对象移动到Survivor区,如果Survivor区中放不下,剩下的活的对象被移动到Old区,收集后Eden为空 当对象在Eden(包括一个Survivor区域)出生后,再经过一次Minor GC后,如果对象还存活,并且能够被另外一块Survivor区域所容纳,则使用复制算法将这些存活的对象复制到另外一块Survivor区中,然后清理所使用的Eden以及Survivor区域,并且将这些对象的年龄设置为1,以后对象在Survivor区,每熬过一次Minor GC,对象年龄 1,当对象年龄到15岁,这些对象就成为老年代
复制算法原理:
从根集合(GC Root)开始 通过Tracing 从From中找到存活对象 拷贝到to区 from和to交换身份 下次内存分配从to开始
eden区 from区 to区 默认8:1:1
优点 效率高 缺点浪费空间
标记整理
标记清除压缩
分代算法
设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域 。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放
垃圾回收器(实践)
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代) Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代) Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
serial
单线程 stop the world。。 它进行垃圾回收的时候 世界都停了 地球都不转了。。
parNew
ParNew收集器实质上是Serial收集器的多线程并行版本
Parallel Scavenge 收集器
- 采用复制算法 1.7默认新生代收集器
- 多线程收集器
- 达到可控制的吞吐量
- 吞吐量:CPU用于运行用户代码时间与CPU消耗总时间的比值 • 吞吐量=执行用户代码时间/(执行用户代码时间 垃圾回收使用的时间) • -XX:MaxGCPauseMillis 垃圾收集停顿时间 并不是越短越好 越短 系统新生代会变小 频繁垃圾回收 吞吐量下降 • -XX:GCTimeRatio 吞吐量大小 范围(0,100) 垃圾收集时间占总时间比
并行的垃圾收集器
减少停顿时间
CMS 收集器 (Concurrent Mark Sweep)
1.8 老年代默认垃圾收集器 CMS是一款基于**“标记-清除”算法**实现的收集器
工作过程四个步骤: • 初始标记 • 并发标记 • 重新标记 • 并发清理
CMS收集过程 初始标记:这一步的作用是标记存活的对象,有两部分:
- 标记老年代中所有的GC Roots对象,如下图节点1;
- 标记年轻代中活着的对象引用到的老年代的对象,如下图节点2、3;
并发标记:从“初始标记”阶段标记的对象开始找出所有存活的对象;
预清理阶段 :这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Direty的Card 如下图所示,在并发标记阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty;
预清理,也是用于标记老年代存活的对象,目的是为了让重新标记阶段的StopTheWorld尽可能短
重新标记 :该阶段的任务是完成标记整个年老代的所有的存活对象。 并发清理 : 这个阶段主要是清除那些没有标记的对象并且回收空间;
优点:并发收集 低停顿 缺点:
- 占用CPU资源
- 无法处理浮动垃圾 在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分 垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
- 出现Concurrent Mode Failure 要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了
- 空间碎片 CMS是一款基于“标记-清除”算法实现的收集器 因此有空间碎片产生
G1收集器
• 历史 • 2004年 Sun公司实验室发表了论文 • JDk7 才使用了G1
jdk9中默认使用 采用标记整理算法
运行示意图:
优势:
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
步骤:
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要 停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。
- 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。
- 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。
G1内存模型:
G1分代模型:
G1 分区模型:
收集集合(CSet)代表每次GC暂停时回收的一系列目标分区
ZGC
ZGC原理
• ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。 • Colored Pointer(着色指针) 和 Load Barrier(并发执行的保证机制)
ZGC 内存结构
ZGC将堆划分为Region作为清理,移动,以及并行GC线程工作分配的单位。分为有2MB,32MB,N× 2MB 三种Size Groups,动态 地创建和销毁Region,动态地决定Region的大小。
ZGC 回收过程
- Pause Mark Start -初始停顿标记 停顿JVM,标记Root对象,1,2,4三个被标为live
- Concurrent Mark -并发标记 并发地递归标记其他对象,5和8也被标记为live
- Relocate - 移动对象 对比发现3、6、7是过期对象,也就是中间的两个灰色region需要被压缩清理,所以陆续将4、5、8 对象移动到最右边的新Region。移动过程中,有个forward table记录这种转向
- Remap - 修正指针 最后将指针更新指向新地址。
何时回收
到达安全点时。。
内存分配策略
优先分配Eden区
代码语言:javascript复制 /**
* 内存分配策略之Eden区优先被分配
*/
public class EdenAllocator {
public static void main(String[] args) {
byte[] data = new byte[20*1024*1024];
}
}
运行程序前设置参数:-verbose:gc -XX: PrintGCDetails
日志信息:
大对象直接分配到老年代
-XX:PretenureSizeThreshold
代码语言:javascript复制/**
* 内存分配策略之大对象直接分配到老年代
* -verbose:gc -XX: PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:PretenureSizeThreshold=6M
*/
public class BigObjectIntoOldGen {
public static void main(String[] args) {
byte[] d1 = new byte[6*1024*1024];
}
}
日志信息:
参数-Xms20M -Xmx20M -Xmn10M 表示java堆大小20M 不可扩展 其中10M分配给新生代 剩下10M为老年代
而代码中的对象6M 正好占老年代60%的空间 正如日志中打印的那样。。
长期存活的对象分配老年代
-XX:MaxTenuringThreshold=15
经过15次gc仍然存活
空间分配担保
-XX: HandlePromotionFailure
检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。
代码语言:javascript复制/**
* 内存分配策略之空间担保
* -verbose:gc -XX: PrintGCDetails -Xms20M -Xmx20M -Xmn10M
*/
public class SpaceGuarantee {
public static void main(String[] args) {
byte[] d1 = new byte[2*1024*1024];
byte[] d2 = new byte[2*1024*1024];
byte[] d3 = new byte[2*1024*1024];
byte[] d4 = new byte[4*1024*1024];
System.gc();
}
}
日志信息:
总共20M 新生代10M eden区8M from区1M to区1M 老年代10M
当创建完d3后 eden区还剩2M 而接下来创建4M对象 不够用 进行空间担保 把新生代的对象移动到老年代 占6M 也就是60% 创建4M对象
分配到 8M的eden区 占50% 还有其他信息占10% 因此日志显示60%
动态对象年龄对象
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代 -XX:TargetSurvivorRatio
逃逸分析与栈上分配
逃逸分析:对象或变量有没有超出方法的范围
栈上分配:对没有逃逸分析的对象进行在栈空间进行分配
代码语言:javascript复制/**
* 逃逸分析和栈上分配
*/
public class StackAllocation {
public StackAllocation obj;
/**
* 逃逸 return了
* @return
*/
public StackAllocation getInstance() {
return obj ==null?new StackAllocation():obj;
}
/**
* 逃逸 给到了成员变量
* @return
*/
public void setObj() {
this.obj = new StackAllocation();
}
/**
* 没有逃逸 进行栈上分配
*/
public void useStackAllocation() {
StackAllocation stackAllocation = new StackAllocation();
}
}