JVM面试题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K55lVLAd-1626996601169)(D:/%25E6%259D%25A8%25E6%2596%25B9%25E8%25B6%2585/java-skill-tree/Java%2520%25E6%258A%2580%25E8%2583%25BD%25E6%25A0%2591.assets/20210425101434404.png)]

1. 详解JVM内存模型

  • 程序计数器:用来存放指向下一条指令的执行地址,如果执行的是 Native 方法,计数器值为Undefined。线程私有 ,不会出现内存溢出
  • 虚拟机栈:存放基本数据类型,对象的引用,方法出口等
    • 每个线程运行时所需要的内存,称为虚拟机栈;每个栈由多个栈帧(Frame)组成,一个栈帧对应着一次方法调用时所占用的内存
    • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
    • 线程私有
    • 会抛出StackOverflowError和OutOfMemoryError异常,递归调用未设置结束条件会导致栈内存溢出,或者单个方法申请的内存过大
  • 本地方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有,会抛出StackOverflowError和OutOfMemoryError异常。
  • 堆:内存最大的一块,所有对象实例、数组都存放在java堆,凡是new出来的对象都放在堆中,GC回收的地方,线程共享,堆中的对象都要考虑线程安全问题。
  • 方法区:方法区中存储一些类相关的信息,比如:类加载器、运行时常量池、类的成员变量、成员方法、构造器方法等。方法区在虚拟机启动时被创建,逻辑上是堆的组成部分,不同的实现对方法区内存的定义也不一样,比如JDK1.8以前的HotSpot JVM有方法区,也叫永久代(permanent generation),1.8之后用元空间替换方法区,不同的是方法区使用的是jvm内存,而元空间使用计算机本地内存。jdk 1.7之后,原先位于方法区里的字符串常量池已被移动到了java堆中,所以说方法区其实只是一种JVM的规范。永久代和元空间都是方法区的具体实现。线程共享,会出现内存溢出

2. 如何判断一个对象可以被当做垃圾回收

1、引用计数法:给对象添加一个引用计数器,当该对象被引用一次,计数器就 +1,引用失效一次就 -1,计数器为0的对象会被当做垃圾回收。

  • 缺点就是无法回收循环引用的对象

2、可达性分析算法:扫描堆中的对象,看是否能沿着GC Root 对象为起点的引用链找到该对象,找不到,表示该对象可以被回收。目前Java虚拟机的垃圾回收采用此算法来查找所有存活的对象。

  • 哪些对象可以作为 GC Root?
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象

3. jvm 中四种引用区别

  • 强引用:所有 GC Root 对象都不通过强引用方式引用该对象,该对象才能被回收
  • 软引用:仅有软引用引用该对象时,在垃圾回收后,如果内存还是不足会再次触发垃圾回收,回收软引用对象,可以配合引用队列来释放软引用自身。
  • 弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会被回收,可以配合引用队列来释放弱引用自身
  • 虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存( 比如unsafe中freeMemory方法 )

4. 什么是双亲委派,如何打破双亲委派

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

如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名类java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类的唯一性将无法保证

打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法。这样就可以自定义加载类机制了

5. 类的加载过程

img

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序进行,而解析阶段则不一定,它在某些情况下可能在初始化阶段后在开始,因为java支持运行时绑定

推荐博客:https://blog.csdn.net/zhaocuit/article/details/93038538

6. 垃圾回收算法

1、标记清除:标记-清除算法分为两个阶段,标记(mark)和清除(sweep),标记阶段,对根(GC Root)可达对象的header进行标记;清除阶段,collector对堆内存进行线性遍历,对没有标记根可达的对象进行回收;

  • 缺点:容易产生大量的内存碎片
  • 优点:效率高

2、标记整理:标记-整理算法分为两个阶段,标记(mark)和整理(sweep),标记阶段,对根(GC Root)可达对象的header进行标记;整理阶段,移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收

  • 优点:没有内存碎片,内存连续
  • 缺点:效率不高

3、复制:复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。

  • 优点:内存连续,没有内存碎片
  • 缺点:内存一分为二,在工作的只有一半内存,比较浪费

分代垃圾回收

对象创建首先分配在新生代的Eden区,如果新生代空间不足时,触发minor GC,Eden和Survivor中的From存活的对象使用复制算法copy到Survivor的to区,存活的对象年龄+1,同时交换From和To区的指针,这里还会判断From区中存活的对象是否达到阈值,如果达到了则晋升到老年代,最大寿命是15(4bit);minor GC 会引发stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行;当老年代空间不足时,会先尝试触发 minor GC,如果空间还是不足,那么触发full GC,也会引发stop the world,暂停的时间会更长。

