判断对象是否存活
垃圾回收需要判断对象是否存活,再进行回收
引用计数算法
(Reference Counting)基本思路:
- 在对象中添加一个引用计数器
- 每当有一个引用的时候,计数器就+1
- 每当有一个引用失效的时候,计数器就-1
- 当计数器的值为0的时候,该对象就是可以被GC回收的垃圾对象
引用计数算法存在的问题:对象循环引用
若a对象引用了b对象,b对象也引用了a对象,a、b对象却没有再被其他对象所引用了,正常来说a、b已经是垃圾了,因为没有其他对象引用了,但是计数器的数值不是0,所以引用计数算法就无法回收
注意:
- jvm不是这种算法
- python在使用
可达性分析算法
(Reachability Analysis) 基本思路:
通过定义一系列称为"GC Roots"
的根对象作为起始节点集,从"GC Roots"
开始,根据引用关系往下进行搜索,查找的路径称为引用链。当一个对象到"GC Roots"
之间没有任何引用链连接时(对象与"GC Roots"
之间不可达),那么该对象就是被GC回收的垃圾对象
可达性分析算法也是JVM默认使用的寻找垃圾算法
例如:下图中Object6、7、8彼此之间有引用关系,但是没有与“GC Roots”相连,所以就会被当作垃圾回收
Java中的四种引用类型
强引用
(Strong Reference)
强引用是最普遍的引用。如果一个对象是强引用,垃圾回收器绝对不会回收,内存空间不足时,JVM宁愿抛出OOM错误,使程序异常终止,也不愿靠回收具有强引用的对象来解决内存不足的问题
// 强引用
String strongReference = new String("abc");
如果强引用对象不使用时,需要弱化从而使GC能够回收;弱化方式:
-
显示地设置对象为null,则GC认为该对象不存在引用,这时就可以回收这个对象。但是,具体什么时候收集要取决于GC算法。例如,
strongReference
时全局变量时,就需要在不用这个对象时赋值为null
,因为强引用不会被垃圾回收strongReference = null;
应用场景:在ArrayList集合类中定义elementData数组,在调用clear()方法清空集合元素时,将每个数组元素被赋值为
null
。目的是为了将内存数组中存放的引用类型进行内存释放,可以及时释放内存。不选择将elementData=null,是为了避免在后续调用add()等方法添加新元素是,需要进行内存的重新分配public void clear() { modCount++; // clear to let GC do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }
-
让对象超出作用范围
应用场景:在一个方法的内部有一个强引用,这个引用保存在VM Stack栈中(
GC Root
),而真正的引用对象(Object)保存在堆中。当这个方法运行完成后,就会退出方法栈,则这个对象会被回收public void test(){ Object strongReference = new Object(); }
软引用
(Soft Reference)
如果一个对象具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存,只要垃圾回收器没有回收它,该对象就可以被程序使用。所以,软引用可以用来实现内存敏感的高速缓存
创建软引用,可以使用SoftReference
:
// 强引用
String strongReference = new String("abc");
// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(srt);
// 访问软引用
softReference.get();
软引用对象在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,最终何时回收,由jvm决定。
所以,当内存不足时,jvm首先将软引用的对象设置为null,然后通知垃圾回收器进行回收
// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(srt);
str = null;
// Notify GC
System.gc();
try{
byte[] buff1 = new byte[900000000]; // 内存充足
// byte[] buff2 = new byte[900000000]; // 一直创建到内存不足报错为止
}catch(Error e){
e.printStackTrace();
}
System.out.println(softReference.get());
// 内存充足时:abc 内存充足,jvm不会回收软引用对象,所以可以访问到
// 内存不足时输出:null 内存不充足,jvm回收软引用对象,所以访问不到
应用场景:
短视频APP中的视频缓存,后退时,显示的短视频内容是重新进行请求还是从缓存中去除呢?
- 如果一个短视频在播放结束时,就进行内容的回收,则后退查看前面播放的短视频时,需要重新请求
- 如果将播放过的短视频存储到内存中,会造成内存的开销,则会造成内存溢出
此时,可以使软引用来解决这个问题:
// 获取视频播放器对象
Player videoAlayer = new Player();
// 加载短视频
Video video = audioAlayer.getVideo();
// 将播放完毕的短视频设置为软引用
SoftReference softReference = new SoftReference(video);
// 回退或者再次播放时
if(softReference.get() !=null){
// 内存充足,还没有被回收器回收,直接获取缓存
video = SoftReference.get();
}else{
// 内存不足,软引用对象已经被回收
video = audioAlayer.getVideo();
// 重新构建软引用
softReference = new new SoftReference(video);
}
弱引用
(Weak Reference)
只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<String>(str);
str = null;
System.gc();
// 一旦发生GC,弱引用对象一定被回收
System.out.println(weakReference.get()); // null
虚引用
(Phantom Reference)
虚引用时最弱的一种引用关系,如果一个对象仅持有虚引用,完全不会对其生存时间构成影响,它就和没有任何引用一样,随时可能会被回收
虚引用,主要用来追踪对象被垃圾回收的活动,可以在垃圾回收时收到一个系统通知
在JDK1.2
之后,用PhantomReference
类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个get()
方法,而且它的get()
方法仅仅返回一个null,也就是说将永远无法通过虚引用来获取对象
public class PhantomReference<T> extends Reference<T>{
public T get(){
return null;
}
}
小结
引用 | 特点 |
---|---|
强引用 | GC时,永远不会被回收 |
软引用 | 内存不足时自动触发GC,才会被回收 |
弱引用 | 无论内存是否充足,只要进行GC就会被回收 |
虚引用 | 如同虚设,和没有引用没什么区别 |
垃圾收集算法
分代收集理论
目前主流JVM的虚拟机中的垃圾收集器,都遵循分代收集理论:
- 弱分代:绝大多数对象都是朝生夕灭
- 强分代:经历越多次垃圾收集过程的对象,越难以回收,难以消亡
按照分代收集理论设计的分代垃圾收集器,则采用的设计原则:收集器应该将java
堆划分成不同的区域,然后将回收对象依据其年龄(年龄即对象经历垃圾收集过程的次数)分配到不同区域的存储
分代存储
如果一个区域中大多数对象都是朝生夕灭**(新生代)**,难以熬过垃圾收集过程的话,把它们集中存储在一起,每次回收时,只关注如何保留少量存活对象,而不是去标记大量将要回收的对象,就能以较低代价回收到大量的空间。
如果一个区域中大多数对象都是难以回收 (老年代),那么把它们集中放在一起, JVM 虚拟机就可以使用较低的频率,来对这个区域进行回收。
这样设计的好处是,兼顾垃圾收集的时间开销和内存空间的有效利用。
分代收集
在Java
堆区划分成不同区域后,垃圾收集器才可以每次只回收其中一个或某些区域,所以才有MinorGC
、MajorGC
、FullGC
等垃圾收集类型划分
在Java
堆区划分成不同区域后,垃圾收集器才可以针对不同的区域,安排与该区域存储对象存亡特征相匹配的垃圾算法:标记-复制算法
、标记-清除算法
、标记-整理算法
等
垃圾收集类型划分:
- 部分回收(Partial GC):没有完整回收整个Java堆的垃圾收集,其中又分为:
- 新生代收集(Minor GC / Young GC)
- 老年代收集(Major GC / Old GC)
- 混合收集(Mixed GC):收集整个新生代和部分老年代的垃圾收集
- 整堆回收(Full GC):收集整个
Java
堆的垃圾收集
标记-清除算法
标记清除算法(Mark-Sweep)实现思路:
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。
标记清除算法会带来两个明显的问题:
- ==执行效率不稳定的问题:==如果执行垃圾收集的区域大部分对象是需要被回收的,则需要大量的标记和清除动作,导致效率变低。
- ==内存空间碎片化问题:==标记清除后产生大连不连续的碎片化,空间碎片太多,会导致分配较大对象,无法找到足够的连续空间,从而会触发新的垃圾收集动作。
标记-复制算法
标记复制算法(Mark-Copying)实现思路:
为了解决“标记-清除”面向大量可回收对象时执行效率地下的问题
该算法将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,九江还存活的对象复制到另一边去,然后将已使用的空间一次清理掉。
标记复制算法特点:
如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法仅需要复制少数存活对象而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效。
标记复制算法的问题:
- 对象存活率较高时,需要进行较多的内存间复制,效率降低
- 浪费过多的内存,使现有的可用空间变为原先的一半
标记-整理算法
标记整理算法(Mark-Compact)实现思路:
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向内存空间一端移动,然后直接清理边界以外的内存,这样清理的机制,不会像标记-整理那样留下大量的内存碎片。
小结
当前虚拟机的垃圾收集器都基于分代收集思想,根据对象存活周期的不同,将内存分为不同的区域,在不用的区域使用不同的垃圾收集算法
例如:Heap堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择和使用合适的垃圾收集算法。
在新生代中,每次收集都会有大量的垃圾对象被回收,所以可以选择**“标记-复制”算法**,只需要付出少量对象的复制成本就可以完成每次垃圾回收
在老年代中,对象存活几率较高,而且没有额外的空间对它进行分配担保,所以选择**“标记-清除”或“标记-整理”算法**进行垃圾收集
垃圾收集器
Serial收集器(新生代)
Serial
(串行)收集器是最基本、历史最悠久的垃圾收集器,采用标记复制算法负责新生代的垃圾收集。他是HotSpot
虚拟机运行在客户端模式下的默认新生代收集器
他是一个单线程收集器,使用一条垃圾收集线程完成垃圾收集工作,并且它在完成垃圾收集工作的时候,必须暂停其他所有的工作线程(“Stop The World”),直到收集结束
这样的实际,带来的好处就是:简单高效。对于内存资源受限制的环境,它是所有收集器中额外内存消耗最少的收集器。适合单核处理器或处理器核心数较少的环境,每次收集几十MB
甚至一两百MB
的新生代内存,垃圾收集的停顿时间完全可以控制在十几毫秒或几十毫秒,最多一百多毫秒。
Serial Old收集器(老年代)
Serial Old
收集器同样是一个单线程收集器,采用标记整理算法负责老年代的垃圾收集,主要用于客户端模式下的HotSopt
使用
如果在服务器端使用,它主要有两种用途:
- 在
JDK5
及其以前版本,与Parallel Scavenge
收集器搭配使用; - 作为
CMS
收集器发生失败时的后备预案;
注:当Serial/Serial Old 收集器工作时,程序的线程停在安全点
ParNew收集器(新生代)
ParNew
收集器是一个多线程的垃圾收集器。他运行在Server
模式下的虚拟机的首要选择,可以与Serial Old
,GMS
垃圾收集器一起搭配工作,采用标记复制算法
Parallel Scavenge收集器(新生代)
Parallel Scavenge
收集器是一款新生代收集器,多线程,标记复制算法
Parallel Scavenge
收集器与其他收集器的目标不同,GMS
等其他收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间。但是Parallel Scavenge
收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗的比值
吞吐量
=
运行代码时间
/
用户代码时间
+
运行垃圾收集时间
吞吐量=运行代码时间/用户代码时间+运行垃圾收集时间
吞吐量=运行代码时间/用户代码时间+运行垃圾收集时间
如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花费掉1分钟,那吞吐量就是99%。停顿时间越短,就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能听声用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务
Parallel Old收集器(老年代)
Parallel Old
收集器是一个多线程的垃圾收集器,使用整理清理算法,是Parallel Scavenge
收集器的老年代版本
Parallel Old
这个收集器直到JDK1.6
才开始提供,在此之前,新生代的Parallel Scavenge
收集器一直处于尴尬状态,因为新生代选择了Parallel Scavenge
收集器,老年代除了Serial Old
收集器以外别无选择,其他表现良好的老年代收集器,如GMS
收集器无法与他配合工作。由于老年代Serial Old
收集器在服务端应用性能上的拖累,使用Parallel Scavenge
收集器也未必能在整体上获得吞吐量最大化的效果。同样,由于单线程的老年代收集器无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一定比ParNew加CMS的组合
优秀
CMS收集器(老年代)
CMS(Concurrent Mark Sweep)
收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记清除算法实现,是HotSpot
虚拟机第一款真正意义上的并发收集器。它第一次实现了垃圾收集线程与用户线程(基本上)同时工作。
目前很大一部分的Java
应用集中在互联网站点或者基于浏览器B/S
架构的服务器端上,这类应用通常会较为关注服务的响应速度,希望系统停顿的时间尽可能短,以给用户带来较好的交互体验。所以,GMS
收集器非常适合这类应用的收集场景
GMS
工作流程,整个过程包括四个步骤:
- 初始标记(
GMS initial mark
):标记一下GC Roots
能直接关联到的对象,速度很快 - 并发标记(
GMS concurrent mark
):从GC Roots
的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户吸纳成,可以与垃圾收集线程一起并发运行 - 重新标记(
GMS remark
):重新标记阶段,是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一搬会比初始标记阶段的时间长,远远比并发标记的阶段时间短 - 并发清除(
GMS concurrent sweep
):清除删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段是可以与用户线程同时并发的
CMS的优点和缺点:
主要优点:并发收集、低停顿
主要缺点:
- **影响用户线程的执行效率:**并发标记和并发清除时,是和用户线程一起运行的,收集过程中肯定占用了用户程序的
CPU
资源,CMS默认启动的回收线程数是(CPU数量+3)/4
,当CPU数量在4以上时,垃圾回收线程占用不少于25%
的CPU资源,势必影响用户线程的执行效率 - 无法处理浮动垃圾:在并发清除阶段,用户线程并没有停止,所以还会继续产生新的垃圾,只能等待下一次收集时才能进行回收,这部分垃圾被称为浮动垃圾
- **产生大连的空间碎片:**因为CMS收集器是基于标记清除算法实现的,所以在大量的垃圾回收时,会产生很多不连续的内存空间,这是使用标记清除算法都会有的缺点
由于垃圾收集阶段用户线程还需要持续进行,所以需要预留足够的内存空间提供给用户线程使用,因此CMS
收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集
- 在
JDK1.6
的默认设置中,CMS收集器的启动阈值为92%,代表老年代使用了92%的空间之后,就会启动CMS收集器 - 如果CMS运行期间,无法满足程序分配新对象的需要,就会出现一次“并发失败”,这是虚拟机将临时启动
Serial Old
收集器进行老年代的垃圾收集
G1收集器
G1垃圾收集器:
G1
(Garbage-Frist
)是一款面向服务器的垃圾收集器,主要针对装配多颗处理器,大容量内寸的机器,他不在严格按照分代的思想进行垃圾回收。G1
采用局部性收集的设计思路基于Region
的内存布局形式。
G1垃圾收集器的结构:
G1
采用局部性收集的思想,对于堆空间的划分,采用Region为单位的内存划分方式:
G1
垃圾回收器把堆划分成为2048
个大小相同的独立区域(Region),每个Region的大小取值范围是1MB-32MB
,且应为:1MB、2MB、4MB、8MB、16MB、32MB
每个Region都会代表某一角色,H、S、E、O。E代表Eden
区,S代表Survivor
区,H代表是Humongous
(G1用来分配大对象的区域),对于Humongous
也分配不下的超大对象,会分配在连续的N
个Humongous
中,O代表Old
区,其余灰色代表空闲的Region
G1垃圾收集器工作流程:
-
**初始标记(Initial Marking):**这个阶段仅仅只是标记GC Roots能直接关联到的对象,这个阶段需要停顿线程,但是耗时很短
-
**并发标记(Concurrent Marking):**从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这个阶段耗时较长,但是可以与用户程序并发执行。
-
**最终标记(Final Marking):**对用户线程做一个短暂的暂停,用于处理并发阶段结束后遗留记录
-
**筛选回收(Live Data Counting and Evacuation):**负责更新Region的同级数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。可以自由选择多个Region来构成会收集,然后把回收的那一部分Region中的存活对象
==>
复制==>
到空的Region中,最后对那些Region进行清空
G1垃圾收集器的特点:
- 并行与并发:
G1
能充分利用CPU
,多核环境下的硬件优势,使用多个CPU
(CPU
或者CPU
核心)来缩短Stop-The-World
停顿时间。部分其他收集器原本需要停顿Java
线程执行的GC
动作,G1
收集器仍然可以通过并发的方式让java程序继续运行 - 分代收集:虽然
G1
可以不需要其他收集器配合就能独立管理整个GC
堆,但是还是保留了分代的概念。但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC
的旧对象来获取更好的收集效果 - 空间整合:
G1
从整体来看是基于==“标记-整理”算法实现的收集器,从局部 (两个Region
之间)上来看是基于“标记-复制”==算法实现的。这意味着 运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC
- 用户指定期望停顿:允许用户指定期望的停顿时间是
G1
收集器很强大的一个功能,设置不同的期望停顿时间,可以让G1
在不同的场景下取得吞吐量和延迟之间的最佳平衡。G1
的默认停顿目标为200
毫秒,一般来说,设置为一百毫秒至两百毫秒这个区间都很正常。如果期望停顿时间设置过短,会导致由于停顿目标时间太短,导致每次筛选出来的回收集只占堆内存很小的一部分,收集器的收集速度会跟不上分配速度,导致垃圾慢慢堆积。
G1垃圾收集器与CMS垃圾收集器的区别:
-
算法不同:
CMS
采用标记清除算法容易产生内存碎片,执行若干次GC之后进行1次碎片整理G1
从整体来看是基于标记整理算法实现的收集器,从内部(两个Region之间)上来看是基于标记复制算法实现的,意味着G1
垃圾收集器不会产生内存空间碎片,垃圾收集完成后,能提供规整的可用内存,不会导致因为大对象分配内存时无法找到连续内存空间而提前触发垃圾收集 -
场景不同:
小内存应用
GMS
的表现表现大概率由于G1
,而在大内存应用中,G1
则能发挥优势。大小内存的参看值分水岭大概在6GB-8GB
总结
垃圾收集器 | 区域 | 采用算法 | 线程 |
---|---|---|---|
Serial收集器 | 新生代 | 标记复制算法 | 单线程 |
Serial Old收集器 | 老年代 | 标记整理算法 | 单线程 |
ParNew收集器 | 新生代 | 标记复制算法 | 多线程 |
Parallel Scavenge收集器 | 新生代 | 标记复制算法 | 多线程 |
Parallel Old收集器 | 老年代 | 标记整理算法 | 多线程 |
CMS收集器 | 老年代 | 标记清除算法 | |
G1收集器 | 混合型 | 整体:标记整理算法 局部:标记复制算法 |