Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇

金三银四面试题之JVM性能调优篇

        • 1. 什么是Java类加载器?
        • 2. 读取class文件的来源有哪些?
        • 3. 谈谈类加载器加载的流程?
        • 4. 类加载器有哪几种?
        • 5. 哪些操作会初始化类加载器?
        • 6. 谈一谈类加载器的双亲委派机制?
        • 7. 类加载器采用双亲委派机制有什么好处呢?
        • 8. 如何自定义一个类加载器?
        • 9. 谈谈ClassLoader的原理?
        • 10. 什么是SPI机制?
        • 11. 如何破坏双亲委派机制?
        • 12. 如何实现热部署的功能?
        • 13. 创建一个java.lang.Object类会报错吗?
        • 14. JVM的运行原理是什么?
        • 15. JVM内存结构是如何布局的?
        • 16. 堆(heap)和栈(stack)的区别?
        • 17.什么是栈内存溢出?
        • 18. 如何避免栈内存溢出?
        • 19. 什么是堆内存溢出?
        • 20. 什么是堆内存泄漏?
        • 21. 什么情况下会导致CPU飙升?
        • 22. 如何排查生产环境CPU飙升的问题?
        • 23. 如何避免生产环境CPU飙升?
        • 24. JDK监控和故障处理工具有哪些?
        • 25. 什么是方法区(元空间)内存溢出?
        • 26. 什么是常量池?
        • 27. 字符串常量池存放在JVM的哪块区域?
        • 28. 如何查看字符串常量池?
        • 29. 运行下面代码输出的结果是什么?
        • 30. 谈谈Java对象的创建过程?
        • 31. 谈谈Java对象的布局?
        • 32. 对象的访问定位有哪两种方式?
        • 33. 如何判断对象是否可被回收?
        • 34. JVM是采用什么方法来判断对象是否可被回收的?
        • 35. 哪些可以作为GC Roots对象呢?
        • 36. 简单谈一谈强引用、软引用、弱引用、虚引用?
        • 37. 什么是引用队列?
        • 38. 能谈一谈堆空间的基本结构吗?对象内存是如何分配的?
        • 39. 堆内存常见的分配策略有哪些?
        • 40. 主要进行GC的区域有哪些?
        • 41. 垃圾收集算法有哪些?
        • 42. 为什么新生代采用标记-复制算法,老年代采用标记-整理/标记清除算法?
        • 43. 什么是Stop-The-World?
        • 44. GC有哪些核心的参数?
        • 45. 有哪些GC日志分析的工具?
        • 46. JDK有哪些核心的垃圾收集器?
        • 47. 串行收集器与并行收集器的区别?
        • 48. 并行收集器与并发收集器的区别?
        • 49. JDK中垃圾收集器有哪些组合方式?
        • 50. 什么是吞吐量?
        • 51. 能简单介绍一下这些收集器吗?
        • 52. 能具体谈一谈CMS收集器的实现过程吗?
        • 53. CMS收集器有什么优缺点?
        • 54. 为什么CMS收集器采用标记-清除算法,而不采用标记-整理算法?
        • 55. 能说说CMS收集器中有哪些配置参数吗?
        • 56. 为什么叫G1收集器?
        • 57. 能具体谈谈G1收集器的分区原理吗?
        • 58. G1收集器的核心参数有哪些?
        • 59. 为什么G1收集器需要设计存放巨型对象(H区)?
        • 60. G1收集器的RSet有什么作用?
        • 61. G1收集器的CSet有什么作用?
        • 62. 谈谈G1收集器的回收过程?
        • 63. 能谈谈三色标记算法吗?
        • 64. 使用三色标记算法会存在什么样的问题?
        • 65. CMS收集器和G1收集器如何解决漏标问题?
        • 66. 如何减少堆内存触发GC的频率?

1. 什么是Java类加载器?

Java类加载器(ClassLoader)是Java运行时环境的一部分,负责将java类编译后的字节码文件(.class)加载到Jvm内存中,生成对应的class对象。

2. 读取class文件的来源有哪些?

  1. 自己编写的java源代码 编译成class文件 从本地硬盘读取;
  2. 通过网络的方式下载class文件;
  3. War、Jar 解压之后都是class文件;
  4. 从数据库中读取class文件;
  5. Java动态代理模式 反射/cglib 生成代理class;

3. 谈谈类加载器加载的流程?

类加载器加载完class文件,还需要经历验证、准备、解析三个阶段,最后才能初始化该类。
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第1张图片

  1. 加载:加载是类加载的第一个阶段,通过类的全限定名来找到对应的class文件,将此class文件生成一个class对象。
  2. 链接:链接分为3个小部分,验证、准备、解析。
    1)验证:验证的目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证;
    2)准备:给静态方法和静态变量赋予初值,比如static int a;给其中的a赋予初值为0,但是这里不会给final修饰的静态变量赋予初值,因为被final修饰的静态变量在编译期间就已经被赋予初值了;
    3)解析:主要将常量池中的符号引用替换为直接引用的过程。
  3. 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化);
  4. 卸载:GC将无用对象从内存中卸载。

