在提到什么是垃圾之前,我们先看下面一张图
从上图我们可以很明确的知道,Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C语言没有垃圾收集技术,需要我们手动的收集。
垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。
关于垃圾收集有三个经典问题:
**垃圾收集机制是Java的招牌能力,极大地提高了开发效率。**如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。
- 什么是垃圾?
垃圾
是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。- 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出。
在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放。比如以下代码:
MibBridge *pBridge= new cmBaseGroupBridge();
//如果注册失败,使用Delete释放该对象所占内存区域
if(pBridge->Register(kDestroy)!=NO ERROR)
delete pBridge;
这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。
有了垃圾回收机制后,上述代码极有可能变成这样
MibBridge *pBridge=new cmBaseGroupBridge(); pBridge->Register(kDestroy);
现在,除了Java以外,C#、Python、Ruby等语言都使用了自动垃圾回收的思想,也是未来发展趋势,可以说这种自动化的内存分配和来及回收方式已经成为了线代开发语言必备的标准。
自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发
oracle官网关于垃圾回收的介绍
GC主要关注于 方法区 和堆中的垃圾收集
垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收
从次数上讲:
在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先**需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。**只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段
。
那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
当p的指针断开的时候,内部的引用形成一个循环,这就是循环引用,从而造成内存泄漏
举例:我们使用一个案例来测试Java中是否采用的是引用计数算法
package com.atguigu.java;
/**
* -XX:+PrintGCDetails
* 证明:java使用的不是引用计数算法
* @author shkstart
* @create 2020 下午 2:38
*/
public class RefCountGC {
//这个成员属性唯一的作用就是占用一点内存
private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB
Object reference = null;
public static void main(String[] args) {
RefCountGC obj1 = new RefCountGC();
RefCountGC obj2 = new RefCountGC();
obj1.reference = obj2;
obj2.reference = obj1;
obj1 = null;
obj2 = null;
//显式的执行垃圾回收行为
//这里发生GC,obj1和obj2能否被回收?
System.gc();
// try {
// Thread.sleep(1000000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
执行结果:
我们能够看到,上述进行了GC收集的行为,将上述的新生代中的两个对象都进行回收了
如果使用引用计数算法,那么这两个对象将会无法回收。而现在两个对象被回收了,说明Java使用的不是引用计数算法来进行标记的。
追踪性垃圾收集(Tracing Garbage Collection)
所谓"GCRoots”根集合就是一组必须活跃的引用。
可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
总结:
总结一句话就是,除了堆空间外的一些结构,比如 虚拟机栈、本地方法栈、方法区、字符串常量池 等地方对堆空间进行引用的,都可以作为GC Roots进行可达性分析
小技巧:
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
可触及的
:从根节点开始,可以到达这个对象。可复活的
:对象的所有引用都被释放,但是对象有可能在finalize()中复活。不可触及的
:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。判定一个对象objA是否可回收,至少要经历两次标记过程:
第一次标记
。第二次标记
。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。package com.atguigu.java;
/**
* 测试Object类中finalize()方法,即对象的finalization机制。
*
*/
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于 GC Root
//此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类重写的finalize()方法");
obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
// 对象第一次成功拯救自己
obj = null;
System.gc();//调用垃圾回收器
System.out.println("第1次 gc");
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面这段代码与上面的完全相同,但是这次自救却失败了
obj = null;
System.gc();
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
在进行第一次清除的时候,我们会执行finalize方法,然后 对象 进行了一次自救操作,但是因为finalize()方法只会被调用一次,因此第二次该对象将会被垃圾清除。
GC Roots溯源
MAT是Memory Analyzer的简称
,它是一款功能强大的Java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况。JVisualVM导出Dump文件
使用MAT打开Dump文件,查看GC ROOT
我们在实际的开发中,一般不会查找全部的GC Roots,可能只是查找某个对象的整个链路,或者称为GC Roots溯源,这个时候,我们就可以使用JProfiler
使用idea插件打开程序/打开dunp文件
当我们程序出现OOM的时候,我们就需要进行排查,我们首先使用下面的例子进行说明
package com.atguigu.java;
import java.util.ArrayList;
/**
* -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
*
*/
public class HeapOOM {
byte[] buffer = new byte[1 * 1024 * 1024];//1MB
public static void main(String[] args) {
ArrayList<HeapOOM> list = new ArrayList<>();
int count = 0;
try{
while(true){
list.add(new HeapOOM());
count++;
}
}catch (Throwable e){
System.out.println("count = " + count);
e.printStackTrace();
}
}
}
运行结果:
生成OOM dump文件
查看分析
我们将生成的dump文件打开,然后点击Biggest Objects就能够看到超大对象
然后我们通过线程,还能够定位到哪里出现OOM
标记-清除算法(Mark-Sweep)
复制算法(copying)
标记-压缩算法(Mark-Compact)
标记-清除算法(Mark-Sweep)是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp语言。
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除
标记
:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
可达性分析是标记不可达对象
,此处标记的是可达对象
清除
:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。
关于空闲列表是在为对象分配内存的时候 提过
- 如果内存规整
- 采用指针碰撞的方式进行内存分配
- 如果内存不规整
- 虚拟机需要维护一个列表
- 空闲列表分配
为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文,“使用双存储区的Lisp语言垃圾收集器CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。M.L.Minsky在该论文中描述的算法被人们称为复制(Copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
把可达的对象,直接复制到另外一个区域中复制完成后,A区就没有用了,里面的对象可以直接清除掉,其实里面的新生代里面就用到了复制算法
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记一清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JvM的设计者需要在此基础之上进行改进。标记-压缩(Mark-Compact)算法由此诞生。
1970年前后,G.L.Steele、C.J.Chene和D.s.Wise等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。
非移动式的回收算法
,标记-压缩是移动式
的。是否移动回收后的存活对象是一项优缺点并存的风险决策。如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞(Bump the Pointer)
。
标记清除(Mark-Sweep) | 标记整理(Mark-Compack) | 复制(Coping) | |
---|---|---|---|
速率 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍空间(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
综合我们可以找到,没有最好的算法,只有最合适的算法
目前几乎所有的GC都采用分代收集(Generation Collection)算法执行垃圾回收的
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代(Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenured Gen)
标记-清除
或者是标记-清除与标记-整理的混合实现
。
以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用serial old执行FullGC以达到对老年代内存的整理。
分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代
上述现有的算法,在垃圾回收过程中,应用软件将处于一种Stop the World的状态。在Stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法
的诞生。
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
注意,这些只是基本的算法思路,实际GC实现过程要复杂的多,目前还在发展中的前沿GC都是复合算法,并且并行和并发兼备。
老师心心念念的ZGC
public class SystemGCTest {
public static void main(String[] args) {
new SystemGCTest();
System.gc();//提醒jvm的垃圾回收器执行gc,但是不确定是否马上执行gc
//与Runtime.getRuntime().gc();的作用一样。
// System.runFinalization();//强制调用使用引用的对象的finalize()方法
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("SystemGCTest 重写了finalize()");
}
}
运行结果,但是不一定会触发销毁的方法,调用System.runFinalization()会强制调用 失去引用对象的finalize()
package com.atguigu.java;
/**
* 手动GC来理解不可达对象的回收
*/
public class LocalVarGC {
public void localvarGC1() {
byte[] buffer = new byte[10 * 1024 * 1024];//10MB
System.gc();
}
public void localvarGC2() {
byte[] buffer = new byte[10 * 1024 * 1024];
buffer = null;
System.gc();
}
public void localvarGC3() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
System.gc();
}
public void localvarGC4() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
int value = 10;
System.gc();
}
public void localvarGC5() {
localvarGC1();
System.gc();
}
public static void main(String[] args) {
LocalVarGC local = new LocalVarGC();
local.localvarGC1();
}
}
- 执行localvarGC1时,很明显buffer还在被引用,为可达对象,不会被回收
- 执行localvarGC2时,在localvarGC1的基础上置空了buffer,为不可达对象,会被回收
- 执行localvarGC3时,虽然作用域已经结束了,但是局部变量表中仍然存在,为可达对象,不会被回收
- 执行localvarGC4时,在localvarGC3的基础上在作用域外重新声明了一个变量,这个变量就会占用buffer的slot,从而使buffer为不可达对象,会被回收
- 执行localvarGC5时,调用localvarGC1方法后再GC,localvarGC1栈帧被弹出,所以为不可达对象,会被回收
首先说没有空闲内存的情况:说明Java虚拟机的堆内存不够。原因有二:
Java虚拟机的堆内存设置不够
。
代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
也称作“存储渗漏”。严格来说
,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏
。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”
。
尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutofMemory异常,导致程序崩溃。
注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
Java使用可达性分析算法,最上面的数据不可达,就是需要被回收的。后期有一些对象不用了,按道理应该断开引用,但是存在一些链没有断开,从而导致没有办法被回收。
单例模式
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
一些提供close的资源未关闭导致内存泄漏
数据库连接(dataSourse.getConnection() ),网络连接(socket)和io连接必须手动close,否则是不能被回收的。
Stop-the-world,简称STW
,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
后台自动发起
和自动完成
的。在用户不可见的情况下,把用户正常的工作线程全部停掉。package com.atguigu.java;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class StopTheWorldDemo {
public static class WorkThread extends Thread {
List<byte[]> list = new ArrayList<byte[]>();
public void run() {
try {
while (true) {
for(int i = 0;i < 1000;i++){
byte[] buffer = new byte[1024];
list.add(buffer);
}
if(list.size() > 10000){
list.clear();
System.gc();//会触发full gc,进而会出现STW事件
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static class PrintThread extends Thread {
public final long startTime = System.currentTimeMillis();
public void run() {
try {
while (true) {
// 每秒打印时间信息
long t = System.currentTimeMillis() - startTime;
System.out.println(t / 1000 + "." + t % 1000);
Thread.sleep(1000);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void main(String[] args) {
WorkThread w = new WorkThread();
PrintThread p = new PrintThread();
w.start();
p.start();
}
}
运行结果:
结论:
我们程序写的是每秒休眠一次,但我们每次打印出的时间间隔都不是一秒,从而验证了STW的存在
从一段时间来看,有多个任务在执行从单一时间片上看,只有一个任务在执行实际上就是CPU在快速切换任务交替执行
在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行。
并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。
并行(Parallel)
。并发,指的是多个事情,在同一时间段
内同时发生了。
并行,指的是多个事情,在同一时间点
上同时发生了。
并发的多个任务之间是互相抢占资源
的。
并行的多个任务之间是不互相抢占资源
的。
只有在多CPU或者一个CPU多核的情况中,才会发生并行。
否则,看似同时发生的事情,其实都是并发执行的。
并发和并行,在谈论垃圾收集器的上下文语境中,它们可以解释如下:
ParNew
、Parallel Scavenge
、Parallel Old
;CMS
、G1
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint)
”。
Safe Point的选择很重要,**如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。**大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征
”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。
安全区域
是指在**一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。**我们也可以把Safe Region看做是被扩展了的Safepoint。
Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,**程序“不执行”的时候呢?例如线程处于sleep-状态或Blocked 状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。**对于这种情况,就需要安全区域(Safe Region)
来解决。
我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是很紧张,则可以抛弃这些对象。
在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为:
这4种引用强度依次逐渐减弱。
除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。如下图,显示了这3种引用类型对应的类,开发人员可以在应用程序中直接使用它们
强引用(StrongReference)
:最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。StringBuffer str = new StringBuffer("hello");
StringBuffer str1 = str;
注意,这里的第一次回收是不可达的对象
软引用(SoftReference)
:在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存流出异常。内存敏感的缓存
。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。引用队列(Reference Queue)
。在JDK1.2版之后提供了SoftReference类来实现软引用
// 声明强引用 Object obj = new Object(); // 创建一个软引用 SoftReference<Object> sf = new SoftReference<>(obj); obj = null; //销毁强引用,这是必须的,不然会存在强引用和软引用
package com.atguigu.java1;
import java.lang.ref.SoftReference;
/**
* 软引用的测试:内存不足即回收
* -Xms8m -Xmx8m
*/
public class SoftReferenceTest {
public static class User {
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int id;
public String name;
@Override
public String toString() {
return "[id=" + id + ", name=" + name + "] ";
}
}
public static void main(String[] args) {
//创建对象,建立软引用
// SoftReference userSoftRef = new SoftReference(new User(1, "songhk"));
//上面的一行代码,等价于如下的三行代码
User u1 = new User(1,"songhk");
SoftReference<User> userSoftRef = new SoftReference<User>(u1);
u1 = null;//取消强引用
//从软引用中重新获得强引用对象
System.out.println(userSoftRef.get());
System.gc();
System.out.println("After GC:");
// //垃圾回收之后获得软引用中的对象
System.out.println(userSoftRef.get());//由于堆空间内存足够,所有不会回收软引用的可达对象。
//
try {
//让系统认为内存资源紧张、不够
byte[] b = new byte[1024 * 1024 * 7];
// byte[] b = new byte[1024 * 7168 - 635 * 1024];
} catch (Throwable e) {
e.printStackTrace();
} finally {
//再次从软引用中获取数据
System.out.println(userSoftRef.get());//在报OOM之前,垃圾回收器会回收软引用的可达对象。
}
}
}
运行结果:
设置堆内存大小让系统认为内存资源紧张、不够:
-Xms8m -Xmx8m
不设置:
结论:
内存够用不回收,内存不够用才回收
弱引用(WeakReference)
:被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。在JDK1.2版之后提供了WeakReference类来实现弱引用
// 声明强引用 Object obj = new Object(); // 创建一个弱引用 WeakReference<Object> sf = new WeakReference<>(obj); obj = null; //销毁强引用,这是必须的,不然会存在强引用和弱引用
弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。
WeakHashMap用来存储图片信息,可以在内存不足的时候,及时回收,避免了OOM
package com.atguigu.java1;
import java.lang.ref.WeakReference;
/**
* 弱引用的测试
*
*/
public class WeakReferenceTest {
public static class User {
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int id;
public String name;
@Override
public String toString() {
return "[id=" + id + ", name=" + name + "] ";
}
}
public static void main(String[] args) {
//构造了弱引用
WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "songhk"));
//从弱引用中重新获取对象
System.out.println(userWeakRef.get());
System.gc();
// 不管当前内存空间足够与否,都会回收它的内存
System.out.println("After GC:");
//重新尝试从弱引用中获取对象
System.out.println(userWeakRef.get());
}
}
运行结果:
结论:
一旦发生GC就会被回收
虚引用(PhantomReference)
:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2版之后提供了PhantomReference类来实现虚引用。
// 声明强引用 Object obj = new Object(); // 声明引用队列 ReferenceQueue phantomQueue = new ReferenceQueue(); // 声明虚引用(还需要传入引用队列) PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue); obj = null;
我们使用一个案例,来结合虚引用,引用队列,finalize进行讲解
package com.atguigu.java1;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
/**
* 虚引用的测试
*
*/
public class PhantomReferenceTest {
public static PhantomReferenceTest obj;//当前类对象的声明
static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//引用队列
public static class CheckRefQueue extends Thread {
@Override
public void run() {
while (true) {
if (phantomQueue != null) {
PhantomReference<PhantomReferenceTest> objt = null;
try {
objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (objt != null) {
System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了");
}
}
}
}
}
@Override
protected void finalize() throws Throwable { //finalize()方法只能被调用一次!
super.finalize();
System.out.println("调用当前类的finalize()方法");
obj = this;
}
public static void main(String[] args) {
Thread t = new CheckRefQueue();
t.setDaemon(true);//设置为守护线程:当程序中没有非守护线程时,守护线程也就执行结束。
t.start();
phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
obj = new PhantomReferenceTest();
//构造了 PhantomReferenceTest 对象的虚引用,并指定了引用队列
PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);
try {
//不可获取虚引用中的对象
System.out.println(phantomRef.get());
//将强引用去除
obj = null;
//第一次进行GC,由于对象可复活,GC无法回收该对象
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
System.out.println("第 2 次 gc");
obj = null;
System.gc(); //一旦将obj对象回收,就会将此虚引用存放到引用队列中。
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
结论:
从上述运行结果我们知道,第一次尝试获取虚引用的值,发现无法获取的,这是因为虚引用是无法直接获取对象的值,然后进行第一次gc,因为会调用finalize方法,将对象复活了,所以对象没有被回收,但是调用第二次gc操作的时候,因为finalize方法只能执行一次,所以就触发了GC操作,将对象回收了,同时将会触发第二个操作就是 将回收的值存入到引用队列中。