2021-Java后端工程师面试指南-(JVM)

2022-07-29 08:57:05 浏览数 (1)

前言

“文本已收录至我的GitHub仓库,欢迎Star:https://github.com/bin392328206/six-finger 种一棵树最好的时间是十年前,其次是现在

Tips

面试指南系列,很多情况下不会去深挖细节,是小六六以被面试者的角色去回顾知识的一种方式,所以我默认大部分的东西,作为面试官的你,肯定是懂的。

“https://www.processon.com/view/link/600ed9e9637689349038b0e4

上面的是脑图地址

叨絮

可能大家觉得有点老生常谈了,确实也是。面试题,面试宝典,随便一搜,根本看不完,也看不过来,那我写这个的意义又何在呢?其实嘛我写这个的有以下的目的

  • 第一就是通过一个体系的复习,让自己前面的写的文章再重新的过一遍,总结升华嘛
  • 第二就是通过写文章帮助大家建立一个复习体系,我会将大部分会问的的知识点以点带面的形式给大家做一个导论

然后下面是前面的文章汇总

  • 2021-Java后端工程师面试指南-(引言)
  • 2021-Java后端工程师面试指南-(Java基础篇)
  • 2021-Java后端工程师面试指南-(并发-多线程)

JVM 作为一个Java工程师,必须要掌握和理解的一个点

聊聊什么是JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

什么是类加载器,类加载器有哪些?

实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。

主要有一下四种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
  2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
  4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

说说JVM类的生命周期和加载过程

类的生命周期就包含了加载过程了,我们JVM类的生命周期有以下7个阶段

  • 加载:
    • 通过全类名获取定义此类的二进制字节流
    • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
    • 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
  • 验证:验证文件格式,字节码验证,魔数验证等
  • 准备 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,如果是基本数据类型,就会给他们设置默认值
  • 解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
  • 初始化:首先明确一点的就是,必须存在以下的行为,才会进行类的初始化
    • 当jvm执行new指令时会初始化类。即当程序创建一个类的实例对象。
    • 当jvm执行getstatic指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
    • 当jvm执行putstatic指令时会初始化类。即程序给类的静态变量赋值。
    • 当jvm执行invokestatic指令时会初始化类。即程序调用类的静态方法。
    • 使用 java.lang.reflect 包的方法对类进行反射调用时如Class.forname("..."),newInstance()等等。,如果类没初始化,需要触发其初始化。
    • 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
    • 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  • 使用:就是我们正常使用了
  • 卸载:卸载类即该类的Class对象被GC。卸载类需要满足3个要求:
    • 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
    • 该类没有在其他任何地方被引用
    • 该类的类加载器的实例已被GC

说说类加载器双亲委派模型机制?说说它的好处

每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

小六六总结一句话总结就是 类加载总是向上检查,向下加载。

双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

说说为啥要打破双亲委派模型,如何打破?

JDBC之所以要破坏双亲委派模式是因为,JDBC的核心在rt.jar中由启动类加载器加载,而其实现则在各厂商实现的的jar包中,根据类加载机制,若A类调用B类,则B类由A类的加载器加载,也就是说启动类加载器要加载jar包下的类,我们都知道这是不可能的,启动类加载器负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,那么JDBC是如何加载这些Driver实现类的?通过Thread.currentThread().getContextClassLoader()得到线程上下文加载器来加载Driver实现类。

还有就是我们可以自定义的加载器继承ClassLoad然后修改的loadClass和find classde 方法,从而可以打破双亲的委派机制

聊聊JVM内存分哪几个区,每个区的作用是什么?

  • 方法区:
    1. 有时候也成为永久代(元空间),在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类 型的卸载
    2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
    3. 该区域是被线程共享的。
    4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编 译时确定,运行时生成的常量也会存在这个常量池中。
  • 虚拟机栈:
    1. 虚拟机栈也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操 作数栈、动态链接和方法出口等信息。
    2. 虚拟机栈是线程私有的,它的生命周期与线程相同。
    3. 局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指 向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定
    4. 操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式
    5. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。
  • 本地方法栈
    • 本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。
    • Java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作 。
  • 程序计数器
    • 内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。

说说如何判断一个对象是否存活?(或者GC对象的判定方法)

  • 虚拟机栈栈帧中引用的变量
  • 本地方法栈中引用的变量
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

你知道Java中垃圾收集的方法有哪些吗

  • 标记-清除算法:该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象,但是会产生大量的空间碎片。
  • 复制算法:为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
  • 标记-整理算法:根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
  • 分代收集算法:当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