4. 类加载器有哪几种?

  1. 启动(Bootstrap)类加载器:加载JVM自身工作需要的类,它由JVM自己实现。它会加载$JAVA_HOME/jre/lib下的文件(比如:rt.jar),底层是C语言实现的,获取ClassLoader的时候会显示为null;
  2. 扩展(Extension)类加载器:它是JVM的一部分,由sun.misc.LauncherExtClassLoader实现,他会加载ExtClassLoader实现,他会加载JAVA_HOME/jre/lib/ext目录下的文件(或者由System.getProperty(“java.ext.dirs”)所指定的文件),底层是Java实现的;
  3. 应用(Application)类加载器:应用类加载器,在工作中接触最多的也是这个类加载器,它由sun.misc.Launcher$AppClassLoader实现。他会加载工程目录classpath下面的class以及jar包,底层是java实现的;
  4. 自定义类加载器:也就是用户自己定义的类加载器;
    Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第2张图片

5. 哪些操作会初始化类加载器?

  1. 调用类的静态方法
  2. invokeStatic 调用静态方法
  3. main
  4. new
  5. Class.forName
  6. 子类初始化一定会初始化父类

初始化一个类,一定会触发类加载器;但是类加载器加载了该类,该类不一定初始化。

6. 谈一谈类加载器的双亲委派机制?

首先类加载器分为四种:自定义类加载器、应用类加载器、扩展类加载器、启动类加载器。当一个类加载器收到请求之后,首先会依次向上查找到最顶层的类加载器(启动类加载器),然后再依次向下加载class文件,如果已经加载到class文件,子加载器不会继续加载该class文件。
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第3张图片

7. 类加载器采用双亲委派机制有什么好处呢?

目的就是为了防御开发者定义的类与jdk定义的源码类产生冲突问题,保证该类在内存中的唯一性。

8. 如何自定义一个类加载器?

继承ClassLoader类,重写findClass()方法,调用defineClass()方法。

9. 谈谈ClassLoader的原理?

Launcher 设定类加载器的依赖关系
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第4张图片

10. 什么是SPI机制?

Java SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
实现方式:

  1. 首先需要在resources目录下创建文件夹:META-INF.services;
  2. 定义接口文件的名称:包名+类名组成;
    Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第5张图片
  3. 获取当前线程对应的应用类类加载器,加载该class:
ServiceLoader<MyService> load = ServiceLoader.load(MyService.class);  
load.forEach((t) -> {  
    System.out.println(t.get());  
});  

11. 如何破坏双亲委派机制?

第一种方式:重写ClassLoader的loadClass()方法;
第二种方式:指定线程类加载器+SPI机制,绕开ClassLoader的loadClass()方法;

12. 如何实现热部署的功能?

首先自定义一个类加载器去加载该类,然后创建一个线程(while循环)去监听该类是否发生了变化(根据最后修改的时间来判断),如果发生了变化的话,则使用类加载器重新读取该类。

13. 创建一个java.lang.Object类会报错吗?

不会报错,但是创建的Object不会被类加载器加载到。

14. JVM的运行原理是什么?

在这里插入图片描述

15. JVM内存结构是如何布局的?

Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第6张图片
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第7张图片
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第8张图片


