转载

浅谈JVM面试题

关于JVM的面试题转载和整理

鸣谢:各路大神!

1. 关于JVM的内存结构

答:JVM内存结构可以大致分为线程私有区域共享区域,线程私有区域由虚拟机栈、本地方法栈、程序计数器组成,而共享区由堆、元数据空间(方法区)组成。

image-20200924210437316

虚拟机/本地方法栈

StackOverflowException异常出现原因:JVM会为每个方法生成栈帧然后将栈帧压入虚拟机栈中,例如设置JVM参数-Xss为1m,如果在方法里创建一个128kb的数组,那这个方法在同一个线程中只能递归四次,待递归到第五次就会报栈溢出异常,因为每次递归都需要128kb的空间,递归到第五次明显超出了。

image-20200924211008671

程序计数器

程序计数器是一个记录当前线程所执行的字节码的行号指示器。JVM的多线程是通过CPU时间片轮转(即线程轮换流并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。简单来说,程序计数器的主要功能是记录当前线程所执行的字节码的行号指示器。

方法区(元数据区)

方法区存储了类的元数据信息、静态变量、常量等数据。

image-20200925115251618

堆(heap)

使用new关键字创建的对象都会进入堆中,堆也是GC重点照顾的区域,堆会被划分为:新生代、老年代,而新生代还会被划分为Eden区和Survivor区。

image-20200925115532388

新生代的Eden区和Survivor区,是根据JVM回收算法来的,只是现在大部分都是使用的分代回收算法,所以在介绍堆将新生代归纳为Eden区和Survivor区。

小结

  • JVM内存模型分为线程私有区域共享区域
  • 虚拟机栈、本地方法栈负责存放线程执行方法栈帧
  • 程序计数器用于记录线程执行指令的位置
  • 方法区(元数据区)存储类的元数据信息、静态变量、常量等数据
  • 堆(heap)使用new关键字创建的对象都会进入堆中,堆被划分为新生代和老生代

2. 对象什么时候被回收

JVM判断对象回收的方式有两种:

**① 引用计数:**JVM为每个对象维护一个引用计数,假设A对象引用计数为零说明没有任务对象引用A对象,那A对象就可以被回收,但是引用计数无法解决循环引用的问题

**② GC Roots:**通过一系列的名为GC Roots的对象作为起始点,从这些节点向下搜索,搜索过程成为引用链。当一个对象到GC Roots没有任何引用链时,则证明对象是不可用的

在Java中,可以作为GC Roots的对象包括以下几种:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法中JNI(Native方法)的引用的对象

小结

当一个对象通过GC Roots搜索不到时,就说明对象可以被回收了

3. 常见的垃圾器回收算法

① 标记清除: 该算法分为两步,标记、清除两个阶段,标记阶段从根集合(GCRoots)开始扫描,没到一个对象就会标记该对象为存活状态,清除阶段在扫描完成后将没有标记的对象清除。

image-20200925132822196

该算法会产生内存碎片,后续大的对象将没有连续的内存可供使用

② 标记整理:标记整理不会发生内存碎片的问题,从根集合(GC Roots)开始扫描进行标记然后清除无用的对象,清除完成后它会整理内存

image-20200925125221160

这种方法会将内存连续,但是存在新的问题,每次都要移动对象,成本高

③ 复制算法:将JVM堆分成两等份,如果堆设置为1GB,那是用复制算法就会被划分为两块区域,各为512MB,总是使用其中的一块给对象分配内存,分配满了后,GC就会标记,然后存货的对象会被移动到另一块空白的区域,然后清除所有没有存活的对象,重复这样的处理,始终有一块空白区域没有被合理利用

image-20200925125800189

image-20200925125811447

两块区域交替使用,最大的问题是会造成空间浪费,堆内存的利用率只有50%

小结

  • 标记清除速度快,但会产生内存碎片
  • 标记整理解决的内存碎片的问题,但每次都要移动对象,成本高
  • 复制算法没有内存碎片也不需要移动对象,但是会导致空间浪费

4. 什么时候对象进入老年代

新创建的对象一开始都会停留在新生代,但随着JVM的运行,存活时间长的对象会慢慢的移动到老年代中

根据对象年龄

JVM会给对象增加一个年龄计数器,对象每熬过一个GC,年龄+1,带对象到达设置的阈值(默认是15岁)就会被移动到老年代,可通过-XX:MaxtrnuringThreshold调整这个阈值

image-20200925151335285

动态年龄判断

根据对象年龄有另一个策略也会让对象进入老年代,不用等15词GC之后进入老年代,规则是,假设存放对象的Survivior,一批对象的总大小大于这块Survivor内存的50%,那么大于这批对象年龄的对象,就直接进入老年代

image-20200925155839893

大对象直接进入老年代

如果设置了-XX:PretenureSizeThreshold这个参数,那么如果你要创建的对象大于这个参数值,如分配一个超大数组,则直接把这个对象放入老年代,不会经过新生代,可以避免大对象GC时消耗太多时间

5. 如何判断对象是否已死

① 引用计数器法:给每一个对象添加一个引用计数器,每当一个地方引用到它时,计数器+1;否则,-1.但是主流的虚拟机没有选择这种算法来管理内存,原因是当某些对象互相引用时,无法判断这些对象是否已死

image-20200925162732196

可达性分析算法

GC Roots,作为垃圾收集的起点,是虚拟机栈中本地变量表中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可及)时,说明该对象不可用,是死对象

