2. Interview-JVM&GC

2020-07-16  本文已影响0人  allen锅

JVM知识图谱

JVM知识图谱 GC知识图谱 美团面试题

1 怎么解决OOM?/ 怎么排查OOM?/ JVM调优

参考:https://blog.csdn.net/BigData_Mining/article/details/80874549

1.1 JDK自带工具

jmap

1.2 阿里开源JVM调优工具arthas

1.3 经验排查

GC log参数

2 JVM参数详解

2.1 JVM参数分类

2.2 关键JVM参数

2.3 常见JVM配置

堆设置

收集器设置

垃圾回收统计信息

并行收集器设置

并发收集器设置

JVM参数配置 JVM参数-并行收集器 JVM参数-CMS JVM参数-辅助信息

3 内存溢出有多少种?

调整JVM参数测试OOM

3.1 Java堆内存溢出Java heap space

什么场景发生堆内存溢出?

代码示例

package cn.homecredit.jvm;

import java.util.ArrayList;
import java.util.List;

/**
 *VM Args:-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError
 *
 */
public class TestOOMHeap {
    static class OOMObject{
    }

    public static void main(String[]args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while(true) {
            list.add(new OOMObject());
        }
    }
}
C:\ProgramFiles\Java\jdk1.8.0_144\bin\java.exe "
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at java.util.Arrays.copyOf(Arrays.java:3181)
    at java.util.ArrayList.grow(ArrayList.java:261)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
    at java.util.ArrayList.add(ArrayList.java:458)
    at cn.homecredit.jvm.TestOOMHeap.main(TestOOMHeap.java:17)

解决方案

3.2 栈内存溢出

发生场景

代码示例

package cn.homecredit.jvm;

/**
 *VM Args:-Xss128k
 *
 */
public class TestOOMStack {
    private int stackLength=1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[]args) throws Throwable {
        TestOOMStack oom=new TestOOMStack();
        try{
            oom.stackLeak();
        }catch(Throwable e) {
            System.out.println("stack length:"+oom.stackLength);
            throw e;
        }
    }
}
C:\ProgramFiles\Java\jdk1.8.0_144\bin\java.exe "
stack length:19417
Exception in thread "main" java.lang.StackOverflowError
    at cn.homecredit.jvm.TestOOMStack.stackLeak(TestOOMStack.java:11)
    at cn.homecredit.jvm.TestOOMStack.stackLeak(TestOOMStack.java:11)
    at cn.homecredit.jvm.TestOOMStack.stackLeak(TestOOMStack.java:11)
    at cn.homecredit.jvm.TestOOMStack.stackLeak(TestOOMStack.java:11)
Process finished with exit code 1

解决方案

3.3 方法区内存溢出PermGen space

出现场景

String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

代码示例

package cn.homecredit.jvm;

import java.util.ArrayList;
import java.util.List;

public class TestOOMPerm {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        int i = 0;
        while(true) {
            i++;
            list.add(String.valueOf(i++).intern());
        }
    }
}

解决方案

3.4 Metaspace内存溢出java.lang.OutOfMemoryError: Metaspace

出现场景

元空间的溢出,系统会抛出java.lang.OutOfMemoryError: Metaspace。出现这个异常的问题的原因是系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法,导致元空间的内存占用很大。

代码示例

OOM-Metaspace

解决方案

默认情况下,元空间的大小仅受本地内存限制。但是为了整机的性能,尽量还是要对该项进行设置,以免造成整机的服务停机。

3.5 直接内存溢出java.lang.OutOfMemoryError: Direct buffer memory

出现场景

越过了DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。因为,虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。

代码示例

解决方案

3.6 数组超限内存溢出Requested array size exceeds VM limit

出现场景

给数组分配了很大的capacity

示例代码

OOM-数组超限

解决方案

3.7 创建本地线程内存溢出

出现场景

剩余内存不足以创建本地线程需要的内存空间。

示例代码

OOM-创建本地线程

解决方案

3.8 超出交换区内存溢出Out of swap space

出现场景

JVM请求总内存大于可用物理内存。

解决方案

3.9 GC超时内存溢出

出现场景

默认的jvm配置GC的时间超过98%,回收堆内存低于2%。

示例代码

OOM-GC超时

解决方案

要减少对象生命周期,尽量能快速的进行垃圾回收。

3.10 系统杀死进程内存溢出Kill process or sacrifice child

出现场景

当内核检测到系统内存不足时,OOM killer被激活,检查当前谁占用内存最多然后将该进程杀掉。

示例代码

OOM-killer

解决方案

虽然增加交换空间的方式可以缓解Java heap space异常,还是建议最好的方案就是升级系统内存,让java应用有足够的内存可用,就不会出现这种问题。

