目录
4. 假如项目中产生了java内存泄露,你说一下你的排查思路?
5. 好的,那现在再来说一种情况,就是说服务器CPU持续飙高,你的排查方案与思路?
Java 中 WeakReference 与 SoftReference 的区别?
11. 怎么获取 Java 程序使用的内存?堆使用的百分比?
JVM 底层 与 GC(Garbage Collection)的面试题答案
2、Serial 与 Parallel GC 之间的不同之处?
3、32 位和 64 位的 JVM,int 类型变量的长度是多少?
4、32 位 JVM 和 64 位 JVM 的最大堆内存分别是多少?
6、 新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
- 不同的操作系统内部会有不同的系统函数,Java技术的最大特征就在于可以与任意的操作系统整合在一起,包括Windows、Linux、MacOS,只要有支持的虚拟机就可以解决Java程序开发的问题,但是这些都是表象,因为即便Java性能再高,它也是通过JVM调用了本地操作系统的函数库,那么只要是调用就一定会有所谓的性能偏差。
1. JVM 调优的参数可以在哪里设置参数值?
我们一般在IDEA中设置的参数都是临时参数,一般我们所关心的都是指在项目部署的时候参数设置的方式,我们一般在项目开发过程中有这么两种种部署项目的方式:
- war包部署在tomcat中设置:第一种是我们的项目在Tomcat中进行部署,它是一个war包。
- jar包部署在启动参数设置:第二种是SpringBoot项目,它的启动方式是比较方便的,使用的是jar包来去启动项目,所以说,我们的参数也都是在项目启动的时候进行设置的。
-
我们当时的项目是 springboot 项目,可以在项目启动的时候, java -jar 中加入参数就行了。
war包部署在tomcat中设置:
- 在Tomcat的安装目录中有一个bin目录,修改TOMCAT_HOME/bin/catalina.sh文件。sh结尾主要指的是Linux下的Tomcat;如果是Windows下的,则是bat结尾。
jar包部署在启动参数设置:
- 通常在linux系统下(在启动命令上)直接加参数启动springboot项目
- 一般我们启动jar包使用的都是Java的命令:java - jar(Java的启动参数),在项目启动的时候,java -jar中加入参数就行了。
- nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &
- nohup : 用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行
- 参数 & :让命令在后台执行,终端退出后命令仍旧执行。
2. JVM调优的参数都有哪些?
调内存、调比例、加机器~!
对于JVM调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型。
调优的思路就是尽量让对象在新生代/青年代就被回收,防止过多的对象晋升到老年代,减少大对象的分配。
Java HotSpot VM 选项 (oracle.com)https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html
- 设置堆空间大小
- 虚拟机栈的设置
- 年轻代中Eden区和两个Survivor区的大小比例
- 年轻代晋升老年代阈值
- 设置垃圾回收收集器
JVM内存参数
要求
-
熟悉常见的 JVM 参数,尤其和大小相关的
2.1 堆内存,按大小设置
解释:
-
-Xms 最小堆内存,设置堆的初始大小(包括新生代和老年代)
-
-Xmx 最大堆内存,设置堆的最大大小(包括新生代和老年代)x为max,表示最大的堆内存
-
不指定默认单位为Byte字节;如果指定单位,则按照指定的单位设置
-
通常建议将 -Xms 与 -Xmx 设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好
-
-XX:NewSize 与 -XX:MaxNewSize 设置新生代的最小与最大值,但一般不建议设置,由 JVM 自己控制
-
-Xmn 设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等
-
保留是指,一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存。下同
堆空间设置多少合适?
- 最大大小的默认值是物理内存的1/4,初始大小是物理内存的1/64
- 堆大小,可能会频繁的导致年轻代和老年代的垃圾回收,会产生STW,暂停用户线程
- 堆内存大肯定是好的,但也存在风险,比如假如发生了Full GC,它会扫描整个堆空间,暂停用户线程的时间长
- 设置参考推荐:尽量大,但也要考察一下当前计算机其它程序的内存使用情况
2.2 堆内存,按比例设置
解释:
-
-XX:NewRatio=2:1 表示老年代:新生代 表示老年代占两份,新生代占一份
-
-XX:SurvivorRatio=4:1 表示新生代中eden区与survivor区的比例,表示新生代分成六份,伊甸园占四份,from 和 to 各占一份
-
表示对象的存活年龄从年轻代晋升老年代阈值:-XX:MaxTenuringThreshold=threshold
-
默认为15 取值范围0-15
-XX:-DisableExplicirGC
-
表示是否(+表示是,-表示否)打开 GC 日志
-XX:PretenureSizeTheshold:当对象达到多大时直接存放在老年代
当发生OOM时自动dump(备份)内存文件:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath="要保存的路径"
2.3 元空间内存设置
解释:
-
class space 存储类的最基本的信息(如类的名称、类的方法入口),最大值受 -XX:CompressedClassSpaceSize(默认是1个G) 控制
-
non-class space 存储除类的基本信息以外的其它信息(如方法字节码、注解等)
-
class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制,默认没有上限,直接使用电脑的物理内存
注意:
-
这里 -XX:CompressedClassSpaceSize 这段空间还与是否开启了指针压缩有关,这里暂不深入展开,可以简单认为指针压缩默认开启
2.4 代码缓存(CodeCache)内存设置
解释:
- 机器码就是存在CodeCache里面的,代码缓存区就是用来缓存编译后的机器码的。
-
如果 -XX:ReservedCodeCacheSize < 240m,所有优化机器代码不加区分存在一起
-
否则,分成三个区域(图中笔误 mthod 拼写错误,少一个 e)
-
non-nmethods - JVM 自己用的代码
-
profiled nmethods - 部分优化的机器码
-
non-profiled nmethods - 完全优化的机器码
-
2.5 线程内存设置
- -Xss:设置线程栈或虚拟机栈的最大内存空间
- Java虚拟机中的栈容量可以沟通过-Xss参数来设置,默认值为1MB。
- 栈容量决定了线程客户以使用的栈空间大小,栈空间用于存储线程的局部变量、方法参数、返回值等数据。栈容量过小可能会导致栈溢出错误,而栈容量过大则会占用过多的内存中内存资源。
- -Xss:对每个线程stack大小的调整,控制每个线程(也就是虚拟机栈)占用的内存的。
- 虚拟机栈的设置:用于存放栈帧、调用参数、局部变量等,但一般256K就够用。-Xss256k
- 如果不设置,它的大小是跟操作系统有关,如果是64位的Linux操作系统,每个线程会占用1M的内存。
2.6 设置垃圾回收收集器
- 通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器。
- Parallel GC是JDK8默认的垃圾回收器
- -XX:+UseParallelGC
- -XX:+UseParallelOldGC
- 也可以设置垃圾回收器为G1:-XX:+UseG1GC
3. 说一下JVM调优的工具有哪些?
我们一般都是使用jdk自带的一些工具(命令工具):
- jps(Java Process Status) 输出JVM中运行的进程状态信息
- jstack[option] <pid> pid指的就是进程ID,查看java进程内线程的堆栈信息
- jmap 用于生成堆转内存快照、内存使用情况,查看堆转信息
- pid指进程ID,tid指线程ID
jmap -heap pid 显示Java堆的信息
jmap -dump:format=b,file=heap.hprof pid
- format=b表示以hprof二进制格式转储Java堆的内存
- file=用于指定快照dump文件的文件名。
- dump文件它是一个进程或系统在某一给定的时间的快照。比如在进程崩溃时,甚至是任何时候,我们都可以通过工具将系统或某进程的内存备份出来供调试分析用。
- dump文件中包含了程序运行的模块信息、线程信息、堆栈调用信息、异常信息等数据,方便系统技术人员进行错误排查。
- jstat JVM统计监测工具
- jstat是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。
- ①:总结垃圾回收统计:jstat -gcutil pid
- ②:垃圾回收统计:jstat -gc pid
可视化工具:
- jconsole 用于对JVM的内存,线程,类等进行监控
- jvisualVM JDK自带的全能分析工具,可以分析内存快照、线程快照、程序死锁,监控内存的变化,能够监控线程,内存情况
jconsole
- 用于对jvm的内存,线程,类的监控,是一个基于 jmx 的 GUI 性能监控工具
- 打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行
VisualVM
- 能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈
- 打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行(只有JDK1.8有,高于JDK1.8的要单独下载)
4. 假如项目中产生了java内存泄露,你说一下你的排查思路?
内存泄漏通常是指堆内存,通常是指一些大对象不被回收的情况。
- 第一呢可以通过jmap指定打印它的内存快照 dump文件(Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中),jmap命令只能在项目运行的时候去使用jmap -dump:format=b,file=heap.hprof pid
- 不过有的情况打印不了,我们会设置vm参数让程序自动生成dump文件 XX:+HeapDumpOnOutOfMemoryError XX:HeapDumpPath=/home/app/dumps/(dump文件存储的路径)
- 第二,可以通过工具去分析 dump文件,jdk自带的VisualVM就可以分析,VisualVM可以加载离线的dump文件:文件-->装入--->选择dump文件即可查看堆快照信息
- 第三,通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
- 第四,找到对应的代码,通过阅读上下文的情况,进行修复即可。
dump文件以.hprof结尾。
什么是宕机?
- 在计算机系统中,“宕机”(Crash)是指硬件或软件故障导致系统无法继续正常工作的状态。宕机通常是指系统发生了不可恢复的错误,导致系统服务无法正常运行并彻底停止。
- 宕机可能出现在操作系统、硬件、应用程序等各个层面,可以是内存溢出、CPU 过载、磁盘损坏等硬件故障,也可以是应用程序崩溃、数据库连接断开等软件故障。无论是硬件故障还是软件故障,都可能导致系统宕机。
- 在企业级应用程序中,需要对系统宕机进行有效的监控和处理。通常需要使用监控系统来检测系统的运行状况,及时发现异常并进行报警和处理。此外,也需要设计系统容错、高可用性等机制,以减少系统宕机的风险和影响。
5. 好的,那现在再来说一种情况,就是说服务器CPU持续飙高,你的排查方案与思路?
- 使用top命令查看占用cpu的情况
- 通过top命令查看后,可以查看是哪一个进程占用cpu较高,记录这个进程id
- 可以通过ps H这个命令查看当前进程中所有的线程信息,看看哪个线程的cpu占用较高(十进制)ps H -eo pid,tid,%cpu | grep +进程ID pid指进程ID,tid指线程ID,并记住这个进程ID和线程ID
- 可以jstack命令打印进程的id(jstack + 进程ID),通过jstack来打印出整个进程中所有线程的信息,(jstack中显示的都是十六进制的线程,而线程ID - tid是十进制,将十进制转成16进制,接着找 nid = 这个16进制的线程ID的,然后找到这个线程,看线程打印的日志,就可以进一步定位问题代码的行号,通过查看jstack分析详情,得到当前线程占用过高是由于死循环还是死锁等原因导致的)
将十进制数转化为十六进制数:
- 通过 jmap生成dump文件,使用JDK自带的 jvisualvm 导入dump文件来进行分析
6. JVM垃圾回收 --- 三色标记与并发漏标问题
三色标记算法是一种JVM中垃圾标记的算法,它可以减少JVM在GC过程中的STW时长,它是CMS、G1等垃圾收集器中主要的标记算法。
在出现三色标记算法之前,JVM中垃圾对象的标记主要采用可达性分析算法以及引用计数法,如果采用引用计数法,但是这两种算法存在一些问题:
1. 引用计数法存在循环引用问题:如果两个对象互相引用,就形成了一个环形结构,如果采用引用计数法的话,那么这两个对象讲永远无法被回收。
2. 可达性分析算法STW时间长:可达性分析的整个过程都需要STW,以避免对象的状态发生改变,这就导致GC停顿时间很长,大大影响应用的整体性能。
为了解决上面的这些问题,就引入了三色标记法。
三色标记
即用三种颜色记录对象的标记状态,三色标记法将对象分为三种状态:白色、灰色和黑色。
-
白色:该对象没有被标记过
-
黑色:该对象已经被标记过了,并且它的全部引用对象也都标记完了
-
灰色:该对象已经被标记过了,但该对象的引用对象还没标记完
三色标记法的标记过程可以分为三个阶段:初始标记(Initial Marking)、并发标记(Concurrent Marking)和重新标记(Remark)。
- 初始标记:遍历所有的根对象,将根对象和直接引用的对象标记为灰色,会STW - Stop The World。
- 并发标记: 不需要STW
- 重新标记:重新标记的主要作用通过是标记在并发阶段中被修改的对象以及未被遍历到的对象,这个过程中,垃圾回收器会将被引用的对象标记为灰色,并将已经遍历过的对象标记为黑色(会STW - Stop The World)~!
- 在重新标记阶段结束之后,垃圾回收器会执行清除操作,将未被标记为可达对象的垃圾对象进行回收,从而释放内存空间,在这个过程中,垃圾回收器会将所有未被标记的对象标记为白色(White)。
7. 项目中什么情况下会内存溢出,怎么解决的
- 误用线程池导致的内存溢出(误用固定大小线程池和带缓冲线程池)
- (单次)查询数据量太大导致的内存溢出
- 动态生成的类过多导致的内存溢出
一个ArrayList对象占24个字节,一个ArrayList对象里面有一个Object[ ]数组,数组都是占16个字节。
8. 类加载过程、双亲委派机制
类加载过程的三个阶段
加载
将类的字节码载入方法区,并创建类.class 对象(类对象)
如果此类的父类没有加载,先加载父类
加载是懒惰执行(就是你真正用到这个类时,它才会触发它的类的加载,否则它是不会提前把类加载到这个方法区中的)
链接
验证 – 验证类是否符合 Class 字节码规范,合法性、安全性检查
准备 – 为 类中的静态static 变量分配空间,设置默认值,此时赋值语句并不会执行
解析 – 将常量池中的符号引用解析为直接引用
初始化
编译器会把静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个
<cinit>
类的初始化方法,在类的初始化时 / 阶段被调用。静态变量在初始化时赋值。static final 修饰的基本类型变量赋值,在链接阶段就已完成
初始化是懒惰执行(真正用到它时才会进行初始化,没有用到它时不会进行初始化)
注意:类加载只会执行一次。
- 类加载是懒惰的,首次用到时才加载(下面的初始化条件满足也会导致类加载)
- 使用了类.class
- 用类加载器的loadClass()方法加载类
- 类初始化是懒惰的,满足条件有
- main方法所在类
- 首次访问静态方法或者静态变量(非final)
- 子类初始化,导致的父类初始化
- Class.forName(类名,true,loader)或Class.forName(类名)
- new,clone,反序列化时
验证手段
使用 jps 查看进程号
使用 jhsdb 调试,执行命令
jhsdb.exe hsdb
打开它的图形界面
Class Browser 可以查看当前 jvm 中加载了哪些类
控制台的 universe 命令查看堆内存范围
控制台的 g1regiondetails 命令查看 region 详情
scanoops 起始地址 结束地址 对象类型
可以根据类型查找某个区间内的对象地址控制台的
inspect 地址
指令能够查看这个地址对应的对象详情使用 javap 命令可以查看 class 字节码 javap -c -v -p 字节码名称(类名.class)
javap -v xx.class 打印堆栈大小,局部变量的数量和方法的参数。
javap -v Application.class 查看字节码结构(类的基本信息、常量池、方法定义)
第一步是先编译代码而不是运行!!!
类对象在java.lang.Class包下。
final修饰的静态变量跟普通static修饰的静态变量到底有什么不一样?
System.out是一个静态变量。
- 使用final修饰的基本类型的变量不会触发类加载
- 使用final修饰的引用类型的变量会导致类的加载
- 每个类都有一个自己的常量池
什么是常量池(Constant Pool)?
就是你的类的运行过程中需要用到的一些常量数据,这些常量数据包括类名....
9. 四种引用
要求:掌握四种引用
面试题:对象引用类型分为哪几类?
强引用
- 普通变量赋值即为强引用,如 A a = new A();
-
只要使用赋值运算符就是建立了变量跟对象之间的强引用关系。
-
通过 GC Root 的引用链,只要所有GC ROOT能找到,就不会被回收,如果强引用不到该对象,该对象才能被回收
软引用(SoftReference)
-
例如:SoftReference a = new SoftReference(new A());
-
软引用需要配合SoftReference使用
-
如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象(软引用自身占用的内存不会被释放)
-
软引用自身需要配合引用队列来释放
-
典型例子是反射数据(反射数据都是软引用的)
弱引用(WeakReference)
-
例如:WeakReference a = new WeakReference(new A());
-
弱引用需要配合WeakReference使用
-
如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
-
弱引用自身需要配合引用队列来释放
-
典型例子是 ThreadLocal,ThreadLocal用的就是弱引用,看以下源码:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v; //强引用,不会被回收
}
}
Map 中的 Entry 对象实现 / 使用了弱引用(也就是内存不够时,它会把Entry对象的key占用内存给它释放掉)
Java 中 WeakReference 与 SoftReference 的区别?
- 虽然 WeakReference 与 SoftReference 都有利于提高 GC 和 内存的效率, 但是 WeakReference ,一旦失去最后一个强引用,就会被 GC 回收,而软引用 虽然不能阻止被回收,但是可以延迟到 JVM 内存不足的时候。
虚引用(PhantomReference)
-
例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);
-
必须配合引用队列(ReferenceQueue)一起使用,当虚引用所引用的对象被回收时,会将虚引用对象入队,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理,由Reference Handler线程释放其关联的外部资源(因为有些Java对象回收后,它们所关联的一些外部资源不是Java占用的,也不是Java的内存,可能占用的是直接内存等,而这些内存的释放需要等Java对象被垃圾回收掉了,才去释放外部的这些资源内存)
-
典型例子是 虚引用有一个实现类 --- Cleaner 释放 DirectByteBuffer 关联的直接内存
- 引用队列的作用:目的都是为了找到哪些Java对象被垃圾回收,从而进行对它们关联的资源进一步清理。
- 为了简化api难度,从jdk9开始引入了Cleaner对象(java.lang.ref.Cleaner)。
- 获取Cleaner对象:Cleaner cleaner = Cleaner.create();Cleaner线程它是一个守护线程。
10. finalize
要求:掌握 finalize 的工作原理与缺点
面试题:请你说一下对finalize方法的理解
finalize
-
一般回答:它是 java.lang.Object 类中的一个方法,也就是说每一个对象都有这么个方法,如果子类重写它,垃圾回收时此方法会被调用,可以在其中进行资源释放和清理工作。
-
一个对象的finalize()方法只会被调用一次,finalize()方法被调用不一定会立即回收该对象,所以有可能调用finalize()方法后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面被调用过一次,所以不会再次调用finalize()方法了,进而产生问题,因为不推荐使用finalize()方法。
-
优秀回答:将资源释放和清理放在 finalize 方法中非常不好,非常影响性能,严重时甚至会引起 OOM,从 Java9 开始就被标注为 @Deprecated,不建议被使用了。
但是,为什么呢?
11. 怎么获取 Java 程序使用的内存?堆使用的百分比?
- 可以通过 java.lang.Runtime 类中与内存相关方法来获取剩余的内存,总内存及 最大堆内存。
- 通过这些方法你也可以获取到堆使用的百分比及堆内存的剩余空间。 Runtime.freeMemory() 方法返回剩余空间的字节数,Runtime.totalMemory() 方法总内存的字节数,Runtime.maxMemory() 返回最大内存的字节数。
JVM 底层 与 GC(Garbage Collection)的面试题答案
1、64 位 JVM 中,int 的长度是多数?
- Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位。意思就 是说,在 32 位 和 64 位 的 Java 虚拟机中,int 类型的长度是相同的。
2、Serial 与 Parallel GC 之间的不同之处?
- Serial 与 Parallel 在 GC 执行的时候都会引起 stop-the-world。
- 它们之间主要 不同 serial 收集器是默认的复制收集器,执行 GC 的时候只有一个线程,而 parallel 收集器使用多个 GC 线程来执行。
3、32 位和 64 位的 JVM,int 类型变量的长度是多少?
- 32 位和 64 位的 JVM 中,int 类型变量的长度是相同的,都是 32 位或者 4 个字节。
4、32 位 JVM 和 64 位 JVM 的最大堆内存分别是多少?
- 理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB,但实际上会比这个 小很多。不同操作系统之间不同,如 Windows 系统大约 1.5 GB,Solaris 大约 3GB。
- 64 位 JVM 允许指定最大的堆内存,理论上可以达到 2^64,这是一个非 常大的数字,实际上你可以指定堆内存大小到 100GB。甚至有的 JVM,如 Azul, 堆内存到 1000G 都是可能的。
5、JRE、JDK、JVM 及 JIT 之间有什么不同?
- JRE 代表 Java 运行时(Java run-time),是运行 Java 引用所必须的。JDK 代 表 Java 开发工具(Java development kit),是 Java 程序的开发工具,如 Java 编译器,它也包含 JRE。JVM 代表 Java 虚拟机(Java virtual machine),它 的责任是运行 Java 应用。
- JIT 代表即时编译(Just In Time compilation),当 代码执行的次数超过一定的阈值时,会将 Java 字节码转换为本地代码,如,主 要的热点代码会被转换为本地代码,这样有利大幅度提高 Java 应用的性能。
6、 新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
-
新生代回收器:Serial、ParNew、Parallel Scavenge
-
老年代回收器:Serial Old、Parallel Old、CMS
-
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;
老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
7、Java 中堆和栈有什么区别?
- JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局 部变量,而对象总是在堆上分配。
- 栈通常都比堆小,也不会在多个线程之间共享, 而堆被整个 JVM 的所有线程共享。