2.Java系列之JVM面试题总结

1. 什么情况下会发生栈内存溢出

栈分配空间太小,或执行的方法递归层数太多创建了太多的栈帧导致溢出
解决方案:配置-Xss参数增加线程栈大小,优化程序也至关重要

2. JVM的内存结构,Eden和Survivor的比例

内存结构:

  • 堆:存放对象
  • 方法区: 存放类和变量
  • JAVA虚拟机栈:存放运行时栈帧
  • 本地方法栈
  • 程序计数器

Eden区是一块,Survivor是两块,均属于堆中的新生代
Eden和Survivor的比例是8:1:1,可以通过-XX:SurvivorRatio来设定

从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中

3. JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor
  • 共享内存区 = 持久代 + 堆
  • 持久代 = 方法区 + 其他
  • 堆 = 新生代 + 老年代 (1:2通过-XX:NewRatio设置)
  • 新生代 = Eden + S0 + S1(8:1:1通过-XX:SurvivorRatio设置)

Survivor具有预筛选保证,只有对象经历了16次Minor GC才会被送到老年代,Survivor可以减少被送到老年代的对象,进而减少Full GC发生。有两个Survivor,在Minor GC后,可以保证一个为空,另一个非空且无内存碎片

4. JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代,说说你知道的几种主要的JVM参数

完整GC流程:

  • 当Eden区满了,JVM触发一次Minor GC,以收集新生代的垃圾,存活下来的对象,会被转移到Survivor区
  • 大对象(如很长的字符串)直接进入老年代
  • 如果对象在Eden出生,每经历过一次MinorGC,并且被Survivor容纳的话,年龄加1,直到年龄超过15,就会进入老年代
  • Major GC发生在老年代,通常会伴随着至少一次Minor GC,比Minor GC慢10倍以上

-Xss: 栈容量
-Xms:设置最小堆内存
-Xmx:设置最大堆内存
-Xmn10M:设置新生代10M
-XX:SurvivorRatio=8:设置Eden、Survivor比例8:1
-XX:PermSize=32M:永久代最小内存32M
-XX:MaxPermSize=64M:永久代最大扩展内存64M
-XX:+HeapDumpOnOutOfMemoryError:堆内存溢出时Dump出当前的内存堆转储快照以便事后分析

5. 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点
  • Serial收集器:新生代单线程的收集器,收集垃圾时,必须stop the world,使用复制算法

  • ParNew收集器:新生代Serial多线程版本收集器,也需要stop the world,使用复制算法

  • Paraller Scavenge收集器:新生代并发的多线程收集器,目标达到一个可控的吞吐量,使用复制算法

  • Serial Old收集器: 是Serial收集器的老年代版本,单线程,使用标记整理算法

  • Parallel Old收集器: Parallel Scavenge收集器的老年代版本,多线程,使用标记整理算法

  • CMS收集器:是一种以最短回收停顿时间为目标的老年代收集器,使用标记清除算法,运行过程:初始标记-并发标记-重新标记-并发清除,收集后会产生大量空间碎片

  • G1收集器:可作用于新生代与老年代,是标记整理算法,运行过程:初始标记-并发标记-最终标记-筛选标记,不会产生碎片,可以精确的控制停顿

- CMS收集器 G1收集器
算法类型 标记-清除 标记-整理
收集范围 老年代 新生代、老年代
目标 最短的停顿时间为目标 可预测的垃圾回收时间
6. 垃圾回收算法的实现原理
  • 引用计数法 堆中每个对象拥有一个引用计数。被引用一次,计数加1,被引用变量值变为null,则计数减1,直到计算为0,则表示变为无用对象。缺点是无法识别循环引用对象
  • 引用可达法(根搜索算法) 从一个节点GC ROOT开始,寻找一个引用的节点,找到后,继续寻找这个节点的引用节点,寻找完毕后,剩余的节点则认为是没有被引用的对象

追问:GC ROOT有哪些?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象
7. 当出现了内存溢出,你怎么排错

JVM除了程序计数器其他区域都可能发生内存溢出

堆溢出OutOfMemoryError
用visualVM工具分析堆快照(-XX:+HeapDumpOnOutOfMemoryError)
如果发生内存泄漏

  1. 找到泄漏的对象 2)找到泄漏对象的GC ROOT 3)根据泄漏对象和GC ROOT找到导致内存泄漏的代码 4) 设法排除泄漏对象和GC ROOT的连接
    如果不存在内存泄漏,看下能否增大JVM堆的容量

栈溢出
一般由于递归,导致栈空间不足,发生OutOfMemoryError:Java heap space说明运行时常量池移到了堆中

方法区溢出
方法区是存放类的地方。如果多个项目有多个相同jar,且都在WEB-INF/lib下,则每个项目都会加载一遍jar,会导致方法区有大量相同类,又不会被GC,则可建立共享lib库,否则尝试增加-XX:MaxPermSize

8. JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等

Java内存模型:
Java模型规定了所有的变量都存储在主内存中,每个线程有自己的工作内存,线程的工作内存保存了所使用变量主内存的副本拷贝。
线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
不同线程之间无法访问对方工作内存的变量,线程间变量传递依赖于主内存。

指令重排:
处理器将指令乱序执行,可以大大提高执行效率,这就是指令重排