堆:Java堆是各线程共享的内存区域,在JVM启动时创建,这块区域是JVM中最大的, 用于存储应用的对象和数组,也是GC主要的回收区,一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行。在JDK1.8之前堆内存分为三部分:新生代、老年代、永久代。
JDK1.8及之后堆可分为:年轻代和老年代两块区域。使用NewRatio参数来设定比例。对于年轻代,一个Eden区和两个Suvivor区,使用参数SuvivorRatio来设定大小。
注意:
Jdk1.6及之前:常量池分配在永久代。
Jdk1.7:有,但已经逐步“去永久代”。
Jdk1.8及之后:无永久代,改用元空间代替(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。

栈(线程栈):Java栈是线程私有的,是在线程创建时创建,它的生命期跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致。基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
每个方法执行的时候都会创建一个栈帧,栈帧中主要存储3类数据:
局部变量表:输入参数和输出参数以及方法内的变量;
栈操作:记录出栈和入栈的操作;
栈帧数据:包括类文件、方法等等。

栈帧:一个方法对应一个栈帧内存空间,每个方法都有自己独立的栈帧内存空间,栈是一种数据结构:保持先进后出的原则。
栈帧内部细节结构:局部变量表、操作数栈、动态链接、方法出口。
栈帧就是每个方法需要的运行时内存空间。

  1. 每个运行时所需要的内存,称作为虚拟机栈;
  2. 每个栈由多个栈帧组成,对应着每次方法调用时占用的内存;
  3. 每个线程只能有一个活动的栈,对应着当前正在执行的方法。

程序计数器(PC寄存器):记录当前线程执行下一行指令的执行地址,作用主要记录多线程因上下文切换过程中记录当前线程的下一行指令。也就是记录当前线程执行的行号。

本地方法栈:本地方法栈和JVM栈发挥的作用非常相似,也是线程私有的,区别是JVM栈为JVM执行Java方法(也就是字节码)服务,而本地方法栈为JVM使用到的Native方法服务。它的具体做法是在本地方法栈中登记native方法,在执行引擎执行时加载Native Liberies.有的虚拟机(比如Sun Hotpot)直接把两者合二为一。也就是 java调用c语言代码 jni技术。

方法区:是线程共享的,主要存储类信息、常量池、静态变量、JIT编译后的代码等数据。方法区理论上来说是堆的逻辑组成部分。
JDK6、JDK7时,方法区就是 PermGen(永久代);JDK8时,方法区就是 Metaspace(元空间)。

16. 堆(heap)和栈(stack)的区别?

空间分配:堆需要程序员申请,而栈由系统自动分配;
线程是否共享:堆是存储单位(线程共享),而栈是运行时的单位,主要存放局部变量信息(线程私有);
缓存方式:堆存放在二级缓存中,生命周期由垃圾回收算法决定,而栈使用的是一级缓存,调用时处于存储空间中,调用完毕立即释放(生命周期和线程保持一致);
数据结构:堆可以被看成是一棵树(堆排序),而栈是一种先进后出的数据结构。

17.什么是栈内存溢出?

方法(栈帧)调用过多会导致栈内存溢出,比如说方法递归调用。原因:在栈空间中产生了非常多的栈帧空间一直没有被释放。JVM就会报java.lang.StackOverflowError的错误。
-Xss256k

18. 如何避免栈内存溢出?

增加栈内存;减少递归深度调用(通过一定条件退出)。

19. 什么是堆内存溢出?

在申请内存的时候,内存不足,产生堆内存溢出,JVM就会报java.lang.OutOfMemoryError: Java heap space的错误。
-Xmx8m

20. 什么是堆内存泄漏?

被占用的内存经过多次长时间的GC操作都无法回收,导致可用内存越来越少,俗称内存泄露,JVM就会报java.lang.OutOfMemoryError: GC overhead limit exceeded错误。
-Xmx3M -Xms3M

21. 什么情况下会导致CPU飙升?

  1. Tomcat部署的项目在高并发的时候,所有线程都处于运行状态,非常消耗CPU资源;
  2. 使用定时任务跑批;
  3. 云服务器Redis,根据6379端口号注入挖矿程序(Redis端口号不应该允许外网访问);
  4. 使用分布式锁,在没有拿到锁的时候,会采用CAS自旋(乐观锁),虽然能够让线程一直处于用户态,但是自旋会消耗CPU资源,所以在自旋的时候一定要有次数限制。

22. 如何排查生产环境CPU飙升的问题?

  1. 如果是Win版本,就查看任务管理器,看哪个进程最消耗CPU资源;
  2. 如果是Linux版本,则使用top命令进行查看。

23. 如何避免生产环境CPU飙升?

  1. 对服务器的接口实现限流、熔断、降级;
  2. 比较耗时的代码不要写成同步,尽量使用MQ;
  3. 定时任务项目一定要和业务逻辑项目分开部署;
  4. 云服务器部署不要随意开放端口号;
  5. 写自旋锁一定要控制死循环次数。

24. JDK监控和故障处理工具有哪些?

JDK命令行工具:

  1. Jps:查看所有Java进程;
  2. Jstat监视虚拟机各种运行状态信息;
  3. jinfo: 实时地查看和调整虚拟机各项参数;
  4. jmap:生成堆转储快照;
  5. jhat: 分析heapdump文件;
  6. jstack :生成虚拟机当前时刻的线程快照

可视化分析工具:jconsole、jvisualvm。

25. 什么是方法区(元空间)内存溢出?

当方法区的内存满了的时候,JVM会报java.lang.OutOfMemoryError: Metaspace(JDK1.8及之后)的错误(JDK1.8之前:java.lang.OutOfMemoryError: PermGen space)。
JDK1.8之前版本:-XX:MaxPermSize=8m
JDK1.8及之后版本:-XX:MaxMetaspaceSize=8m
-XX:-UseCompressedClassPointers

26. 什么是常量池?

通过一张表,虚拟机根据该常量表找到执行的类名、方法名、参数类型、字面量。常量池的分为:静态常量池、字符串常量池、运行时常量池。

静态常量池(class常量池):我们都知道,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

运行时常量池:则是JVM虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在元空间中,我们常说的常量池,就是指方法区中的运行时常量池。

字符串常量池:JVM在运行时使用了全局的StringTable(就是一个哈希表)。Hotspot从Java7开始,存放于堆里。比如String a=”a”。

27. 字符串常量池存放在JVM的哪块区域?

JDK1.6及之前常量池都是放入方法区(永久区);
Jdk1.7常量池放入到堆中(不合理);
Jdk1.8只是将字符串常量池放入到堆,其他常量都是放在元空间。

28. 如何查看字符串常量池?

Javap -c -v class文件地址
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第9张图片

29. 运行下面代码输出的结果是什么?

String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;// StringBuilder
String s5 = "ab";
String s6 = s4.intern();// 将对象放入到字符串常量池中,如果存在,则不会放入
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);

