JVM面试题

JVM面试题

1.JVM内存结构?
  • 程序计数器
    • 每个线程都有自己的程序计数器,线程私有的(生命周期与线程一致),是一块很小但是运行速度非常快的内存空间,用于记录当前线程正在执行方法的指令地址。(如果是本地方法则是undefined)
    • 执行引擎的工作完全依赖于程序计数器,也是程序流程控制的基础,唯一没有OOM的情况。
  • 虚拟机栈
    • 每个线程在创建时,都会有创建一个虚拟机栈,线程私有(生命周期与线程一致),内部保存一个个的栈帧,对应着一次次的方法调用,线程私有的。栈帧存储局部变表、操作数栈、动态链接、方法返回等信息。对于栈来说不存在垃圾回收的问题。
  • 本地方法栈
    • 线程私有,保存native方法信息,调用native方法时,jvm不会在虚拟机栈中为该线程创建栈帧。
    • 所有线程共享的内存,几乎所有的对象和数组都分配在堆上,所以堆是垃圾回收的重点区域,方法结束后,堆中的对象不会立即被移除,而是等垃圾回收。
    • 堆又分为新生代和老年代,新生代分为Eden区、survivor0、surviver1 占比为8:1:1。几乎所有的对象都在Eden区被创建。
  • 方法区
    • 独立于堆的内存空间,所有线程共享的内存区域。主要存放类元信息和运行时常量池。java8被替换为元空间,存放在本地内存。

    • 永久代为什么被元空间替代?
      • 对永久代设置空间大小是很难确定的,元空间不再虚拟机中,在本地内存。而且对永久代的调优是很困难的。
2.使用程序计数器存储字节码指令地址有什么用?
  • 因为CPU需要不停的切换各个线程,需要知道切换回来从哪里开始执行。
  • 执行引擎需要通过程序计数器中的指令地址完成工作。
3.程序计数器为什么被设置为线程私有?
  • 为了确保准确的记录各个线程正在执行的当前指令地址,最好的办法就是每个线程都分配一个,这样各个线程独立计算不会出现干扰。
4.栈可能出现的异常?
  • 如果栈大小固定且超出容量:StackOverFlowError;
  • 如果栈大小动态扩展,在扩展时没有内存:则抛出OutOfMemoryError;
5.栈运行原理?
  • 一个时间点上,只会有一个活动的栈帧,即当前栈帧(栈顶位置)
  • 执行引擎运行的字节码指令只针对当前栈帧,如果该方法调用其他方法,会创建对应的新的栈帧,放在栈顶。
  • 不同线程的栈帧不允许相互引用,正常返回和异常返回都会弹出栈帧。
6.局部变量表有什么特点?
  • 建立栈在帧上,线程私有,不存在数据安全问题。
  • 局部变量表的大小是在编译期定下来的,在运行期间不会改变。
  • 局部变量表中的变量只在当前方法中有效,方法结束后随着栈帧的销毁而销毁。
  • 在栈帧中,与调优最为密切的就是局部变量表,局部变量是重要的垃圾回收根节点。
7.Slot的理解?
  • 局部变量表最基本的存储单元是Slot,32位的类型占用一个slot,64位的类型占用两个slot(byte、short、char、Boolean会被转成int)。
  • slot可以重复利用。
8.如何理解动态链接?
  • 静态链接:如果目标方法在编译期可知,且运行期不变,则在编译期将方法的符号引用转换为直接引用,对应静态分派,绑定方法的类型。
  • 动态链接:如果目标方法在编译期不可知,只能在运行期将方法的符号引用转换为直接引用,对应动态分派
  • 非虚方法:编译期可知,静态方法(invokestatic),私有方法,final方法,构造器,父类方法(invokespecial)。
  • 虚方法:invokevirtual、invokeinterface(接口)
  • 动态调用指令:invokedynamic = lambda
