在对象中添加一个引用计数 器,当有地方引用这个对象的时候, 计数器+1,当失效的时候,计数器-1
引用计数法无法解决循环引用问题
证明没有用引用计数法:
/**
* 循环引用
*/
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
单线程 stop the world。。 它进行垃圾回收的时候 世界都停了 地球都不转了。。
ParNew收集器实质上是Serial收集器的多线程并行版本
并行的垃圾收集器
减少停顿时间
1.8 老年代默认垃圾收集器 CMS是一款基于**“标记-清除”算法**实现的收集器
工作过程四个步骤: • 初始标记 • 并发标记 • 重新标记 • 并发清理
CMS收集过程 初始标记:这一步的作用是标记存活的对象,有两部分:
并发标记:从“初始标记”阶段标记的对象开始找出所有存活的对象;
预清理阶段 :这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Direty的Card 如下图所示,在并发标记阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty;
预清理,也是用于标记老年代存活的对象,目的是为了让重新标记阶段的StopTheWorld尽可能短
重新标记 :该阶段的任务是完成标记整个年老代的所有的存活对象。 并发清理 : 这个阶段主要是清除那些没有标记的对象并且回收空间;
优点:并发收集 低停顿 缺点:
• 历史 • 2004年 Sun公司实验室发表了论文 • JDk7 才使用了G1
jdk9中默认使用 采用标记整理算法
运行示意图:
优势:
步骤:
G1内存模型:
G1分代模型:
G1 分区模型:
收集集合(CSet)代表每次GC暂停时回收的一系列目标分区
• ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。 • Colored Pointer(着色指针) 和 Load Barrier(并发执行的保证机制)
ZGC将堆划分为Region作为清理,移动,以及并行GC线程工作分配的单位。分为有2MB,32MB,N× 2MB 三种Size Groups,动态 地创建和销毁Region,动态地决定Region的大小。
到达安全点时。。
/**
* 内存分配策略之Eden区优先被分配
*/
public class EdenAllocator {
public static void main(String[] args) {
byte[] data = new byte[20*1024*1024];
}
}
运行程序前设置参数:-verbose:gc -XX:+PrintGCDetails
日志信息:
-XX:PretenureSizeThreshold
/**
* 内存分配策略之大对象直接分配到老年代
* -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
检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。
/**
* 内存分配策略之空间担保
* -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
逃逸分析:对象或变量有没有超出方法的范围
栈上分配:对没有逃逸分析的对象进行在栈空间进行分配
/**
* 逃逸分析和栈上分配
*/
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();
}
}