结果:false,true,true

30. 谈谈Java对象的创建过程?

Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第10张图片

  1. 类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

  2. 分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
    内存分配的两种方式:
    选择以下两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。
    Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第11张图片
    内存分配并发问题:
    创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
    CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
    TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。

  3. 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  4. 设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

  5. 执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

31. 谈谈Java对象的布局?

Java对象分为:对象头、实例数据、对齐填充组合。
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第12张图片
对象头:HotSpot虚拟机的对象头(Object Header)包括两部分信息:
第一部分"Mark Word":用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等.
第二部分"Klass Pointer":对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。)

Mark Word:
在这里插入图片描述
注意:在64位的虚拟机情况下mark word占用64位,32位虚拟机占32位。64位=8字节

Klass Pointer:这一部分用于存储对象的类型指针,该指针指向它的类元数据,jvm通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64的JVM将会比32位的JVM多耗费50的内存。为了节约内存可以使用选项 -XX:+UseCompressedOops 开启指针压缩。其中 oop即ordinary object pointer 普通对象指针。
-XX:+UseCompressedOops 开启指针压缩
-XX:-UseCompressedOops 不开启指针压缩
对象头:Mark Word+Klass Pointer类型指针 未开启压缩的情况下
32位 Mark Word =4bytes ,类型指针 4bytes ,对象头=8bytes =64bits
64位 Mark Word =8bytes ,类型指针 8bytes ,对象头=16bytes=128bits;
注意:默认情况下,开启了指针压缩 可能只有12字节。

实例属性:就是定义类中的成员属性

对齐填充:对齐填充并不是必然存在的,也没有特定的含义,仅仅起着占位符的作用。
由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

32. 对象的访问定位有哪两种方式?

句柄方式和直接指针方式。
句柄:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第13张图片
直接指针:如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象的地址。
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第14张图片
这两种对象访问方式各有优势。使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

33. 如何判断对象是否可被回收?

核心思想:堆内存中的对象没有被任何引用。

34. JVM是采用什么方法来判断对象是否可被回收的?

引用计数法:每次当该对象被引用一次的时候,引用次数都会+1,如果引用的次数为0,则认为没有被引用,直接被垃圾回收器清理掉。
最大的缺陷:当两个对象相互引用时,虽然他们不会再被引用了,但他们的计数不能归为0,所以无法垃圾回收(如下图)。Java 没有使用此类算法。
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第15张图片
可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
JVM采用可达性分析算法判断对象是否可被回收。(解决了循环引用的问题)

35. 哪些可以作为GC Roots对象呢?

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 元空间中类静态属性引用的对象。
  3. 本地方法栈中JNI(即一般说的Native方法)引用的对象。
    常量池。

36. 简单谈一谈强引用、软引用、弱引用、虚引用?

强引用:被引用关联的对象永远不会被垃圾收集器回收。
例如:Object object = new Object();那object就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

软引用:软引用关联的对象,在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。使用SoftReference类创建。

弱引用:无论内存是否足够,只要JVM开始进行垃圾回收,那些被弱引用关联的对象都会被回收。使用WeakReference类创建。

虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。使用PhantomReference类创建。

37. 什么是引用队列?

效果:引用队列可以配合软引用、弱引用及虚引用使用,当引用的对象将要被JVM回收时,会将其加入到引用队列中。
应用:通过引用队列可以了解JVM垃圾回收情况。

38. 能谈一谈堆空间的基本结构吗?对象内存是如何分配的?

Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。
Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第16张图片
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
经过这次 GC 后,Eden 区和"From"区已经被清空。这个时候,“From"和"To"会交换他们的角色,也就是新的"To"就是上次 GC 前的“From”,新的"From"就是上次 GC 前的"To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。

39. 堆内存常见的分配策略有哪些?

  1. 对象优先在eden区分配;
    大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC,如果Survivor空间足够,直接存放在Survivor区,如果Survivor区空间不足,则会通过分配担保机制将新生代的对象提前转移到老年代中去,如果老年代空间不足的话,则会出现Full GC。-XX:+PrintGCDetails(打印GC日志)
  2. 大对象直接进入老年代;
    大对象就是需要大量连续内存空间的对象(比如:字符串、数组),目的:为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
  3. 长期存活的对象将进入老年代;
    如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。(CMS是6岁)
  4. 动态对象年龄判定;
    Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值。
  5. 空间分配担保;

Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第17张图片

40. 主要进行GC的区域有哪些?

部分收集 (Partial GC):
新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。

整堆收集 (Full GC):收集整个Java堆和方法区。

41. 垃圾收集算法有哪些?

  1. 标记-清除算法:
    根据gcRoots对象的引用链,发现如果该对象没有被引用的情况下,则标记为垃圾,再清除。
    优点:算法简单
    缺点:容易产生内存碎片
    Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第18张图片
  2. 标记-整理算法:
    根据gcRoots对象的引用链,发现如果该对象没有被引用的情况下,则标记为垃圾,标记整理与标记清除区别在于:避免标记清除算法产生的碎片问题,清除垃圾过程中,会将可用的对象实现移动,内存空间更加具有连续性。
    优点:没有内存的碎片问题;
    缺点:整理过程中会产生内存地址移动,效率可能偏低。
    Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第19张图片
  3. 标记-复制算法:
    根据gcRoots对象的引用链,发现如果该对象没有被引用的情况下,将正在被引用的对象拷贝到to区中,然后再直接清理整个from区,再交换位置。
    优点:不会产生内存碎片;
    缺点:比较占内存空间。
    Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第20张图片
  4. 分代收集算法:
    主要分为新生代和老年代,针对不同的场景使用不同的回收算法。新生代采用标记-复制算法,老年代采用标记-清除/标记-整理算法。
    默认:新生代(Young)与老年代(Old)的比例值为1:2 (该值可以通过参数-XX:NewRatio来指定);Eden:from:to=8:1:1 (可以通过参数-XX:SurvivorRatio 来设定)。
    新生代:刚创建的对象都存放在新生代中eden区,当eden区空间内存满之后,则根据GC可达分析算法,将幸存的对象拷贝到to区中,并且寿命+1. 如果该对象的寿命>15的情况下,则将该对象放入到老年代中。
    老年代:Minor GC新生代GC ,回收多次如果该对象还一直被引用的情况下,则放入到老年代中,如果新生代和老年代内存都满的情况下,则会触发FullGC。
    总结:
    1.对象首先会分配到eden区(伊田园区);
    2.当新生代空间不足时,触发Minor GC,将eden区(伊田园区)存活的对象采用标记复制算法放入到to区中,并且该对象的寿命+1;
    注意:Minor GC因为标记复制算法,会触发stop the world 暂停其他用户的线程,等待垃圾回收结束之后,其他的用户线程才会继续执行;
    3.如果该对象的寿命>15的情况下,则将该对象放入到老年代中,对象寿命放入在对象头中;
    4.当老年代空间不足的时候,会触发FullGC 采用标记清理/整理算法;
    5.新生代GC非常频繁,速度比老年代GC要高;
    Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第21张图片

42. 为什么新生代采用标记-复制算法,老年代采用标记-整理/标记清除算法?

因为新生代gc非常频繁,所以选择效率比较高的垃圾回收算法。

43. 什么是Stop-The-World?

在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
补充:所谓的Stop the World机制,简称STW,即在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行。这些工作都是由虚拟机在后台自动发起和自动完成的,是在用户不可见的情况下把用户正常工作的线程全部停下来,这对于很多的应用程序,尤其是那些对于实时性要求很高的程序来说是难以接受的。所以在服务器项目中,应该设计能够去减少Stop-The-World问题。
注意:市面上所有的垃圾收集器都有Stop-The-World问题,开发中尽量不要调用 System.gc(),有可能会导致Stop-The-World问题。
1)停止所有的Java执行线程(“stop the world”)可达性分析必须在一致性的快照中进行,一致性指的是不可以出现分析过程中对象引用关系还在不断变化的情况。这点是导致GC进行时必须停顿所有java线程的一个原因。
2)准确式GC:当系统停下来时,不需要一个不漏的检查完所有执行上下文和全局的引用位置。HotSpot的实现是:在特定位置上(即安全点),虚拟机通过OopMap数据结构在类加载时,将对象内什么偏移量上是什么类型的数据计算出来,并存储到其中,来达到这个目的。在OopMap的协助下,HotSpot可以快速且准确的完成GCRoots的枚举。

44. GC有哪些核心的参数?

  1. -Xms:初始大小内存,默认为物理内存 1/64,等价于 -XX:InitialHeapSize
  2. -Xmx:最大分配内存,默认为物理内存的 1/4,等价于 -XX:MaxHeapSize
  3. -Xss:设置单个线程栈的大小,一般默认为 512-1024k,等价于 -XX:ThreadStackSize
  4. -Xmn:设置年轻代的大小,整个JVM内存大小 = 年轻代大小 + 年老代大小 + 持久代大小,持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
  5. -XX:MetaspaceSize:设置元空间大小,元空间的本质和永久代类似,都是对JVM规范中的方法区的实现。元空间与永久代之间最大区别:元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制,元空间默认比较小,我们可以调大一点。
  6. -XX:+PrintGCDetails:输出详细GC收集日志信息
  7. -XX:SurvivorRatio:设置新生代中eden和S0/S1空间比例,默认-XX:SurvivorRatio=8,Eden:S0:S1=8:1:1。
  8. -XX:NewRatio:配置年轻代和老年代在堆结构的占比,默认 -XX:NewRatio=2 新生代占1,老年代占2,年轻代占整个堆的 1/3。
  9. -XX:MaxTenuringThreshold:设置垃圾最大年龄。
  10. -XX:+PrintGCDetails -verbose:gc -XX:SurvivorRatio=2 -XX:NewRatio=1:新生代老年代参数比例设置。