9.逃逸分析是什么?
  • 当一个对象再方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。如果被外部方法引用(参数传递),则认为是发生逃逸。
  • 如果经过逃逸分析发现,一个对象没有逃逸出方法的话,就可能会被优化为栈上分配,无需在对堆上分配内存空间,也无需进行垃圾回收,常见的堆外存储技术,降低GC频率,提升GC效率。

总结】:所以说,能使用局部变量的,就不在方法外定义

10.JVM中的几种常量池?
  • 字符串常量池:存放字符串实例的引用(java8),用一个长度不可变的HashTable实现,不会存放相同的字符串。

    • 两种创建字符串的方式:

      • 字面量方式:如果字符串常量池中没有,则在堆中创建一个字符串对象实例,将引用放入字符串常量池中。有就返回
      • new 的方式:先在堆中创建该对象,再从字符串常量池中查找,如果没有,操作同上
    • intern方法:

      • 在字符串常量池中查找该字符串,如果有返回引用,如果没有,将该对象的引用放入字符串常量池,并返回该引用。
  • class文件常量池:不是内存层面的常量池,存放字面量和符号引用,在类加载时放入运行时常量池。

    • 字面量:文本字符串、final的常量
    • 符号引用:类和接口的全限定名、方法、字段的名称和描述符。符号引用与内存无关。
  • 运行时常量池:方法区的一部分,类加载后将常量池的内容存放在运行时常量池,同时将符号引用转换为直接引用。具有动态性,可以在运行时生成常量。

  • 包装类常量池:在java层面实现了常量池技术,以integer为例,其内部类IntegerCache中的static final类型的integer数组实现的。

11.栈和堆的区别?
  1. 申请方式:
    • 栈:系统自动分配
    • 堆:手动申请空间
  2. 系统响应:
    • 栈:只要栈的剩余空间大于所申请空间,即可提供内存
    • 堆:当系统收到申请时,遍历空闲地址的链表,寻找空间大于申请空间的堆节点,从链表中删除。
  3. 申请大小:
    • 栈:连续的内存,空间较小,速度快
    • 堆:逻辑上连续,物理上可以是不连续的,空间较大,速度较慢
12.如何排查并解决OOM?
  • 除了程序计数器其他内存空间都有OOM的风险;jstat查看监控jvm内存和GC情况。
  • 首先分析内存中对象是否是必要的,也就是内存溢出还是内存泄漏。
    • 如果是内存泄漏,查看GC roots的引用链。
    • 如果是内存溢出,从代码上检查是否存在对象生命周期过长,是否可以调大堆参数。
13.垃圾回收的原理?
  • 首先,需要判断哪些对象属于垃圾对象,两种算法:

    1. 引用计数法:给每个对象设置一个引用计数器,有引用就+1,引用失效就-1。
      • 缺点:如果A引用B,B引用A,则引用计数都不为0,无法解决循环引用的问题。引用计数器造成的额外空间消耗。
    2. 可达性分析算法:从一个作为GC roots的对象向下搜索,如果一个对象到GC roots没有任何引用连接时,说明是垃圾对象(判断的是强引用)。
      • GC roots的种类:虚拟机栈中的对象、本地方法栈中的对象、同步锁持有的对象、方法区中static属性引用的对象、final常量引用的对象。
  • 满足条件并不会立即回收,还需要判断改对象是否有finalize()方法(只执行一次):

    • 不存在,标记为垃圾对象。
    • 存在,将该对象放入队列中,由finalize线程执行该方法。该方法不保证一定执行。执行过后如果仍没有GC roots连接,表记为垃圾对象。
  • 判断完垃圾对象后,开始垃圾回收的过程,有以下垃圾回收算法:

    1. 标记-清除算法:
      • 第一步:利用可达性分析,标记存活对象。
      • 第二步:对整个堆内存进行遍历,回收垃圾对象。
      • 缺点:速度慢,并且回收过后会产生内存碎片,需要维护一个的空闲列表。
    2. 标记-整理算法:
      • 第一步:与标记清除算法一样,标记存活对象。
      • 第二步:将所有存活的对象压缩到内存的一端,按顺序排放。然后清理边界外的空间。
      • 优点:解决了标记清除算法导致的内存碎片问题,相对于复制算法,不需要额外的一半内存。
      • 缺点:需要更新存活对象的引用地址。速度差与复制算法。
    3. 复制算法:
      • 将空闲内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用内存中存活的对象复制到另一块内存中,然后清除使用的内存块,交换两个内存块的角色。适用于新生代
      • 优点:节省了标记过程,运行高效,没有碎片化的问题。
      • 缺点:需要两倍的内存空间。
    4. 分代收集算法:
      • 将堆内存分为新生代和老年代,根据他们各自特点进行垃圾回收。
      • 新生代:又分为Eden区和两个survivor区,这个区域相对老年代较小,对象生命周期短,存活率低,高频回收,适合复制算法。
      • 老年代:区域较大、对象生命周期长,回收频率低,一般由标记清除和标记整理算法混合实现。
