Java虚拟机(JVM)面试专题 中(初级程序员P6)

Java虚拟机(JVM)面试专题 中(初级程序员P6)

四、内存溢出

1. 误用线程池导致的内存溢出

(1)问题描述(newFixedThreadPool)

解决方案

(2)问题描述(newCachedThreadPool)

解决方案 

2. 查询数据量太大导致的内存溢出

3. 动态生成类导致的内存溢出

问题描述

解决方案

五、类加载

1. 类加载过程的三个阶段

(1)加载

(2)链接

(3)初始化

类初始化的原理 

final 修饰的基本数据类型的变量 

将常量池的符号引用变成直接引用(解析阶段)

2. jdk 8 的类加载器

3. 双亲委派机制

双亲委派的目的有两点

【BUG】对双亲委派的误解


Java虚拟机(JVM)面试专题 中(初级程序员P6)

四、内存溢出

  1. 误用线程池导致的内存溢出
  2. 查询数据量太大导致的内存溢出
  3. 动态生成类导致的内存溢出

1. 误用线程池导致的内存溢出

我们模拟一个发送短信的案例

一般来说我们要发送短信,都是调用第三方的API,所以有可能发生因网络问题导致的调用超时,或者是第三方服务“挂了”

(1)问题描述(newFixedThreadPool)

我们来看一下下面的代码 

// 设置堆内存为-Xmx64m  64MB
// 模拟短信发送超时,但这时仍有大量的任务进入队列
public class TestOomThreadPool {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        LoggerUtils.get().debug("begin...");

