JVM垃圾回收篇(重要概念解析)
Posted ProChick
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM垃圾回收篇(重要概念解析)相关的知识,希望对你有一定的参考价值。
1.System.gc方法
-
基本概述
- 默认情况下,通过调用
System.gc()
或者Runtime.getRuntime().gc()
方法会显式触发 Full GC的回收,然后对堆空间、方法区进行回收,尝试释放被丢弃对象所占用的内存 - 然而
System.gc()
方法无法保证对垃圾收集器的调用时间,也就是说方法执行后不能确保垃圾回收过程及时执行 - 开发者可以在某些场景下通过
System.gc()
方法手动调用JVM的GC行为,但是一般情况下,垃圾回收应该是由JVM自动进行的
- 默认情况下,通过调用
-
代码测试
测试调用 System.gc() 垃圾回收器不会立即执行
public class SystemGCTest { public static void main(String[] args) { new SystemGCTest(); // 尝试多次运行,观察是否出现finalize里面的输出语句 System.gc(); // 如果调用此方法,则意味着强制执行finalize()方法 // System.runFinalization(); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("SystemGCTest重写了finalize()"); } }
测试不同对象的回收行为
// 不会回收,因为对象引用存在 public void localvarGC1() { byte[] buffer = new byte[10 * 1024 * 1024]; System.gc(); } // 会回收,因为对象引用置空了 public void localvarGC2() { byte[] buffer = new byte[10 * 1024 * 1024]; buffer = null; System.gc(); } // 不会回收,因为虽然在代码块外buffer变量已经无效了,但对象引用依旧存在 public void localvarGC3() { { byte[] buffer = new byte[10 * 1024 * 1024]; } System.gc(); } // 会回收,因为在局部变量表中,变量buffer的位置被变量num替代,对象失去引用 public void localvarGC4() { { byte[] buffer = new byte[10 * 1024 * 1024]; } int num = 10; System.gc(); } // 会回收,因为方法执行结束后,栈帧出栈,对象失去引用 public void localvarGC5() { localvarGC1(); System.gc(); }
2.内存溢出与泄露
-
内存溢出概述
- 内存溢出是引发程序崩溃的罪魁祸首之一
- 内存溢出代表当前没有空闲内存,并且通过垃圾收集器进行回收后也无法提供更多内存
- 内存溢出之前,通常伴随有垃圾收集器的触发,然后尽其所能去清理出空间。但是也不是在任何情况下垃圾收集器都会被触发的,当我们去分配一个比整个堆空间还要大的对象时,JVM完全可以判断出垃圾收集并不能解决这个问题,所以就会直接拋出OOM异常
- 由于GC一直在发展,一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现O0M的情况
-
内存溢出原因
-
堆内存不足
很有可能就是堆的大小设置不合理造成的,比如:我们要处理比较大的数据量,但是没有显式指定JVM堆空间的大小或者指定的数值偏小
-
存在大对象
代码中创建了大量的大对象,并且长时间不能被垃圾收集器回收。对于老版本的JDK来说,因为永久代的大小是有限的,并且JVM对永久代垃圾回收非常不频繁,所以当我们不断添加大类型对象的时候,永久代很容易出现内存溢出。对于新版本的JDK来说,虽然随着元数据区的引入,方法区内存已经不再那么窘迫,但是如果直接内存不足,也会导致内存溢出
-
-
内存泄漏概述
-
也称作“存储渗漏”,严格意义上来说,当出现对象不会再被程序用到了,但是GC又不能回收它们的情况,这才叫内存泄漏
-
由于不规范的代码书写,出现了大量对象的生命周期很长,从而导致OOM,这种情况也可以叫做宽泛意义上的“内存泄漏”
-
尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步耗尽,最终导致程序崩溃
-
-
内存泄漏举例
- 在单例模式中,单例对象的生命周期和应用程序是一样长的,所以如果该对象又持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则有可能会导致内存泄漏的产生
- 一些提供close方法的资源用完后要及时关闭,比如:数据库连接,网络连接、IO连接等,这些都必须手动调用close方法,否则有可能会导致内存泄漏的产生
3.Stop The World
- Stop The World,简称STW,指的是在垃圾回收事件的发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,这个时间段称为STW
- 可达性分析算法中枚举根节点的过程也会导致所有Java执行线程停顿
- STW是JVM在后台自动发起和自动完成的,它是在用户不可见的情况下,把用户正常的工作线程全部停掉的
- 被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉程序像是卡顿一样, 所以我们需要减少STW的发生
- STW的发生和采用哪款垃圾回收器无关,因为所有的垃圾回收器都会发生这个事件,只能说垃圾回收器越来越优秀,回收效率越来越高,这在一定程度上缩短了暂停时间而已
程序案例
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();
}
}
} 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();
}
}
从以下打印结果,我们发现打印的时间间隔明显要多于我们设定的1秒,说明在垃圾回收的过程中,发生了STW
4.垃圾回收的并行与并发
-
系统并发
- 在操作系统中,是指一个时间段中有几个程序都处于运行的状态,且这几个程序都是在同一个处理器上运行的
- 并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片,然后在指定的时间片内来回进行程序的切换,由于CPU处理的速度非常快,只要时间间隔处理得当,那么给用户感觉是多个应用程序同时在进行
-
系统并行
- 在系统中存在多个CPU的时候(
或者一个CPU有多个处理核
),一个CPU执行一个进程时,另一个CPU可以执行另一个进程,这两个进程互不抢占CPU资源,可以同时进行,我们称之为并行 - 这种过程适合科学计算、后台处理等弱交互场景
- 在系统中存在多个CPU的时候(
-
二者对比
- 对于并发来讲,多个进程是在
同一时间段
内同时发生了,它们之间是互相抢占资源的 - 对于并行来讲,多个进程是在
同一时间点
内同时发生了,它们之间是不互相抢占资源的
- 对于并发来讲,多个进程是在
-
垃圾回收
-
串行:采用单线程执行,只允许一个垃圾收集线程在工作,此时用户线程将处于等待状态(STW)
-
并行:指多个垃圾收集线程并行工作,此时用户线程将处于等待状态(STW)
-
并发:指用户线程与垃圾收集线程在同一时间段内交替执行,降低了前面两种方式产生STW的时间
-
5.安全点与安全区域
-
安全点
- 在程序执行时,并非在所有地方都能停顿下来开始进行垃圾回收,只有在特定的位置才能停顿下来开始垃圾回收,那这些特定的位置我们称为“安全点 (
SafePoint
) ” - 安全点的选择很重要,如果设定的太少可能导致GC等待的时间很长,如果设定的太多可能导致运行时出现性能问题
- 我们发现大部分指令的执行时间都非常短暂,所以通常会根据
是否具有让程序长时间执行的特征
为标准,选择那些执行时间较长的指令作为安全点, 比如:方法调用、循环跳转、异常跳转等
如何判断在垃圾回收发生的时候,其它所有用户线程都到达了所谓的安全点呢?
- 抢先式中断法:首先中断所有线程,如果发现还存在线程不在安全点,那就恢复该线程让它跑到安全点
- 主动式中断法:设置一个中断标志,各个线程运行到安全点的时候主动查询这个标志。如果中断标志为真,则将该线程中断挂起,否则继续运行。这也是当下虚拟机所采用的的方式
- 在程序执行时,并非在所有地方都能停顿下来开始进行垃圾回收,只有在特定的位置才能停顿下来开始垃圾回收,那这些特定的位置我们称为“安全点 (
-
安全区域
- 安全点保证了程序线程在正常执行时,线程能够响应JVM发出的信号中断请求。但是,当程序线程处于Sleep状态或Blocked状态时,该线程无法响应JVM的中断请求,同时JVM也不太可能等待线程被唤醒。所以对于这种情况,就需要设定安全区域来解决
- 安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始垃圾回收都是安全的
- 当线程进入安全区域时,首先进行标识,如果这段时间内发生垃圾回收,则JVM会忽略这些已被标识的线程
- 当线程离开安全区域时,首先检查JVM是否已经完成了垃圾回收工作,如果完成了,则允许线程离开,否则线程必须等待垃圾回收完成后离开
以上是关于JVM垃圾回收篇(重要概念解析)的主要内容,如果未能解决你的问题,请参考以下文章