JVM之所以能够自动回收内存,是因为JVM的开发人员使用了一些垃圾回收算法,来让JVM自己判断哪些对象可以回收,哪些对象不可以回收。
实际上垃圾收集并不是Java语言或者JVM专属的,垃圾回收算法主要分为两个阶段的算法,分别是标记算法
和回收算法
。
标记算法一般用来判断对象是否为垃圾,是否可以回收。
回收算法一般用来回收被标记为垃圾的对象。
二者一般结合使用。
引用计数算法
和可达性分析算法
。
主流的Java虚拟机并没有采用这个算法
,因为需要额外考虑的情况太多,维护成本高。读者可以自行创建两个互相引用对方属性的对象,然后将两个对象置空,会发现对象依然可以被GC回收。下面虽然会介绍引用计数法,但读者需要了解,这个算法并没有被JVM采用。
引用计数算法(Java虚拟机并未采用)
:
优点
:执行效率高,程序执行受垃圾回收影响较小。缺点
:无法检测出循环引用的情况,如果像个对象指向互相引用,会造成无法回收,内存泄漏。可达性分析算法(Java虚拟机的主流标记算法)
:
分代收集理论
:在介绍垃圾收集算法之前,首先要先说明一下分代收集理论。因为目前的商业虚拟机中的垃圾收集器,大多数都遵循了“分代收集”理论来设计,有时也会被称为分代回收算法/分代收集算法。分代收集虽说是理论,其实是一种经验假说,其建立在两种假说之上:
根据这两种假说,现代的商业Java虚拟机一般至少将堆内存分成两个部分——新生代
和老年代
两个区域。顾名思义,新生代的对象一般都是刚刚创建的,根据弱分代假说,大多数对象会在短时间内被新生代的垃圾收集器回收掉,而熬过了多次回收的对象,则会进入老年代。
于是Java的垃圾收集就可以每次只针对其中某个区域进行回收,提高回收效率,因而才有了"Minor GC"
、"Major GC"
、"Full GC"
等回收类型的划分。也针对不同区域的对象特征发展除了不同的垃圾收集算法——"标记-清除算法"
、"标记-复制算法"
、"标记-整理算法"
等针对性的垃圾收集算法。下面将会具体展开介绍,但是读者需要明白,这一切都始于分代收集理论。
标记-清除算法
:
标记-复制算法
:
对象面
和空闲面
,每次只使用对象面创建对象,当对象面内存用完了,就将对象面中存活的对象复制到控线面中,然后清空对象面,此时空闲面就变成了对象面,对象面就变成了空闲面。标记-整理算法
:
"Stop The World"
。标记清除算法延迟低但是吞吐量低
,标记整理算法虽然延迟高,但是吞吐量大
。根据不同的需求,就可以选择不同的回收算法。HotSpot
这一最主流虚拟机为例来讲解其分代收集算法的实现。其堆空间划分如下图所示:
Minor GC
、Major GC
和Full GC
:前文已经说明分代收集理论是为了分区域针对不同对象特点来进行内存回收,于是就有了这三个概念,分别对应年轻代GC、老年代GC和整堆GC。这里提前明确概念以便后文阅读。
新生代/年轻代
:
HotSpot
虚拟机中,新生代又划分为一个Eden区
和两个Survivor区
。两个Survivor区分别以from
和to
命名,用来进行基于复制算法
的垃圾回收,代表了复制算法中的空闲面和对象面。Eden区和两个Survivor区的内存比例是8:1:1
,其内存结构如下图所示:
HotSpot
虚拟机中,大多数情况下,对象在新生代Eden区中分配(特别大的对象可能直接放入老年代),当Eden区没有足够空间进行分配的时候,就会触发一次Minor GC,来释放新生代的空间。垃圾回收工作时,会将Eden区和一个Survivor区中存活的对象放进另外一个Survivor区,然后清除Eden和第一个Survivor区。当一个对象经历了足够多的Minor GC依然存活的话(默认是15次),就会被放入老年代。
分配担保机制
提前转移到老年代去。
老年代
:
StopTheWorld
:JVM由于要执行GC而暂停用户进程。任何GC算法中都会发生。多数GC优化通过减少StopTheWorld发生的时间来提高程序的性能。SafePoint
:分析过程中对象引用关系不会发生变化的点,如方法调用,循环跳转,异常跳转等。GC一般就发生在安全点。JVM的运行模式
:JVM分为Server
和Client
两种模式,前者是重量级JVM,启动慢,但稳定后性能高;后者是轻量级JVM,启动速度快,但长时间运行没有Server模式性能高。 Parallel Scabenge
收集器Serial
收集器。吞吐量
:运行用户代码时间/(运行用户代码时间+垃圾收集时间)。其中,连线表示两个收集器之间可以组合使用,而一些组合(Serial+CMS和ParNew+Parallel Old)在JDK8中被声明废弃,在JDK9中被取消支持。
Serial
垃圾收集器:
单线程
的垃圾收集器,采用标记-复制
算法进行垃圾收集工作的时候必须暂停所有工作线程,直到收集结束。是Client模式下的JVM默认年轻代垃圾收集器
。其垃圾收集逻辑如下图:
ParNew
收集器:
多线程
版本,除了是多线程并发进行垃圾回收外,别的行为和Serial收集器完全一样。ParNew收集器的工作过程如下所示:
Parallel Scavenge
收集器:
Server模式下JVM默认的新生代收集器
,而且是使用了标记-复制算法的多线程的收集器。这样看来与ParNew收集器似乎没有什么不同,实际上Parallel Scavenge收集器的特点是该收集器的性能关注点与其他收集器不一样。
吞吐量
,使其尽可能达到一个可控制的值。Serial Old
收集器:
标记-整理算法
,这个收集器的主要意义是在Client模式下是使用,是Client模式默认的老年代收集器
。如果是在Server模式下,当CMS收集器失败时,Serial Old收集器将作为后备预案。
Parallel Old
收集器:
标记-整理
算法实现,也是主要关注系统的吞吐量。
CMS
收集器:
标记-清除
算法,一些官方文档中也称之为并发低停顿收集器
。目前主要应用于B/S系统的服务端上,提高用户交互体验。初始标记(需要StopTheWorld)
:单线程标记一下GC Roots能够直接关联到的对象,速度很快,但是需要暂停用户线程。并发标记
:从GC Roots的直接关联对象开始遍历整个对象图的过程,此过程耗时较长,但是无需停顿用户线程。重新标记(需要StopTheWorld)
:该阶段是为了修正在并发标记期间因用户线程运行而导致标记产生变动的那一部分对象标记记录,重新标记会导致用户线程停顿,比初始标记停顿要长,总体耗时远远小于并发标记阶段。并发清除
:清理删除标记阶段判断为已死亡的对象,由于是标记清除算法,不需要移动对象,所以该阶段也无需停顿用户线程。缺点
: G1收集器
(Garbage First):
ZGC
:
Region
堆内存模型,但是ZGC不设分代
,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法
,以低延迟为首要目标。小型Region
:容量固定为2MB,用于放置小于256KB的小对象。中型Region
:容量固定为32MB,用于放置大于等于256KB,但是小于4MB的对象。大型Region
:容量不固定,可以动态变化,但是必须是2MB的整数倍。用于放置大于等于4MB的对象。每个大型Region中只会放置一个大对象。在Java的GC机制中,判断对象是否为垃圾,主要依靠的还是对象间的引用关系,为了更好地控制对象之间的引用关系强弱程度,所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)
、软引用(Soft Reference)
、弱引用(Weak Reference)
、虚引用(Phantom Reference)
4 种,这 4 种引用的强度依次减弱。
Java中的一切都被视为对象,但是我们在编程时直接操作的却是变量
,它实际上是对象的一个引用
(reference),通过这个引用标识符来指向内存中的某块区域上存储的对象,就可以利用引用来操作对象了。
强引用(Strong Reference)
:强引用是Java中默认的声明引用强度,当我们使用new
关键字来创建对象的时候,变量和对象间就是强引用的关系,如Object o = new Object();
这段代码就会在内存中创建一组强引用关系,内存结构如下图所示:
软引用(Soft Reference):
软引用是用来描述一些非必须但仍有用的对象。
java.lang.ref.SoftReference
类来中继,表示软引用,演示语句如下:SoftReference<Object> softRf = new SoftReference<>(new Object()); 在内存中的结构如下图所示:
弱引用(Weak Reference)
:弱引用的引用强度比软引用更弱。
java.lang.ref.WeakReference
类来中继,表示弱引用,演示语句如下:WeakReference<Object> weakRfObj = new WeakReference<>(new Object()); 在内存中的结构如下图:
虚引用(Phantom Reference)
:虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有引用一样,随时可能被回收。 java.lang.ref.PhantomReference
类来中继,表示虚引用。PhantomReference类源码中只有一个构造函数和一个get()
方法,而且get()
方法仅仅返回一个空,也就是说永远无法通过虚引用直接获取对象,虚引用必须和ReferenceQueue
引用队列一起使用才可以。演示语句如下:ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<Object> phantomRfObj = new PhantomReference<>(new Object(), queue);
管理直接内存
: DirectByteBuffer
对象来对这块内存的引用进行操作,避免数据在Java堆与Native堆中数据的来回复制。也就是说通过一种特殊的指针将JVM中的对象与JVM外的内存连接了起来。DirectByteBuffer
对象关联起来:DirectByteBuffer实例化的会创建一个Cleaner实例,Cleaner是PhantomReference的子类。ReferenceQueue
中,然后用户线程就可以从队列中收到这个通知
,针对性地通过Native的操作来释放堆外内存。四大引用类型总结
: