JVM总结

常见面试题

  • 请你谈谈你对JVM的理解?
  • java8虚拟机和之前的变化和更新
  • 什么是OOM?什么是栈溢出?怎么分析?
  • JVM常见的调优参数有哪些?
  • 内存快照如何抓取,怎么分析Dump文件?
  • 谈谈JVM中,类加载器你的认识

常见名词

  • JVM的位置
  • JVM的体系结构
  • 类加载器
  • 双亲委派机制
  • 沙箱安全机制
  • Native
  • PC寄存器
  • 方法区
  • 三种JVM
  • 新生区、老年区、永久区
  • 堆内存调优
  • GC常用算法
  • JMM

JVM的位置

  • JVM运行在操作系统之上,相当于一个软件
  • JVM是使用C/C++语言写的

JVM结构

简单示例图:

JVM示意图

  • JVM调优几乎都是在调堆里面的东西
  • lombok插件,实际上是在执行引擎上动态加载了setter、getter方法

类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。——《深入理解java虚拟机》

当一个对象创建之后,它的引用放在栈里面,而对象本身放在堆里面

public class App {
    private String name;

    public static void main(String[] args) {
        App app = new App();
        app.name = "小明";
        App app1 = new App();
        app1.name = "小红";
        System.out.println(app == app1); // false
        Class<? extends App> clazz = app.getClass();
        Class<? extends App> clazz1 = app1.getClass();
        System.out.println(clazz == clazz1); // true
        System.out.println(clazz == App.class); // true

        ClassLoader cl = App.class.getClassLoader();
        System.out.println(cl); // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(cl.getParent()); // sun.misc.Launcher$ExtClassLoader@76fb509a
        System.out.println(cl.getParent().getParent()); // null
        // 这里出现null是因为由于JVM底层是C++语言实现的,java程序调用不到
    }
}
public static void main(String[] args) {
    System.out.println(ClassLoader.getSystemClassLoader());
    // sun.misc.Launcher$AppClassLoader@18b4aac2
}

简单示例图:

类加载器

类加载器的种类:

  • 启动类加载器(Bootstrap ClassLoader),或者叫根加载器。这个类加载器主要是去加载你在本机配置的环境变量 Java_Home/jre/lib 目录下的核心API,如rt.jar
  • 扩展类加载器(Extension ClassLoader)。这个加载器负责加载 Java_Home/jre/lib/ext 目录下的所有jar包。
  • 应用程序类加载器(Application ClassLoader)。这个加载器加载的是你的项目工程的ClassPath目录下的类库。如果用户没有自定义自己的类加载器,这个就是程序默认的类加载器。

面试题:Class.forName 和 ClassLoader 有什么区别?
简单总结:

  • Class.forName() 方法实际上也是调用的 CLassLoader 来实现的。
  • Class.forName 加载类是将类进了初始化,而 ClassLoader 的 loadClass 并没有对类进行初始化,只是把类加载到了虚拟机中。

双亲委派机制

当一个类在加载的时候,都会先委派它的父加载器去加载,这样一层层的向上委派,直到最顶层的启动类加载器。如果顶层无法加载(即找不到对应的类),就会一层层的向下查找,直到找到为止。这就是类的双亲委派机制。

目的:保证安全,防止程序员自定义与源码中相同的类。

package java.lang;

/**
 * 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
 * public static void main(String[] args)
 * 否则 JavaFX 应用程序类必须扩展javafx.application.Application
 */
public class String {
    public String toString() {
        return "Hello";
    }

    public static void main(String[] args) {
        String str = new String();
        str.toString();
    }
}

因为双亲委派机制的存在,去加载我们自己定义的“java.lang.String”类的时候,会最终委派到顶层的启动类加载器,然后找到了rt.jar包下的“java.lang.String”。找到之后,就直接加载rt.jar包的String类(也就是我们经常使用的那个字符串类),不再去向下查找,也就加载不了我们自定义的String类了。由于,rt.jar包下的String类中确实没有main方法,所以才会有以上的报错信息。

参考文章:https://mp.weixin.qq.com/s/gt9IjakAxk4ahzLpsEIBwg

沙箱安全机制

参考文章:https://blog.csdn.net/qq_30336433/article/details/83268945

Native

凡是带有native关键字的,说明java的作用域达不到了,会去调用底层C语言的库。

native关键字修饰的方法会进入本地方法栈,然后调用本地方法,本地接口(JNI,即java native interface)

JNI的作用:扩展java的使用,融合不同的编程语言为java所用!

历史原因:java诞生初期,C、C++横行,要想立足,必须有调用C、C++的程序,于是java在内存中开辟了“本地方法栈”,用于登记native方法,最终执行的时候通过JNI加载本地方法库中的方法。native方法可以驱动打印机,操作电脑上的硬件等等,但是native关键字的使用在企业级应用中较为少见。

例如:线程类中的start0()方法,就是native方法

线程中的JNI

以及我们常用的currentTimeMillis()方法

public static native long currentTimeMillis();

PC寄存器

即程序计数器(Program Counter Register)

每一个线程都有一个程序计数器,是线程私有的,相当于一个指针,指向方法区中方法的字节码,在执行引擎读取下一条指令。是一个非常小的内存空间,几乎可以忽略不计。

方法区

方法区被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间。

静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法无关(static、final、Class)

相关问题:为什么main函数先执行,最后结束?

因为main函数最先被压入栈中,最后弹出

栈内存主管程序的运行,生命周期和线程同步,线程结束,栈内存就会释放,不存在垃圾回收机制。一旦线程结束,栈就结束