14.对象的分配过程?
  • new的对象先放Eden区
  • 当Eden区满时,垃圾回收器对Eden区进行回收。将Eden中存活的对象放入survivor0区。
  • 如果再次触发垃圾回收,存活的对象会放入survivor1区,下一次回收将存活对象放入survivor0区,如此反复。如果survivor to区放不下,就放入老年代
  • 如果一个对象反复移动15次,则进入老年代。

【注】只有Eden区满了才会触发young GC,survivor区满不会触发。

15.minor GC、full GC、major GC触发机制?
  • minor GC:
    • Eden区空间不足,java对象大多都在新生代被回收,所以minor GC非常频繁,速度比较快
    • Minor GC会引发STW,暂停用户线程,等回收结束,恢复运行
  • Major GC:
    • 出现Major GC经常伴随着Minor GC,先尝试Minor GC不行再触发Major GC,速度慢非常多,STW时间更长
    • 回收后如果内存依然不足,会报OOM
  • full GC:
    • system.gc(),不是必然执行
    • 老年代、方法区空间不足
    • 由Eden区和Survivor(from)区 向Survivor(to)区复制存活对象时,to区放不下对象,对象放入老年代,老年代也空间不足。
    • 通过minor GC后进入老年代的平均大小,大于老年代的空间。
16.四种引用类型之间的区别?
  • 强引用:普通的对象引用关系,只要引用关系还在,就不会回收被引用的对象,内存泄漏的主要原因。
  • 软引用:当内存足够时,不回收软引用对象,内存不够时,回收软引用对象。通常用来实现内存敏感的缓存。
  • 弱引用:只要有垃圾回收,就会被回收掉,无论内存是否充足
  • 虚引用:无法通过虚引用获得一个对象实例,主要用于跟踪对象被垃圾回收的活动。
17.安全点和安全区域是什么意思?
  • 安全点:
    • 程序并非任意地方都可以停下来,只有在特定的位置才能停下来GC,称为安全点。

    • 通常选择执行时间较长的指令作为安全点,比如:方法调用、循环跳转和异常跳转。

    • 如何在GC发生时,检查线程到达安全点?

      • 抢先式中断:先中断所有线程,恢复不在安全点的线程,让其到达
      • 主动式中断:设置一个中断标志,各个线程运行到安全点时,主动轮询这个标志,如果为真,将自己挂起
  • 安全区域:
    • 当程序不再执行的时候,比如处于sleep、blocked状态。这时线程无法到达安全点,需要安全区域的概念,这个区域任何位置开始GC都是安全的。
18.内存泄漏?
  • 指对象不再使用,但是GC没有回收该对象。

  • 例如:单例模式,单例模式中的对象生命周期是和程序一致的,所以当单例对象持有对外部的引用时,这个对象是不可以被回收的,会产生内存泄漏。

    ​ 一些提供close方法的资源未关闭,IO连接、数据库连接、网络连接没有手动关闭,是不会被回收的。

