java基础知识,垃圾回收
知识要点:
垃圾回收要点知识
垃圾回收算法
垃圾回收器
垃圾回收机制
GC所关心的东西:“这块数据是不是一个指针”
GC所关心最重要的几件事情:
哪些内存要回收?
什么时候回收?
如何回收?
垃圾回收要点知识
引用计数
给对象添加一个引用计数器。每当有一个地方引用这个对象,这个计数器就加1;每当引用失效,这个计数器就减1;当计数器为0的时候就代表该对象不能再被使用。
public class ReferenceCountingGC{
public Object instance = null;
private static final int_1MB = 1024*1024;
/**
* 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过
*/
private byte[] bigSize = new byte[2*_1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假设在这行发生GC,objA和objB是否能被回收?
System.gc();
}
}
上面的例子,两个对象虽然再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。
可达性分析
JVM中对内存进行回收时,需要判断对象是否仍在使用中,可以通过GC Roots Tracing辨别。
通过一系列名为”GCRoots”的对象作为起始点,从这个节点向下搜索,搜索走过的路径称为ReferenceChain,当一个对象到GCRoots没有任何ReferenceChain相连时,(图论:这个对象不可到达),则证明这个对象不可用。
在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
四种引用
在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、 软引用(Soft Reference)、 弱引用(Weak Reference)、 虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
强引用
在程序代码之中普遍存在类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null。
软引用
用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收没有足够的内存,才会抛出内存溢出异常。
public class SoftRef {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
SoftReference aSoftRef = new SoftReference(object);
//当结束object对这个MyObject实例的强引用,MyObject对象成为了软引用对象
object = null;
//重新获得对该实例的强引用。而回收之后,调用get()方法就只能得到null了
Object anotherRef = (Object) aSoftRef.get();
System.out.println(anotherRef == null);
}
}
弱引用
也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "[name:" + name + ",age:" + age + "]";
}
}
public class WeakRef {
public static void main(String[] args) {
WeakReference<Person> reference = new WeakReference<Person>(new Person("zhouqian", 20));
System.out.println(reference.get());
//通知JVM回收资源
System.gc();
System.out.println(reference.get());
}
}
虚引用
也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
// 幽灵引用
public class PhanReference {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> pr = new PhantomReference<>(new String("hello"), queue);
System.out.println(pr.get());
}
}
垃圾回收算法
标记清除
最基础的收集算法是“标记-清除”(Mark-Sweep)算法。该算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
该算法有两个主要不足:
- 一个是效率问题,标记和清除的效率都不高;
- 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片。
标记整理
标记-整理(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
复制
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:内存缩小为了原来的一半,内存利用率太低
HotSpot算法
枚举根节点
从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。
另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。这点是导致GC进行时必须停顿所有Java执行线程(Sun将这个称为“Stop The World”)的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
GC分类
保守式GC
JVM选择不记录任何类型的数据,那么它就无法区分内存里某个位置上的数据到底应该解读为引用类型还是整型还是别的什么。这种条件下,实现出来的GC就会是“保守式GC(conservative GC)”。在进行GC的时候,JVM开始从一些已知位置开始扫描内存,扫描的时候每看到一个数字就判断它“像不像是一个指向GC堆中的指针”。
半保守式GC
JVM可以选择在对象上记录类型信息。这样的话,扫描到GC堆内的对象时因为对象带有足够类型信息了,JVM就能够判断出在该对象内什么位置的数据是引用类型了。这种是“半保守式GC”,也称为“根上保守(conservative with respect to the roots)”。
为了支持半保守式GC,运行时需要在对象上带有足够的元数据。如果是JVM的话,这些数据可能在类加载器或者对象模型的模块里计算得到,但不需要JIT编译器的特别支持。
准确式GC
从外部记录下类型信息,存成映射表。现在三种主流的高性能JVM实现,HotSpot、JRockit和J9都是这样做的。其中,HotSpot把这样的数据结构叫做OopMap。
OopMap
在HotSpot中,对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以从对象开始向外的扫描可以是准确的;这些数据是在类加载过程中计算得到的。
安全点
在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。
安全点太多,GC 过于频繁,增大运行时负荷;安全点太少,GC 等待时间太长。一般会在如下几个位置选择安全点:
①循环的末尾
②方法临返回前
③调用方法之后
④抛异常的位置
安全区域
假如线程处于Sleep或者Blocked状态,这时候线程无法响应JVM的中断请求,也就无法到达Safepoint的地方中断挂起。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。 也可以把Safe Region看做是Safepoint的扩展。
JDK垃圾回收器
Serial
单线程的收集器,它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,最重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束
- 开启参数:-XX:+UseSerialGC
- 适用场景:用户的桌面应用场景
ParNew
Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、 收集算法、 Stop The World、 对象分配规则、 回收策略等都与Serial收集器完全一样
- 开启参数:-XX:+UseParNewGC
- 适用场景:Server首选的新生代收集器
Parallel Scavenge
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,它的目标是达到一个可控制的吞吐量(Throughput)
吞吐量计算公式:运行用户代码时间/(运行用户代码时间+垃圾收集时间)
垃圾收集停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge提供了两个参数用于精确控制吞吐量
- -XX:MaxGCPauseMillis // 最大垃圾收集停顿时间(大于0毫秒数)
- -XX:GCTimeRatio // 吞吐量大小(大于0且小于100的整数,吞吐量百分比)
- -XX:+UseAdaptiveSizePolicy // 内存调优委托给虚拟机管理。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、 晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量
- 开启参数:-XX:+UseParallelGC
- 适用场景:后台计算不需要太多交互
Serial Old
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。 这个收集器的主要意义也是在于给Client模式下的虚拟机使用
- 适用场景:用户的桌面应用场景
Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器从JDK 1.6中才开始提供的。
- 开启参数:-XX:+UseParallelOldGC
- 适用场景:用户的桌面应用场景
Concurrent Mark Sweep
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记—清除”算法实现的。整个过程分为4个步骤
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
缺点
- CMS收集器对CPU资源非常敏感。
- CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
- CMS是一款基于“标记—清除”算法实现的收集器**,这意味着收集结束时会有大量空间碎片产生。
- 开启参数:-XX:+UseConcMarkSweepGC
- 适用场景:互联网站或者WEB服务端
G1
G1算法将堆划分为若干个区域(Region),但它仍然属于分代收集器。
不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。
老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。
G1提供了两种GC模式(两种都是Stop The World的)
- Young GC
-
Mixed GC
YoungGC
阶段1:根扫描。静态和本地对象被扫描
阶段2:更新RS(Remembered Set)。找出老年代到年轻代的引用并更新RS
阶段3:处理RS。检测从年轻代指向年老代的对象
阶段4:对象拷贝。拷贝存活的对象到survivor/old区域
阶段5:处理引用队列。软引用,弱引用,虚引用处理
Mix GC
①初始标记 该阶段仅仅只是标记一下GC Roots能直接关联到的对象。
②并发标记 该阶段是从GCRoot开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
③最终标记 该阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。
④筛选回收 该阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。因为只回收一部分Region,所以时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
- 开启参数:-XX:+UseG1GC
- 适用场景:服务端应用
垃圾回收机制
垃圾回收机制后续再补齐!