3.11 堆内存泄露导致OOM

4 内存泄漏(Memory Leak)和内存溢出(Memory Overflow)有什么区别?

ArrayList防止内存泄漏

5 java内存划分?静态类放在哪里?

Java内存划分 java内存划分-new JVM内存划分

线程共享

线程私有

操作数栈

6 java内存模型(JMM)

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图


java内存模型

JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的(稍后会分析)。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。关于JMM中的主内存和工作内存说明如下:

主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发生线程安全问题。

主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

java内存模型与硬件内存架构

主内存与工作内存8种交互操作

JMM的三大特性:

7 volatile关键字

volatile禁止指令重排序也有一些规则:
a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
b.在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。

volatile不能解决原子性,解决办法:

volatile原理:

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
I. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内
存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
II. 它会强制将对缓存的修改操作立即写入主存;
III. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

CAS导致的ABA问题:

CAS可以有效的提升并发的效率,但同时也会引入ABA问题。

如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。

所以JAVA中提供了AtomicStampedReference/AtomicMarkableReference来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。

8 GC需要完成哪3件事?

8.1 哪些内存需要回收?/ GC作用的对象是什么?/ 怎么确定垃圾?

8.2 什么时候回收?/ GC在什么时候工作?

这种回答大约占30%,遇到的话一般我就会准备转向别的话题,譬如算法、譬如SSH看看能否发掘一些他擅长的其他方面。

这种回答大约占55%,大部分应届生都能回答到这个答案,起码不能算错误是吧,后续应当细分一下到底是语言表述导致答案太笼统,还是本身就只有这样一个模糊的认识。

到了这个层次,基本上能说对GC运作有概念上的了解,譬如看过《深入JVM虚拟机》之类的。这部分不足10%。

列举一些我期望的回答:eden满了minor gc,升到老年代的对象大于老年代剩余空间full gc,或者小于时被HandlePromotionFailure参数强制full gc;gc与非gc时间耗时超过了GCTimeRatio的限制引发OOM,调优诸如通过NewRatio控制新生代老年代比例,通过MaxTenuringThreshold控制进入老年前生存次数等……能回答道这个阶段就会给我带来比较高的期望了,当然面试的时候正常人都不会记得每个参数的拼写,我自己写这段话的时候也是翻过手册的。回答道这部分的小于2%。

总结:程序员不能具体控制时间,系统在不可预测的时间调用System.gc()函数的时候;当然可以通过调优,用NewRatio控制newObject和oldObject的比例,用MaxTenuringThreshold 控制进入oldObject的次数,使得oldObject 存储空间延迟达到full gc,从而使得计时器引发gc时间延迟OOM的时间延迟,以延长对象生存期。

8.3 GC如何回收内存?/ GC做了些什么?

分析:同问题2第一点。40%。

起码把问题具体化了一些,如果像答案1那样我很难在回答中找到话题继续展开,大约占40%的人。

也是看过《深入JVM虚拟机》的基本都能回答道这个程度,其实到这个程度我已经比较期待了。同样小于10%。

总结:删除不使用的对象,回收内存空间;运行默认的finalize,当然程序员想立刻调用就用dipose调用以释放资源如文件句柄,JVM用from survivor、to survivor对它进行标记清理,对象序列化后也可以使它复活。

9 Java四种引用类型

软引用SoftReference 弱引用WeakReference 虚引用PhantomReference-管理堆外直接内存-DirectByteBuffer 弱引用 ThreadLocal

10 为什么分代?GC分代/堆(内存)分代/JVM分代管理

GC为什么分代?

堆分代或者GC分代回收的唯一理由就是优化GC性能,提高GC效率。

新生代:young区或者年轻代

老年代:old区

永久代:permanent区

11 GC触发条件

GC分类

young GC / Minor GC触发条件

Full GC / Major GC触发条件

12 垃圾回收算法

12.1 复制-清理算法-Copy Sweep

12.2 标记-清理算法-Mark Sweep

12.3 标记-整理算法 / 标记-压缩算法-Mark Compact

12.4 HotSpot的算法实现

13 垃圾回收器

十种垃圾回收器

13.0 三色标记

13.1 串行垃圾回收器Serial GC(几十M甚至200M内存,STW在几十最多100多毫秒)

13.2 并行垃圾回收器Parallel GC(几个G内存)

1 并行垃圾回收器ParNew GC

2 并行垃圾回收器Parallel Scavenge GC

3 并行垃圾回收器Parallel Old GC

13.3 CMS垃圾回收器(几十个G内存)

CMS特点

CMS工作流程

CMS三大缺陷