image-20200925163505534

上面的死对象并非必死无疑,可达性分析后,会将对象进行一次标记,接着判断如果对象没有覆盖Object的finalize()方法或者finalize()方法已经被虚拟机调用过,那么它们会被清除;如果对象覆盖了finalize()方法且还没被调用,则会执行finalize()方法中的内容,所以在finalize()方法中如果重新与GC Roots引用链上的对象关联就可以拯救自己,但一般不推荐这么做

③ 方法区回收:上面说的都是对堆内存中对象的判断,方法区中主要回收的是废弃的常量和无用的类。判断常量是否被废弃可以判断是否有地方引用这个变量,如果没有引用则为废弃的常量。判断类是否废弃需要用时满足如下条件:该类所有的实例已经被回收(堆中不存在任何该类的实例)。加载该类的ClassLoader已经被回收。该类对应的java.lang.Class对象在任何地方没有被引用(无法通过反射访问该类的方法)

6. 什么是空间分配担保策略

JVM在Minor GC之前,会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,则此次Minor GC是安全的;如果小于,虚拟机会查看handlePromotionFailure设置的值是否允许担保失败。如果值为true,会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象平均大小,如果大于则尝试一次Minor GC,但这次Minor GC依然是有风险的。如果值为false,则改为进行一次Full GC

image-20200925165207751

7. 如何优化减少Full GC

以4C 8G机器为例,系统预留4G,其它4G如下分配:

  • 堆内存:3G

  • 新生代:1.5G

  • 新生代Eden区:1228M

  • 新生代Survivior区:153M

  • 方法区:256M

  • 虚拟机栈:1M/thread

    设置参数如下:

    -Xms3072m
    -Xmx3072m-Xmn1536m-Xss=1m-XX:PermSize=256m-XX:MaxPermSize=256m-XX:HandlePromotionFailure-XX:SurvivorRatio=8
    

    image-20200925172735824

估计系统每秒占用内存数量

在优化JVM之前,需要估算系统每秒占用的内存数量,计算多长时间触发一次Minor GC。如有个日活百万的商场系统,每日下单量在20w左右,按照一天8个小时算,那订单服务的每秒大概会有500个请求,然后粗略的估算下每个请求占用多少内存,计算出每秒要花费多少内存。

假设是每秒500个请求,每个请求需要分配100k的空间,那1秒需要分配大约50m的内存。计算下多长时间触发一次Minor GC按照之前的估算1秒需要分配大约50m的内存的话,Eden区的空间是1228m那平均每25秒就要执行一次Minor GC

检查Survivior区是否足够

按照上面的模型,每25秒就要执行一次Minor GC,GC执行期间并不能回收掉所有的新生代中的对象,那每秒50m那每次GC执行期间还会剩下大约100m无法回收的对象会进入Survivor区,但是别忘记JVM有动态年龄判断机制,这样设置下来Survivor的空间明显小了一点,所以将新生代设置2048m,才能避免触发动态年龄判断

大对象直接进入老年代

大对象一般是长期存活和使用的对象,一般来说设置1M的对象直接进入老年代,这样避免大对象一直处于新生代中来回复制,所以加上PretenureSizeThreshold=1m参数

合理设置对象年龄阈值Minor GC后默认躲过15次垃圾回收后自动升入老年代,按照我们的评估25秒触发一次Minor GC,如果按照MaxTenuringThreshold参数的默认值,躲过15次GC后,应该是6分钟之后的事了,结合当前业务场景这里可以降低一点,让那些本应该进入老年代的对象,尽快的进入老年代,避免复制成本和浪费新生代空间,从而导致新生代Survivor空间不足,引发Full GC

正文到此结束
本文目录