45. 有哪些GC日志分析的工具?

GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat。

46. JDK有哪些核心的垃圾收集器?

  1. 串行收集器:Serial、Serial old(采用单线程回收垃圾,适合于堆内存空间比较小,个人小项目);
  2. 并行收集器:ParNew、Parallel Scavenge、Parallel old 多核多线程、堆内存空间比较大;
  3. 并发收集器:CMS、G1(分区算法)减少GC暂停用户线程时间尽可能最短。

Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第22张图片

47. 串行收集器与并行收集器的区别?

串行收集器:SerialGC 只有一个GC线程清理堆内存垃圾,堆内存不是很大;
并行(多线程)收集器: Parallel 开启多个GC线程同时清理堆内存垃圾。
共同特征:当GC线程开始清理堆内存垃圾的时候,都会让我们用户线程暂停,因为采用标记清理或者复制算法引用地址有可能会发生变化。
最大的缺点:会导致STW非常长。

48. 并行收集器与并发收集器的区别?

并行(Parallel)收集器:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;如ParNew、Parallel Scavenge、Parallel Old;
并发(Concurrent)收集器:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;如CMS、G1(也有并行)。

49. JDK中垃圾收集器有哪些组合方式?

JDK8(含Jdk8)之前的组合关系:Serial/Serial 0ld、Serial/CMS、 ParNew/Serial 0ld、 ParNew/CMS、Paral1el Scavenge/Serial 0ld、Paral1el Scavenge/Parallel 0ld、G1;
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第23张图片

50. 什么是吞吐量?

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
比如:应用程序运行了100s,其中垃圾收集花费1s,那么吞吐量占比为99%。(100/100+1)。

51. 能简单介绍一下这些收集器吗?

Serial收集器和Serial Old收集器:是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。新生代采用标记复制算法,老年代采用标记整理算法。优点:简单高效。主要应用于:桌面应用程序(堆内存空间很小)。参数配置:-XX:+PrintCommandLineFlags -XX:+UseSerialGC。

ParNew收集器:其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。采用标记-复制算法。应用场景:在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。参数配置:"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器;"-XX:+UseParNewGC":强制指定使用ParNew;"-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;-XX:+PrintCommandLineFlags -XX:+UseParNewGC。

Parallel Scavenge收集器:Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。采用标记-复制算法。参数配置:"-XX:MaxGCPauseMillis"(减少用户线程暂停时间):控制最大垃圾收集停顿时间,大于0的毫秒数;MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;因为可能导致垃圾收集发生得更频繁;"-XX:GCTimeRatio" (吞吐量优先):设置垃圾收集时间占总时间的比率,0

Parallel Old收集器:Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

CMS(Concurrent Mark Sweep)收集器:以获取最短回收停顿时间【也就是指Stop The World的停顿时间】为目标,多数应用于互联网站或者B/S系统的服务器端上。其中“Concurrent”并发是指垃圾收集的线程和用户执行的线程是可以同时执行的。CMS是基于“标记-清除”算法实现的。

G1(Garbage一First)收集器:是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征,JDK9中已经默认使用G1收集器,可以全功能的垃圾收集器,采用标记整理算法避免堆空间冗余性问题。
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第24张图片

52. 能具体谈一谈CMS收集器的实现过程吗?

CMS是基于“标记-清除”算法实现的,整个过程分为4个步骤:

  1. 初始标记(CMS initial mark)。
  2. 并发标记(CMS concurrent mark)。
  3. 重新标记(CMS remark)。
  4. 并发清除(CMS concurrent sweep)。

注意:“标记”是指将存活的对象和要回收的对象都给标记出来,而“清除”是指清除掉将要回收的对象。
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。

  1. 初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。
  2. 并发标记阶段【也就说明不会阻碍业务线程继续执行,因为它所以还会有下面要说的“重新标记”阶段了】就是进行GC Roots Tracing【其实就是从GC Roots开始找到它能引用的所有其它对象】的过程。
  3. 重新标记阶段则是为了修正并发标记期间因用户程序继续动作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  4. CMS收集器的动作步骤如下图所示,在整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的:
    Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第25张图片
    Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第26张图片

53. CMS收集器有什么优缺点?

优点:

  1. 并发收集器 GC线程可以与用户线程同时并发执行;
  2. 降低用户线程等待时间(STW);