CMS:Incremental Update,解决三色标记的问题,将节点置为灰色

三色标记-CMS-Incremental Update

13.4 G1垃圾回收器Garbage First GC(上百G内存)

G1特点

三色标记-G1-SATB

G1跨Region回收策略

G1垃圾回收流程

G1的缺陷

G1中的屏障

所以一句简单的Java赋值语句,例如Object.Field=other_object,实际上被JVM处理成3条伪代码,如下所示:

RSet原理

RSet是一个抽象概念,记录对象在不同代际之间的引用关系,目的是加速垃圾回收的速度。JVM使用的是根对象引用的收集算法,即从根集合出发,标记所有存活的对象,然后遍历对象的每一个成员变量并继续标记,直到所有的对象标记完毕。在分代垃圾回收中,我们知道新生代和老生代处于不同的回收阶段,如果还是采用这样的标记方法,不合理也没必要。假设我们只回收新生代,如果标记时把老生代中的活跃对象全部标记,但回收时并没有回收老生代,则浪费了时间。同理,在回收老生代时有同样的问题。当且仅当我们要进行FGC时,才需要对内存做全部标记。所以算法设计者做了这样的设计——用一个RSet记录从非收集部分指向收集部分的指针的集合,而这个集合描述就是对象的引用关系。通常有两种方法记录引用关系,第一种为Point Out,第二种为Point In。假设有这样的引用关系,对象A的成员变量指向对象B(伪代码为:ObjA.Field = ObjB),对于Point Out的记录方式来说,会在对象A(ObjA)的RSet中记录对象B(ObjB)的地址;对于Point In的记录方式来说,会在对象B(ObjB)的RSet中记录对象A(ObjA)的地址,这相当于一种反向引用。这二者的区别在于处理时有所不同:Point Out记录简单,但是需要对RSet做全部扫描;Point In记录操作复杂,但是在标记扫描时可以直接找到有用和无用的对象,不需要进行额外的扫描,因为RSet里面的对象可以看作根对象。G1中使用的是Point In的方式,为了提高RSet的存储效率,使用了3种数据结构:

RSet的3种数据结构

G1新引入了Refine线程,它实际上是一个线程池,有两大功能:

虽然RSet是为了记录对象在代际之间的引用,但是并不是所有代际之间的引用都需要记录。我们简单地分析一下哪些情况需要使用RSet进行记录。分区之间的引用关系可以归纳为:

所以有必要对RSet进行优化,根据垃圾回收的原理,我们来逐一分析哪些引用关系需要记录在RSet中:

需要使用RSet保存引用关系的情况

SATB算法介绍

并发标记指的是标记线程和应用程序线程并发运行。那么标记线程如何并发地进行标记?并发标记时,一边标记垃圾对象,一边还在生成垃圾对象,如何能正确标记对象?为了解决这个问题,以前的垃圾回收算法采用串行执行方式,这里的串行指的是标记工作和对象生成工作不同时进行。而G1中引入了新的算法SATB,在介绍算法之前,我们先回顾一下对象分配。
在堆分区中分配对象时,对象都是连续分配的,所以可以设计几个指针,分别是Bottom、Prev、Next和Top。用Bottom指向堆分区的起始地址,用Prev指针指向上一次并发处理后的地址,用Next指向并发标记开始之前内存已经分配成功的地址,当并发标记开始之后,如果有新的对象分配,可以移动Top指针,使用Top指针指向当前内存分配成功的地址。Next指针和Top指针之间的地址就是应用程序线程新增对象使用的内存空间。如果假设Prev指针之前的对象已经标记成功,在并发标记的时候从根出发,不仅仅标记Prev和Next之间的对象,还标记了Prev指针之前活跃的对象。当并发标记结束之后,只需要把Prev指针设置为Next指针即可开始新一轮的标记处理。
Prev和Next指针解决了并发标记工作内存区域的问题,还需要引入两个额外的数据结构来记录内存标记的状态,典型的是使用位图(BitMap)来指示哪块内存已经使用,哪块内存还未使用,所以并发标记引入两个位图PrevBitmap和NextBitmap,用PrevBitmap记录Prev指针之前内存的标记状况,用NextBitmap表示整个内存从Bottom到Next指针之前的标记状态。
也许你会奇怪,NextBitmap包含了整个使用内存的标记状态,那为什么要引入PrevBitmap这个数据结构?这个数据结构在什么时候使用?我们可以想象,如果并发标记每次都成功,我们确实不需要用到PrevBitmap,只需要根据NextBitmap这个位图对对象进行清除即可。但是如果标记失败将会发生什么?我们将丢失上一次对Prev指针之前所有内存的标记状况,也就是说当不能完成并发标记时,将需要重新标记整个内存,这显然是不对的。我们通过示意图来演示一下并发标记的过程。
假定初始情况如图1-8所示。


