🍎作者简介:硕风和炜,CSDN-Java领域新星创作者🏆,保研|国家奖学金|高中学习JAVA|大学完善JAVA开发技术栈|面试刷题|面经八股文|经验分享|好用的网站工具分享💎💎💎
🍎座右铭:人生如棋,我愿为卒,行动虽慢,可谁曾见我后退一步?🎯🎯🎯
🍎关注我,私信回复面试题 ,获取《2万字8千字72道大厂JVM面试题【金三银四(金九银十)面试小抄之Java经典面试题JVM篇总结】(附答案)》pdf🍪🍪🍪
目录
- 一.前言
- 二.Java经典面试题之JVM篇
- 1.什么是JDK、JRE、JVM?
- 2.JVM内存模型
- 3.JVM中的类加载机制你有了解过吗?
- 3.1 装载
- 3.2 链接
- 3.2.1 验证校验
- 3.2.2 准备
- 3.2.3 解析
- 3.3 初始化
- 3.4 使用
- 3.5 卸载
- 4.JVM中你知道的类加载器有哪些?作用是什么?分别用来加载什么文件?什么内容的呢?
- 4.1 Bootstrap ClassLoader 启动类加载器的作用以及加载的文件内容
- 4.2 Extension ClassLoader 扩展类加载器的作用以及加载的文件内容
- 4.3 Application ClassLoader 应用程序类加载器的作用以及加载的文件内容
- 4.4 Custom ClassLoader 自定义类加载器的作用以及加载的文件内容
- 5.类加载器之间的关系模型
- 6.什么是双亲委派机制?(父类委托机制)
- 7.JVM的三种类加载机制
- 8.如何打破双亲委派机制?
- 9.打破双亲委派机制的案例?
- 10.什么是SPI机制呢?
- 11.沙箱机制
- 12.双亲委派机制的优势
- 13.双亲委派机制的缺点
- 14.运行时数据区的结构都有哪些?
- 15.运行时数据区的结构一共有五个部分
- 16.哪些是共享的呢?哪些是非共享的呢?
- 17.详细的介绍一下运行时数据区结构各部分的作用?
- 18.JVM运行时数据区的方法区可以详细聊聊吗?
- 19.什么是Perm Space?什么是Meta Space?
- 20.为什么要使用元数据区域?
- 21.JVM运行时数据区的虚拟机栈你知道吗?
- 22.虚拟机栈的基本结构是什么呢?
- 23.栈帧各个结构的作用是什么呢?
- 24.栈帧结构中的动态链接?
- 25.堆的内存布局?
- 26.堆为什么会进行分代设计?
- 27.为什么需要Survivor区?只有Eden不行吗?
- 28.为什么需要两个Survivor区?
- 29.为什么Eden:s0:s1是8:1:1?
- 30.什么是分配担保机制?
- 31.对象创建后在堆中分配内存的过程?
- 32.类的实例化过程
- Java对象的内存布局
- 33.为什么提出TLAB呢?
- 34.你了解TLAB吗?
- 35.堆中分配内存的俩种解决方案?
- 36.TLAB堆上内存分配是怎么样的?
- 37.Thread Local Allocation Buffer的注意事项
- 38.怎么判断一个对象是否为垃圾?
- 39.可达法分析算法中能作为GC Root?
- 40.什么时候才会进行垃圾回收?
- 41.对象被判定为不可达对象之后就会立即被回收吗?
- 42.介绍一下强引用、软引用、弱引用、虚引用的区别?
- 42.1 强引用
- 42.2 软引用
- 42.3 弱引用
- 42.4虚引用
- 43.说说你知道的垃圾回收算法?
- 43.1 标记-复制(Mark-Copying)
- 43.2 标记-清除(Mark-Sweep)算法
- 43.3 标记-整理(Mark-Compact)算法
- 43.4 分代收集算法
- 44.垃圾收集器
- 44.1 Serial
- 44.2 Serial Old
- 44.3 ParNew
- 44.4 Parallel Scavenge
- 44.5 Parallel Old
- 45.CMS垃圾回收器?
- 46.CMS在并发标记的时候可能会产生浮动垃圾,什么是浮动垃圾呢?
- 47.G1(Garbage-First)垃圾回收器?
- 48.ZGC垃圾回收器?
- 49.垃圾回收器好坏评价的标准?吞吐量和响应时间?
- 50.生产环境中,如何选择合适的垃圾收集器
- 51.如何判断是否使用G1垃圾收集器
- 52.JVM调优经验-JVM调优常用命令
- 53.G1调优策略
- 54.JVM性能监控工具
- 54.1 jconsole
- 54.2 jvisualvm
- 54.3 arthas
- 55.JVM内存分析工具
- 55.1 MAT(Memory Analyzer Tool)
- 55.2 heaphero
- 55.3 perfma
- 56.JVM日志分析工具
- 56.1 GCViewer
- 56.2 gceasy
- 56.3 gcplot
- 57.JVM性能调优的原则有哪些?
- 58.什么情况下需要JVM调优?
- 59.在JVM调优时,你关注哪些指标?
- 60.JVM常用参数有哪些?
- 61.线上排查问题的一般流程是怎么样的?
- 62.什么情况下,会抛出OOM呢?
- 63.**系统OOM之前都有哪些现象?**
- 64.如何进行堆Dump文件分析?
- 65.如何进行GC日志分析?
- 66.线上死锁是如何排查的?
- 67.如何解决线上gc频繁的问题?
- 68.内存溢出的原因有哪些,如何排查线上问题?
- 69.线上YGC耗时过长优化方案有哪些?
- 70.线上频繁FullGC优化方案有哪些?
- 71.如何进行线上堆外内存泄漏的分析?(Netty尤其居多)
- 72.线上元空间内存泄露优化方案有哪些?
- 三.下节预告
- 四.共勉
一.前言
之前的一篇文章帮大家总结了一篇关于Java基础篇的面试文章,大家可以直接进行跳转学习,地址附到了下面。6万字144道耗时72小时吐血整理【金三银四(金九银十)面试小抄之Java经典面试题基础篇总结】(附答案)
上篇文章最后我们说了接下来要为大家整理JVM的面试题,君子一言,驷马难追,必须给大家安排到位。这篇文章也是干货满满,花费了很多时间和精力,如果你看到了这里,先赶快给作者点个关注再开始吧,感谢你的鼓励,好了,那我们也不说废话,直接上干货。
顺便提一嘴,部分图片知识引用来自恩师马士兵教育,常怀感恩之心,特此感谢。
二.Java经典面试题之JVM篇
1.什么是JDK、JRE、JVM?
JDK、JRE、JVM三者之间的层次关系如下图所示:
- JDK是Java Development Kit的缩写,是Java的开发工具包。JDK : Java Development ToolKit(Java开发工具包)。JDK是整个JAVA的核心,包括了Java运行环境(JRE),Java工具(javac/java/jdb等)和Java基础的类库(即Java API )。
- JRE(Java Runtime Environment,Java运行环境),包含JVM标准实现及Java核心类库。 JRE中包含了Java virtual machine(JVM),runtime class libraries和Java application launcher,这些是运行Java程序的必要组件。通过它,Java的开发者才得以将自己开发的程序发布到用户手中,让用户使用。
- JVM(Java Virtual Mechinal),Java虚拟机,是JRE的一部分。它是整个java实现跨平台的最核心的部分,负责解释执行字节码文件,是可运行java字节码文件的虚拟计算机。所有平台的上的JVM向编译器提供相同的接口,而编译器只需要面向虚拟机,生成虚拟机能识别的代码,然后由虚拟机来解释执行。
2.JVM内存模型
总结的非常详细,该图片来自马士兵教育,大家可以收藏起来,对于建立自己的知识体系非常有帮助。
3.JVM中的类加载机制你有了解过吗?
3.1 装载
- 将我们编译好的classFile以字节流的形式通过类加载器加载到我们的内存中
- 将我们字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在我们的堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中数据的访问入口
3.2 链接
3.2.1 验证校验
- 文件格式验证
- 元数据验证-----是否符合Java的语言规范
- 字节码验证------确定程序语义合法,符合逻辑
- 符号引用验证------确保下一步的解析能正常执行
3.2.2 准备
- 为类的静态变量分配内存,并赋值(当前类型的默认值)
private static int a = 1;那么它在我们的准备阶段他的值就是0
3.2.3 解析
- 解析是从运行时常量池中的符号引用动态确定具体值的过程
符号引用(方法引用、属性引用等等)转为直接引用
3.3 初始化
- 方法执行到了Clint阶段,初始化静态变量的值。
- 初始化静态代码块,如果存在父子静态代码块,先初始化父类静态代码块,然后再初始化子类静态代码块。然后再初始化当前类的父类,最后初始化当前类的子类。
3.4 使用
程序之间的相互调用,通过引用、赋值等一系列的操作对其进行使用。
3.5 卸载
最后当我们不使用的时候,也就是销毁一个对象的时候,一般情况下中有JVM垃圾回收器完成。代码层面的销毁只是将引用置为null。
4.JVM中你知道的类加载器有哪些?作用是什么?分别用来加载什么文件?什么内容的呢?
4.1 Bootstrap ClassLoader 启动类加载器的作用以及加载的文件内容
- 启动类加载器主要用来加载$JAVA_HOME中jre/lib/rt.jar里所有的class
- Xbootclasspath选项指定的jar包
4.2 Extension ClassLoader 扩展类加载器的作用以及加载的文件内容
- 加载Java平台中扩展功能的一些jar包,包括$JAVA_HOME中的jre/lib/*.jar
- -Djava.ext.dirs指定目录下的jar包
4.3 Application ClassLoader 应用程序类加载器的作用以及加载的文件内容
- 加载classpath中指定的jar包
- Djava.class.path所指定目录下的类和jar包
4.4 Custom ClassLoader 自定义类加载器的作用以及加载的文件内容
- 通过java.lang.ClassLoader的子类自定义加载的class,属于应用程序。
- 根据自身需要自定义的ClassLoader,如Tomcat,Jboss都会根据j2ee规范自行实现ClassLoader
5.类加载器之间的关系模型
6.什么是双亲委派机制?(父类委托机制)
- 检查某个类是否已经加载
自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个Classloader已加载,
就视为已加载此类,保证此类只所有ClassLoader加载一次。 - 加载的顺序
自顶向下,也就是由上层来逐层尝试加载此类。父类加载,子类不加载。
7.JVM的三种类加载机制
- 全盘负责 :就是当一个类加载器负责加载某个Class 时,该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托 :就是当一个类加载器负责加载某个Class时,先让父类加载器试图加载该 Class ,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制 :保证所有加载过的 Class 都会被缓存,当程序需要使用某个 Class 对象时,类加载器先从缓存区中搜索该 Class ,只有当缓存区中不存在该 Class对象 时,系统才会读取该类对应的二进制数据,并将其转换成 Class对象 ,存储到缓存区
8.如何打破双亲委派机制?
- 双亲委派模型的第一次“被破坏”是重写自定义加载器的loadClass(),jdk不推荐。一般都只是重写findClass(),这样可以保持双亲委派机制.而loadClass方法加载规则由自己定义,就可以随心所欲的加载类
- 双亲委派模型的第二次“被破坏”是ServiceLoader和Thread.setContextClassLoader()。即线程上下文类加载器(contextClassLoader)。双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢?线程上下文类加载器就出现了。
- SPI。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
- 线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。,所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题
- 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
9.打破双亲委派机制的案例?
打破双亲委派机制的场景有很多:JDBC、JNDI、Tomcat等等,这块展开来说就比较多了,刚兴趣的同学可以先自行学习,后续我们会专门讲一讲这个怎么Tomcat打破双亲委派的典型案例。
10.什么是SPI机制呢?
SPI机制是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在SpringBoot中就使用到了SPI机制。
11.沙箱机制
我们创建一个自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
12.双亲委派机制的优势
- 双亲机制可以避免类的重复加载
- 沙箱机制保护程序安全,防止核心API被随意篡改
自定义类:java.lang.String(报错:阻止创建 java.lang开头的类)
13.双亲委派机制的缺点
- 优势很突出,问题也比较明显,检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
14.运行时数据区的结构都有哪些?
运行时数据区的结构如下图所示:
15.运行时数据区的结构一共有五个部分
方法区、堆、虚拟机栈、本地方法栈、程序计数器
16.哪些是共享的呢?哪些是非共享的呢?
线程共享:堆(实例 对象) 方法区(类信息 静态变量 常量 编译后的代码)
线程私有:程序计数器(记录并保存此时线程执行的位置) 本地方法栈 虚拟机栈
17.详细的介绍一下运行时数据区结构各部分的作用?
- 方法区:线程共享的区域,方法区是逻辑上堆的一部分,所以它有个名字:非堆。方法区包括运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化和接口初始化中使用的特殊方法,如果方法区域中的内存无法满足分配请求,Java 虚拟机将抛出一个
OutOfMemoryError
- 堆:线程共享 堆是为所有类实例和数组分配内存的运行时数据区域 内存不足
OutOfMemoryError
- java虚拟机栈:执行java方法的 线程私有的
StackOverflowError、OutOfMemoryError
- 本地方法栈:执行本地方法 线程私有
StackOverflowError、OutOfMemoryError
- 程序计数器:记录程序执行到的位置 线程私有
18.JVM运行时数据区的方法区可以详细聊聊吗?
- 方法区是各个线程共享的内存区域,在虚拟机启动时创建
- 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非
堆),目的是与Java堆区分开来 - 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
19.什么是Perm Space?什么是Meta Space?
JVM运行时数据区是一种规范,方法区真正的实现在JDK 8中体现的就是Metaspace,在JDK6或7中就是Perm Space
Perm Space 持久代 JDK在1.7方法区的实现,占用的是JVM中的内存。
Meta Space 元空间、元数据区 JDK在1.8方法区的实现,占用的是直接内存。
20.为什么要使用元数据区域?
有可能我们的项目很大,占用了JVM中大量的内存,可能直接启动就Full GC,非常灵活,不好控制,所以使用元数据区替代了直接永久代。
21.JVM运行时数据区的虚拟机栈你知道吗?
- 虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的
运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创
建。 - 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
22.虚拟机栈的基本结构是什么呢?
- 虚拟机栈的最小组成单位:栈帧,栈帧的基本作用:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
- 每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
23.栈帧各个结构的作用是什么呢?
- 局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。
- 操作数栈:以压栈和出栈的方式存储操作数。
- 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
- 方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。
24.栈帧结构中的动态链接?
-
动态链接是为了支持方法的动态调用过程 。
-
动态链接将这些符号方法引用转换为具体的方法引用。
-
符号引用转变为直接引用,为了支持java的多态。
25.堆的内存布局?
一块是非堆区(方法区),一块是堆区
堆区分为两大块,一个是Old区,一个是Young区
Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区
S0和S1一样大,也可以叫From和To区
26.堆为什么会进行分代设计?
方法区在逻辑上又叫非堆
将内存按照对象的生命周期的长短分为Old区和Young区,
但90%+以上的对象都是朝生夕死的。
年轻代年龄为15后,去到Old区。
对象的分代年龄,存储在对象的对象头中的Markwork中。
27.为什么需要Survivor区?只有Eden不行吗?
- 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。
- 老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了FullGC)。
- 老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。
- 频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。
所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
28.为什么需要两个Survivor区?
为了解决Young区内存空间的不连续性、空间碎片?
将Young区继续分为Eden区和Survivor区,Survivor区分为s0和s1,
Survivor区分为俩个主要是为了能够清空另外一个空间。
最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。
29.为什么Eden:s0:s1是8:1:1?
Eden区域越小,就会不停的触发我们的GC,GC是会不断的消耗我们的性能。
新生代中的可用内存:复制算法用来担保的内存为9:1
可用内存中Eden:S1区为8:1
即新生代中Eden:S1:S2 = 8:1:1
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是“朝生夕死”的。
这个就是一个经验值,大量的实验得出的最佳结论。
30.什么是分配担保机制?
一些大对象的产生,在Eden区中是没有位置存放的,这个时候会有一个担保机制的出现,Old区作为这个大对象的担保,即使这个对象的年龄是没有达到15的,依旧可以被存储到Old区中,但是,这样的大对象的产生,是非常不好的,可能会随时出发Full GC,对我们的内存空间的产生可谓是灾难性的。
31.对象创建后在堆中分配内存的过程?
细心一点,看一下下面这个图就会非常清晰的知道对象创建后在堆中分配内存的过程。
32.类的实例化过程
- 类加载
- 分配内存
- 初始化零值
- 状态设置
- 构造函数
上面对应下面具体执行过程:
- 在JVM中,对象的创建遵循如下过程: 当JVM遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。
- 内存分配完成之后,虚拟机必须将分配到的内存空间都初始化为零值,如果使用了TLAB(Thread Loacl Allocation Buffer)的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
- 接下来,虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
- 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始——构造函数,即Class文件中的
<init>()
方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。 一般来说,new指令之后会接着执行<init>()
方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
注意:知识补充扩展
- 内存分配的过程中有俩种算法,一种是双指针,一种是链表法
- 内存分配的过程中会使用到TLAB过程,使用的主要目的其实就是为了解决多线程并发申请内存,使用内存的问题,JVM自有的一套解决方案。
Java对象的内存布局
一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充。
33.为什么提出TLAB呢?
我们都知道运行时数据区中的堆区是共享区域,任何线程都是可以访问到堆中的共享区域,由于对象实例的创建在JVM中是非常频繁的,因此再并发环境下堆区中划分内存空间是不安全的,为了避免多个线程操作同一个地址,需要使用加锁的操作,从而解决问题。
虽然加锁帮助我们解决了问题,但是也会引出一些问题,就是效率低了,怎么解决呢?TLAB问世。
34.你了解TLAB吗?
- TLAB(Thread Local Allocation Buffer),从内存模型而不是垃圾收集器的角度,对Eden区域继续进行划分,JVM为每一个线程分配了一个私有的缓存区域,它包含在Eden空间内。
- 多线程同时分配内存的时候,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配的方式称之为快速分配策略。
35.堆中分配内存的俩种解决方案?
- 对分配内存空间的动作做同步处理,采用CAS机制,配置失败重试的方式保证更新操作的线程安全性。
- 每个线程在Java堆中分配了一小块内存,然后再给对象分配内存的时候,直接在这块私有的内存中分配,当这块部分区域用完之后,再次分配新的线程私有区域。
36.TLAB堆上内存分配是怎么样的?
37.Thread Local Allocation Buffer的注意事项
-
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
-
在程序中,开发人员可以通过选项:“-XX:UserTLAB” 设置是否开启TLAB空间
-
默认情况下,TLAB空间的内存非常小,仅占整个Eden空间的1%,当然我们可以通过选项“-XX:TLABWasteTarget’Percent” 设置TLAB空间所占用Eden 空间的百分比大小。
-
一旦对象在TLAB空间分配内存失败时,JVM就会尝试通过使用加锁机制确保数据的原子性,从而直接在Eden空间中分配内存。
38.怎么判断一个对象是否为垃圾?
- 引用计数法
对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。
存在的问题:循环引用的问题,会导致内存泄漏的问题,内存泄漏的堆积会直接导致内存溢出。 - 可达法分析算法
从一个根节点位置出发搜索,构成一条类似于链表的引用链;
39.可达法分析算法中能作为GC Root?
可以作为GC ROOT:静态变量、常量、栈帧中的局部变量元素,JNI也可以(GC ROOT是一个指针,不是对象)。
40.什么时候才会进行垃圾回收?
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。
当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是
具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决
定。但是不建议手动调用该方法,因为GC消耗的资源比较大。
- 当Eden区或者S区不够用了
- 老年代空间不够用了
- 方法区空间不够用了
- System.gc() 时机不确定 FullGC
41.对象被判定为不可达对象之后就会立即被回收吗?
真正宣告一个对象死亡,至少要经历两次标记过程:
- 第一次标记
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。反之,该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。 - 第二次标记
稍后,收集器将对F-Queue中的对象进行第二次小规模的标记。如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合。如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
finalize()方法是对象逃脱死亡命运的最后一次机会,需要注意的是,任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。另外,finalize()方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。
简单概括一下:
不是的,我们会进行垃圾筛选回收,第一次是我们判断没有被GC ROOT标记的对象是否执行了Finalize()方法,如果没有执行,意味着当前对象没有必要被回收,当时如果执行了该方法,我们就会把它放到F-Queue对象汇总,通过一个Finalizaler线程对他进行筛选回收。第二次,我们判断队列中的对象此时是否有引用指向它,如果有的话,直接移除,如果还没有话,我们最后直接回收。
42.介绍一下强引用、软引用、弱引用、虚引用的区别?
42.1 强引用
JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用
42.2 软引用
-
基本概念:软引用是用来描述一些还有用但是非必须的对象。对于软引用关联的对象,在系统将于发生内存溢出异常之前,将会把这些对象列进回收范围中进行二次回收。(当你去处理占用内存较大的对象 并且生命周期比较长的,不是频繁使用的)
-
存在的问题:软引用可能会降低应用的运行效率与性能。比如:软引用指向的对象如果初始化很耗时,或者这个对象在进行使用的时候被第三方施加了我们未知的操作。
-
使用场景: 软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
- 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
- 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
42.3 弱引用
弱引用(Weak Reference)对象与软引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于Weak引用对象, GC总是进行回收。因此Weak引用对象会更容易、更快被GC回收
42.4虚引用
也叫幽灵引用和幻影引用,为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。也就是说,如果一个对象被设置上了一个虚引用,实际上跟没有设置引用没有任何的区别 一般不用,辅助咱们的Finaliza函数的使用
43.说说你知道的垃圾回收算法?
43.1 标记-复制(Mark-Copying)
内存空间一分为二,每次只使用其中一块,同时也会清理一半的内存空间,会导致空间的大量浪费。具体过程如下图所示:
当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。
标记-复制(Mark-Copying)算法的缺点:空间利用率降低。
43.2 标记-清除(Mark-Sweep)算法
简单概括:
标记:找到GC Root
清除:递归遍历整个堆,清除垃圾
缺点:效率慢,还会产生内存碎片,该回收的没有回收,不需要回收的回收了(业务线程和GC线程并行执行)
为了解决这个问题:STW,当我们的GC线程在运行的时候,业务线程是不可以运行的
详细概括:
第一步:标记->找出内存中需要回收的对象,并且把它们标记出来
此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时
第二步:清除->清除掉被标记需要回收的对象,释放出对应的内存空间
标记-清除(Mark-Sweep)算法缺点:
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(1)标记和清除两个过程都比较耗时,效率不高;
(2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
43.3 标记-整理(Mark-Compact)算法
也叫做标记-整理-清除算法,在标记清除的算法上多加了一个整理的操作。
拓展知识:整理过程有不同的实现算法可以自行补充学习,此处不做过多的阐述。
1.标记压缩
2.随机整理
3.线性整理:
4.滑动整理:双指针算法,第一个指针找空闲的位置,第二个指针找存活的对象,将找到的存活对象放到第一个指针指向的位置,然后双指针继续向下一个位置移动,当俩个指针碰撞到一起的时候,后面的位置对象全部清除。
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
让所有存活的对象都向一端移动,清理掉边界意外的内存。
43.4 分代收集算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有100%存活的极端情况,所以老年代一般不能直接选用这种算法。
Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,可以选择标记清除或者标记整理)
44.垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
44.1 Serial
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。
它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程。
优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器
44.2 Serial Old
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算法",运行过程和Serial收集器一样。
44.3 ParNew
可以把这个收集器理解为Serial收集器的多线程版本。
优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器
44.4 Parallel Scavenge
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量。
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。
-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,
-XX:GCRatio直接设置吞吐量的大小。
44.5 Parallel Old
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回收,也是更加关注系统的吞吐量。
45.CMS垃圾回收器?
CMS垃圾回收器官网
CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间
为目标的收集器。
采用的是"标记-清除算法",整个过程分为4步
(1)初始标记 CMS initial mark 标记GC Roots直接关联对象,不用Tracing,速度很快
(2)并发标记 CMS concurrent mark 进行GC Roots Tracing
(3)重新标记 CMS remark 修改并发标记因用户程序变动的内容
(4)并发清除 CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为浮动垃圾
由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量
46.CMS在并发标记的时候可能会产生浮动垃圾,什么是浮动垃圾呢?
问:CMS中的浮动垃圾理解?
书上说:并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。这些垃圾有个专业名词:浮动垃圾。
这个浮动垃圾如何理解?难道不是在本次GC重新标记remark的过程中被发现然后清理吗?为何还要等下次GC才能清理?
答:重新标记(Remark) 的作用在于:之前在并发标记时,因为是 GC 和用户程序是并发执行的,可能导致一部分已经标记为 从 GC Roots 不可达 的对象,因为用户程序的(并发)运行,又可达了,Remark 的作用就是将这部分对象又标记为可达对象。 至于 “浮动垃圾”,因为 CMS 在 并发标记 时是并发的,GC 线程和用户线程并发执行,这个过程当然可能会因为线程的交替执行而导致新产生的垃圾(即浮动垃圾)没有被标记到;而重新标记的作用只是修改之前并发标记所获得的不可达对象,所以是没有办法处理 “浮动垃圾” 的。
47.G1(Garbage-First)垃圾回收器?
G1(Garbage-First)垃圾回收器官网
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂
如果对象太大,一个Region放不下超过Region大小的50%,那么就会直接放到H中
设置Region大小:-XX:G1HeapRegionSize=XXXM
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域
(1)分代收集(仍然保留了分代的概念)
(2)空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
(3)可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)
工作过程可以分为如下几步
初始标记(Initial Marking) 标记以下GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程
并发标记(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程
筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划
48.ZGC垃圾回收器?
ZGC垃圾回收器官网
JDK11新引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了
会分为一个个page,当进行GC操作时会对page进行压缩,因此没有碎片问题
只能在64位的linux上使用,目前用得还比较少
(1)可以达到10ms以内的停顿时间要求
(2)支持TB级别的内存
(3)堆内存变大后停顿时间还是在10ms以内
49.垃圾回收器好坏评价的标准?吞吐量和响应时间?
- 停顿时间和吞吐量基本概念
停顿时间:垃圾收集器进行垃圾回收中断应用执行响应的时间
吞吐量:运行用户代码时间/(运行用户代码时间+垃圾收集时间) - 停顿时间和吞吐量优势之处
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验;
高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
50.生产环境中,如何选择合适的垃圾收集器
oracle官网给出的建议
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100M,使用串行收集器
- 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
- 如果允许停顿时间超过1秒,选择并行或JVM自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
51.如何判断是否使用G1垃圾收集器
判断是否使用G1垃圾收集器标准
JDK 7开始使用,JDK8非常成熟,JDK9默认的垃圾收集器,适用于新老生代。
是否使用G1收集器?
适用的场景,这个做一个简单的翻译:
(1)50%以上的堆被存活对象占用
(2)对象分配和晋升的速度变化非常大
(3)垃圾回收时间比较长
52.JVM调优经验-JVM调优常用命令
1.查看java进程
jps
2.实时查看和调整JVM配置参数
jinfo
3.查看用法(查看某个java进程的name属性的值)
jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID
4.修改(参数只有被标记为manageable的flags可以被实时修改)
jinfo -flag [+|-] PID
jinfo -flag <name>=<value> PID
5.查看曾经赋过值的一些参数
jinfo -flags PID
6.查看虚拟机性能统计信息
jstat
7.查看类装载信息
jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次
8.查看垃圾收集信息
jstat -gc PID 1000 10
9.查看线程堆栈信息
jstack
10.查看某一个线程堆栈信息
jstack PID
53.G1调优策略
1.不要手动设置新生代和老年代的大小,只要设置整个堆的大小
G1的自动调优:https://blogs.oracle/poonam/increased-heap-usage-with-g1-gc
G1收集器在运行过程中,会自己调整新生代和老年代的大小
其实是通过adapt代的大小来调整对象晋升的速度和年龄,从而达到为收集器设置的暂停时间目标
如果手动设置了大小就意味着放弃了G1的自动调优
2.不断调优暂停时间目标
一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能总是得到满足。
3.使用-XX:ConcGCThreads=n来增加标记线程的数量
IHOP如果阀值设置过高,可能会遇到转移失败的风险,比如对象进行转移时空间不足。如果阀值设置过低,就会使标记周期运行过于频繁,并且有可能混合收集期回收不到空间。
IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,调高ConcGCThreads。
4.MixedGC调优
-XX:InitiatingHeapOccupancyPercent
-XX:G1MixedGCLiveThresholdPercent
-XX:G1MixedGCCountTarger
-XX:G1OldCSetRegionThresholdPercent
5.适当增加堆内存大小
6.不正常的Full GC**
注意:有时候会发现系统刚刚启动的时候,就会发生一次Full GC,但是老年代空间比较充足,一般是由Metaspace区域引起的。可以通过MetaspaceSize适当增加其大家,比如256M。
54.JVM性能监控工具
54.1 jconsole
JConsole工具是JDK自带的图形化性能监控工具。并通过JConsole工具, 可以查看Java应用程序的运行概况, 监控堆信息、 元空间使用情况及类的加载情况等。
54.2 jvisualvm
官网:https://docs.oracle/javase/8/docs/technotes/tools/unix/jvisualvm.html
Java VisualVM 是一个直观的图形用户界面,可在基于 Java 技术的应用程序(Java 应用程序)在指定的 Java 虚拟机 (JVM) 上运行时提供有关它们的详细信息。
Java VisualVM 将多个监控、故障排除和分析实用程序组合到一个工具中。例如,独立工具jmap、jinfo和提供jstat,的大部分功能jstack都集成到 Java VisualVM 中。其他功能,例如jconsole命令提供的一些功能,可以作为可选插件添加。
Java VisualVM 对于 Java 应用程序开发人员对应用程序进行故障排除以及监控和改进应用程序的性能非常有用。Java VisualVM 使开发人员能够生成和分析堆转储、跟踪内存泄漏、执行和监视垃圾收集以及执行轻量级内存和 CPU 分析。
54.3 arthas
github:https://github/alibaba/arthas
Arthas allows developers to troubleshoot production issues for Java
applications without modifying code or restarting servers.
Arthas 是Alibaba开源的Java诊断工具,采用命令行交互模式,是排查jvm相关问题的利器。
55.JVM内存分析工具
55.1 MAT(Memory Analyzer Tool)
MAT是一款非常强大的内存分析工具,在Eclipse中有相应的插件,同时也有单独的安装包。在进行内存分析时,只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前的内存信息。一般说来,这些内存信息包含:
- 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
- 所有的类信息,包括classloader、类名称、父类、静态变量等
- GCRoot到所有的这些对象的引用路径
- 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)
55.2 heaphero
https://heaphero.io/
55.3 perfma
笨马是一个JVM调优工具,甚至会给你相应的JVM调优建议,但是他是一个收费工具,不过如果你仅仅是希望调优参数,可以使用试用版。但是他的调优建议都是简单的参数设置。
https://console.perfma/
56.JVM日志分析工具
要想分析日志的信息,得先拿到GC日志文件才行,所以得先配置一下,根据前面参数的学习,下面的配置很容易看懂。比如打开windows中的catalina.bat,在第一行加上
XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
-Xloggc:$CATALINA_HOME/logs/gc.log
56.1 GCViewer
java -jar gcviewer-1.36-SNAPSHOT.jar
56.2 gceasy
http://gceasy.io
56.3 gcplot
https://it.gcplot/
57.JVM性能调优的原则有哪些?
- 多数的Java应用不需要在服务器上进行GC优化,虚拟机内部已有很多优化来保证应用的稳定运行,所以不要为了调优而调优,不当的调优可能适得其反
- 在应用上线之前,先考虑将机器的JVM参数设置到最优(适合)
- 在进行GC优化之前,需要确认项目的架构和代码等已经没有优化空间。我们不能指望一个系统架构有缺陷或者代码层次优化没有穷尽的应用,通过GC优化令其性能达到一个质的飞跃
- GC优化是一个系统而复杂的工作,没有万能的调优策略可以满足所有的性能指标。GC优化必须建立在我们深入理解各种垃圾回收器的基础上,才能有事半功倍的效果
- 处理吞吐量和延迟问题时,垃圾处理器能使用的内存越大,即java堆空间越大垃圾收集效果越好,应用运行也越流畅。这称之为GC内存最大化原则
- 在这三个属性(吞吐量、延迟、内存)中选择其中两个进行jvm调优,称之为GC调优3选2
58.什么情况下需要JVM调优?
- Heap内存(老年代)持续上涨达到设置的最大内存值
- Full GC 次数频繁
- GC 停顿(Stop World)时间过长(超过1秒,具体值按应用场景而定)
- 应用出现OutOfMemory 等内存异常
- 应用出现OutOfDirectMemoryError等内存异常( failed to allocate 16777216 byte(s) of direct memory (used: 1056964615, max: 1073741824))
- 应用中有使用本地缓存且占用大量内存空间
- 系统吞吐量与响应性能不高或下降
- 应用的CPU占用过高不下或内存占用过高不下
59.在JVM调优时,你关注哪些指标?
- **吞吐量:**用户代码时间 / (用户代码执行时间 + 垃圾回收时间)。是评价垃圾收集器能力的重要指标之一,是不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用程序达到的最高性能指标。吞吐量越高算法越好。
- **低延迟:**STW越短,响应时间越好。评价垃圾收集器能力的重要指标,度量标准是缩短由于垃圾收集引起的停顿时间或完全消除因垃圾收集所引起的停顿,避免应用程序运行时发生抖动。暂停时间越短算法越好
- 在设计(或使用)GC 算法时,我们必须确定我们的目标:一个 GC 算法只可能针对两个目标之一(即只专注于最大吞吐量或最小暂停时间),或尝试找到一个二者的折衷
- MinorGC尽可能多的收集垃圾对象。我们把这个称作MinorGC原则,遵守这一原则可以降低应用程序FullGC 的发生频率。FullGC 较耗时,是应用程序无法达到延迟要求或吞吐量的罪魁祸首
- 堆大小调整的着手点、分析点:
- 统计Minor GC 持续时间
- 统计Minor GC 的次数
- 统计Full GC的最长持续时间
- 统计最差情况下Full GC频率
- 统计GC持续时间和频率对优化堆的大小是主要着手点
- 我们按照业务系统对延迟和吞吐量的需求,在按照这些分析我们可以进行各个区大小的调整
- 一般来说吞吐量优先的垃圾回收器:-XX:+UseParallelGC -XX:+UseParallelOldGC,即常规的(PS/PO)
- 响应时间优先的垃圾回收器:CMS、G1
60.JVM常用参数有哪些?
- Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢
- Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常
- Xss 是指设定每个线程的堆栈大小。这个就要依据你的程序,看一个线程大约需要占用多少内存,可能会有多少线程同时运行等
- -Xmn、-XX:NewSize/-XX:MaxNewSize、-XX:NewRatio
- 高优先级:-XX:NewSize/-XX:MaxNewSize
- 中优先级:-Xmn(默认等效 -Xmn=-XX:NewSize=-XX:MaxNewSize=?)
- 低优先级:-XX:NewRatio
- 如果想在日志中追踪类加载与类卸载的情况,可以使用启动参数 **-XX:TraceClassLoading -XX:TraceClassUnloading **
61.线上排查问题的一般流程是怎么样的?
- CPU占用过高排查流程
- 利用 top 命令可以查出占 CPU 最高的的进程pid ,如果pid为 9876
- 然后查看该进程下占用最高的线程id【top -Hp 9876】
- 假设占用率最高的线程 ID 为 6900,将其转换为 16 进制形式 (因为 java native 线程以 16 进制形式输出) 【printf ‘%x\n’ 6900】
- 利用 jstack 打印出 java 线程调用栈信息【jstack 9876 | grep ‘0x1af4’ -A 50 --color】,这样就可以更好定位问题
- 内存占用过高排查流程
- 查找进程id: 【top -d 2 -c】
- 查看JVM堆内存分配情况:jmap -heap pid
- 查看占用内存比较多的对象 jmap -histo pid | head -n 100
- 查看占用内存比较多的存活对象 jmap -histo:live pid | head -n 100
62.什么情况下,会抛出OOM呢?
- JVM98%的时间都花费在内存回收
- 每次回收的内存小于2%
满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump。并不是内存被耗空的时候才抛出
63.系统OOM之前都有哪些现象?
- 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
- FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
- 老年代的内存越来越大并且每次FullGC后,老年代只有少量的内存被释放掉
64.如何进行堆Dump文件分析?
可以通过指定启动参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/app/data/dump/heapdump.hpro 在发生OOM的时候自动导出Dump文件
65.如何进行GC日志分析?
为了方便分析GC日志信息,可以指定启动参数 【-Xloggc: app-gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps】,方便详细地查看GC日志信息
- 使用 【jinfo pid】查看当前JVM堆的相关参数
- 继续使用 【jstat -gcutil 2315 1s 10】查看10s内当前堆的占用情况
- 也可以使用【jmap -heap pid】查看当前JVM堆的情况
- 我们可以继续使用 【jmap -F -histo pid | head -n 20】,查看前20行打印,即查看当前top20的大对象,一般从这里可以发现一些异常的大对象,如果没有,那么可以继续排名前50的大对象,分析
- 最后使用【jmap -F -dump:file=a.bin pid】,如果dump文件很大,可以压缩一下【tar -czvf a.tar.gz a.bin】
- 再之后,就是对dump文件进行分析了,使用MAT分析内存泄露
- 参考案例: https://www.lagou/lgeduarticle/142372.html
66.线上死锁是如何排查的?
- jps 查找一个可能有问题的进程id
- 然后执行 【jstack -F 进程id】
- 如果环境允许远程连接JVM,可以使用jconsole或者jvisualvm,图形化界面检测是否存在死锁
67.如何解决线上gc频繁的问题?
- 查看监控,以了解出现问题的时间点以及当前FGC的频率(可对比正常情况看频率是否正常)
- 了解该时间点之前有没有程序上线、基础组件升级等情况。
- 了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理。
- 再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。
- 针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo 命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象。
- 通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。
68.内存溢出的原因有哪些,如何排查线上问题?
- java.lang.OutOfMemoryError: …java heap space… 堆栈溢出,代码问题的可能性极大
- java.lang.OutOfMemoryError: GC over head limit exceeded 系统处于高频的GC状态,而且回收的效果依然不佳的情况,就会开始报这个错误,这种情况一般是产生了很多不可以被释放的对象,有可能是引用使用不当导致,或申请大对象导致,但是java heap space的内存溢出有可能提前不会报这个错误,也就是可能内存就直接不够导致,而不是高频GC.
- java.lang.OutOfMemoryError: PermGen space jdk1.7之前才会出现的问题 ,原因是系统的代码非常多或引用的第三方包非常多、或代码中使用了大量的常量、或通过intern注入常量、或者通过动态代码加载等方法,导致常量池的膨胀
- java.lang.OutOfMemoryError: Direct buffer memory 直接内存不足,因为jvm垃圾回收不会回收掉直接内存这部分的内存,所以可能原因是直接或间接使用了ByteBuffer中的allocateDirect方法的时候,而没有做clear
- java.lang.StackOverflowError - Xss设置的太小了
- java.lang.OutOfMemoryError: unable to create new native thread 堆外内存不足,无法为线程分配内存区域
- java.lang.OutOfMemoryError: request {} byte for {}out of swap 地址空间不够
69.线上YGC耗时过长优化方案有哪些?
- 如果生命周期过长的对象越来越多(比如全局变量或者静态变量等),会导致标注和复制过程的耗时增加
- 对存活对象标注时间过长:比如重载了Object类的Finalize方法,导致标注Final Reference耗时过长;或者String.intern方法使用不当,导致YGC扫描StringTable时间过长。可以通过以下参数显示GC处理Reference的耗时-XX:+PrintReferenceGC
- 长周期对象积累过多:比如本地缓存使用不当,积累了太多存活对象;或者锁竞争严重导致线程阻塞,局部变量的生命周期变长
- 案例参考: https://my.oschina/lishangzhi/blog/4703942
70.线上频繁FullGC优化方案有哪些?
- 线上频繁FullGC一般会有这么几个特征:
- 线上多个线程的CPU都超过了100%,通过jstack命令可以看到这些线程主要是垃圾回收线程
- 通过jstat命令监控GC情况,可以看到Full GC次数非常多,并且次数在不断增加
- 排查流程:
- top找到cpu占用最高的一个 进程id
- 然后 【top -Hp 进程id】,找到cpu占用最高的 线程id
- 【printf “%x\n” 线程id 】,假设16进制结果为 a
- jstack 线程id | grep ‘0xa’ -A 50 --color
- 如果是正常的用户线程, 则通过该线程的堆栈信息查看其具体是在哪处用户代码处运行比较消耗CPU
- 如果该线程是 VMThread,则通过 jstat-gcutil命令监控当前系统的GC状况,然后通过 jmapdump:format=b,file=导出系统当前的内存数据。导出之后将内存情况放到eclipse的mat工具中进行分析即可得出内存中主要是什么对象比较消耗内存,进而可以处理相关代码;正常情况下会发现VM Thread指的就是垃圾回收的线程
- 再执行【jstat -gcutil **进程id】, **看到结果,如果FGC的数量很高,且在不断增长,那么可以定位是由于内存溢出导致FullGC频繁,系统缓慢
- 然后就可以Dump出内存日志,然后使用MAT的工具分析哪些对象占用内存较大,然后找到对象的创建位置,处理即可
- 参考案例:https://mp.weixin.qq/s/g8KJhOtiBHWb6wNFrCcLVg
71.如何进行线上堆外内存泄漏的分析?(Netty尤其居多)
- JVM的堆外内存泄露的定位一直是个比较棘手的问题
- 对外内存的泄漏分析一般都是先从堆内内存分析的过程中衍生出来的。有可能我们分析堆内内存泄露过程中发现,我们计算出来的JVM堆内存竟然大于了整个JVM的Xmx的大小,那说明多出来的是堆外内存
- 如果使用了 Netty 堆外内存,那么可以自行监控堆外内存的使用情况,不需要借助第三方工具,我们是使用的“反射”拿到的堆外内存的情况
- 逐渐缩小范围,直到 Bug 被找到。当我们确认某个线程的执行带来 Bug 时,可单步执行,可二分执行,定位到某行代码之后,跟到这段代码,然后继续单步执行或者二分的方式来定位最终出 Bug 的代码。这个方法屡试不爽,最后总能找到想要的 Bug
- 熟练掌握 idea 的调试,让我们的“捉虫”速度快如闪电(“闪电侠”就是这么来的)。这里,最常见的调试方式是预执行表达式,以及通过线程调用栈,死盯某个对象,就能够掌握这个对象的定义、赋值之类
- 在使用直接内存的项目中,最好建议配置 -XX:MaxDirectMemorySize,设定一个系统实际可达的最大的直接内存的值,默认的最大直接内存大小等于 -Xmx的值
- 排查堆外泄露,建议指定启动参数: -XX:NativeMemoryTracking=summary - Dioty.leakDetection.targetRecords=100-Dioty.leakDetection.level=PARANOID,后面两个参数是Netty的相关内存泄露检测的级别与采样级别
- 参考案例: https://tech.meituan/2018/10/18/netty-direct-memory-screening.html
72.线上元空间内存泄露优化方案有哪些?
- 需要注意的一点是 Java8以及Java8+的JVM已经将永久代废弃了,取而代之的是元空间,且元空间是不是在JVM堆中的,而属于堆外内存,受最大物理内存限制。最佳实践就是我们在启动参数中最好设置上 -XX:MetaspaceSize=1024m -XX:MaxMetaspaceSize=1024m。具体的值根据情况设置。为避免动态申请,可以直接都设置为最大值
- 元空间主要存放的是类元数据,而且metaspace判断类元数据是否可以回收,是根据加载这些类元数据的Classloader是否可以回收来判断的,只要Classloader不能回收,通过其加载的类元数据就不会被回收。所以线上有时候会出现一种问题,由于框架中,往往大量采用类似ASM、javassist等工具进行字节码增强,生成代理类。如果项目中由主线程频繁生成动态代理类,那么就会导致元空间迅速占满,无法回收
- 具体案例可以参见: [https://zhuanlan.zhihu/p/200802910](
三.下节预告
都看到这里了,不点个赞是不是都不好意思走了,哈哈,那就赶快点个赞收藏起来我,创作不易,感谢关注!
如果关于JVM的面试知识你已经很好的掌握了,那么接下来给你安排的就是【2023金三银四面试小抄之多线程与高并发专题篇】,敬请期待!
四.共勉
最后,我想送给大家一句一直激励我的座右铭,希望可以与大家共勉!
更多推荐
2万字8千字72道大厂JVM面试题【金三银四(金九银十)面试小抄之Java经典面试题JVM篇总结】(附答案)
发布评论