常见面试题
- 请你谈谈你对JVM的理解?
- java8虚拟机和之前的变化和更新
- 什么是OOM?什么是栈溢出?怎么分析?
- JVM常见的调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?
- 谈谈JVM中,类加载器你的认识
常见名词
- JVM的位置
- JVM的体系结构
- 类加载器
- 双亲委派机制
- 沙箱安全机制
- Native
- PC寄存器
- 方法区
- 栈
- 堆
- 三种JVM
- 新生区、老年区、永久区
- 堆内存调优
- GC常用算法
- JMM
JVM的位置
- JVM运行在操作系统之上,相当于一个软件
- JVM是使用C/C++语言写的
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方法
以及我们常用的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