19.有哪几种垃圾回收器?
1.Serial、Serial Old
  • serial:串行回收、复制算法、必须STW。

  • serial old:回收老年代,标记-整理算法,其他一致。

2.ParNew
  • serial的多线程版本。
3.Parallel Scavenge
  • 与ParNew区别只在于可以控制吞吐量。高吞吐量适用于后台运算,没有太多交互的场景。

  • java6中Parallel Old 代替Serial Old,Parallel Old是并行回收老年代。java8中的默认垃圾回收器

4.CMS
  • concurrent-mark-sweep,java5中第一款并发收集器。垃圾回收线程与用户线程一起工作。jdk9标记废弃,jdk14被移除。

  • 工作原理:
    1. 初始标记:需要STW,仅标记GC roots能直接关联的对象。直接关联的对象较少,速度很快。
    2. 并发标记:从直接关联的对象开始遍历所有关联的对象,这个过程耗时长但是不需要暂停用户线程。
    3. 重新标记:再次标记,为了修正并发标记阶段的改动。
    4. 并发清除:清理垃圾对象,不需要移动存活对象,所以可以与用户线程并发执行。
  • 问题:
    1. 多并发导致CPU资源紧张,吞吐量降低

    2. 无法清理浮动垃圾。为什么还会产生浮动垃圾?

      • 因为并发标记阶段和用户线程并发执行的,这个期间可能会产生新的垃圾,CMS无法对这些垃圾进行标记。
      • 而重新标记阶段的目的是为了,将并发标记期间用户新创建的对象标记为可达。如果要解决浮动垃圾的问题,由可达变为不可达需要从GC roots根节点开始遍历,相比这种开销,浮动垃圾是可容忍的。
    3. 可能会有并发失败的问题,由于垃圾清除阶段是与用户线程并发运行的,那么就需要预留足够的空间,如果内存不足,就会并发失败,这时会启动后备预案-Serial Old进行回收,这样停顿时间就很长了。

    4. 并发清除后会产生内存碎片,可能导致无法分配大对象。

5.G1
  • 把堆内存分割为很多个region,表示Eden、Survivor、老年代等,region之间是复制算法,整体上看是标记-整理算法。根据允许的收集时间,优先回收价值最大的region,Garbage First,在延迟可控的情况下,尽可能提高吞吐量。java7启动,java9默认的。

  • 工作原理:
    • 主要包括三个环节:年轻代GC----->年轻代GC + 并发标记过程------>混合回收----->年轻代GC
    • 当年轻代的Eden空间不足时开始young GC回收过程;这个过程是一个并行的独占式收集器,需要STW。
    • 当内存使用到一定值(默认45%)时,开始老年代并发标记的过程。
    • 标记完成后开始mixed GC,老年代和年轻代一起被回收,将老年区间存活对象移动到空闲区间,特别的是,G1只回收一部分老年代的region,并不是回收整个老年代,
  • 优势:

    1. 具有并行性和并发性。

    2. 分代收集。

    3. 空间整合,没有内存碎片。

    4. 可预测的停顿时间模型,在允许时间内收集价值最大的region。

6.ZGC
  • 在尽可能吞吐量大的情况下,实现垃圾回收时间在10毫秒以内的低延迟。
  • 基于region的布局,使用读屏障、染色指针、内存多重映射技术实现可并发的标记-整理算法。
  • 并发标记-并发预备重分配-并发重分配-并发重映射,四个工作阶段
  • 除了初始标记是STW的,其他都是并发执行的
