【JVM系列8】JVM经典面试问题(内存溢出和内存泄露)解答及调优实战分析

前言

JVM系列介绍到这里,其实理论知识和基本工具的使用基本上都介绍过了,当然,JVM的理论知识也不仅仅只是这些,如果想要更深入的里面还是会有很多细节值得深入了解,但是就目前来说,掌握了前面几篇文章介绍的内容,我们已经可以对JVM进行基本的调优工作了,所以本篇文章会以一些常见问题并结合实际例子来进行分析。

常见问题及调优实战

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<Object> list = null;

    public static void main(String[] args) throws InterruptedException {
        list = new ArrayList<>();
        //首次自救
        list = null;
        System.gc();
        Thread.sleep(1000);
        print();
        
    }

    static void print(){
        if (null == list){
            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();
        list = new ArrayList<>();
    }
}

这里是无法实现自救的,因为这里要救的对象是List,而finalize()并不属于List,是属于ObjEscapeByFinalize1对象,所以这一点也是需要明确地。

不过虽然finalize()可以完成对象自救,但是由于这个方法的代价比较大而且运行时有不确定性,一般情况下还是不建议使用

4、Young GC会有STW吗

不管是什么类型的GC,都会有 stop-the-world,只是发生时间的长短,目前Java中所有的垃圾回收器均需要STW,唯一的区别只是时间的长短问题。

5、Major GC和Full GC的区别

之前我们提到了,Major GC通常会伴随着Minor GC,也就等于触发了Full GC,但是虽然如此,Major GC和Full GC并不是完全等价的,因为Full GC 的同时会对方法区(jdk1.8的metaspace,jdk1.7的永久代)进行GC,所以严格来说:Full GC=Major GC+Minor GC+方法区GC

6、方法区会发生GC吗

答案是肯定的。虽然方法区中的回收收益一般都不高,但是也是会被GC的,而方法区中被回收的最主要的就是对废弃常量无用类的回收,判定一个废弃常量比较简单,但是判定一个类是无用类是比较困难的,那么方法区中的怎么判断一个类是无用类呢?
判断一个类是否无用,需要达到以下三个条件:

  • 1、该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 2、加载该类的类加载器ClassLoader已经被回收(从这个条件可以看出,一般只有大量使用了反射,动态代理或者字节码框架等场景条件下才会满足这个条件)。
  • 3、该类对应的 java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

这三个条件实际上是非常苛刻的,而即使达到以上三个条件,无用类也仅仅是可以被回收,但是是不是一定会被回收,还是取决于Java虚拟机。HotSpot虚拟机中提供了参数-Xnoclassgc来控制。

7、什么是直接内存

直接内存(Direct Memory)不属于运行时数据区,也被称之为堆外内存,通常访问直接内存的速度会优于Java堆。直接没存也有可能会发生OutOfMemoryError异常,Java 1.4中新加入的nio包下的ByteBuffer就操作了直接内存,直接内存可以通过参数-XX:MaxDirectMemorySize控制大小。

8、CMS收集器和G1收集器的区别

作为同样是并行的2款垃圾收集器,G1的目前是用来取代CMS收集器的,其主要有如下区别:

  • 1、CMS收集器是老年代的收集器,需要和其他新生代收集器配合使用,而G1同时适用于新生代和老年代,不需要和其他收集器配合使用
  • 2、CMS收集器以最小的停顿时间为目标的并发收集器,G1收集器是一种可预测垃圾回收的停顿时间
  • 3、CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片,G1使用了 Region方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生

9、类加载机制经过哪些步骤

类加载机制主要经过了:加载(Loading)连接(Linking)初始化(Initialization)使用(Using)卸载(Unloading) 五个大阶段,而其中连接(Linking)又分为:验证(Verification),准备(Preparation),**解析(Resolution)**三个阶段。想要详细了解每个阶段做了什么事情,可以点击这里

10、系统CPU经常100%,如何定位

  • 1、首先要确认哪个进程占用CPU高,可以使用top命令
    在这里插入图片描述
  • 2、找到之后可以继续执行top -Hp PID命令查询出占用最大的线程
    在这里插入图片描述
  • 3、执行jstack命令生成线程快照信息:
jstack -l 进程PID >jstack.log

输出之后,我们找到上面占用CPU最高的一个线程pid=11566,将其转换为16进制,得到的结果是2d2e,然后进入生成的jstack.log文件找到这个线程可以查看线程信息。
在这里插入图片描述

  • 4、上面就可以定位到了线程调用的方法了,接下来就可以去分析对应的代码寻找问题了

总结

本文主要列举了一些其他比较经典的,而前面在JVM系列其他文章中又没有过多进行说明的问题,JVM学习之后需要不断实战积累调优经验,虽然还有一些理论在JVM系列中没有提及,但是我想如果可以认真把我 JVM系列至本篇为止的8篇文章相关知识和理论都掌握的话,那至少可以说已经具备了调优的理论基础了,剩下的就是不断积累经验,当然,推荐大家可以去通读一下JVM规范,毕竟所有的Java虚拟机都是按照JVM规范来实现的,或者有必要的可以自己去编译JDK来进行更深一步的研究。
请关注我,和孤狼一起学习进步

猜你喜欢

转载自blog.csdn.net/zwx900102/article/details/108304453