内存屏障:
内存屏障,也叫内存栅栏,是一种CPU指令,用于控制特定条件下的重排序与内存可见性问题

  • LoadLoad屏障: 对于语句Load1;LoadLoad;Load2,在Load2及后续读取操作要读取的数据被访问前,保证load1要读取的数据被读取完毕
  • StoreStore屏障:对于语句Store1;StoreStore;Store2,在Store2及后续写入操作执行时,保证Store的写入操作对其他处理器可见
  • LoadStore屏障: 对于语句Load1;LoadStore;Store2,在Store2及后续写入操作执行时,保证Load要读取的数据被读取完毕
  • StoreLoad屏障: 对于语句Store1;StoreLoad;Load1,在Load及以后所有的读取操作执行前,保证Store1的写入操作对所有处理器可见。它的开销是最大的,这个屏障是个万能屏障,兼具其他三种屏障的功能

happen-before原则:

happends-before是JMM最核心的概念,理解happends-befores是理解JMM的关键。

1. happends-before关系定义如下
  1. 如果一个操作happends-before另一个操作,那么另一个操作执行的结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
  2. 两个操作之间存在happends-before关系,并不意味着java平台的具体实现必须要按happends-before关系执行的顺序来执行,如果重排序之后的执行结果,与按happends-before关系来执行的结果一致,那么这种重排序并不非法。
2. happends-before规则

程序顺序规则:一个线程的每个操作,happends-before于该线程中任意的后续操作
监视器锁规则:对一个锁的解锁,happends-before于任意后续对这个锁的加锁
volatile变量规则:对一个volatile的写,happends-before于任意后续对该volatile域的读
传递性:如果A happends-before B 且 B happends-before,那么 A happends before C
start()规则: 如果线程A执行操作ThreadB.start(),那么A线程的ThreadB.start()操作happends before于线程B的任意操作
join()规则:如果线程A执行操作ThreadB.join()方法并成功返回,则线程B中的任意操作happends before于线程A从ThreadB.join()操作成功返回

9. 简单说说你了解的类加载器,可以打破双亲委派么,怎么打破

什么是类加载器?

类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象

  • 启动类加载器(Bootstrap ClassLoader): 由C++语言实现(针对HotSpot),负责将存放在\lib目录或-Xbootclasspath参数指定路径中的类库
  • 其他类加载器:由Java语言实现,继承自抽象类ClassLoader
    • 扩展类加载器(Extension ClassLoader): 负责加载\lib\ext或java.ext.dirs系统变量指定路径类库
    • 应用程序类加载器(Application ClassLoader): 负责加载用户路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况下,如果我们没有自定义类加载器就默认使用这个加载器

双亲委派模型:
如果一个类加载器收到类加载请求时,首先不会尝试自己加载这个类,而是把这个请求委派给父加载器完成。每个加载器都是如此,只有当父加载器在自己的搜索范围找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载

为什么需要双亲委派模型?
如果没有双亲委派,那么用户自定义一个java.lang.Object, java.lang.String类,并把它放在classpath中,那么类之间的比较结果及类的唯一性将无法保证。双亲委派模型,防止内存中出现多份同样的字节码

怎么打破双亲委派模型?
继承ClassLoader类,重写loadClass和findClass方法

10. 你们线上应用的JVM参数有哪些

思路: 可以说一下堆栈配置相关的,垃圾收集器相关的,还有一下辅助信息相关的。

堆栈配置相关

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-XX:MaxPermSize=16m -XX:NewRatio=4 -XX:SurviorRatio=4 -XX:MaxTenuringThreshold=0

-Xmx3550m: 最大堆大小为3550m
-Xms3550m: 最小堆大小为3550m
-Xmn2g: 设置新生代大小为2g
-Xss128k: 设置每个线程的栈大小为128K
-XX:MaxPermSize: 设置永久代的大小为16m
-XX:NewRatio: 设置新生代(包括Eden和两个Survivor区)与老年代的比值(除去永久代)
-XX:SurvivorRatio: 设置新生代Eden与Survivor区的大小比值,设置为4,则两个Survivor区与Eden区比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxTenuringThreshold: 设置垃圾最大年龄。如果设置为0的话,则新生代不经过Survivor区,直接进入老年代

垃圾收集器相关

-XX:+UseParallelGC # 选择垃圾收集器为并行收集器
-XX:ParallelGCThreads=20 # 配置并行收集器的线程数
-XX:+UseConcMarkSweepGC # 设置老年代为并发收集
-XX:CMSFullGCsBeforeCompaction=5 # 由于CMS收集器不对内存空间进行压缩整理,所以会产生内存碎片。此值设置运行5次Full GC后对内存空间压缩整理
-XX:+UseCMSCompactAtFullCollection # 打开对老年代的压缩,可能会影响性能,但是可以消除碎片

辅助信息相关

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError

-XX:+PrintGC 输出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails 输出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs

11. 怎么打出线程栈信息

思路: 可以说一下jps,top,jstack这几个命令,再配合一次排查线上问题进行解答。

  • 首先执行jps,获取进程号
  • 然后执行top -Hp pid,获取本进程所有线程的CPU耗时性能
  • jstack pid,查看当前java进程的堆栈信息
  • 或者 jstack -l /tmp/output.txt 把堆栈信息打到一个txt文件
  • 可以使用fastthread进行堆栈定位

欢迎关注公众号算法小生,本文持续更新中

你可能感兴趣的:(微服务,jvm,java,面试)