        // 不断发送短信
        while (true) {
            executor.submit(()->{
                try {
                    // 模拟调第三方API
                    LoggerUtils.get().debug("send sms...");
                    // 模拟超时30s
                    TimeUnit.SECONDS.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

运行结果 

显然内存溢出了!

我们来看看 newFixedThreadPool() 的源码

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
}

根据我们上面代码的传参来说:

最大线程数核心线程数都是2

new LinkedBlockingQueue<Runnable>():表示任务队列,也就是说,当我们这个线程池中最大线程数被占用后,其它的待处理任务会被放到这个任务队列,当任务队列容量超过内存,则发生内存溢出!!!(不会拒绝,只不断添加)

解决方案

不要使用 工具类Executors.newFixedThreadPool(2) 来创建线程池

使用 ThreadPoolExecutor 的构造方法来创建!这样子我们可以指定 LinkedBlockingQueue 的策略。

(2)问题描述(newCachedThreadPool)

// 设置堆内存为-Xmx64m  64MB
// 模拟短信发送超时,但这时仍有大量的任务进入队列
public class TestOomThreadPool {
    
    static AtomicInteger c = new AtomicInteger();

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();

        while (true) {
            System.ont.println(c.incrementAndGet());
            executor.submit(()->{
                try {
                    TimeUnit.SECONDS.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

运行结果 

我们来看看 newCachedThreadPool() 的源码 

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());
}

显然,这里和上面的 newFixedThreadPool() 不同, newFixedThreadPool() 的核心线程数默认是0,但是临时线程可以开“无数”个!

SynchronousQueue<Runnable>():表示最多只有一个任务!

临时线程数 = 最大线程数 - 核心线程数

所以,这里的错误是因为Java线程池开辟的线程数达到了系统的上限

解决方案 

和上面的那个一样,就是不要用 Executors 自带的方法来设置线程池,要自己来new 一个ThreadPoolExecutor

2. 查询数据量太大导致的内存溢出

在开发中一次性查询过多内容导致内存不足,引发内存溢出!

这里的解决方法很简单 

就是一定要使用 limit 关键字!!! 

3. 动态生成类导致的内存溢出

问题描述

// -XX:MaxMetaspaceSize=24m
// 模拟不断生成类, 但类无法卸载的情况
public class TestOomTooManyClass {

    static GroovyShell shell = new GroovyShell();

    public static void main(String[] args) {
        AtomicInteger c = new AtomicInteger();
        while (true) {
            try (FileReader reader = new FileReader("script")) {
                shell.evaluate(reader);
                System.out.println(c.incrementAndGet());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果 

每次调用evaluate() 方法的时候,都会动态生成一个类,这个类是通过GroovyClassLoader加载器加载的!

但是只要类的类加载器存在,类存在,类的实例对象存在,那么这个类是不能被卸载的!类不能被卸载,会导致云空间的内存不能释放!!!

又因为这个GroovyShell是一个 static ,所以不会被垃圾回收,从而就导致了元空间内存不足

解决方案

很简单,就是不要将其设置为static,将其设置为局部变量,这样子,当内存空间不足的时候,就进行类卸载,可以触发垃圾回收!

五、类加载

1. 类加载过程的三个阶段

类加载过程有三个阶段——加载、链接、初始化

(1)加载

  • 将类的字节码载入方法区,并创建类.class 对象(堆中)
  • 如果此类的父类没有加载,先加载父类
  • 加载是懒惰执行(用到才加载)

(2)链接

可以分成3个小的步骤 

  • 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
  • 准备 为 static 变量分配空间,设置默认值(注意,并不是赋值!)
  • 解析 – 将常量池符号引用解析为直接引用

(3)初始化

  • 执行 静态代码块非final的静态变量赋值(如果是final的变量,在链接阶段就会赋好值的!)
  • 初始化是懒惰执行

初始化的原理 

static int a = 0x77; // 类初始化
static {
    System.out.println("Student.class init");
} // 类初始化
static int b = 0x88;

static final int c = 0x99;
static final int m = Short.MAV_VALUE + 1;
static final Object n = new Object; //类初始化

看上述代码,编译器会把静态变量的赋值语句静态代码块中的语句、用final修饰的引用类型的静态变量 全部合到一起,变成一个方法,在类初始化的时候调用!

c 和 m在链接过程就已经赋值了!

final 修饰的基本数据类型的变量 

如果只是使用到final修饰的基本类型,是不会触发类加载的!

public class TestFinal {
    public static void main(String[] args) throws IOException {
        System.out.println(Student.c); // c 是 final static 基本类型
        System.in.read();

        System.out.println(Student.m); // m 是 final static 基本类型
        System.in.read();

        System.out.println(Student.n); // n 是 final static 引用类型
        System.in.read();
    }
}

上述代码中的c与m是final修饰,又是基本类型,其它类想要使用c和m的时候,根本就不会用到它们所在的类(Student);而是直接从常量池中去拿c和m的,并复制一份到TestFinal 类中!

将常量池的符号引用变成直接引用(解析阶段

在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道引用类的地址,所以就用符号引用来代替,而在解析阶段就是为了把这个符号引用转化成真正的地址的阶段

2. jdk 8 的类加载器

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap,显示为 null
Application ClassLoaderclasspath上级为 Extension
自定义类加载器自定义上级为 Application

String.class 就是由 Bootstrap ClassLoader 加载器加载

自定义的 Student.class 在上级加载器(Bootstrap、Extension)中没有找到,所以最后由Application加载!

3. 双亲委派机制

所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器

  • 能找到这个类,由上级加载,加载后该类也对下级加载器可见(避免重复加载)

  • 找不到这个类,则下级类加载器才有资格执行加载

双亲委派的目的有两点

  • 上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类
  • 让类的加载有优先次序,保证核心类优先加载

【BUG】对双亲委派的误解

【问题一】自己编写类加载器就能加载一个假冒的 java.lang.System 吗?

答案是不行!!!

  • 假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的 java.lang.System,自然不会加载假冒的

  • 假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的 java.lang.System 时,它需要先加载父类 java.lang.Object,而你没有用委派,找不到 java.lang.Object 所以加载会失败

  • 以上也仅仅是假设!事实上操作你就会发现,自定义类加载器加载以 java. 打头的类时,会抛安全异常,在 jdk9 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了

posted @ 2022-09-19 01:38  金鳞踏雨  阅读(17)  评论(0编辑  收藏  举报  来源