image.png

图1-8并发标记开始之前

这里用Bottom表示分区的底部,Top表示分区空间使用的顶部,TAMS指的是Top-At-Mark-Start,Prev就是前一次标记的地址,即Prev TAMS,Next指向的是当前开始标记时最新的地址,即Next TAMS。并发标记开始是从根对象出发开始并发的标记。在第一次标记时PrevBitmap为空,NextBitmap待标记。开始进行并发标记,结束后如图1-9所示。


image.png

图1-9并发标记结束后的状态

并发标记结束后,NextBitmap记录了分区对象存活的情况,假定上述位图中黑色区域表示堆分区中对应的对象还活着。在并发标记的同时应用程序继续运行,所以Top指针发生了变化,继续增长。
这个时候,可以认为NextBitmap中活跃对象以及Next和Top之间的对象都是活跃的。在进行垃圾回收的时候,如果分区需要被回收,则会把这些对象都进行复制;如果分区可用空间比较多,那么分区不需要回收。当应用程序继续执行,新一轮的并发标记启动时,初始状态如图1-10所示。
在新一轮的并发标记开始时,交换Bitmap,重置指针。根据根对象对Bottom和Next TAMS之间的内存对象进行标记,标记结束后,状态如图1-11所示。


image.png

图1-10第二次并发标记开始之前的状态

image.png

图1-11第二次并发标记结束后的状态

当标记完成时,如果分区垃圾对象满足一定条件(如分区的垃圾对象占用的内存空间达到一定的数值),分区就可以被回收。
这里演示的仅仅是并发标记的SATB算法,但是还有一个主要的问题没有解决,那就是应用程序和并发标记工作线程对同一个对象进行修改,如何保证标记的正确性?

13.5 ZGC(4T内存)

ZGC特点

颜色指针

ZGC缺陷

ZGC的内部逻辑(ZGC为什么能控制STW在10ms内)

不同垃圾回收器并发执行支持力度

13.6 shenandoah(几T内存)

13.7 Epsilon

14 JVM类加载机制(7个阶段)

JVM类加载机制

虚拟机把描述类的数据文件(字节码)加载到内存,并对数据进行验证、准备、解析以及类初始化,最终形成可以被虚拟机直接使用的java类型(java.lang.Class对象)。

15 JVM类加载器

Java类加载器

从Java虚拟机的角度来说,只存在两种不同的类加载器:

从开发者的角度,类加载器可以细分为:

16 双亲委派模型

双亲委派模型

17 如何打破双亲委派模型?

重写ClassLoader类的loadClass(),一般是重写findClass()。

18 Java对象创建时机

19 引起类加载的五个行为

20 stackoverflow和outofmerroy区别

21 内存溢出进程还在吗?其他线程还能正常运行吗?程序还能正常访问吗?

22 静态类放在哪里?静态类会被垃圾回收吗?

23 Java堆的-Xms和-Xmx设置成一样有什么好处?

把xmx和xms设置一致可以让JVM在启动时就直接向OS申请xmx的commited内存,好处是:

  1. 避免JVM在运行过程中向OS申请内存
  2. 延后启动后首次GC的发生时机
  3. 减少启动初期的GC次数
  4. 尽可能避免使用swap space
堆内存的收缩与扩容机制

24 -verbose:class和-verbose:gc分别代表什么意思?

25 -verbose:gc和-XX:+PrintGC有什么区别?

26 Java产生dump日志的方式?

这种方式会产生dump日志,再通过jvisualvm.exe 或者Eclipse Memory Analysis Tools 工具进行分析。

27 Eden区快满了,再实例化一个对象会发生什么?

28 HotSpot虚拟机对象详解

28.1 对象的创建

类加载过程

对象的内存分配方式

解决内存分配多线程竞争方式

半初始化

对象创建过程-半初始化

对象头设置

初始化

28.2 对象的内存分配(栈-TLAB-Eden-Old)

对象分配流程

栈上分配

堆上分配

28.3 对象的内存布局

对象在内存中的存储布局 JOL-Java Object Layout ClassLayout

28.4 对象的访问定位

对象的访问定位方式-句柄&直接指针

29 为什么压缩指针超过32G失效?

29.1 查看JDK是否开启压缩指针?

29.2 为什么需要压缩指针?

29.3 JVM怎么实现压缩指针?

29.4 哪些信息会被压缩?

29.5 哪些信息不会被压缩?

29.6 压缩指针32g指针失效问题

30 阅读GC日志

上一篇下一篇

猜你喜欢

热点阅读