系列博客目录
文章目录
- 系列博客目录
- 前言
- 1. JVM组成
- 1.1 JVM由那些部分组成,运行流程是什么?3 4
- 1.2 什么是程序计数器?3 4
- 总结
- 1.3 你能给我详细的介绍Java堆吗? 3 4
- 总结
- 1.4 什么是虚拟机栈 3 4
- 总结
- 1.6 能不能解释一下方法区? 3 3
- 1.6.1 概述
- 1.6.2 常量池
- 1.6.3 运行时常量池
- 总结
- 1.7 你听过直接内存吗? 3 3
- 总结
- 2. 类加载器
- 2.1 什么是类加载器,类加载器有哪些? 4 3
- 总结
- 2.2 什么是双亲委派模型?4 4
- 2.3 JVM为什么采用双亲委派机制
- 总结
- 2.4 说一下类装载的执行过程? 5 3
- 总结
- 3. 垃圾收回 这一章节问的很多在面试中
- 3.2 对象什么时候可以被垃圾器回收 4 4
- 3.2.1 引用计数法
- 3.2.2 可达性分析算法
- 总结
- 3.3 JVM 垃圾回收算法有哪些? 3 4
- 3.3.1 标记清除算法 用的最少,多的是后面两种
- 3.3.2 复制算法
- 3.3.3 标记整理算法
- 面试文稿
- 3.4 说一下JVM中的分代回收(3.4 分代收集算法) 3 4
- 面试文稿
- 3.5 说一下 JVM 有哪些垃圾回收器?4 4
- 3.5.1 串行垃圾收集器
- 3.5.2 并行垃圾收集器
- 3.5.3 CMS(并发)垃圾收集器
- 面试文稿
- 3.6 详细聊一下G1垃圾回收器 4 4
- 3.6.1 概述
- 3.6.2 Young Collection(年轻代垃圾回收)
- 3.6.3 Young Collection + Concurrent Mark (年轻代垃圾回收+并发标记)
- 3.6.4 Mixed Collection (混合垃圾回收)
- 面试文稿
- 3.7 强引用、软引用、弱引用、虚引用的区别?4 3
- 3.7.1 强引用
- 3.7.2 软引用
- 3.7.3 弱引用
- 3.7.4 虚引用
- 面试文稿
- 4. JVM实践(调优)
- 4.1 JVM 调优的参数可以在哪里设置参数值? 2 3
- 4.1.1 tomcat的设置vm参数
- 4.1.2 springboot项目jar文件启动
- 面试文稿
- 4.2 用的 JVM 调优的参数都有哪些?3 4
- 设置堆空间大小
- 虚拟机栈的设置
- 年轻代中Eden区和两个Survivor区的大小比例
- 年轻代晋升老年代阈值
- 设置垃圾回收收集器
- 面试文稿
- 4.3 说一下 JVM 调优的工具? 4 4
- 4.3.1 命令工具
- 4.3.1.1 jps(Java Process Status)
- 4.3.1.2 jstack
- 4.3.1.3 jmap
- 4.3.1.4 jhat
- 4.3.1.5 jstat
- 4.3.2 可视化工具
- 4.3.2.1 jconsole
- 4.3.2.2 VisualVM
- 5.面试文稿总结
- 5.1 JVM组成
- 5.2 类加载器
- 5.3 垃圾回收
- 5.4 JVM实践(调优)
前言
为什么要学JVM,应对面试,中高级程序员必备,深入了解JAVA。
1. JVM组成
1.1 JVM由那些部分组成,运行流程是什么?3 4
JVM是什么?
Java Virtual Machine Java程序的运行环境(java二进制字节码的运行环境)。
好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收机制
JVM由哪些部分组成,运行流程是什么?
从图中可以看出 JVM 的主要组成部分
- ClassLoader(类加载器)
- Runtime Data Area(运行时数据区,内存分区)
- Execution Engine(执行引擎)
- Native Method Library(本地库接口)
运行流程:
(1)类加载器(ClassLoader)把Java代码转换为字节码。
(2)运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行。
(3)执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU去执行,此时需要调用其他语言的本地库接口(Native Method Library)来实现整个程序的功能。
以下是JVM常见的20多个面试题。
1.2 什么是程序计数器?3 4
程序计数器:线程私有的,每个线程一份,内部保存字节码的行号。用于记录正在执行的字节码指令的地址。
javap -verbose xx.class 打印堆栈大小,局部变量的数量和方法的参数。我们可以得到下图。
java虚拟机对于多线程是通过线程轮流切换并且分配线程执行时间。在任何的一个时间点上,一个处理器只会处理执行一个线程,如果当前被执行的这个线程它所分配的执行时间用完了【挂起】。处理器会切换到另外的一个线程上来进行执行。并且这个线程的执行时间用完了,接着处理器就会又来执行被挂起的这个线程。
那么现在有一个问题就是,当前处理器如何能够知道,对于这个被挂起的线程,它上一次执行到了哪里?那么这时就需要从程序计数器中来回去到当前的这个线程他上一次执行的行号,然后接着继续向下执行。
程序计数器是JVM规范中唯一一个没有规定出现OOM的区域,所以这个空间也不会进行GC。
总结
程序计数器:线程私有的,每个线程一份,内部保存字节码的行号。用于记录正在执行的字节码指令的地址。
1.3 你能给我详细的介绍Java堆吗? 3 4
线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
- 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象(对象会从Eden到Survivor再到老年代)将被移动到老年代区间。
- 老年代主要保存生命周期长的对象,一般是一些老的对象
- 元空间保存的类信息、静态变量、常量、编译后的代码
为了避免方法区出现OOM,所以在java8中将堆上的方法区【永久代】给移动到了本地内存上,重新开辟了一块空间,叫做元空间。那么现在就可以避免掉OOM的出现了。
总结
-
线程共享的区域:主要用来保存对象实例,数组等,内存不够则抛出OutOfMemoryError异常。
-
组成:年轻代+老年代
- 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区
- 老年代主要保存生命周期长的对象,一般是一些老的对象
-
Jdk1.7和1.8的区别
1.7中有有一个永久代,存储的是类信息、静态变量、常量、编译后的代码1.8移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出
1.4 什么是虚拟机栈 3 4
Java Virtual machine Stacks (java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈,顺序是先进后出
- 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
-
垃圾回收是否涉及栈内存?垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放,所以不涉及栈内存。
-
栈内存分配越大越好吗?未必,默认的栈内存通常为1024k,栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半。
-
方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
- 比如以下代码:
第一个不安全是因为形参,第二个是因为返回值(如果主函数中调用了m3方法,并且使用了其返回值)。这样都可能会造成两个线程对一个变量进行操作,导致不安全。
-
栈内存溢出情况,什么时候会导致栈溢出?栈帧过多导致栈内存溢出,典型问题:递归调用(如下图所示)、栈帧过大导致栈内存溢出.。
总结
- 什么是虚拟机栈
每个线程运行时所需要的内存,称为虚拟机栈
每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 - 垃圾回收是否涉及栈内存?
垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放 - 栈内存分配越大越好吗?
未必,默认的栈内存通常为1024k,栈帧过大会导致线程数变少 - 方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 - 什么情况下会导致栈内存溢出?
栈帧过多导致栈内存溢出,典型问题:递归调用
栈帧过大导致栈内存溢出 - 堆栈的区别是什么?
栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会,栈内存是线程私有的,而堆内存是线程共有的。
两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。
1.6 能不能解释一下方法区? 3 3
1.6.1 概述
- 方法区(Method Area)是各个线程共享的内存区域
- 主要存储类的信息、运行时常量池
- 虚拟机启动的时候创建,关闭虚拟机时释放
- 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace
1.6.2 常量池
可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
查看字节码结构(类的基本信息、常量池、方法定义)javap -v xx.class
比如下面是一个Application类的main方法执行,源码如下:
java">public class Application {public static void main(String[] args) {System.out.println("hello world");}}
找到类对应的class文件存放目录,执行命令:javap -v Application.class
查看字节码结构。
java">D:\code\jvm-demo\target\classes\com\heima\jvm>javap -v Application.classClassfile /D:/code/jvm-demo/target/classes/com/heima/jvm/Application.classLast modified 2023-05-07; size 564 bytes //最后修改的时间MD5 checksum c1b64ed6491b9a16c2baab5061c64f88 //签名Compiled from "Application.java" //从哪个源码编译public class com.heima.jvm.Application //包名,类名minor version: 0major version: 52 //jdk版本flags: ACC_PUBLIC, ACC_SUPER //修饰符Constant pool: //常量池#1 = Methodref #6.#20 // java/lang/Object."<init>":()V#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;#3 = String #23 // 往下找到 #23 发现是hello world#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V#5 = Class #26 // com/heima/jvm/Application#6 = Class #27 // java/lang/Object#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 LocalVariableTable#12 = Utf8 this#13 = Utf8 Lcom/heima/jvm/Application;#14 = Utf8 main#15 = Utf8 ([Ljava/lang/String;)V#16 = Utf8 args#17 = Utf8 [Ljava/lang/String;#18 = Utf8 SourceFile#19 = Utf8 Application.java#20 = NameAndType #7:#8 // "<init>":()V#21 = Class #28 // java/lang/System#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;#23 = Utf8 hello world#24 = Class #31 // java/io/PrintStream#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V#26 = Utf8 com/heima/jvm/Application#27 = Utf8 java/lang/Object#28 = Utf8 java/lang/System#29 = Utf8 out#30 = Utf8 Ljava/io/PrintStream;#31 = Utf8 java/io/PrintStream#32 = Utf8 println#33 = Utf8 (Ljava/lang/String;)V{public com.heima.jvm.Application(); //构造方法descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/heima/jvm/Application;public static void main(java.lang.String[]); //main方法descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String hello world5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 7: 0line 8: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 args [Ljava/lang/String;}SourceFile: "Application.java"
下图,左侧是main方法的指令信息,右侧 constant pool 是常量池
main方法按照指令执行的时候,需要到常量池中查表翻译找到具体的类和方法地址去执行
#23这种, 还能在常量池中继续往下查找,比如#23最后代表hello world。
1.6.3 运行时常量池
常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
总结
- 能不能解释一下方法区?
方法区(Method Area)是各个线程共享的内存区域
主要存储类的信息、运行时常量池
虚拟机启动的时候创建,关闭虚拟机时释放
如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError:Metaspace - 介绍一下运行时常量池
常量池:可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名方法名、参数类型、字面量等信息
当类被加载,"它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
1.7 你听过直接内存吗? 3 3
直接内存:并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存,常见于 NIO (相对于BIO吞吐量高很多)操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高。
举例:
需求,在本地电脑中的一个较大的文件(超过100m)从一个磁盘挪到另外一个磁盘
java"> /**
演示 ByteBuffer 作用
*/
public class Demo1_9 {static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";//一共100多Mbstatic final String TO = "E:\\a.mp4";static final int _1Mb = 1024 * 1024;public static void main(String[] args) {io(); // io 用时:1535.586957 1766.963399 1359.240226directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592}private static void directBuffer() {long start = System.nanoTime();try (FileChannel from = new FileInputStream(FROM).getChannel();FileChannel to = new FileOutputStream(TO).getChannel();) {ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);while (true) {int len = from.read(bb);if (len == -1) {break;}bb.flip();to.write(bb);bb.clear();}} catch (IOException e) {e.printStackTrace();}long end = System.nanoTime();System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);}private static void io() {long start = System.nanoTime();try (FileInputStream from = new FileInputStream(FROM);FileOutputStream to = new FileOutputStream(TO);) {byte[] buf = new byte[_1Mb];while (true) {int len = from.read(buf);if (len == -1) {break;}to.write(buf, 0, len);}} catch (IOException e) {e.printStackTrace();}long end = System.nanoTime();System.out.println("io 用时:" + (end - start) / 1000_000.0);}
}
可以发现,使用传统的IO的时间要比NIO操作的时间长了很多了,也就说NIO的读性能更好。
这个是跟我们的JVM的直接内存是有一定关系,如下图,是传统阻塞IO的数据传输流程。
首先我们知道JAVA本身并不具备磁盘读写的能力,它本身需要调用OS提供的函数,即native修饰的方法来操作磁盘文件,这里面就涉及CPU的状态,从用户态切换到内核态, 然后才可以通过两个缓冲区来一部分一部分的读取磁盘文件,java代码是无法访问到系统缓冲区的,这时候就涉及到不必要的数据复制(磁盘文件复制到系统缓冲区,系统缓冲区复制到java缓冲区)。
下图是NIO传输数据的流程,在这个里面主要使用到了一个直接内存,不需要在堆中开辟空间进行数据的拷贝,jvm可以直接操作直接内存,从而使数据读写传输更快。
总结
你听过直接内存吗?
直接内存并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统。
直接内存常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JM 内存回收管理
2. 类加载器
2.1 什么是类加载器,类加载器有哪些? 4 3
要想理解类加载器的话,务必要先清楚对于一个Java文件,它从编译到执行的整个过程。
- 类加载器:用于装载字节码文件(.class文件) (到运行时数据区)
- 运行时数据区:用于分配存储空间 (再进行执行即可)
- 执行引擎:执行字节码文件或本地方法
- 垃圾回收器:用于对JVM中的垃圾内容进行回收
类加载器
JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。
现有的类加载器基本上都是java.lang.ClassLoader的子类,该类的只要职责就是用于将指定的类找到或生成对应的字节码文件,同时类加载器还会负责加载程序所需要的资源。
类加载器种类
类加载器根据各自加载范围的不同,划分为四种类加载器:
- 启动类加载器(BootStrap ClassLoader):
- 该类并不继承ClassLoader类,其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的(核心)类库(jar包)。
- 扩展类加载器(ExtClassLoader):
- 该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录(其实就是扩展目录)中的类库(jar包)。
- 应用类加载器(AppClassLoader):
- 该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。
- 自定义类加载器:
- 开发者自定义类继承ClassLoader,实现自定义类加载规则。
上述三种类加载器的层次结构如下如下:
类加载器的体系并不是“继承”体系,而是委派体系,类加载器首先会到自己的parent中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次。
总结
1.什么是类加载器
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。
2.类加载器有哪些
- 启动类加载器(BootStrap ClassLoader):加载JAVA HOME/jre/lib目录下的库
- 扩展类加载器(ExtClassLoader):主要加载JAVA HOME/jre/lib/ext目录中的类
- 应用类加载器(AppClassLoader):用于加载classPath下的类,一般用来加载自己写的代码。
- 自定义类加载器(CustomizeClassLoader):自定义类继承ClassLoader,实现自定义类加载规则。
2.2 什么是双亲委派模型?4 4
加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类。上面四种加载器的继承关系如下图所示,上面的是下面的父类。
如果加载String类,会向上委托,扩展加载器是否能够加载,再向上,启动加载器能否加载,启动加载器回到对应目录下进行寻找,找到了String就可以进行加载。
2.3 JVM为什么采用双亲委派机制
(1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
(2)为了安全,保证类库API不会被修改
在工程中新建java.lang包,接着在该包下新建String类,并定义main函数
java">public class String {public static void main(String[] args) {System.out.println("demo info");}}
此时执行main函数,会出现异常,在类 java.lang.String 中找不到 main 方法。
出现该信息是因为由双亲委派的机制,java.lang.String的在启动类加载器(Bootstrap classLoader)得到加载,因为在核心jre库中有其相同名字的类文件,但该类中并没有main方法。这样就能防止恶意篡改核心API库。
总结
- 什么是双亲委派模型?
加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类 - JVM为什么采用双亲委派机制?
- 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
- 为了安全,保证类库API不会被修改
2.4 说一下类装载的执行过程? 5 3
类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。
类加载过程详解
1.加载
- 通过类的全名,获取类的二进制数据流。
- 解析类的二进制数据流为方法区内的数据结构(Java类模型)
- 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
现在person类被类加载器加载后,就会存储在运行时数据区,在运行时数据区中,有两块区域进行存储,当然作用是不同的,方法区存储的是类的信息,比如person类的构造函数,方法,字段等类的结构, 堆中存取person类的class对象,作用就是后期创建person对象的时候都需要继续person.class的class对象来创建,把两个对象的对象头指向person.class的class对象,然后通过它,找到方法区中的person类的信息,获取其数据结构,来来使用其中的字段,方法等创建两个对象。
2.验证
3.准备
为类变量分配内存并设置类变量初始值
- static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成 下面b
- static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成 下面c d
- static变量是final的引用类型,那么赋值也会在初始化阶段完成 下面obj
4.解析
把类中的符号引用转换为直接引用
比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。(直接找到真正的方法,而不是符号)
5.初始化
对类的静态变量,静态代码块执行初始化操作
- 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
- 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
6.使用
JVM 开始从入口方法开始执行用户的程序代码
- 调用静态类成员信息(比如:静态字段、静态方法)
- 使用new关键字为其创建对象实例
7.卸载
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存
总结
说一下类装载的执行过程?
- 加载:查找和导入class文件
- 验证:保证加载类的准确性
- 准备:为类变量分配内存并设置类变量初始值
- 解析:把类中的符号引用转换为直接引用
- 初始化:对类的静态变量,静态代码块执行初始化操作
- 使用:JVM 开始从入口方法开始执行用户的程序代码
- 卸载:当用户程序代码执行完毕后,JM便开始销毁创建的Class对象。
3. 垃圾收回 这一章节问的很多在面试中
3.2 对象什么时候可以被垃圾器回收 4 4
垃圾在堆中。
简单一句就是:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法。
3.2.1 引用计数法
一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收。
java">String demo = new String("123");
我们新建一个String变量,此时它的引用为1。
java"> String demo = null;
当对象间出现了循环引用的话,则引用计数法就会失效。
先执行右侧代码的前4行代码
目前上方的引用关系和计数都是没问题的,但是,如果代码继续往下执行,如下图
虽然a和b都为null,但是由于a和b存在循环引用,这样a和b永远都不会被回收。
优点:
- 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
- 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报OOM错误。
- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。
缺点:
- 每次对象被引用时,都需要去更新计数器,有一点时间开销。
- 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
- 无法解决循环引用问题,会引发内存泄露。(最大的缺点)
3.2.2 可达性分析算法
现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。
会存在一个根节点【GC Roots】,引出它下面指向的下一个节点,再以下一个节点节点开始找出它下面的节点,依次往下类推。直到所有的节点全部遍历完毕。
核心是:判断某对象是否与根对象有直接或间接的引用,如果没有被引用,则可以当做垃圾回收。
哪些对象可以作为GC Root呢?
这里视频中,我感觉上面文字不对,应该是,比如第三个是方法区中常量引用可以作为GC ROOT,比如上图右下角的变量a。(GPT:GC Root 本身并不是对象,而是对象的引用(或根节点的起始点)。)
总结
对象什么时候可以被垃圾器回收
如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾如果定位了垃圾,则有可能会被垃圾回收器回收。
定位垃圾的方式有两种
- 引用计数法
- 可达性分析篡法
3.3 JVM 垃圾回收算法有哪些? 3 4
3.3.1 标记清除算法 用的最少,多的是后面两种
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
- 根据可达性分析算法得出的垃圾进行标记
- 对这些标记为可回收的内容进行垃圾回收
可以看到,标记清除算法解决了引用计数算法中的循环引用的问题,没有从root节点引用的对象都会被回收。
优点:标记和清除速度较快
缺点:碎片化较为严重,内存不连贯的
3.3.2 复制算法
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。
1)将内存区域分成两部分,每次操作其中一个。
2)当进行垃圾回收时,将正在使用的内存区域中的存活对象移动到另一块区域中未使用的内存区域。当移动完对这部分内存区域一次性清除。
3)周而复始。
优点:
- 在垃圾对象多的情况下,效率较高
- 清理后,内存无碎片
缺点:
- 分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低
3.3.3 标记整理算法
标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而解决了碎片化的问题。
1)标记垃圾。
2)需要清除向右边走,不需要清除的向左边走。
3)清除边界以外的垃圾。
优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。
与复制算法对比:复制算法标记完就复制,但标记整理算法得等把所有存活对象都标记完毕,再进行整理
面试文稿
JVM 垃圾回收算法有哪些?
- 标记清除算法:垃圾回收分为2个阶段,分别是标记和清除,效率高,有磁盘碎片,内存不连续
- 标记整理算法:标记清除算法一样,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低
- 复制算法:将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低
3.4 说一下JVM中的分代回收(3.4 分代收集算法) 3 4
面试文稿
说一下JVM中的分代回收
一、堆的区域划分
- 堆被分为了两份:新生代和老年代【1:2】
- 对于新生代,内部又被分为了三个区域。Eden区,幸存者区survivor(分成from和to)【8:1:1】
二、对象回收分代回收策略
- 新创建的对象,都会先分配到eden区
- 当伊甸园内存不足,标记伊甸园与from(现阶段没有)的存活对象
- 将存活对象采用复制算法复制到to中,复制完毕后,伊甸园和from 内存都得到释放
- 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将其复制到from区
- 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会提前晋升)
MinorGc、Mixed Gc、FullGC的区别是什么
MinorGC【young GC】发生在新生代的垃圾回收,暂停时间短(STW)
Mixed GC 新生代 +老年代部分区域的垃圾回收,G1 收集器特有
FullGC:新生代+老年代完整垃圾回收,暂停时间长(STW),应尽力避免
STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成
3.5 说一下 JVM 有哪些垃圾回收器?4 4
在jvm中,实现了多种垃圾收集器,包括:
- 串行垃圾收集器
- 并行垃圾收集器
- CMS(并发)垃圾收集器
- G1垃圾收集器
3.5.1 串行垃圾收集器
Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑
- Serial 作用于新生代,采用复制算法
- Serial Old 作用于老年代,采用标记-整理算法
垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
3.5.2 并行垃圾收集器
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器
- Parallel New作用于新生代,采用复制算法
- Parallel Old作用于老年代,采用标记-整理算法
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
3.5.3 CMS(并发)垃圾收集器
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。
初始标记通过可达性分析算法中来标记,这时候只会标记跟GC ROOT直接关联的对象,这时候会有STW,再进行并发标记,这时候会找到BCD对象,这时候其他线程可以运行,再进行重新标记,为什么重新呢,防止X原来在前两个标记阶段,没有被GC ROOT连接,但是随着其他线程运行,又连接上了的话,这时候我们应该把X标记为存活对象,后者也存在B与D现在不关联了,我们应该垃圾回收D。
面试文稿
说一下JVM有哪些垃圾回收器?
在jvm中,实现了多种垃圾收集器,包括
- 串行垃圾收集器:Serial Gc、Serial Old GC
- 并行垃圾收集器:Parallel Old GC、ParNewGC
- CMS(并发)垃圾收集器:CMSGC,作用在老年代
- G1垃圾收集器,作用在新生代和老年代
3.6 详细聊一下G1垃圾回收器 4 4
3.6.1 概述
- 应用于新生代和老年代,在JDK9之后默认使用G1
- 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
- 采用复制算法
- 响应时间与吞吐量兼顾
- 分成三个阶段:新生代回收、并发标记、混合收集
- 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
3.6.2 Young Collection(年轻代垃圾回收)
-
初始时,所有区域都处于空闲状态
-
创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象(伊甸园区是有数量限制的)
-
当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象(用可达性分析算法),需要暂停用户线程
其他的区域释放掉。
-
随着时间流逝,伊甸园的内存又有不足
-
将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代
3.6.3 Young Collection + Concurrent Mark (年轻代垃圾回收+并发标记)
当老年代占用内存超过阈值(默认是45%,触发条件)后,触发并发标记,这时无需暂停用户线程(也就是并发执行)
- 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。
- 这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)。
3.6.4 Mixed Collection (混合垃圾回收)
产生一个新的S,或者有些可以放入新的O中。
复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集
下图中H叫做巨型对象,如果对象非常大,会开辟一块连续的空间存储巨型对象。
面试文稿
详细聊一下G1垃圾回收器
- 应用于新生代和老年代,在JDK9之后默认使用G1
- 划分成多个区域,每个区域都可以充当eden,survivor,old,humongous其中 humongous 专为大对象准备
- 采用复制算法
- 响应时间与吞吐量兼顾
- 分成三个阶段:新生代回收(stw)、并发标记(重新标记stw)、混合收集如果并发失败(即回收速度赶不上创建新对象速度),会触发 FuGC
3.7 强引用、软引用、弱引用、虚引用的区别?4 3
不同引用垃圾回收方式不同。
3.7.1 强引用
强引用:只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
java">User user = new User();
3.7.2 软引用
软引用:仅有软引用引用该对象时,在垃圾回收后(第一次这个对象不会被回收),内存仍不足时会再次触发垃圾回收(第二次会)
java">User user = new User();
SoftReference softReference = new SoftReference(user);
3.7.3 弱引用
弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
java">User user = new User();
WeakReference weakReference = new WeakReference(user);
延伸话题:ThreadLocal内存泄漏问题
ThreadLocal用的就是弱引用,看以下源码:
java">static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v; //强引用,不会被回收}
}
Entry的key是当前ThreadLocal,value值是我们要设置的数据。
WeakReference表示的是弱引用,当JVM进行GC时,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。但是value是强引用,它不会被回收掉。
ThreadLocal使用建议:使用完毕后注意调用清理方法。
3.7.4 虚引用
虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存。
面试文稿
强引用、软引用、弱引用、虚引用的区别?
- 强引用:只要所有 GC Roots 能找到,就不会被回收
- 软引用:需要配合SoftReference使用,当垃圾多次回收,内存依然不够的时候会回收软引用对象
- 弱引用:需要配合WeakReference使用,只要进行了垃圾回收,就会把弱引用对象回收
- 虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队由 Reference Handler 线程调用虚引用相关方法释放直接内存
4. JVM实践(调优)
4.1 JVM 调优的参数可以在哪里设置参数值? 2 3
4.1.1 tomcat的设置vm参数
修改TOMCAT_HOME/bin/catalina.sh文件,如下图
java">JAVA_OPTS="-Xms512m -Xmx1024m"
4.1.2 springboot项目jar文件启动
通常在linux系统下直接加参数启动springboot项目
java">nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &
nohup : 用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行
参数 & :让命令在后台执行,终端退出后命令仍旧执行。
面试文稿
JVM 调优的参数可以在哪里设置参数值
war包部署在tomcat中设置修改TOMCAT HOME/bin/catalina.sh文件
jar包部署在启动参数设置java -Xms512m -Xmx1024m -jar xxxx.jar
4.2 用的 JVM 调优的参数都有哪些?3 4
对于JVM调优,主要就是调整年轻代、年老大、元空间的内存空间大小及使用的垃圾回收器类型。
官网比较全,下面只介绍几个主要的
设置堆空间大小
虚拟机栈的设置
每个线程默认会开启1M的内存,用于存放、调用参数、局部变量等,但一般256K就够用。通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。栈太小,虽然线程多了,但是容易栈内存溢出。建议设置256K或者512K。
对每个线程stack大小的调整,-Xss128kXss
年轻代中Eden区和两个Survivor区的大小比例
设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1。通过增大Eden区的大小来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优。
XXSurvivorRatio=8,表示年轻代中的分配比率:survivor:eden=2:8
年轻代晋升老年代阈值
-XX:MaxTenuringThreshold=threshold
默认为12,即挪动12次晋升到老年代
取值范围0-15
设置垃圾回收收集器
通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器。
XX:+UseParallelGC
XX:+UseRarallelOldGC
-XX:+UseG1GC // 使用G1的垃圾回收器
面试文稿
用的 JVM 调优的参数都有哪些? 至少说前三条
设置堆空间大小
虚拟机栈的设置
设置垃圾回收收集器(使用哪种)
年轻代中Eden区和两个Survivor区的大小比例
年轻代晋升老年代阈值
4.3 说一下 JVM 调优的工具? 4 4
命令工具
- jps 进程状态信息
- jstack 查看java进程内线程的堆栈信息
- jmap 查看堆转信息
- jhat 堆转储快照分析工具
- jstat JVM统计监测工具
可视化工具
- jconsol 用于对jvm的内存,线程,类的监控
- VisualVM 能够监控线程,内存情况
4.3.1 命令工具
4.3.1.1 jps(Java Process Status)
输出JVM中运行的进程状态信息(现在一般使用jconsole)
4.3.1.2 jstack
查看java进程内线程的堆栈信息。
java">jstack [option] <pid> //pid就是上面查到的进程的id
java案例
java">package com.heima.jvm;public class Application {public static void main(String[] args) throws InterruptedException {while (true){Thread.sleep(1000);System.out.println("哈哈哈");}}
}
使用jstack查看进行堆栈运行信息(如果发生死锁就可以用jstack查看线程运行的情况)
4.3.1.3 jmap
用于生成堆转内存快照、内存使用情况
jmap [options] pid 内存映像信息
jmap -heap pid 显示Java堆的信息
jmap -dump:format=b,file=heap.hprof pid
format=b表示以hprof二进制格式转储Java堆的内存
file=用于指定快照dump文件的文件名。
这个文件如何打开呢,之后我们再打开。
知识小贴士
它是一个进程或系统在某一给定的时间的快照。比如在进程崩溃时,甚至是任何时候,我们都可以通过工具将系统或某进程的内存备份出来供调试分析用。dump文件中包含了程序运行的模块信息、线程信息、堆栈调用信息、异常信息等数据,方便系统技术人员进行错误排查。
例:显示了某一个java运行的堆信息
java">C:\Users\yuhon>jmap -heap 53280
Attaching to process ID 53280, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.321-b07using thread-local object allocation.
Parallel GC with 8 thread(s) //并行的垃圾回收器Heap Configuration: //堆配置MinHeapFreeRatio = 0 //空闲堆空间的最小百分比MaxHeapFreeRatio = 100 //空闲堆空间的最大百分比MaxHeapSize = 8524922880 (8130.0MB) //堆空间允许的最大值NewSize = 178257920 (170.0MB) //新生代堆空间的默认值MaxNewSize = 2841640960 (2710.0MB) //新生代堆空间允许的最大值OldSize = 356515840 (340.0MB) //老年代堆空间的默认值NewRatio = 2 //新生代与老年代的堆空间比值,表示新生代:老年代=1:2SurvivorRatio = 8 //两个Survivor区和Eden区的堆空间比值为8,表示S0:S1:Eden=1:1:8MetaspaceSize = 21807104 (20.796875MB) //元空间的默认值CompressedClassSpaceSize = 1073741824 (1024.0MB) //压缩类使用空间大小MaxMetaspaceSize = 17592186044415 MB //元空间允许的最大值G1HeapRegionSize = 0 (0.0MB)//在使用 G1 垃圾回收算法时,JVM 会将 Heap 空间分隔为若干个 Region,该参数用来指定每个 Region 空间的大小。Heap Usage:
PS Young Generation
Eden Space: //Eden使用情况capacity = 134217728 (128.0MB)used = 10737496 (10.240074157714844MB)free = 123480232 (117.75992584228516MB)8.000057935714722% used
From Space: //Survivor-From 使用情况capacity = 22020096 (21.0MB)used = 0 (0.0MB)free = 22020096 (21.0MB)0.0% used
To Space: //Survivor-To 使用情况capacity = 22020096 (21.0MB)used = 0 (0.0MB)free = 22020096 (21.0MB)0.0% used
PS Old Generation //老年代 使用情况capacity = 356515840 (340.0MB)used = 0 (0.0MB)free = 356515840 (340.0MB)0.0% used3185 interned Strings occupying 261264 bytes.
4.3.1.4 jhat
用于分析jmap生成的堆转存快照(一般不推荐使用,而是使用Ecplise Memory Analyzer)
4.3.1.5 jstat
jstat是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。
4.3.2 可视化工具
4.3.2.1 jconsole
用于对jvm的内存,线程,类 的监控,是一个基于jmx 的 GU性能监控工具。
打开方式:java 安装目录 bin目录下 直接启动jconsole.exe 就行
4.3.2.2 VisualVM
能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈。JDK8以及之前有,现在没了
打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行。
5.面试文稿总结
5.1 JVM组成
面试官:JVM由那些部分组成,运行流程是什么?
候选人:
嗯,好的~~
在JVM中共有四大部分,分别是ClassLoader(类加载器)、Runtime Data Area(运行时数据区,内存分区)、Execution Engine(执行引擎)、Native Method Library(本地库接口)
它们的运行流程是:
第一,类加载器(ClassLoader)把Java代码转换为字节码
第二,运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是由执行引擎运行
第三,执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口(Native Method Library)来实现整个程序的功能。
面试官:好的,你能详细说一下 JVM 运行时数据区吗?
候选人:
嗯,好~
运行时数据区包含了堆、方法区、栈、本地方法栈、程序计数器这几部分,每个功能作用不一样。
- 堆解决的是对象实例存储的问题,垃圾回收器管理的主要区域。
- 方法区可以认为是堆的一部分,用于存储已被虚拟机加载的信息,常量、静态变量、即时编译器编译后的代码。
- 栈解决的是程序运行的问题,栈里面存的是栈帧,栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈与栈功能相同,本地方法栈执行的是本地方法,一个Java调用非Java代码的接口。
- 程序计数器(PC寄存器)程序计数器中存放的是当前线程所执行的字节码的行数。JVM工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。
面试官:好的,你再详细介绍一下程序计数器的作用?
候选人:
嗯,是这样~
java虚拟机对于多线程是通过线程轮流切换并且分配线程执行时间。在任何的一个时间点上,一个处理器只会处理执行一个线程,如果当前被执行的这个线程它所分配的执行时间用完了【挂起】。处理器会切换到另外的一个线程上来进行执行。并且这个线程的执行时间用完了,接着处理器就会又来执行被挂起的这个线程。这时候程序计数器就起到了关键作用,程序计数器在来回切换的线程中记录他上一次执行的行号,然后接着继续向下执行。
面试官:你能给我详细的介绍Java堆吗?
候选人:
好的~
Java中的堆术语线程共享的区域。主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
在JAVA8中堆内会存在年轻代、老年代
1)Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用。在Eden区变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。
2)Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区。
面试官:能不能解释一下方法区?
候选人:
好的~
与虚拟机栈类似。本地方法栈是为虚拟机执行本地方法时提供服务的。不需要进行GC。本地方法一般是由其他语言编写。
面试官:你听过直接内存吗?
候选人:
嗯~~
直接内存(Direct Memory)是指在 Java 中通过 JVM 提供的机制,直接在堆之外分配的一块内存区域,这块内存不受 JVM 的垃圾回收机制管理,而是由操作系统直接分配和释放,是虚拟机的系统内存,常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高
面试官:什么是虚拟机栈
候选人:
虚拟机栈是描述的是方法执行时的内存模型,是线程私有的,生命周期与线程相同,每个方法被执行的同时会创建栈桢。保存执行方法时的局部变量、动态连接信息、方法返回地址信息等等。方法开始执行的时候会进栈,方法执行完会出栈【相当于清空了数据】,所以这块区域不需要进行 GC。
面试官:能说一下堆栈的区别是什么吗?
候选人:
嗯,好的,有这几个区别
第一,栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会。
第二、栈内存是线程私有的,而堆内存是线程共有的。
第三、两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。
5.2 类加载器
面试官:什么是类加载器,类加载器有哪些?
候选人:
嗯,是这样的
JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。
常见的类加载器有4个
第一个是启动类加载器(BootStrap ClassLoader):其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。
第二个是扩展类加载器(ExtClassLoader):该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。
第三个是应用类加载器(AppClassLoader):该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。
第四个是自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则。
面试官:说一下类装载的执行过程?
候选人:
嗯,这个过程还是挺多的。
类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)
1.加载:查找和导入class文件
2.验证:保证加载类的准确性
3.准备:为类变量分配内存并设置类变量初始值
4.解析:把类中的符号引用转换为直接引用
5.初始化:对类的静态变量,静态代码块执行初始化操作
6.使用:JVM 开始从入口方法开始执行用户的程序代码
7.卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存
面试官:什么是双亲委派模型?
候选人:
嗯,它是是这样的。
如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传说到顶层的启动类加载器中,只有当父类加载器返回自己无法完成这个加载请求(它的搜索返回中没有找到所需的类)时,子类加载器才会尝试自己去加载
面试官:JVM为什么采用双亲委派机制
候选人:
主要有两个原因。
第一、通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
第二、为了安全,保证类库API不会被修改
5.3 垃圾回收
面试官:简述Java垃圾回收机制?(GC是什么?为什么要GC)
候选人:
嗯,是这样~
为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC(Garbage Collection)。
有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。
在进行垃圾回收时,不同的对象引用类型,GC会采用不同的回收时机
面试官:强引用、软引用、弱引用、虚引用的区别?
候选人:
嗯嗯~
- 强引用最为普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其进行回收
- 软引用表示一个对象处于有用且非必须状态,如果一个对象处于软引用,在内存空间足够的情况下,GC机制并不会回收它,而在内存空间不足时,则会在OOM异常出现之间对其进行回收。但值得注意的是,因为GC线程优先级较低,软引用并不会立即被回收。
- 弱引用表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收。同样的,因为GC线程优先级较低,所以弱引用也并不是会被立刻回收。
- 虚引用表示一个对象处于无用的状态。在任何时候都有可能被垃圾回收。虚引用的使用必须和引用队列Reference Queue联合使用
面试官:对象什么时候可以被垃圾器回收
候选人:
思考一会~~
如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法
通常都使用可达性分析算法来确定是不是垃圾
面试官: JVM 垃圾回收算法有哪些?
候选人:
我记得一共有四种,分别是标记清除算法、复制算法、标记整理算法、分代回收
面试官: 你能详细聊一下分代回收吗?
候选人:
关于分代回收是这样的
在java8时,堆被分为了两份:新生代和老年代,它们默认空间占用比例是1:2
对于新生代,内部又被分为了三个区域。Eden区,S0区,S1区默认空间占用比例是8:1:1
具体的工作机制是有些情况:
1)当创建一个对象的时候,那么这个对象会被分配在新生代的Eden区。当Eden区要满了时候,触发YoungGC。
2)当进行YoungGC后,此时在Eden区存活的对象被移动到S0区,并且当前对象的年龄会加1,清空Eden区。
3)当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和S0中的对象,移动到S1区中,这些对象的年龄会加1,清空Eden区和S0区。
4)当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和S1中的对象,移动到S0区中,这些对象的年龄会加1,清空Eden区和S1区。
5)对象的年龄达到了某一个限定的值(默认15岁 ),那么这个对象就会进入到老年代中。
当然也有特殊情况,如果进入Eden区的是一个大对象,在触发YoungGC的时候,会直接存放到老年代
当老年代满了之后,触发FullGC。FullGC同时回收新生代和老年代,当前只会存在一个FullGC的线程进行执行,其他的线程全部会被挂起。 我们在程序中要尽量避免FullGC的出现。
面试官:讲一下新生代、老年代、永久代的区别?
候选人:
嗯!是这样的,简单说就是
新生代主要用来存放新生的对象。
老年代主要存放应用中生命周期长的内存对象。
永久代指的是永久保存区域。主要存放Class和Meta(元数据)的信息。在Java8中,永久代已经被移除,取而代之的是一个称之为“元数据区”(元空间)的区域。元空间和永久代类似,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存的限制。
面试官:说一下 JVM 有哪些垃圾回收器?
候选人:
在jvm中,实现了多种垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器(JDK8默认)、CMS(并发)垃圾收集器、G1垃圾收集器(JDK9默认)
面试官:Minor GC、Major GC、Full GC是什么
候选人:
嗯,其实它们指的是不同代之间的垃圾回收
Minor GC 发生在新生代的垃圾回收,暂停时间短
Major GC 老年代区域的垃圾回收,老年代空间不足时,会先尝试触发Minor GC。Minor GC之后空间还不足,则会触发Major GC,Major GC速度比较慢,暂停时间长
Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免
5.4 JVM实践(调优)
面试官:JVM 调优的参数可以在哪里设置参数值?
候选人:
我们当时的项目是springboot项目,可以在项目启动的时候,java -jar中加入参数就行了
面试官:用的 JVM 调优的参数都有哪些?
候选人:
嗯,这些参数是比较多的
我记得当时我们设置过堆的大小,像-Xms和-Xmx
还有就是可以设置年轻代中Eden区和两个Survivor区的大小比例
还有就是可以设置使用哪种垃圾回收器等等。具体的指令还真记不太清楚。
面试官:嗯,好的,你们平时调试 JVM都用了哪些工具呢?
候选人:
嗯,我们一般都是使用jdk自带的一些工具,比如
jps 输出JVM中运行的进程状态信息。
jstack查看java进程内线程的堆栈信息。
jmap 用于生成堆转存快照。
jstat用于JVM统计监测工具。
还有一些可视化工具,像jconsole和VisualVM等。
java_1053">面试官:假如项目中产生了java内存泄露,你说一下你的排查思路?
候选人:
嗯,这个我在之前项目排查过。
第一呢,可以通过jmap指定打印他的内存快照 dump文件,不过有的情况打印不了(比如项目闪退或者直接执行不起来),我们会设置vm参数让程序自动生成dump文件。
第二,可以通过工具去分析 dump文件,jdk自带的VisualVM就可以分析。
第三,通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题。
第四,找到对应的代码,通过阅读上下文的情况,进行修复即可。
面试官:好的,那现在再来说一种情况,就是说服务器CPU持续飙高,你的排查方案与思路?
候选人:
嗯,我思考一下~
可以这么做~~
第一,可以使用使用top命令查看占用cpu的情况
第二,通过top命令查看后,可以查看是哪一个进程占用cpu较高,记录这个进程id
第三,可以通过ps 查看当前进程中的线程信息,看看哪个线程的cpu占用较高
第四,可以jstack命令打印进行的id,找到这个线程,就可以进一步定位问题代码的行号