Java面试—JVM 篇

1、JVM的内存区域,即运行时数据区域是什么?

先从三个版本图来看:
image.png
image.png
image.png
       三个版本迭代的情况大致就这些,其中最明显的变化也是永久代元空间
       然后逐一介绍一些主要的几个区域(按照jdk1.8为例),线程私有的:程序计数器、虚拟机栈、本地方法栈;线程共享的:堆、方法区、直接内存。

  1. 程序计数器(PC寄存器):每个线程都有一个程序计数器,是线程私有的,是一个指针,指向方法区中的字节码,在执行引擎执行完,读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
  2. 虚拟机栈:也是线程私有的,生命周期和线程相同,就像是一个栈一样的数据结构,基本所有的Java方法调用都是通过栈来实现的,方法调用的数据通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。(容易出现)
  3. 本地方法栈:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
  4. 堆:堆内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存,它是垃圾收集器管理的主要区域,因此也被称作为GC堆,所以从垃圾回收的角度来看,可以分为:新生代(Eden、幸存者1区、幸存者2区)老年代元空间(永久代)

image.png

如果伊甸区放不下就会触发一次轻GC,存活下来的就会跑到幸存区(这里有两个,不停地切换),如果伊甸区和幸存区都放不下的话就会触发一次重GC,存活下来的就会跑到老年代中,如果都满的话,就会发生OOM

  1. 方法区:方法区中会存储已被虚拟机加载的类信息、字段信息、方法信息、静态变量、即时编译器后的代码缓存等数据,HotSpot方法区实现的两种方式。

image.png

为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?

  1. 永久代有一个固定的大小上限,而元空间大小受本机内存的限制。
  2. 元空间中存放的类是元数据,这样加载多少类的元数据都由系统的实际可用空间来控制,这样能加载的类就更多了。
  3. 在JDK1.8中,合并HotSpot和JRockit的代码时,JRockit从来没有一个叫永久代的东西,合并之后就没有必要额外的设置这么一个永久代的地方了
  1. 直接内存:它是一种特殊的内存缓冲区,并不在Java堆中或方法区中分配的,而是同过JNI的方式在本地内存中分配的。

2、Java内存模型(JMM),Happens-Before 规则?

       Java内存模型(JMM):它是对共享内存的并发模型,如果一个线程想要去操作共享变量,它会将共享变量复制一份到自己的工作内存中,进行操作后,再把最新的变量值写回到主内存中。
image.png
       Happens-Before向我们保证了在多线程环境中,上一个操作对下一个操作的有序性和操作结果的可见性。
Happens-Before规则:

规则 说明
程序次序规则 同一个线程中代码的执行时有序的,前面的操作 happens-before 后面的操作,发生了指令重排后,代码的结果和顺序执行还是一样的
管程锁定规则 对一个锁的解锁操作happens-before后续对这个锁的加锁操作,后续的加锁操作能够感知到前面解锁的操作synchronized 就是管程的实现
volatile变量规则 对 valatile 修饰的变量的更新操作 happens-before 后续对此变量的任意操作,了解一下内存屏障和缓存一致性
传递性规则 A happens-before B,B happens-before C,则 A happens-before C,偏序关系具有传递性

3、常见的垃圾回收算法?垃圾回收器?

垃圾回收算法:

  1. 标记-清除算法:从根集合进行第一次扫描,把存活的对象进行标记,然后进行第二次扫描,把没有标记的对象进行回收。优点:不需要额外的空间;缺点:两次扫描,浪费时间,会产生内存碎片。
  2. 复制算法:比如将一块内存分为相同的两块,每次使用其中的一块,当这一块内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉,这样就使每次的内存回收都是对内存区间的一半进行回收,适用于新生代垃圾回收。
  3. 标记-整理算法:和标记-清除算法差不多,但是在回收完没有标记的对象后,再将标记的对象,整体向一个方向移动,这样就解决了内存碎片的问题,但是多增加了一次移动,又造成了资源的消耗,适用于老年代垃圾回收。

垃圾回收器:

  • CMS:它是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是第一次实现了让垃圾收集与用户线程(基本上)同时工作。

主要流程分为四步:

  1. 初始标记:单线程运行,需要Stop The World,标记GC Roots能直达的对象。
  2. 并发标记:无停顿,和用户线程同时运行,从GC Roots直达对象开始遍历整个对象图
  3. 重新标记:多线程运行,需要Stop The World,标记并发标记阶段产生对象
  4. 并发清除:无停顿,和用户线程同时运行,清理掉标记阶段标记的死亡的对象。
  • G1:它是一款面向服务端的的垃圾回收器,它把连续的Java堆分为多个大小相同的独立区域(Region),收集器能够对扮演不同角色的Region采用不同的策略去处理,这样可以按照若干个Region集进行收集,同时维护一个优先级列表,跟踪各个Region回收的“价值”,优先收集价值高的Region。

主要流程分为四步:

  1. 初始标记:Stop The World,标记了从GC Root开始直接关联可达的对象
  2. 并发标记:和用户线程并发执行,从GC Root开始对堆中对象进行可达性分析,找出要回收的对象
  3. 最终标记:STW,并发再标记过程中产生的垃圾。
  4. 筛选回收:指定回收计划,选择多个Region构成回收集,把回收集中Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间。
  • 还有一些其他的垃圾回收器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old