缺点:

  1. 会发生内存碎片化(标记清除);
  2. 用户线程空间不足,无法存放大对象的情况下,有可能会触发FULLGC;
  3. 消耗CPU资源(与用户线程同时执行);
  4. CMS收集器无法处理浮动垃圾 在并发标记阶段如果产生了新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时的回收,从而只能在下一次执行GC时释放这些之前未被回收的对相关。
  5. 如果存放的对象大于新生代的内存空间,会直接晋升老年代,如果对象不是很频繁使用,会非常浪费堆内存空间。

54. 为什么CMS收集器采用标记-清除算法,而不采用标记-整理算法?

因为如果采用标记-整理算法,为了保证内存空间的连续性,必须移动内存中的地址,而在内存地址的移动过程中,会暂停所有的用户线程,STW的时间增长,与CMS收集器以获取最短回收停顿时间为目标的观念相背离。虽然采用标记-清除算法,会导致内存空间不连续,在存放大对象的时候无法进行存放,从而导致Full GC,但是当触发Full GC的时候开始采用备选方案(串行老年代GC),使用标记-整理算法整理堆内存空间。
简述:因为CMS收集器采用并行的方式,清除垃圾与用户线程可以同时运行,为了保证用户线程与GC线程同时运行,所以采用标记清除算法,如果采用标记整理算法,有可能会导致移动内存地址,会发生的stw问题。

55. 能说说CMS收集器中有哪些配置参数吗?

-XX:+UseConcMarkSweepGc 手动指定使用CMS收集器执行内存回收任务。
开启该参数后会自动将-XX: +UseParNewGc打开。即:ParNew(Young区用)+CMS(0ld区用)+Serial 0ld的组合。
-XX:CMS1nitiatingOccupanyFraction设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一 次CMS 回收。JDK6 及以上版本默认值为92%,如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。
-XX:+UseCMSCompactAtFullCollection用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
-XX:CMSFullGCsBeforeCompaction设置在执行多少次Full GC后对内存空间进行压缩整理。
-XX:ParallelCMSThreads设置CMS的线程数量。CMS默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是年轻代并行收集器的线程数。当CPU资源比较紧张时,受到CMs收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。新生代回收线程数:和当前cpu核数相等。

56. 为什么叫G1收集器?

因为G1是一个并行/并发回收器,它把堆内存分割为很多不相关的区域(Region) (物理上 不连续的)。使用不同的Region来表示Eden、幸存者0(S0)区,幸存者(S1)1区,老年代等。由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)。G1 GC有计划地避免在整个Java 堆中进行全区域的垃圾收集。G1跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

57. 能具体谈谈G1收集器的分区原理吗?

使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB、2MB、4MB、8MB、16MB、32MB。
可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。
虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。
一个region(分区)只能属于一个角色,有可能为eden区、S区、老年代等,E表示为Eden区、S区表示为S1,S0区,老年代O区,空白的表示为未使用的分配的内存,H区存放巨型对象。
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第27张图片

58. G1收集器的核心参数有哪些?

JDK9已经默认开启了G1收集器,如果在JDK8开启G1收集器。需要配置
-XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB 到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。 也就是G1收集器最小堆内存应该是2GB以上,最大堆内存64GB
-XX:MaxGCPauseMillis 设置期望达到的最大Gc停顿时间指标 ,默认值是200ms
-XX:ParallelGCThread 设置垃圾回收线程数 最大设置为8
-XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
-XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
-XX:+UseG1GC 设置开启G1收集器
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGCDetails -verbose:gc

59. 为什么G1收集器需要设计存放巨型对象(H区)?

在G1收集器中也有一个新的内存区域,称作为:Humongous (H)区(巨型对象),主要存放一些比较大的对象,一个对象大于region的一半时,称之为巨型对象,G1不会对巨型对象进行拷贝,回收时会考虑优先回收。
在以前收集器中,如果是一个大对象是直接放入到老年代中,而触发老年代GC不是很频繁,万一该大对象不是非常频繁的使用,则会非常浪费堆内存,为了解决这个问题在G1收集器专门弄一个H区存放巨型对象,如果一个H区装不下的情况下,则会寻找连续H区存储,如果还没有足够的空间,有可能会引发FULLGC。

60. G1收集器的RSet有什么作用?

在一个region中可能会引入到其他的region,为了避免不必要的全局扫描,在每个region中都对应一个Remembered Set(记忆集),使用CarTable记录每个region区相互引用的关系。
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第28张图片

61. G1收集器的CSet有什么作用?

收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。
简述:CSet用于记录用户线程暂停的区域。

62. 谈谈G1收集器的回收过程?

  1. 新生代GC(Young GC):当新生代eden区内存满的时候,G1年轻代收集器会采用并行多线程的方式清理堆内存垃圾,这时候会暂停所有用户的线程,让后新生代存活的对象会拷贝到S区或者老年代中,和我们在以前所学习的新生代收集器原理基本相同。(标记复制算法)
  2. 新生代和并发标记过程(Concurrent Marking):在新生代进行回收时,进行GCRoot初始化标记与CMS实现原理基本相同,老年代达到堆内存空间阈值时,会实现并发标记(不会stw),jvm配置参数-XX:InitiatingHeapOccupancyPercen=45%
  3. 混合收集(Mixed GC):当越来越多的对象晋升到老年代的时候,为了避免堆内存耗尽,会触发混合收集器,即Mixed GC。回收整个Young Region(新生代区域),部分的老年代区域,如果G1无法有足够的空间复制对象的时候,有可能会引发FullGc。
  4. FullGC收集。
    Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第29张图片
    Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第30张图片