有一种特殊情况,就是大对象内存大小比Eden大,放不下,不会触发Minor GC ,直接晋升到老年代;一个线程内的OOM,不会导致线程外的主线程结束

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G6N8GDRa-1626996601171)(D:/%25E6%259D%25A8%25E6%2596%25B9%25E8%25B6%2585/java-skill-tree/Java%2520%25E6%258A%2580%25E8%2583%25BD%25E6%25A0%2591.assets/image-20210425161506631.png)]

7. 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点

  • Serial收集器: 单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。
  • ParNew收集器: Serial收集器的多线程版本,也需要stop the world,复制算法。
  • Parallel Scavenge收集器: 新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。
  • Serial Old收集器: 是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
  • Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
  • CMS(Concurrent Mark Sweep) 收集器: 是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。
  • G1收集器: 标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。

CMS收集器和G1收集器的区别:

  • CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
  • G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
  • CMS收集器以最小的停顿时间为目标的收集器;
  • G1收集器可预测垃圾回收的停顿时间
  • CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
  • G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。

8. jvm内存调试工具有哪些

jps 工具:查看当前系统中有哪些java进程(jps命令查看进程)

>jps

jmap 工具:查看堆内存占用情况(某一时刻)

>jmap -heap 13768

jconsole 工具:图形化界面,多功能的监测工具,可以连续监测。(输入jconsole命令会跳出以下图形化界面)

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

  • 栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型
  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果。
  • 如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。(线程启动过多)
  • 参数 -Xss 去调整JVM栈的大小

10. JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor

jvm内存中共享的有堆空间和永久代(也叫方法区,jdk1.8之后叫元空间),堆空间分为新生代和老年代,新生代又分为Eden区和Survivor区,Survivor又分为from和to。

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定)
Survivor区中的对象被复制次数为15(对应虚拟机参数 -XX:+MaxTenuringThreshold)

  • 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
  • Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
  • 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

11. JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代

  • 当 Eden 区的空间满了, Java虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到 Survivor区。
  • 大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年态
  • 如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态
  • 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代
  • Major GC 发生在老年代的GC清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上

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

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

指令重排序 :

public class PossibleReordering {

    static int a,b,x,y = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread one = new Thread(new Runnable() {
            @Override
            public void run() {
                a = 1;
                x = b;
            }
        });

        Thread two = new Thread(new Runnable() {
            @Override
            public void run() {
                b = 1;
                y = a;
            }
        });

        one.start();two.start();
        one.join();two.join();
        System.out.println("("+ x + "," + y + ")");

    }
}

运行结果可能为(1,0)、(0,1)或(1,1),也可能是(0,0)。因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。而这就是指令重排。

内存屏障 :

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

  • LoadLoad屏障: 禁止处理器把上面的volatile变量读与下面的普通读重排序
  • StoreLoad屏障: 避免volatile写与后面可能有的volatile读/写操作重排序
  • **StoreStore屏障:**可以保证在volatile变量写之前,其前面的所有普通写操作都已经刷新到主内存中
  • LoadStore屏障: 禁止处理器把上面的volatile变量读与下面的普通写重排序

happen-before原则:

  • 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。

  • 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。

  • volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。

  • happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。

  • 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。

  • 线程中断的happen-before原则 :对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。

  • 线程终结的happen-before原则: 线程中的所有操作都happen-before线程的终止检测。

  • 对象创建的happen-before原则: 一个对象的初始化完成先于他的finalize方法调用。

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

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

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

14. 说说你知道的几种主要的JVM参数

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

-Xmx3550m: 最大堆大小为3550m。

-Xms3550m: 设置初始堆大小为3550m。

-Xmn2g: 设置年轻代大小为2g。

-Xss128k: 每个线程的堆栈大小为128k。

-XX:MaxPermSize: 设置持久代大小为16m

-XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。

-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。

-XX:+UseParallelGC
-XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC 
-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSCompactAtFullCollection:

-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。

-XX:ParallelGCThreads=20: 配置并行收集器的线程数

-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。

-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。

-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片

15. 怎么打出线程栈信息

  • 输入jps,获得进程号。
  • top -Hp pid 获取本进程中所有线程的CPU耗时性能
  • jstack pid命令查看当前java进程的堆栈状态
  • 或者 jstack -l > /tmp/output.txt 把堆栈信息打到一个txt文件。

产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。

-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片

15. 怎么打出线程栈信息

  • 输入jps,获得进程号。
  • top -Hp pid 获取本进程中所有线程的CPU耗时性能
  • jstack pid命令查看当前java进程的堆栈状态
  • 或者 jstack -l > /tmp/output.txt 把堆栈信息打到一个txt文件。

推荐博客:https://blog.csdn.net/qq_41701956/article/details/100074023

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值