栈存放的内容:

  • 8大基本类型
  • 对象引用
  • 实例方法

栈运行原理:

栈

程序正在执行的方法,一定在栈的顶部

如果递归次数太多,会发生栈溢出错误:

/**
 * Exception in thread "main" java.lang.StackOverflowError
 */
public class Application {
    public static void main(String[] args) {
        new Application().a();
    }

    public void a() {
        b();
    }

    public void b() {
        a();
    }
}
对象创建的过程

示例代码:

class Pet {
    String name;

    public Pet(String name) {
        this.name = name;
    }

    public void say() {
        System.out.println("hello, world");
    }
}

public class Application {
    public static void main(String[] args) {
        Pet pet = new Pet("小花");
        Pet pet1 = new Pet("小丽");
    }
}

1、首先从main函数开始执行,main函数首先入栈,加载Application类

第一步

2、当创建第一个对象Pet(即遇到new关键字)的实例的时候,加载Pet类

第二步

3、创建pet对象,在栈中储存对象的引用、在堆中储存对象实例

第三步

三种JVM

  • Sun公司的HotPot:普遍使用的JVM版本
  • BEA 的JRockit
  • IBM公司的J9VM

我们所学习的堆的优化使用的是HotPot版本的JVM。

Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,一般把类的实例放在堆中,即堆保存的是引用类型的真实对象

堆分为三个区域

  • 新生区
    • 伊甸园Eden Space
    • 幸存0区
    • 幸存1区
  • 养老区
  • 永久存储区

垃圾回收分为两种:轻GC、重GC,垃圾回收一般在伊甸园区和养老区

如果堆内存满的话,会出现OOM错误,如下:

/**
 * java.lang.OutOfMemoryError:Java heap space
 */
public class Application {
    public static void main(String[] args) {
        String str = "hello";
        while (true) {
            str += "world";
        }
    }
}

出现OOM的几种情况

  • 一个启动类加载了大量第三方jar包
  • Tomcat部署了太多应用
  • 大量动态生成反射类
永久区

这个区域常驻内存,用来存放JDK自身携带的Class对象、interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收,关闭JVM虚拟机才会释放这个区域的内存。JDK8以后,永久储存区改了个名字,叫做“元空间”

  • JDK1.6之前:永久代,常量池在方法区中
  • JDK1.7 :永久代慢慢退化,常量池在堆中
  • JDK1.8之后:无永久代,常量池在元空间中(方法区也在元空间中)

我们可以使用Runtime.getRuntime()方法拿到我们当前的运行环境

public class Application {
    public static void main(String[] args) {
        // 返回虚拟机视图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();
        // 返回JVM的初始化总内存
        long total = Runtime.getRuntime().totalMemory();

        System.out.println("max:" + max / 1024 / 1024 + "MB");
        System.out.println("total:" + total / 1024 / 1024 + "MB");

        // max:2706MB
        // total:184MB
    }
}

使用参数,可以调节上面的内存数量:

// -Xms1024m -Xmx1024m -XX:+PrintGCDetails
// -Xms 表示设置初始化内存大小,默认为计算机内存的1/64
// -Xmx 表示设置最大分配内存,默认为计算机内存的1/4
// -XX:+PrintGCDetails表示打印GC垃圾回收信息

运行结果:

max:981MB
total:981MB
Heap
 PSYoungGen      total 305664K, used 20971K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 262144K, 8% used [0x00000000eab00000,0x00000000ebf7afb8,0x00000000fab00000)
  from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
  to   space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
 ParOldGen       total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
 Metaspace       used 3499K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 383K, capacity 388K, committed 512K, reserved 1048576K

面试题:在写程序的过程中,遇见过OOM吗?怎么解决的?
1、尝试扩大堆内存,看效果
2、使用专业工具分析内存,看一下那个地方出现了问题

使用jprofiler

使用方法:

  • 官网:https://www.ej-technologies.com/products/jprofiler/overview.html
  • IDEA插件名称jprofiler
  • 安装完插件之后要在setting-->tools-->jprofiler处设置jprofiler.exe的路径

运行程序(参数:-Xms100m -Xmx800m -XX:+HeapDumpOnOutOfMemoryError):

-XX:+HeapDumpOnOutOfMemoryError 表示生成Dump文件记录OOM的信息

package com.qianyu.demo;

import java.util.*;

/**
 * java.lang.OutOfMemoryError: Java heap space
 * Dumping heap to java_pid16576.hprof ...
 * Heap dump file created [685667719 bytes in 3.876 secs]
 * 程序执行了:164
 * java.lang.OutOfMemoryError: Java heap space
 * at com.qianyu.demo.Application.<init>(Application.java:7)
 * at com.qianyu.demo.Application.main(Application.java:15)
 */
public class Application {

    int[] arr = new int[1024 * 1024];

    public static void main(String[] args) {
        ArrayList<Application> list = new ArrayList<>();
        int count = 0;
        try {
            while (true) {
                count++;
                list.add(new Application());
            }
        } catch (Error e) {
            System.out.println("程序执行了:" + count);
            e.printStackTrace();
        }
    }
}

打开项目根目录之后,会出现以hprof结尾的文件,双击即可打开

文件位置

查看大对象,基本可以判断是哪一个对象引起的OOM

查看大对象

定位到某一行:

定位

GC

GC算法总结:

参考文章:https://mp.weixin.qq.com/s/x83h2MKCxsHJDpoWbVtnig

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值