常见问题及调优实战1、内存泄漏与内存溢出的区别 内存泄漏(Memory Leak):指的是对象无法得到及时的回收,导致其持续占用内存空间,造成了内存空间的浪费。 内存泄露一般是强引用才会出现问题,其他像软引用,弱引用和虚引用影响不大。 内存溢出(Out Of Memory):内存泄漏到一定的程度就会导致内存溢出,但是内存溢出也有可能是大对象导致的。 这两个区别结合下面的问题2可以更好的理解。2、如何防止内存泄露 我们先来看下面一个简单的例子:package com.zwx.jvm; public class JVMTuningDemo { public static void main(String[] args) { { byte[] bytes = new byte[1024 * 1024 * 64]; } System.gc(); } } 调用之后打开gc日志,如果不知道怎么获取gc日志的,可以点击这里。 可以看到GC之后,对象并没有回收掉,从代码上来说,因为有{},所以理论上已经离开作用域了,bytes会被回收(如果不加{}是肯定不会被回收的,因为没有离开作用域),但是这里为什么还是没有被回收? 回答这个问题之前我们先对上面的代码改进一下package com.zwx.jvm; public class JVMTuningDemo { public static void main(String[] args) { { byte[] bytes = new byte[1024 * 1024 * 64]; bytes = null; } System.gc(); } } 这时候再来看,会发现已经被回收了 这是因为之前虽然已经离开作用域了,但是却并没有收回引用,也就是说栈帧中的局部变量表数组中所对应的slot(局部变量表中数组的每一个位置都被称之为slot)还是有值的,并没有被切断引用,而将其置为Null就等于切断了引用,所以可以被回收。 如果看过我的并发编程系列文章中对AQS同步队列以及阻塞队列的源码分析,那么也应该可以看到,这些源码中也是大量使用了这种方式来帮助虚拟机进行gc: 在有些场景这种设置为null的方式确实是一种解决方式,但是其实最优雅的方式还是以恰当的变量作用域来控制回收变量。 我们再对上面的例子进行改写:package com.zwx.jvm; public class JVMTuningDemo { public static void main(String[] args) { { byte[] bytes = new byte[1024 * 1024 * 64]; } int i = 0; System.gc(); } } 运行之后打开gc日志: 我们会发现,bytes对象确实也被回收了,这又是为什么呢? 这是因为栈帧中的局部变量表内的每一个slot都是可以复用的,当bytes变量离开了其作用域之后,Java虚拟机知道这个slot已经无效了,但是虽然无效,引用却还在,所以如果没有新的变量过来占用bytes变量所在的slot,是无法将bytes回收的,而一旦有新的变量过来占用slot,自然而然bytes对象的引用就被切断了,从而被gc掉。3、GCRoot不可达的对象一定会被回收吗 答案是不一定的。 即使在可达性分析法中被判定不可达的对象,也并非是"非死不可"的,这时候它们暂时处于"缓刑阶段",对象依然有"逃生"的机会。 一个对象在第一次被标记为不可达对象时,并不会立刻被回收,而是会进行判断是否有必要执行finalize()方法,那么什么时候会执行finalize()方法呢?有两种情况:1、Java虚拟机已经调用过当前对象的finalize()方法2、finalize()方法被我们重写了 如果不满足这两种情况,那么对象就相当于是"死刑立即执行",没有机会逃生,但是一旦满足执行finalize()方法的条件,而我们又在finalize()方法中将对象重新和引用链中的对象进行了关联,这时候对象就可以顺利"逃生"。 我们来看下面一个例子:package com.zwx.jvm; import java.util.ArrayList; import java.util.List; public class ObjEscapeByFinalize { public static ObjEscapeByFinalize objEscapeByFinalize = null; public static void main(String[] args) throws InterruptedException { objEscapeByFinalize = new ObjEscapeByFinalize(); //首次自救 objEscapeByFinalize = null; System.gc(); Thread.sleep(1000);//finalize()方法优先级比较低,稍微停顿一会等一等 print(); //再次自救 objEscapeByFinalize = null; System.gc(); Thread.sleep(1000); print(); } static void print(){ if (null == objEscapeByFinalize){ System.out.println("obj has been gc"); }else{ System.out.println("obj escape success"); } } @Override protected void finalize() throws Throwable { System.out.println("come in method:finalize"); super.finalize(); objEscapeByFinalize = this; } } 运行结果为:come in method:finalize obj escape success obj has been gc 从结果可以看到,第一次自救成功,而第二次已经没有了自救机会,因为当前对象已经执行过一次finalize()方法了,而如果我们把finalize()方法中的:objEscapeByFinalize = this; 替换为:objEscapeByFinalize = new ObjEscapeByFinalize(); 这时候就可以一直自救成功,因为每次自救之后就产生了一个新的对象,新的对象并没有执行过finalize()方法。 上面的demo还有一点需要注意的是,finalize()方法针对的是对象,假如上面的静态对象换成一个其他对象,而finalize()方法又写在当前对象,那么是无效的,例如如下例子:package com.zwx.jvm; import java.util.ArrayList; import java.util.List; public class ObjEscapeByFinalize1 { public static List