你有没有遇到过OutOfMemory问题?你是怎么来处理这个问题的?处理 过程中有哪些收获?

  • 集合类中有对对象的引用,使用后未清空,GC不能进行回收;
  • 代码中存在循环产生过多的重复对象;
  • 启动参数堆内存值小。
  • 频发的创建超大对象

JDK 1.8之后Perm Space有哪些变动? MetaSpace⼤⼩默认是⽆限的么? 还是你们会通过什么⽅式来指定⼤⼩

JDK 1.8后用元空间替代了 Perm Space;字符串常量存放到堆内存中。MetaSpace大小默认没有限制,一般根据系统内存的大小。JVM会动态改变此值。-XX:MetaspaceSize:分配给类元数据空间(以字节计)的初始大小(Oracle逻辑存储上的初始高水位,the initial high-water-mark)。此值为估计值,MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。-XX:MaxMetaspaceSize:分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。

说说发生mior GC的条件是什么

我们知道新生代一般分为三个区 eden s1 和 s2,而我们每次创建的对象在新生代里面,他只会分配在eden和其中一个s,当他们都满了,就会发生一次mior gc ,对象进入老年代。

说说进入老年代的条件吧

  • 第一个就是我们上面说的 新生代满了 eden和一个s满了,对象经过mior gc之后进入老年代
  • 大对象直接进入老年代:如果老年代剩余的连续内存空间大于之前Minor GC晋升老年代对象的平均大小的话,就进行Minor GC,如果小于的话就直接进行Full GC。对于parNew 是XX:PretenureSizeThreshold 设置大对象大小
  • 长期存活的对象将进入老年代 XX:MaxTenuringThreshold cms默认是6次,
  • Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值,也就是说当一个s的一半以上的对象都是这个,那么他们就会进入老年代了。

说说jdk1.8默认的垃圾回收器,你们的线上环境用的是哪个垃圾回收器呢?

1.8默认的是 UseParallelGC,ParallelGC 默认的是 Parallel Scavenge(新生代) Parallel Old(老年代)

小六六自己负责的项目是我配置的,用的parNew CMS;

为啥要用CMS呢?是这样的,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

那你说说CMS收集的过程

  • 初始标记:暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
  • 并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清除:开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

他只有第一个 和第三个阶段需要stw

它的缺点:它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

其实我建议呢?如果自己的内存够大还是用G1吧,只要达到8G的内存,我们建议使用G1,而且jdk9开始已经没有cms了

那你聊聊G1吧

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

其实,我们只要控制了GC的每次回收时间,对于用户来说感知就不会那么大了。

那你说说G1 和cms parNew的好处体现在哪?

其实呢 G1 和cms 很大程度上很相似,怎么说,就是当我们回收垃圾的时候,都是要经历 初始标记,并发标记,重新标记,这些阶段。但是有几个区别

  • 第一个就是假设我们这个应用很多,我们把新生代的内存设置的很大,对不对,这样垃圾回收的时间就会很小,但是如果刚好,有一个用户刚好请求的时候,它刚好再gc,那你想想这个GC的时间会不会很长,那对于用户来说是不是体验差呢?所以说大内存其实并不是那么适合cms
  • G1就不同,他并不需要说一定要等到,达到内存的多少才开始回收垃圾,他可以设置我们垃圾回收的时间,来判断什么时候来回收,这样对于大部分用户来说,相当于平均了gc的时候,那么体验上会好很多。

说说你一般用来排查问题的工具呗

jps jmap jstat MAT arthas等,用的比较多,还有就是最后打印出我们的gc日志,通过gc日志去分析gc问题

总结一下你的JVM调优的一些心得

这个是小六六自己的一些见解,不一定对哈,其实大部分我们去分析gc日志,然后去调优并不一定说要去修改JVM的参数,很多时候是我们自己代码的问题,所以我们要把自己的代码先去排查,如果代码没有问题了,那么就是JVM的参数,我们有以下原则

  • 第一个就是让那些应该在新生代被回收的对象,尽量不要进入到老年代,让他们再新生代被回收
  • 让那些长期存活的对象,尽快的进入到老年代
  • 如果内存够大,尽量使用G1
  • 写代码的使用 如果你使用完一个对象,最好把那个对象的引用置空

结束

JVM写完了,可能也不全部,但是呢,这些问题你熟悉的话基本上问题不大了,下一章就MySql吧

0 人点赞