20.RememberSet和CardTable
  • 记忆集

    • JVM为了解决分代收集器所带来的跨代引用问题(分区域收集器也同样适用)。(主要解决老年代引用新生代)

    • 收集器在新生代上建立一个记忆集的数据结构,(G1每个region都有一个记忆集),记忆集将老年代划分为若干个小块,标识老年代会发生跨代引用的内存块。在young GC时只将包含了跨代引用的内存块加入到GC roots中,避免了全局扫描。

  • 卡表

    • 记忆集是抽象的数据结构,具体实现是卡表。卡表就是一个字节数组,卡表每个元素对应一张512kb的卡。一张卡中通常存放有多个对象,如果一张卡内有跨代引用的对象,称这张卡为脏卡。垃圾收集时将脏卡中的对象加入到GC roots中。每次应用类型的写操作时,都加入一个写屏障,用来维护卡表的状态。
21.什么是类加载?类加载过程?

类加载就是将一个class文件加载到jvm内存中,经过一系列过程,变为可以直接使用的class对象。

  • 加载
    • 查找并加载类的二进制数据,生成class实例,主要分为三步:

      1. 通过类的全限定名,获取类的二进制数据。
      2. 解析类的二进制数据为方法区内的数据结构。
      3. 创建class实例。(堆中)

      【注】:数组本身是由jvm直接创建的,而数组的元素类型仍然需要类加载器创建。

  • 链接
    • 验证:验证class文件中的数据是复合虚拟机要求的。
    • 准备:为静态变量分配内存,初始化为默认值。(不包含final的,final在编译期就分配了)
    • 解析:将符号引用转换为直接引用。(可以在初始化之后再开始,为了支持动态绑定)
  • 初始化:
    • 执行类初始化方法:()方法,该方法由静态成员的赋值语句和静态代码块合并产生的,只能由jvm调用。这个阶段才开始执行java代码。
    • 加载一个类前会先加载该类的父类,所以父类的()方法先被调用。
    • 如果类中没有静态变量、静态变量没有明确的初始化操作、静态变量由final修饰。则不会产生()方法。
22.什么是类加载器?常见的类加载器?
  • 所有的class都是由ClassLoader加载的,ClassLoader负责通过各种方式将class文件的二进制数据读入jvm内存,转换为一个与其对应的Class实例。
  • 启动类加载器(引导类加载器):加载java的核心类库,并不继承java.lang.ClassLoader没有父类加载器
  • 扩展类加载器:加载Java的扩展库,继承于ClassLoader,父加载器是启动类加载器
  • 系统类加载器:继承于ClassLoader,应用程序类中的加载器默认是系统类加载器。
  • 自定义类加载器:实现插件机制,应用隔离等等。
23.loadClass()源码
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {	//保证同步操作
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);	//获取当前父加载器,如果存,调用父加载器进行加载
                    } else {	//说明父加载器是引导类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {	//当前类加载器的父加载器没有加载
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);	//调用findClass方法

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

loadClass方法中实现了双亲委派机制,所以自定义类加载器可以重写findClass方法,这样自定义类自然可以保证满足双亲委派机制。

24.什么是双亲委派机制?
  • 一个类加载器收到了类加载请求,他不会先去自己加载,而是把请求委托给父类加载器。父类再进一步委托,直到顶层的启动类加载器。如果父类加载器无法完成加载任务,则自己再尝试加载。
  • 优点:避免类的重复加载,确保一个类的全局唯一性。保护程序安全,避免核心API被篡改。
  • 弊端:顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
  • 结论:jvm没有明确一定要使用双亲委派机制,只是建议,例如Tomcat,类加载器所采用的加载机制就有所区别:先自行加载,失败后交给其父加载器加载,也是Servlet规范的一种做法。
25.打破双亲委派的例子?
  • JNDI:通过引入线程上下文加载器。为了完成高级类加载器访问子类加载器的加载类。
  • Tomcat:先自行加载,失败后委托给父类加载器
  • OSGI:实现热部署,每个模块都自定义了类加载器,需要更换模块时,与类加载器一起更换。
  • java9:扩展类加载器被平台类加载器替换。当平台类加载器收到请求后,在委派给父类前先判断该类是否能够归属到某一个系统模块中。如果可以找到归属关系,则优先委派给负责的那个模块完成加载。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值