JVM—垃圾回收相关概念补充
一、System.gc()
-
在默认情况下,通过System.gc ()或者Runtime . getRuntime() .gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
-
然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。
- 手动调用System.gc()后,GC不一定会立马执行垃圾回收操作。
-
JVM实现者可以通过system.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之,间调用System.gc()。
/**
* @Des : System.gc()方法:提醒jvm的垃圾回收器执行gc,但是不确定是否马上执行gc;
* 与Runtime.getRuntime().gc();的作用一样。
* 1、注释掉 System.runFinalization();
* 不一定立马执行gc
* 2、调用 System.runFinalization();
* 立即进行gc
*
* System.runFinalization():强制调用失去引用的对象的finalize()方法
*/
public class SystemGCTest {
public static void main(String[] args) {
new SystemGCTest();
System.gc();
System.runFinalization();
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("SystemGCTest 重写了finalize()");
}
}
//System.gc()源码如下
public static void gc() {
Runtime.getRuntime().gc();
}
1.1 手动调用GC测试对象的回收行为
public class LocalVarGCTest {
@Test
public void localvarGC1() {
byte[] buffer = new byte[10 * 1024 * 1024];//10MB
System.gc();
//buffer不会被回收
//[GC (System.gc()) [PSYoungGen: 19432K->10744K(76288K)] 19432K->11710K(251392K), 0.0064676 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
//[Full GC (System.gc()) [PSYoungGen: 10744K->0K(76288K)] [ParOldGen: 966K->11549K(175104K)] 11710K->11549K(251392K), [Metaspace: 5070K->5070K(1056768K)], 0.0081347 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
}
@Test
public void localvarGC2() {
byte[] buffer = new byte[10 * 1024 * 1024];//10MB
buffer = null;
System.gc();
//buffer会被回收
//[GC (System.gc()) [PSYoungGen: 19432K->1488K(76288K)] 19432K->1496K(251392K), 0.0015472 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//[Full GC (System.gc()) [PSYoungGen: 1488K->0K(76288K)] [ParOldGen: 8K->1309K(175104K)] 1496K->1309K(251392K), [Metaspace: 5071K->5071K(1056768K)], 0.0067953 secs] [Times: user=0.09 sys=0.00, real=0.01 secs]
}
@Test
public void localvarGC3() {
{
byte[] buffer = new byte[10 * 1024 * 1024];//10MB
}
System.gc();
//buffer不会被回收,因为buffer作为GC Roots,此时存在于局部变量表中
//[GC (System.gc()) [PSYoungGen: 19432K->10744K(76288K)] 19432K->11677K(251392K), 0.0061076 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
//[Full GC (System.gc()) [PSYoungGen: 10744K->0K(76288K)] [ParOldGen: 933K->11549K(175104K)] 11677K->11549K(251392K), [Metaspace: 5070K->5070K(1056768K)], 0.0082207 secs] [Times: user=0.05 sys=0.05, real=0.01 secs]
}
@Test
public void localvarGC4() {
{
byte[] buffer = new byte[10 * 1024 * 1024];//10MB
}
int value = 10;
System.gc();
//buffer会被回收,上面提到buffer存在于局部变量表中,不会被回收,但是局部变量表会被复用,此时buffer已经出了它的作用域,所以它的位置被value复用了,进而导致buffer被回收
//[GC (System.gc()) [PSYoungGen: 19432K->1496K(76288K)] 19432K->1504K(251392K), 0.0017104 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//[Full GC (System.gc()) [PSYoungGen: 1496K->0K(76288K)] [ParOldGen: 8K->1309K(175104K)] 1504K->1309K(251392K), [Metaspace: 5071K->5071K(1056768K)], 0.0065614 secs] [Times: user=0.11 sys=0.00, real=0.01 secs]
}
@Test
public void localvarGC5() {
localvarGC1();
System.gc();
//buffer正常被回收
//[GC (System.gc()) [PSYoungGen: 19432K->10744K(76288K)] 19432K->11665K(251392K), 0.0067555 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
//[Full GC (System.gc()) [PSYoungGen: 10744K->0K(76288K)] [ParOldGen: 921K->11549K(175104K)] 11665K->11549K(251392K), [Metaspace: 5070K->5070K(1056768K)], 0.0078026 secs] [Times: user=0.08 sys=0.01, real=0.01 secs]
//[GC (System.gc()) [PSYoungGen: 0K->0K(76288K)] 11549K->11549K(251392K), 0.0007482 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//[Full GC (System.gc()) [PSYoungGen: 0K->0K(76288K)] [ParOldGen: 11549K->1309K(175104K)] 11549K->1309K(251392K), [Metaspace: 5070K->5070K(1056768K)], 0.0062398 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
}
}
二、内存泄漏与溢出
2.1 内存泄漏
也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏
尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现0utOfMemory异常,导致程序崩溃。
注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
案例
- 1、单例模式
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。 - 2、一些提供close的资源未关闭导致内存泄漏数据库连接( dataSourse. getConnection()),网络连接(socket)和io连接必须手动close,否则是不能被回收的。
2.2 内存溢出
内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。
大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。
javadoc中对OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
首先说没有空闲内存的情况(Java虚拟机的堆内存不够)。原因有二:
- (1)Java虚拟机的堆内存设置不够。
比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。我们可以通过参数 -Xms、-Xmx来调整堆内存大小。 - (2)**代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)**对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关: “java. lang. OutOfMemoryError: PermGen space”。
随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的00M有所改观,出现OOM,异常信息则变成了:“java. lang. OutOfMemoryError: Metaspace"。 直接内存不足,也会导致OOM。
这里面隐含着一层意思是,在抛出OutOfMemoryError之 前,通常垃圾收集器会被触发,尽其所能去清理出空间。 具体案例请参照《运行时数据区域—堆》中的2.4.4案例。
- 例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等。
- 在java.nio.BIts.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。
当然,也不是在任何情况下垃圾收集器都会被触发的
- 比如,我们去分配一一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接拋出OutOfMemoryError
三、STW(Stop The World)
指的是Gc事件发生过程中,会产生应用程序的停顿。此时,用户所有线程都被挂起,没有任何响应,有点像卡死的感觉,这个停顿称为STW。.
- ➢可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。.
- 分析工作必须在一个能确保一致性的快照 中进行
- 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上V- - 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样, 所以我们需要减少STW的发生。
STW事件和采用哪款GC无关,所有的GC都有这个事件。
哪怕是G1也不能完全避免Stop一the一world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
开发中不要用System.gc();会导致Stop一the一world的发生。
测试案例
public class StopTheWorldTest {
public static class WorkThread extends Thread {
List<byte[]> list = new ArrayList<byte[]>();
@Override
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();
@Override
public void run() {
try {
while (true) {
// 每秒打印时间信息
long t = System.currentTimeMillis() - startTime;
System.out.println(t / 1000 +"." + t % 10);
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();
}
}
输出结果
0.3
1.9
2.2
3.2
4.1
正常来说一秒钟打印一次,上面结果之间明显有停顿现象。