一、关于JVM常见的面试题
- 对JVM的理解
- java8的JVM变化点
- OOM,栈溢出以及如何分析
- JVM调优常设置的参数
- 内存快照的获取以及分析Dump文件
- JVM中的类加载器
二、JVM概念
1)操作系统上一层是JVM(下一层是硬件),JVM可以理解为一个软件或者环境(java项目运行其中,JRE包含JVM),底层是用C编写;
2) 流程: .java文件---->.class文件 ---->类加载器class loader —>运行时数据区
其中99%的调优都是在堆heap,栈和程序计数器是不可能有垃圾的。
3)类加载器:
作用是加载.class文件
1)分为:虚拟机本身带有的加载器,根加载器(启动类加载器),拓展类加载器,应用程序加载器
注:jre里面的加载器或者jar包可以更改,但是不建议更改,如果改不好整个环境都会崩溃;
package com.byxrs.jvm;
/**
* 1. Person.class --> Class Loader加载初始化 -->得到反射对象 Person class(可用于实例化)--->new 实例化
* 2. 对象转为 Person class,采用getClass
*/
public class Person {
public static void main(String[] args) {
/*
Class是模板对象,全局的,模板只有一个
类是抽象的,对象是具体的
*/
Class<Person> carClass = Person.class;
/*
p1 p2 引用存在栈中,new出来的对象存在堆中,通过内存地址产生对应的关系
*/
Person p1 = new Person();
Person p2 = new Person();
// 2133927002
System.out.println(p1.hashCode());
// 1836019240
System.out.println(p2.hashCode());
/*
getClass获得模板
*/
Class<? extends Person> aClass1 = p1.getClass();
Class<? extends Person> aClass2 = p2.getClass();
// aClass1 aClass2是否一样?
System.out.println(aClass1.hashCode());// 21685669
System.out.println(aClass2.hashCode());// 21685669
/*
获取类加载器
*/
ClassLoader classLoader = aClass1.getClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2 应用程序加载器
System.out.println(classLoader);
//sun.misc.Launcher$ExtClassLoader@135fbaa4 jre1.8.0_231\lib\ext
System.out.println(classLoader.getParent());
//null 可能不存在,或者java程序获取不到根加载器rt.,jar jre1.8.0_231\lib
System.out.println(classLoader.getParent().getParent());
}
}
延伸出来的内容:双亲委派机制(重点)
作用:保证安全
执行流程:
1)类加载器收到类加载的请求;
2)将请求一直向上2委托给父类加载器执行,直到启动类加载器。(APP加载器—ext加载器----boot根加载器);
3)启动类加载器检查能否加载这个类,能加载则结束,不能则抛出异常,让子加载器进行加载。
常见错误:ClassNotFound
3 沙箱安全机制(了解)
java安全的核心是沙箱(作用是限制系统资源的访问),沙箱就是限制程序运行的环境。沙箱机制既java代码在JVM特定运行范围中,并且访问本地系统资源也是有着严格的限制,保证了代码的有效隔离。
目前安全机制引入域Domain,JVM会把代码加载到不同的系统域(与关键资源交互)和应用域中,每个域有自己的权限。
package com.byxrs.jvm;
public class TestShaXiang {
public static void main(String[] args) {
new TestShaXiang().test1();
}
public void test1() {
this.test2();
}
public void test2() {
this.test1();
}
}
运行结果:栈溢出了,是错误,而不是异常,错误会使得虚拟机停止运行。
Exception in thread "main" java.lang.StackOverflowError
at com.byxrs.jvm.TestShaXiang.test1(TestShaXiang.java:15)
at com.byxrs.jvm.TestShaXiang.test2(TestShaXiang.java:19)
at com.byxrs.jvm.TestShaXiang.test1(TestShaXiang.java:15)
at com.byxrs.jvm.TestShaXiang.test2(TestShaXiang.java:19)
沙箱组件:
1)字节码校验器,确保遵循规范
2)类装载器,可以防止恶意代码攻击我们的代码;可以守护被信任的类库边界;可以将代码归入保护域;
3)存取控制器
4)安全管理器
4 native 寄存器 方法区
native(重点)
package com.byxrs.jvm;
public class TestNative {
public static void main(String[] args) {
new Thread().start();
/*
Thread类中
private native void start0();
native关键字,说明java作用域达不到,需要调用底层C语言的库;
会进入本地方法栈;
会调用JNI本地方法接口(作用是拓展java功能,融合不同的语言为java使用,比如通过JNI调用python功能);
产生背景:当时C C++盛行,很多情况会需要调用C C++写的程序
在内存区域中专门开了一块标记区域(本地方法栈),登记native方法;
执行时,通过JNI加载本地方法库中的方法
应用场景:java链接打印机或者机械链接
调用其他接口:socket,webservice,http
*/
}
}
寄存器(了解):
每个线程都有一个程序计数器,是线程私有的,占有空间小;
方法区(重点):
所有线程共享,方法区存放静态变量static,常量final,类信息(构造方法,接口定义),运行时的常量池。实例变量存放在堆中,与方法区无关。
5.栈
5-1 是一种数据结构,先进后出(区别于队列–先进先出);
栈内存是管理程序的运行,生命周期和线程同步,线程结束,栈内存也就释放了,为此栈是不存在垃圾回收的情况。
比如执行程序,首先把main()方法压到栈中,然后放入不同的方法,执行方法则将方法拿出栈,最后拿出main()程序运行结束
5-1 栈存放8大基本类型,对象引用,实例方法
5-3 栈运行原理:栈帧
栈是怎么存数据?
对象实例化过程是什么样的?
6 堆
目前使用的JVM是sun公司的
一个JVM只有一个堆内存,大小是调整的;
堆一般是放类,方法,常量,变量,引用类型的真实对象,堆内存分为三个区(新生区(伊甸园区Eden,幸存0区和1区),养老区,永久区(元空间))。
GC垃圾回收分为 轻量级垃圾回收(针对新生区)和重量级垃圾回收(针对养老区)
新生区
类的产生,成长和死亡的区域
Eden:当Eden区满了之后会触发轻GC,后存活下来的数据会到幸存S0或者S1区
S0:与S1是动态交换的工作方法
S1:
老年区
当以上3个区数据都满了则会触发重GC来清楚3个区的数据,能存活下来的就会存在老年区中,当老年区满了则会报错OOM错误;
注:99%的数据是临时的,很少见到OOM;
元空间
java8以后常量池(方法区中,而方法区在元空间中)存放在这个区;
该区为常驻内存,用来存放JDK自身的class对象,存放运行时的环境。该区域不存在垃圾;
生命周期是关闭JVM。
注:1 一个启动类加载大量的第三方jar包可能导致内存满
2 还有就是一个tomcat部署太多的应用,动态生成大量的反射类,不断被加载,也容易内存满。
3 占用的空间比较小,可以被线程共享。
4 逻辑上存在,但是物理上不存在。
package com.byxrs.jvm;
public class JVMParam {
public static void main(String[] args) {
long maxMemory = Runtime.getRuntime().maxMemory();
long totalMemory = Runtime.getRuntime().totalMemory();
/*
在Edit Configurations的VM Options不设置参数
maxMemory:3563
totalMemory:241
*/
System.out.println("maxMemory:" +maxMemory/(double)1024 / 1024);
System.out.println("totalMemory:" + totalMemory /(double) 1024 / 1024);
//在Edit Configurations的VM Options设置: -Xms1024m -Xmx1024m -XX:+PrintGCDetails
/*
maxMemory:981.5
totalMemory:981.5
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 3239K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 349K, capacity 388K, committed 512K, reserved 1048576K
*/
/*
面试:碰到OOM怎么办?
假设电脑内存足够
1.增加堆内存看结果,如果还是报错意味是代码的问题
2.分析内存,查看哪个代码出问题(JProfile工具)
JProfile工具作用:
分析Dump内存文件,快速定位内存泄漏
获取堆的数据
获取大的对象
*/
}
}
package com.byxrs.jvm;
import java.util.Random;
//设置参数 -Xms8m -Xmx8m -XX:+PrintGCDetails
public class OOMTest {
public static void main(String[] args) {
String line = "sdfghjklqwueito";
while (true) {
line += line + new Random().nextInt(666666666);
}
/**运行结果,先轻GC几次后重GC:
* [GC (Allocation Failure) [PSYoungGen: 1534K->512K(2048K)] 1534K->734K(7680K), 0.0018363 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [GC (Allocation Failure) [PSYoungGen: 1829K->511K(2048K)] 2051K->1308K(7680K), 0.0010115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [GC (Allocation Failure) [PSYoungGen: 1698K->512K(2048K)] 2495K->2265K(7680K), 0.0009816 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [GC (Allocation Failure) [PSYoungGen: 1695K->512K(2048K)] 3449K->2647K(7680K), 0.0009032 secs] [Times: user=0.16 sys=0.02, real=0.00 secs]
* [GC (Allocation Failure) [PSYoungGen: 1713K->512K(2048K)] 5379K->4185K(7680K), 0.0006835 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [GC (Allocation Failure) [PSYoungGen: 1320K->512K(2048K)] 4993K->4950K(7680K), 0.0011169 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [Full GC (Ergonomics) [PSYoungGen: 512K->0K(2048K)] [ParOldGen: 4438K->1415K(5632K)] 4950K->1415K(7680K), [Metaspace: 3154K->3154K(1056768K)], 0.0069688 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
* [GC (Allocation Failure) [PSYoungGen: 796K->96K(2048K)] 3742K->3041K(7680K), 0.0005062 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [GC (Allocation Failure) [PSYoungGen: 96K->64K(2048K)] 3041K->3009K(7680K), 0.0005389 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [Full GC (Allocation Failure) [PSYoungGen: 64K->0K(2048K)] [ParOldGen: 2945K->2902K(5632K)] 3009K->2902K(7680K), [Metaspace: 3155K->3155K(1056768K)], 0.0090333 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
* [GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 2902K->2902K(7680K), 0.0006158 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 2902K->2883K(5632K)] 2902K->2883K(7680K), [Metaspace: 3155K->3155K(1056768K)], 0.0088098 secs] [Times: user=0.24 sys=0.00, real=0.01 secs]
*
* Heap
* PSYoungGen total 2048K, used 121K [0x00000000ffd80000, 0x0000000100000000, 0x0000000100000000)
* eden space 1536K, 7% used [0x00000000ffd80000,0x00000000ffd9e4d8,0x00000000fff00000)
* from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
* to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
* ParOldGen total 5632K, used 2883K [0x00000000ff800000, 0x00000000ffd80000, 0x00000000ffd80000)
* object space 5632K, 51% used [0x00000000ff800000,0x00000000ffad0c48,0x00000000ffd80000)
* Metaspace used 3252K, capacity 4496K, committed 4864K, reserved 1056768K
* class space used 351K, capacity 388K, committed 512K, reserved 1048576K
*
* Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
* at java.util.Arrays.copyOf(Arrays.java:3332)
* at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
* at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
* at java.lang.StringBuilder.append(StringBuilder.java:208)
* at com.byxrs.jvm.OOMTest.main(OOMTest.java:10)
* */
}
}
如何Dump文件(dump文件是JVM的快照)?需要安装软件和设置参数
package com.byxrs.jvm;
import java.util.ArrayList;
/**
* -Xms 设置初始化内存分配大小 默认1/64
* -Xmx 设置最大分配内存 默认1/4
* -XX:+HeapDumpOnOutOfMemoryError Dump文件
* -XX:+PrintGCDetails 打印GC
*/
public class JProfileTest {
byte[] array = new byte[1*1024*1024];
public static void main(String[] args) {
ArrayList<JProfileTest> jProfileList = new ArrayList<JProfileTest>();
int count =0;
try {
while(true){
jProfileList.add(new JProfileTest());
count ++;
}
}catch (Error ex){
System.out.println("count:"+count);
ex.printStackTrace();
}
/*
如何Dump文件?
//设置参数 -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
再次运行后生成Dump文件 java_pid37420.hprof:位置在项目文件下 如 F:\IdeaProjects\spark
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid37420.hprof ...
Heap dump file created [7738142 bytes in 0.015 secs]
直接双击打开文件
*/
}
}
总结:由于JVM分为新生带,幸存区和老年区,但是进行GC时,大部分还是在新生代。
GC又分为轻GC,重GC。
JVM的内存模型和分区是怎么样的,以及每个区是放什么东西?
堆的分区有哪些?Eden, from,to,老年区
GC算法常见的有标记清除法,标记整理,复制算法,引用计数法。
引用计数法:计数器本身会有消耗,每用一次对象计数器加1,但是这个方法不高效。
复制算法:目前主要用复制算法,流程是每次GC都会将Eden存活的对象移动到幸存区中,一旦Eden区被GC,就会变为空。当一个对象经历默认15次(该参数可以设定)GC后还是说存活的,就会进入老年区。优势是没有内存碎片,劣势是浪费了内存空间,有一半空白的空间是作为TO的,极端情况下,假设对象100%从新生代存活下来到幸存区to,to和from复制的时候会浪费大量的资源。
复制算法最佳使用场景:对象存活率较低的情况。
标记清除法:GC回收时对存活的对象进行标记,清除阶段对没有标记的对象进行清除。
优势是不需要额外的空间,劣势是两次扫描会严重浪费时间,产生内存碎片。
优化的方法是:采用压缩,防止内存碎片产生,再次扫描,向一端移动存活的对象,但是还是多了一个移动成本资源。
轻GC和重GC运行原理?
对比以上3个方法分析:
内存效率:复制算法>标记清除算法>标记压缩算法
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法 = 标记清除算法>复制算法
采用何种算法取决于实际场景分析:
新生代:存活率低,采用复制算法;
老年带:区域大:存活率,采用标记清除法和标记压缩算法混合使用。
推荐书籍:《深入理解JVM》
面对一个新事物的快节奏学习:是什么,干什么,怎么干,看面试题!!!
补充:
JVM只有一个主内存,每个线程都有自己的工作内存,是从主内存拷贝的;
线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存;