JVM04——垃圾回收器和回收算法
1.如何判断是否是垃圾?
引用计数器:Java中通过引用和对象进行关联。所以可以通过引用计数判断对象是否可以回收。如果一个对象没有任何地方与之关联,则说明该对象基本不太可能在其他地方被使用到,这个对象就成为可被回收的对象。(简单高效,但不能解决循环引用问题,Python在使用)
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
// 相互引用,计数永远不为0
object1.object = object2;
object2.object = object1;
// 对象为空,不能被引用
object1 = null;
object2 = null;
}
}
class MyObject{
public Object object = null;
}
可达性分析:以“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达),证明此对象是垃圾,被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了(finalize()方法最终对象是否存活)。
在Java语言中,可作为GC Roots的对象包含以下几种:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象
2)方法区中静态属性(static修饰)引用的对象
3)方法区中常量(static final修饰)引用的对象
4)本地方法栈中引用的对象,既引用Native方法的对象
**在JDK1.2之后,Java对引用的概念做了扩充,将引用分为四种,这四种引用的强度依次递减。
⑴强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
Object a = new Object();
⑵软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
String string= "";
SoftReference<String> softReference = new SoftReference<>(string);
string = softRefence.get();
⑶弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
WeakReference<Man> weak=new WeakReference<Man>(new Man());
man = weak.get();
⑷虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
PhantomReference<String> pa =
new PhantomReference<String>("S",referenceQueueStr);
(5)引用队列(ReferenceQueue)
在软引用,弱引用,虚引用的构造函数都有一个引用队列的参数,只不过虚引用是必须的参数,其他的两个引用里面是可选的。
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Man> referenceQueue= new ReferenceQueue<Man>();
WeakReference<Man> weak=new WeakReference<Man>(new Man(),referenceQueue);
System.out.println("man对应的虚引用对象:"+weak.hashCode());
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println("垃圾回收后 man为"+weak.get());
System.out.println("引用队列里面的对象:"+referenceQueue.poll().hashCode());
}
// 运行结果
man对应的虚引用对象:1450495309
垃圾回收后 man为null
引用队列里面的对象:1450495309
程序中最快的缓存就是直接在内存构建缓存,但使用强引用,一旦缓存过大,无法回收就会OOM。这时使用弱引用进行高速缓存是一个好方法,思路:把需要储存的对象用软引用引用,放到Map中,key值唯一。一旦程序内存不够用的时候,就会清除一部分软引用。这个时候我们就没有办法通过map里面的软引用获取到实际对象了。我们需要手动去清除map里面已经被回收的对象的软引用对象,判断的方式就是引用队列。
@Data
public class Man {
private String name;
private Integer age;
private Integer id;
}
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.concurrent.ConcurrentHashMap;
public class ManCache {
// 软引用对象
private static ManReference manReference;
// 引用队列
private static ReferenceQueue<Man> referenceReferenceQueue;
// 储存软引用对象的map
private static ConcurrentHashMap<Integer, ManReference> cacheMap;
private ManCache() {
referenceReferenceQueue = new ReferenceQueue<Man>();
}
private static class ManCacheSingle {
static ManCache manCache = new ManCache();
}
public static ManCache getManCahce() {
return ManCacheSingle.manCache;
}
public static void cacheInto(Man man) {
cacheClean();
int id = man.getId();
ManReference manReference = new ManReference(man);
cacheMap.put(id, manReference);
}
public static void cacheClean() {
ManReference manReference;
//如果对象被回收,那么就把这个对象在map里面对应的值删除
while ((manReference = (ManReference) referenceReferenceQueue.poll()) == null) {
int id = manReference.id;
cacheMap.remove(id);
}
}
public static Man getMan(Integer id) throws RuntimeException {
check(id);
Man man = cacheMap.get(id).get();
if (man == null) {
man = getFromDatabase(id);
cacheInto(man);
}
return man;
}
private static class ManReference extends SoftReference<Man> {
int id;
public ManReference(Man referent) {
super(referent, referenceReferenceQueue);
id = referent.getId();
}
}
private static void check(Integer id) {
if (id == null) {
throw new RuntimeException("id不可以为null");
}
}
/**
* 数据库取值
*
* @param id
* @return
*/
private static Man getFromDatabase(Integer id) {
return new Man();
}
}
2.垃圾回收算法
1)标记-清除(Mark-and-Sweap)
原理:可达性分析,标记存活对象;对堆进行遍历,清除未标记对象,解决了循环引用问题。
缺点:暂停整个应用(STW);内存碎片化
2)复制(Copying)
原理:内存分为两块,每次只使用一块。垃圾回收时,遍历使用区域,把存活对象复制到另一块区域。不会产生碎片化
缺点:缺点:暂停整个应用(STW);浪费内存
3)标记-整理(Mark-Compact)
原理:标记存活对象,将其压缩到内存一端,清除边界以外的垃圾
4)分代收集
其他算法基本上都是上述三种算法的相互结合使用
3.垃圾回收器
// 查看垃圾回收期
java -XX:+PrintCommandLineFlags -version
1)Serial
单线程,只会有一个CPU或GC线程进行回收,回收时暂停其他所有工作线程(STW-stop the world),适用与Client模式、单核服务器,简单高效,不用来回切换线程。采用Copying算法。
参数:-XX:+UserSerialGC #选择Serial作为新生代垃圾收集器
2)Serial Old
和Serial配合使用,清除老年代,Mark-Compact算法
适合Clinet模式、单核服务器,作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用
3)Parallel Scavenge
新生代的多线程收集器,采用Copying算法。其追求CPU高吞吐量(=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),能够在较短时间完成任务,适合不需要太多后台交互的后台运算。
4)Parallel Old
和新生代Parallel Scavenge搭配使用,采用Mark-Compact算法,追求高吞吐量。Jdk8默认使用PS/PO垃圾回收器
5)ParNew
和Parallel Scavenge工作机制一样,但是其追求降低GC时用户线程停顿时间,适合交互式应用,反应速度良好。
6)CMS(Concurrent Mark Sweep)
以获取最短回收停顿时间为目标,采用Mark-and-Sweap算法。
初始标记(inital Mark):通过GCRoot找到根对象(此过程STW,但是时间很短);
并发标记(Concurrent Mark):并发标记会发生很多次,而且在并发的同时,其他工作线程也在不断改变这些引用的指向,这个阶段最耗时间,所以选择并发执行(即不产生STW,响应比较及时),如果是在并发标记过程中变成非垃圾(有引用指向)漏标,或有非垃圾变成垃圾错标(浮动垃圾,下次清理),这时候就会进入remark阶段;
预清理(Concurrent Preclean):与用户线程同时运行;
重新标记(remark):有些垃圾有了引用指向,需要重新标记,从垃圾变成不是垃圾,把漏标的重新标记(此过程STW,但是时间很短);
并发清理(Concurrent Sweap):把不用的垃圾回收,回收的过程中产生新垃圾,也就是浮动垃圾,会在下一轮进行回收;
调整堆大小,设置CMS在清理之后进行内存压缩,清理内存中的碎片;
并发重置状态等待下次CMS的触发,与用户线程同时运行;
缺点:吞吐量低;无法清除浮动垃圾,并发模式失败(concurrent mode failure),导致频繁Full GC(会调用CMS备用方案,Serial Old);内存碎片化(CMS提供两个参数对内存进行压缩整理,
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=*);
三色标记算法——漏标(垃圾转为非垃圾)问题的引入
*针对漏标问题,CMS采用Incremental Update+写屏障(增量更新):对黑色对象增加对白色对象的引用进行记录,在最后标记时重新扫描,缺点是浪费时间,但增量更新任然会发生漏标
m1(垃圾回收线程):正在标记A,属性1已标完,属性2正在标记;
m2(业务逻辑线程):把属性1指向了白色对象;
m3(垃圾回收线程):把A标为了灰色;
m1(垃圾回收线程):属性2标记完,认为标完了所有属性,把A设为黑色,D漏标;
7)G1(Carbage First)—简化了JVM调优(开启G1;设置堆的大内存;设置大的停顿时间;eg:-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200)
面向服务端应用的垃圾回收器,Jdk9默认的回收器,目标是用在多核、大内存的机器上,他在大多数情况下可以实现执行的GC暂停时间短,同时还能保持较高的吞吐量,他是在逻辑上分代物理上不分代的,将堆划分为若干个Region。
每一份region在逻辑上依然属于某一个分代,这个分代分为4种,第一种old区都是放老对象的、survivor放存活对象、Eden放新生对象、Humongous大对象区域,对象特别大可能会跨两个region;
G1采用三色标记算法的原始快照TLAB(snapshot-at-the-begining)+写屏障:当一个灰色对象取消了对白色对象的引用,把白色对象标为灰色
*三色标记算法原理:
漏标的两个冲要条件(1)至少一个黑色对象在自己被标记之后指向了白色对象;(2)灰色对象在标记完成之前删除了对白色对象的引用
(1)情况一:已经标好了 ab,还没 d,如下,此时B=>D 消失,并且突然A=D了,因为 A已黑了,不会再 看他的孩子,于是 D 被漏标了!
8)ZGC:JDK11,颜色指针+读屏障
9)Shenandoah:JDK12,颜色指针+读屏障