63. 能谈谈三色标记算法吗?

GC如果想查找到存活的对象,根据可达分析算法 根据GCRoot引用链遍历存活对象。根据GC Root遍历过程中,按照是否访问过该对象分为三种不同颜色:
白色:本对象没有访问过(有可能是垃圾对象);
灰色:本对象已经被访问过,且本对象的所有属性没有访问过;本对象所有属性都访问过后,本对象由灰色变为黑色;
黑色:本对象已经被访问过,且本对象的所有属性都被访问过;
Mr. Cappuccino的第19杯咖啡——金三银四面试题之JVM性能调优篇_第31张图片
原理:

  1. 初始时,所有对象都放在白色容器中;
  2. 初始标记阶段,暂停用户线程,根据GC Roots查找到直接关联的对象A和B,并把A和B由白色变为灰色,同时放在灰色的盒子中;
  3. 并发标记阶段,获取灰色容器中的所有对象(A和B),并作为起始点扫描整个引用链,如果B对象的所有属性都被扫描完了,则将B对象由灰色变成黑色,其扫描到的引用对象(C对象)由白色变成灰色,依次循环直至扫描完毕(当灰色容器中的对象为空时,则说明全部扫描完毕),最后,能够被GC Roots引用的对象都会变成黑色;
  4. 扫描完毕后,白色容器中剩下的对象则认为是不可达对象,可以被垃圾回收线程清理掉。

64. 使用三色标记算法会存在什么样的问题?

在并发标记阶段,由于用户线程和GC线程同时运行,可能会存在多标和漏标问题。

多标问题(浮动垃圾):
在并发标记阶段,用户线程与GC线程同时运行,如果现在C对象已经扫描完毕,C对象由灰色变成黑色,扫描到的E对象和D对象由白色变成灰色,但是用户线程执行C.E=null,也就是说此时的E对象已经变为了垃圾对象,可是GC线程仍然会认为E对象是可达对象,会将E对象作为起始点继续扫描E对象以下的整个引用链,整个引用链的对象则是多标对象。该问题可以在重新标记阶段进行修复。
在并发清除阶段,用户线程与GC线程同时运行,会产生新的对象,但是没有及时被GC清理,只能在下一次的GC中进行修复。

漏标问题:
在并发标记阶段,用户线程与GC线程同时运行,如果现在B对象已经扫描完毕,B对象由灰色变成黑色,扫描到的C对象由白色变成灰色,然后用户线程执行C.E=null,C与E断开连接,则认为E是不可达对象,可是用户线程突然又执行了B.E=E,此时的E对象应该是可达对象,但是由于B对象已经变为了黑色对象不会继续扫描,导致E对象没有被GC线程扫描到,则认为E对象是不可达对象,这里的E对象则是漏标对象。

漏标的两个充要条件:

  1. 至少有一个黑色对象指向了白色对象;
  2. 所有的灰色对象在自己的引用扫描完成之前删除了对白色对象的引用;

65. CMS收集器和G1收集器如何解决漏标问题?

为了解决漏标问题,需要破坏漏标的两个充要条件。
强三色不变式:保证永远不会存在黑色对象到白色对象的引用(破坏情况1)。
弱三色不变式:所有被黑色对象引用的白色对象都处于灰色保护状态,即直接或间接从灰色对象可达(破坏情况2)。

CMS收集器采用增量更新+写屏障的方式实现强三色不变式:当黑色对象指向了白色对象之后,利用写屏障把这个引用记录下来,在重新标记阶段,再以黑色对象为根,对它的引用进行重新扫描。优点:避免浮动垃圾;缺点:需要对整个链进行扫描,效率低。

G1收集器采用原始快照(Snapshot At The Beginning,SATB)+写屏障的方式实现弱三色不变式:当灰色对象取消对白色对象的引用时,利用写屏障把这个引用记录下来,在重新标记阶段,把白色对象变成灰色对象,以灰色对象为根,继续对它的引用进行扫描。优点:效率高,无需扫描整个引用链;缺点:会产生浮动垃圾。

写屏障(Store Barrier):所谓的写屏障,其实就是指在赋值操作前后,加入一些处理(可以参考AOP的概念):记录赋值的操作。

66. 如何减少堆内存触发GC的频率?

  1. 项目启动的时候堆内存初始值与最大值保持一致;
  2. 不建议生产环境调用system.gc()方法;
  3. 减少全局变量和大对象的使用 触发Full GC。

你可能感兴趣的:(金三银四,mr,java,intellij-idea,jvm.gc,jvm)