16.1 System.gc()的理解
- 默认情况下,通过System.gc()或Runtime.getRuntime().gc()的调用,会显示触发Full GC,同时堆老年代和新生代进行回收,尝试释放被丢弃对象占用的内存
- 然而System.gc()调用附带一个免责声明,无法保证堆垃圾收集器的调用
- JVM实现者可以通过System.gc()调用来决定JVM的GC的行为。一般情况下,垃圾回收应该使自动进行的,无须手动触发,否则过于麻烦。
public class SystemGCTest {
public static void main(String[] args) {
new SystemGCTest();
// 提醒JVM进行垃圾回收,但不确定是否马上执行GC
System.gc();
// 强制调用使用引用的对象的finalize()方法
//System.runFinalization();
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("SystemGCTest 执行了 finalize方法");
}
}
手动GC来理解不可达对象的回收
public class LocalVarGC {
/**
* 触发Minor GC没有回收对象,然后在触发Full GC将该对象存入old区
*/
public void localvarGC1() {
byte[] buffer = new byte[10*1024*1024];
System.gc();
}
/**
* 触发YoungGC的时候,已经被回收了
*/
public void localvarGC2() {
byte[] buffer = new byte[10*1024*1024];
buffer = null;
System.gc();
}
/**
* 不会被回收,因为它还存放在局部变量表索引为1的槽中
*/
public void localvarGC3() {
{
byte[] buffer = new byte[10*1024*1024];
}
System.gc();
}
/**
* 会被回收,因为它还存放在局部变量表索引为1的槽中,但是后面定义的value把这个槽给替换了
*/
public void localvarGC4() {
{
byte[] buffer = new byte[10*1024*1024];
}
int value = 10;
System.gc();
}
/**
* localvarGC5中的数组已经被回收
*/
public void localvarGC5() {
localvarGC1();
System.gc();
}
public static void main(String[] args) {
LocalVarGC localVarGC = new LocalVarGC();
localVarGC.localvarGC3();
}
}
16.2 内存溢出与内存泄漏
内存溢出(OOM)
- javadoc中堆OutOfMemoryError的解释是,没有空间内存,并且垃圾收集器也无法提供更多内存
- 没有空闲内存的情况,说明Java虚拟机的堆内存不够,原因有二:
1.Java虚拟机的堆内存设置不够,可能存在内存泄漏问题,也可能是堆的大小不合理,可以通过参数-Xms、-Xmx来调整
2.代码中创建了大量的大对象,且长时间不能给垃圾收集器收集(存在被引用)。老版本JDK,永久代大小是有限的,对永久代垃圾回收不积极,出现OOM非常多见,尤其运行时存在大量动态类型生成的场合,类似intern字符串缓存占用太多空间,“OOM:PermGen Space”。JDK8后元数据区的加入,出现“OOM:Metaspace” - 抛出OOM之前,垃圾收集器会被触发,尽其所能去清理空间。分配一个超大对象,超过堆的最大值,JVM可以判断垃圾收集不能解决这个问题,不会触发垃圾收集器,直接OOM
内存泄漏(Memory Leak)
- 严格上,只有对象不会被程序用到,但GC又回收不了,才叫内存泄漏
- 宽泛上,导致对象生命周期很长导致OOM
- 尽管内存泄漏不会立即导致程序崩溃,但一旦发生内存泄漏,程序中的可用内存被逐步蚕食,直至耗尽所有内存
举例
- 单例模式
单例的生命周期和应用程序时一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个对象时不能被回收的,则会导致内存泄漏的产生
- 一些提供close的资源未关闭导致内存泄漏
数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接必须手动close,否则是不能被回收的
16.3 Stop The World
- GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程会被暂停,没有任何响应,称为STW
- 可达性分析算法中枚举根节点(GC Roots)会导致Java执行线程停顿
- 被STW中断的应用程序线程会在GC后恢复
- 所有垃圾回收器都会发生STW,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能缩短暂停时间
- 开发中不要用System.gc(),会导致STW的发生
16.4 垃圾回收的并行与并发
程序中的并发(Concurrent)‘
- 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行
- 并发并不是同时进行,只是CPU把一个时间段划分成几个时间区间,然后再这几个时间区间之间切换,由于CPU处理速度快,时间处理得当,让用户感觉是多个程序同时进行
程序的并行(Parallel)
- 当系统有一个以上CPU,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占资源,可以同时进行
- 决定并行的因素不是CPU的数量,而是CPU的核心数
- 适合科学计算,后台处理等弱交互场景
并发VS并行
并发,指的是多个事情,在同一时间段内同时发生了
并行,指的是多个事情,在同一时间点上同时发生了
并发的多个任务之间是互相抢占资源的
并行的多个任务之间是不互相抢占资源的
只有在多CPU或一CPU多核的情况中,才会有并行
否则,看似同时发生,其实都是并发
垃圾回收的并行和并发
并行(Parallel):多条垃圾收集线程并行工作,此时用户线程仍处于等待状态,如ParNew、Parallel Scavenge、Parallel Old
串行(Serial):相较于并行的概念,单线程执行。如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾收集。收集完,再启动程序的线程
并发(Concurrent):用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程再执行时不会停顿用户程序的运行,用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上,如CMS、G1
16.5 安全点与安全区域
安全点(SafePoint)
- 程序执行时并非在所有地方多能停顿下来开始GC,只有在特定位置才能停顿下来开始GC,这些位置称为安全点
- SafePoint选择很重要,如果太少可能导致GC等待时间太长,如果太频繁可能导致运行时的性能问题。通常会根据“是否具有让程序长时间执行的特征”选择SafePoint,如方法调用、循环跳转、异常跳转
如果发生GC时,如何检查所有线程都跑到最近的安全点停顿下来呢?
1.抢先式中断(目前没有虚拟机采用)
首先终端所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点
2.主动式中断
设置一个中断标志,各个线程运行到SafePoint的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起
安全区域(SafeRegion)
- 安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置GC都是安全的,也可以把SafeRegion看做是被扩展了的SafePoint
- 实际执行时:
- 1.当线程运行到SafeRegion的代码时,首先标识已经进入了SafeRegion,如果这段时间内发生GC,JVM会忽略标识为SafeRegion状态的线程
- 2.当线程即将离开SafeRegion时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开SafeRegion的信号为止
16.6 再谈引用
- 我们希望描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是很紧张,则可以回收这些对象。
- 【既偏门又高频的面试题】四种引用有什么区别,具体使用场景?
- 在JDK1.2后,Java对引用的概念进行了扩充,将引用分为
- 强引用(Strong Reference)、
- 软引用(Soft Reference)、
- 弱引用(Weak Reference)、
- 虚引用(Phantom Reference),4种引用程度依次减弱
Reference子类中只有终结器引用是包内可见的,其他3种引用类型均为public,可以在应用程序中直接使用
- 强引用:最传统的引用的定义,指在代码中普遍存在的引用赋值,Object obj = new Object();。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
- 软引用:在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常
- 弱引用:被弱引用关联的对象只能生存到下一次垃圾收集前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象
- 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过弱引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
强引用:死也不回收
- 在Java中,最常见的引用类型就是强引用,99%以上,也是默认的引用类型
- 在Java中,使用new创建一个新对象,并赋值给一个变量时,这个变量就成为指向该对象的强引用
- 强引用的对象是可触及的,垃圾收集器永远不会回收被引用的对象
- 强引用是造成Java内存泄漏的主要原因之一
软引用:内存不足即回收
- 通常用来实现内存敏感的缓存。高速缓存就用软引用,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,就能保证了使用缓存的同时,不会耗尽内存
- 垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放在一个引用队列(Reference Queue)
- 在JDK 1.2后,提供了java.lang.ref.SoftReference类来实现软应用
// 声明强引用
Object obj = new Object();
// 创建一个软引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //销毁强引用,这是必须的,不然会存在强引用和软引用
弱引用:发现即回收
- 只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象
- 但由于垃圾回收器的线程通常优先级很低,并不一定很快发现持有弱引用的对象。也可能存在较长的时间
- 软引用、弱引用都非常适合来保存那些可有可无的缓存数据
// 声明强引用
Object obj = new Object();
// 创建一个弱引用
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; //销毁强引用,这是必须的,不然会存在强引用和弱引用
弱引用对象与软引用对象的最大不同在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是回收。弱引用对象更容易、更快被GC回收
面试题:了解WeakHashMap吗?
WeakHashMap用来存储图片信息,可以在内存不足的时候,及时回收,避免了OOM
虚引用:对象回收跟踪
- 也称为“幽灵引用”、“幻影引用”,所有引用最弱的一个
- 如果一个对象仅持有虚引用,那么他和没有引用几乎一样,随时都可能被回收
- 不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法获取对象时,总是null
- 虚引用必须和引用队列一起使用
- 由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录
// 声明强引用
Object obj = new Object();
// 声明引用队列
ReferenceQueue phantomQueue = new ReferenceQueue();
// 声明虚引用(还需要传入引用队列)
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;
一个案例,来结合虚引用,引用队列,finalize进行理解
public class PhantomReferenceTest {
// 当前类对象的声明
public static PhantomReferenceTest obj;
// 引用队列
static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类的finalize方法");
obj = this;
}
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while(true) {
if (phantomQueue != null) {
PhantomReference<PhantomReferenceTest> objt = null;
try {
objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
} catch (Exception e) {
e.getStackTrace();
}
if (objt != null) {
System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了");
}
}
}
}, "t1");
thread.setDaemon(true);
thread.start();
phantomQueue = new ReferenceQueue<>();
obj = new PhantomReferenceTest();
// 构造了PhantomReferenceTest对象的虚引用,并指定了引用队列
PhantomReference<PhantomReferenceTest> phantomReference = new PhantomReference<>(obj, phantomQueue);
try {
System.out.println(phantomReference.get());
// 去除强引用
obj = null;
// 第一次进行GC,由于对象可复活,GC无法回收该对象
System.out.println("第一次GC操作");
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 不是 null");
}
System.out.println("第二次GC操作");
obj = null;
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 不是 null");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
}
终结器引用
- 用以实现对象的finalize()方法,
- 无需手动编码,其内部配合引用队列使用
- 在GC时,终结器引用入队。有Finalizer线程通过终结器引用找到被引用对象并调用他的finalize()方法,第二次GC时才能回收被引用对象