4、有了CMS,为什么还要引入G1?

CMS:

  1. 优点:并发收集、低停顿
  2. 缺点:会导致内存碎片比较多、并发能力依赖于CPU资源、并发清除阶段用户线程依然在运行,会产生所谓的“浮动垃圾”

G1:主要是解决了CMS的内存碎片过多的问题。

5、类的生命周期?类加载过程是什么?双亲委派机制是什么?

  1. 类的生命周期主要分为7个阶段:加载、验证、准备、解析、初始化、使用、卸载

  2. 类加载:Class文件需要加载到虚拟机中之后才能运行使用,那么虚拟机是如何加载这些Class文件。

image.png

  • 加载:加载就是把Class字节码文件从各个来源通过类加载器装入内存中。

类加载器:包括启动类加载器、扩展类加载器、应用类加载器、自定义加载器

  • 验证:保证加载进来的字节码符合虚拟机的规范,包括文件格式的验证、元数据的验证、字节码的验证、符号引用的验证
  • 准备:主要是为类变量(不是实例变量)分配内存,并且赋予初值
  • 解析:将常量池内的符号引用替换为直接引用的过程
  • 初始化:这个阶段主要是对类变量初始化,是执行类构造器的过程
  1. 双亲委派机制:如果一个类加载器A收到了类加载的请求,它首先会把这个请求委派给父加载器B去完成,一直委派给最顶层,如果顶层加载不了,再交给它的下一层去加载,如果最后回到了类加载器A这里,这时候类加载器A才能去加载它。

image.png
       双亲委派机制的好处:为了安全,也保证了Java程序的稳定运行,可以避免类的重复加载,也保证了Java核心的API不被篡改

6、如何打破双亲委派机制?

       自定义加载器的话,需要继承 ClassLoader。如果我们不想打破双亲委派模型,就重写 ClassLoader类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

Tomcat的类加载机制破坏了双亲委派原则。

7、如何进行JVM调优?

  • 分析和定位当前系统的瓶颈:
  1. CPU指标:查看占用cpu最多的线程、进程,查看线程堆栈快照信息,常见工具JProfiler
// 显示系统各个进程的资源使用情况
top
// 查看某个进程中的线程占用情况
top -Hp pid
// 查看当前 Java 进程的线程堆栈信息
jstack pid
  1. JVM内存指标:查看当前 JVM 堆内存参数配置是否合理、查看堆中对象的统计信息、查看堆存储快照,分析内存的占用情况。
// 查看当前的 JVM 参数配置
ps -ef | grep java
// 查看 Java 进程的配置信息,包括系统属性和JVM命令行标志
jinfo pid
// 输出 Java 进程当前的 gc 情况
jstat -gc pid
// 输出 Java 堆详细信息
jmap -heap pid
// 显示堆中对象的统计信息
jmap -histo:live pid
// 生成 Java 堆存储快照dump文件
jmap -F -dump:format=b,file=dumpFile.phrof pid

       例子:(使用JProfiler),可以在VM处加上-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError这个命令,当运行的时候错误的时候,就会生成一个的文件。比如下面:
image.png
       然后我们打开它就可以定位到一些报错的地方,甚至可以找到对应的java代码的行数。
image.png
image.png

  1. JVM GC指标:查看每分钟GC时间是否正常、查看每分钟YGC、FGC次数是否正常
/ 打印GC的详细信息
-XX:+PrintGCDetails
// 打印GC的时间戳
-XX:+PrintGCDateStamps
// 在GC前后打印堆信息
-XX:+PrintHeapAtGC
// 打印Survivor区中各个年龄段的对象的分布信息
-XX:+PrintTenuringDistribution
// JVM启动时输出所有参数值,方便查看参数是否被覆盖
-XX:+PrintFlagsFinal
// 打印GC时应用程序的停止时间
-XX:+PrintGCApplicationStoppedTime
// 打印在GC期间处理引用对象的时间(仅在PrintGCDetails时启用)
-XX:+PrintReferenceGC
  • 制定优化的方案
  • 对比优化前后的指标,统计优化效果
  • 持续观察和跟踪优化

这东西,有时候还是要去尝试,有可能是代码有问题,参数有问题,配置有问题,还有可能是其他不是后端的锅的问题,所以说要从多个方面去考虑、分析出现的问题,选择合适的方案。

8、Young GC和Full GC什么时候触发?

       Young GC/Minor GC:新创建的对象在新生代Eden区进行分配,如果Eden区没有足够的空间时,就会触发Young GC来清理新生代。
       Full GC/Major GC:举个例子:对于一个大对象,我们首先会在Eden尝试创建,如果创建不了,就会触发Minor GC,随后继续尝试在Eden区存放,发现仍然放不下,尝试直接进入老年代,如果老年代也放不下的话,就会触发Major GC清理老年代的空间,放的下的话就成功了,如果放不下的话就报OOM错误。

猜你喜欢

转载自blog.csdn.net/weixin_52487106/article/details/131051355