2023跳槽最新面试题整理——JVM系列

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

      今天是农历2022年腊月二十七了和往常的春节假期、五一假期和十一假期一样都是团队中坚持到最后的一个。没几天也要快过年了我先提前向大家拜个早年——祝大家兔年大吉新春快乐财源滚滚万事如意。
      今年从十一假期之后由于种种原因也没有更新文章今天最后一天由于做其他的也没有心情那就老规划——最后一天就自己静下心来整理文章来和大家分享再加上今年由于YQ放开大家都在积蓄力量准备明年跳槽那今天我就开始给大家来持续更新面试题系列吧这篇文章先来更新JVM系列打算完整的给大家整理出面试问题重点然后再看时间是否充裕再来判断是否给大家整理详细问题答案。

1、类加载机制

    1.1、类加载过程

            类加载过程可以初步分成如下几步
            加载—>验证—>准备—>解析—>初始化
在这里插入图片描述

  • 加载 根据类的全限定类名从磁盘获取类的二进制字节流。
  • 验证 字节码验证、文件格式验证、元数据验证和符号引用验证
  • 准备 静态变量分配内存并且赋予零值
  • 解析 将在编译阶段的符号引用转化为直接引用直接指向其内存地址
  • 初始化 将解析过程中的零值更改为程序员赋值的值

    1.2、类加载器分类

            类加载器分为启动类加载器、扩展类加载器、应用程序类加载器和自定义类加载器

  • 启动类加载器(Bootstrap Class Loader) 加载< JAVA-HOME >\lib目录下的文件
  • 扩展类加载器(Extension Class Loader) 加载< JAVA-HOME >\lib\ext目录下的文件
  • 应用程序类加载器(Application Class Loader) 加载用户类路径下的所有类包也就是用户自己写的类
  • 自定义类加载器(User Class Loader) 负责加载用户自定义的类路径下的类包

    1.3、双亲委派机制

            双亲委派机制流程图如下所示
在这里插入图片描述

             1.3.1、双亲委派如何运行

                   这段我就用大白话来给大家讲解了哈。比如当我自己写的一个类需要加载的时候它会先去应用程序类加载器中看是否被加载过发现没有然后交由应用程序类加载器的父类扩展类加载器看是否被加载过发现也没有然后它就会交由扩展类加载器的父类启动类加载器加载发现同样也没有上面的这些交由父类加载器就是向上委托此时会进行向下指派启动类加载器指派给它的子类扩展类加载器扩展类加载器指派给应用程序类加载器此时应用程序类加载器没有向下指派的了那么就由应用程序类加载器加载。
                   上面如果大家不太明白你们可以根据我的上段所说的内容去画流程图那么大家在画流程图之后就完全明白了。

             1.3.2、双亲委派这种机制好处

                   采用双亲委派机制加载主要有一下两点好处
                   1、保证沙箱安全比如JDK中的java.lang.String中如果我对这个类重写了那么是不生效的因为它在走到启动类加载器的时候发现已经被加载了那么就会直接返回此时就保证了其安全。
                   2、防止重复加载

             1.3.3、为什么在父类加载器没有加载的时候为什么不自己加载反而交给子类加载器加载

                   如果直接直接在父类加载器加载此时加载看着比较省事了因为不需要再向下指派但是在下一次需要加载的时候又需要向上进行查找那么是比较麻烦的所以直接进行向下委派给子类加载器加载。

2、JVM运行时数据区

       JVM运行时数据区主要包括堆、虚拟机栈、方法区、本地方法栈和程序计数器。
       运行时数据区图如下所示
在这里插入图片描述       下面我详细介绍下运行时数据区各个部分其功能

线程共享

  • 主要存放new出来的对象还有数组和JDK1.8存放字符串常量池
  • 方法区 主要存放类信息、方法信息静态变量和常量池等等

线程独享

  • 虚拟机栈 虚拟机栈是由一个个栈帧组成当每调用一个方法时就是生成一个栈帧压入虚拟机栈中栈帧中包含局部变量、操作数栈、动态链接和方法出口
  • 本地方法栈 本地方法栈和虚拟机栈的原理一样只是虚拟机栈调用的是Java方法但是本地方法栈调用的是本地方法(Native);
  • 程序计数器 在Java程序运行过程中会发生线程上下文切换当线程重新获得CPU时间片时要保证从上次执行的行数继续执行那么程序计数器就是保证从原来位置继续执行

3、JVM对象创建和内存分配机制

       3.1、堆内存结构介绍

              堆内存分为新生代和老年代新生代:老年代=1:2新生代中又分为Eden、S0和S1区Eden:S0:S1=8:1:1
在这里插入图片描述

       3.2、对象创建流程

              类加载流程如下图所示
在这里插入图片描述       详细介绍其每一步流程
              1、加载 当new对象时会根据双亲委派机制检查下该类是否被加载过如果没有加载过则执行类加载过程加载—>验证—>准备—>解析—>初始化
              2、分配内存 当加载完成之后接下来就要为新生的对象去分配内存空间当类加载完成之后就知道了对象所需要内存空间就从堆中找出一块对象所需要内存空间大小的内存给该对象
                     划分内存方法
                        1、指针碰撞法 如果Java堆中内存是绝对规整的所有用过的内存都放在一边空闲的内存放在另一边中间放着一个指针作为分界点 的指示器那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
                        2、空闲列表法 如果Java堆中的内存并不是规整的已使用的内存和空闲的内存相互交错那就没有办法简单地进行指针碰撞了虚拟机就必须维护一个列表记录上哪些内存块是可用的在分配的时候从列表中找到一块足够大的空间划分给对象实例 并更新列表上的记录。
                     解决并发分配内存产生问题
                     在并发情况下可能出现正在给对象A分配内存指针还没来得及修改对象B又同时使用了原来的指针来分配内存的情况。
                     CAScompare and swap法
                     虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
              3、初始化 内存分配完成后虚拟机需要将分配到的内存空间都初始化为零值不包括对象头这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用程序能访问到这些字段的数据类型所对应的零值。
注意 这里的初始化和加载过程的初始化是不一样的加载的初始化是将static修饰的静态变量赋予零值。
              4、设置对象头 初始化零值之后要对对象做些必要设置例如对象属于哪个类的实例对象GC年龄对象hashcode和如何找到类的元数据信息等等。
              在虚拟机中对象在内存中布局分为三部分对象头、实例数据和对齐填充
              对象头主要包含两部分信息一部分是运行时自身数据hash码、GC年龄、偏向锁ID线程持有的锁等等另一部分是对象持有类的指针即对象指向它类元数据的指针虚拟机通过这个指针找到该对象归属于哪个类
在这里插入图片描述              5、执行init()方法
                   执行方法即对象按照程序员的意愿进行初始化。对应到语言层面上讲就是为属性赋值注意这与上面的赋零值不同这是由程序员赋的值和执行构造方法。

       3.3、对象内存分配

              对象内存分配流程图如下所示
在这里插入图片描述
              一般我们认为对象会直接分配到堆中新生代Eden区中这是不对的对象可能分配到虚拟机栈中也有可能分配到堆中的老年代中这都是可能发生的。下面我来介绍下这几种情况

       3.3.1、对象在栈上分配

              当对象发生了逃逸分析时那么此时对象就会直接分配到栈中而不是分配到堆中。这样做的目的是能够减少GC的压力因为分配到栈中之后会随着调用这个方法结束而出栈销毁。
              什么是逃逸分析
                  这个通俗讲就是我在方法内部new一个对象后该对象只在该方法中使用不会让方法以外的使用那么此时就发生了逃逸分析。

       3.3.2、对象在Eden区分配

              正常情况下大量的对象都是分配到堆中新生代的Eden区。

       3.3.3、大对象直接分配至老年代

              当我们创建的是大对象时会直接分配到老年代中这样做的的目的减少它在新生代中每次GC来回复制并且能减少触发minor GC次数而引发的实际不是真正长时间存活的对象而存放老年代所引发的一系列问题。
              大对象是如何判断的呢
                   在JVM中设置-XX:PretenureSizeThreshold 可以设置大对象的大小

       3.3.4、长期存活的对象进入老年代

               这种也是正常的当对象创建之后放入堆中新生代的Eden区然后当新生代满了之后触发minor GC每次minor GC存活的对象他们的对象头的GC年龄就会+1当达到15次之后就会讲对象存放进老年代此时就是长期存活对象进入老年代。

       3.3.5、对象动态年龄判断

               当某一年龄的对象占Survivor区的一半以上时那么他就会将大于等于它的GC年龄直接存放进老年代中。

       3.3.6、老年带空间担保机制

               当触发minor GC的时候都会去看下老年代的可用空间大小如果老年代的可用空间大小大于等于新生代对象所有之和那么可以直接触发minor GC否则会查看是否开启了分配担保机制如果没有开启那么会直接触发full GC 如果开启了就会看每次minor GC存活的对象平均大小是否小于等于老年代可用空间大小如果满足则直接minor GC否则触发Full GC。
在这里插入图片描述

4、垃圾收集

       4.1、如何判断一个对象是否存活

           判断对象是否存活有两种方式——计数器算法和可达性分析算法

                4.1.1、计数器算法

                   此方法比较简单每个对象就是专门有个计数的变量当该对象被引用一次就会+1引用失效一次就会-1当计数变量为0时代表该对象没有被其他对象引用可以被垃圾回收。
                   该方法有个弊端就是当两个对象互相引用是比如A对象引用B对象B对象又引用A对象他们之间不可能将计数器减为0这就无法将对象进行垃圾回收。

                 4.1.2、可达性分析算法

                      将GC roots为起点向下遍历遍历到的对象说明被其他引用需要存活没有遍历到的对象说明被引用是垃圾对象会被回收。
                      GC roots根节点可以是静态变量、局部变量等等
在这里插入图片描述

       4.2、垃圾收集算法

               垃圾收集算法主要有复制算法、标记清除算法和标记整理算法

               4.2.1、复制算法

                   效率比较高主要是年轻代中使用该垃圾算法它可以将内存分为大小相同的两块每次使用其中的一块。当这一块的内存使用完后就将还存活的对象复制到另一块去然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
               特点 效率比较高但是内存利用率不高每次只能利用50%的内存
在这里插入图片描述

               4.2.2、标记清除算法

               算法分为“标记”和“清除”阶段标记存活的对象 统一回收所有未被标记的对象
                特点 1、效率比较低(如果需要标记的对象非常多那么效率就会下降)2、空间利用率较高但是会产生空间碎片化导致大对象在存放时不能存放
在这里插入图片描述

               4.2.3、标记整理算法

                根据老年代的特点特出的一种标记算法标记过程仍然与“标记-清除”算法一样但后续步骤不是直接对可回收对象回收而是让所有存活的对象向一端移动然后直接清理掉端边界以外的内存。
在这里插入图片描述

       4.3、垃圾收集器

               常见的垃圾收集器有如下几种
在这里插入图片描述
               如果说收集算法是内存回收的方法论那么垃圾收集器就是内存回收的具体实现。

          4.3.1、Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

                    Serial串行 收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它 的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程 “Stop The World” 直到它收集结束。
在这里插入图片描述
优点
        它简单而高效与其他收集器的单线程相比。Serial 收集器由于没有线程交互的开销自然可以获得很高的单线程收集效率。
        Serial Old收集器是Serial收集器的老年代版本它同样是一个单线程收集器。它主要有两大用途一种用途是在JDK1.5 以及以前的版本中与Parallel Scavenge收集器搭配使用另一种用途是作为CMS收集器的后备方案 当CMS在垃圾收集时垃圾回收的内存远远不够对象创建所使用的内存那么此时就会触发Seral Old收集器做兜底会STW整个线程不能使用。

          4.3.2、Parallel收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))

                Parallel收集器其实就是Serial收集器的多线程版本除了使用多线程进行垃圾收集外其余行为控制参数、收集算法、回收策略等等和Serial收集器类似。默认的收集线程数跟cpu核数相同当然也可以用参数(XX:ParallelGCThreads)指定收集线程数但是一般不推荐修改。 Parallel Scavenge收集器关注点是吞吐量高效率的利用CPU。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间提高用户体验。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
                 新生代采用复制算法老年代采用标记-整理算法。
在这里插入图片描述
                Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU资源的场合都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器)。

          4.3.3、ParNew收集器(-XX:+UseParNewGC)

                ParNew收集器其实跟Parallel收集器很类似区别主要在于它可以和CMS收集器配合使用。
                新生代采用复制算法老年代采用标记-整理算法。

          4.3.4、CMS收集器(-XX:+UseConcMarkSweepGC(old))

                CMSConcurrent Mark Sweep收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用它是HotSpot虚拟机第一款真正意义上的并发收集器它第一次实现了让垃圾收集线程与用户线程 基本上同时工作
                从名字中的Mark Sweep这两个词可以看出CMS收集器是一种 “标记-清除”算法实现。
过程步骤如下
       1、初始标记 暂停所有的其他线程(STW)并记录下gc roots直接能引用的对象速度很快
       2、并发标记 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程 这个过程耗时较长但是不需要停顿用户线程 可以与垃圾收集线程一起并发运行。因为用户程序继续运行可能会有导致已经标记过的对象状态发生改变。
       3、重新标记 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录这个阶段的停顿时间一般会比初始标记阶段的时间稍长远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。也是STW
       4、并发清理 开启用户线程同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。
       5、并发重置 重置本次GC过程中的标记数据。
在这里插入图片描述

       从它的名字就可以看出它是一款优秀的垃圾收集器主要优点并发收集、低停顿。但是它有下面几个明显的缺点
       1、对CPU资源敏感会和服务抢资源
       2、无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾这种浮动垃圾只能等到下一次gc再清理了)
       3、它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生
       4、执行过程中的不确定性会存在上一次垃圾回收还没执行完然后垃圾回收又被触发的情况特别是在并发标记和并发清理阶段会出现一边回收系统一边运行也许没回收完就再次触发full gc也就是"concurrent mode failure"此时会进入stop the world用serial old垃圾收集器来回收。

          4.3.5、G1收集器

              G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC 停顿时间要求的同时,还具备高吞吐量性能特征
在这里插入图片描述              G1将Java堆划分为多个大小相等的独立区域RegionJVM最多可以有2048个Region。 一般Region大小等于堆大小除以2048比如堆大小为4096M则Region大小为2M当然也可以用参数"XX:G1HeapRegionSize"手动指定Region大小但是推荐默认的计算方式。
              G1保留了年轻代和老年代的概念但不再是物理隔阂了它们都是可以不连续Region的集合。
              默认年轻代对堆内存的占比是5%如果堆大小为4096M那么年轻代占据200MB左右的内存对应大概是100个 Region可以通过“-XX:G1NewSizePercent”设置新生代初始占比在系统运行中JVM会不停的给年轻代增加更多 的Region但是最多新生代的占比不会超过60%可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和 Survivor对应的region也跟之前一样默认8:1:1假设年轻代现在有1000个regioneden区对应800个s0对应100 个s1对应100个。
              一个Region可能之前是年轻代如果Region进行了垃圾回收之后可能又会变成老年代也就是说Region的区域功能 可能会动态变化
              G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样唯一不同的是对大对象的处理G1有专门分配 大对象的Region叫Humongous区而不是让大对象直接进入老年代的Region中。
              在G1中大对象的判定规则就是一个大对象超过了一个Region大小的50%比如按照上面算的每个Region是2M只要一个大对象超过了1M就会被放 入Humongous中而且一个大对象如果太大可能会横跨多个Region来存放
              Full GC的时候除了收集年轻代和老年代之外也会将Humongous区一并回收
G1收集器一次GC的运作过程大致分为以下几个步骤
       1、初始标记initial markSTW 暂停所有的其他线程并记录下gc roots直接能引用的对象速度很快
       2、并发标记Concurrent Marking 同CMS的并发标记
       3、最终标记RemarkSTW 同CMS的重新标记
       4、筛选回收CleanupSTW 筛选回收阶段首先对各个Region的回收价值和成本进行排序根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划比如说老年代此时有1000个 Region都满了但是因为根据预期停顿时间本次垃圾回收可能只能停顿200毫秒那么通过之前回收成本计算得知可能回收其中800个Region刚好需要200ms那么就只会回收800个Region(Collection Set要回收的集合)尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行但是因为只回收一部分Region时间是用户可控制的而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代回收算法主要用的是复制算法将一个region中的存活对象复制到另一个region中这种不会像CMS那样 回收完因为有很多内存碎片还需要整理一次G1采用复制算法回收几乎不会有太多内存碎片。(注意CMS回收阶段是跟用户线程一起并发执行的G1因为内部实现太复杂暂时没实现并发回收不过到了Shenandoah就实现了并发收集Shenandoah可以看成是G1的升级版本)。
在这里插入图片描述       G1收集器在后台
维护了一个优先列表
每次根据允许的收集时间优先选择回收价值最大的Region(这也就是它的名字 Garbage-First的由来)比如一个Region花200ms能回收10M垃圾另外一个Region花50ms能回收20M垃圾在回收时间有限情况下G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式保证了G1收集器在有限时间内可以尽可能高的收集效率
       G1垃圾收集分类
              1、YoungGC
                  YoungGC并不是说现有的Eden区放满了就会马上触发G1会计算下现在Eden区回收大概要多久时间如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值那么增加年轻代的region继续给新对象存放不会马上做Young GC直到下一次Eden区放满G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值那么就会触发Young GC。
              2、MixedGC
                  不是FullGC老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区正常情况G1的垃圾收集是先做MixedGC主要使用复制算法需要把各个region中存活的对象拷贝到别的region里去拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC。
              3、Full GC
                  停止系统程序然后采用单线程进行标记、清理和压缩整理好空闲出来一批Region来供下一次MixedGC使用这 个过程是非常耗时的。(Shenandoah优化成多线程收集了)

       4.4、垃圾收集底层算法

           4.4.1、三色标记

               在并发标记的过程中因为标记期间应用线程还在继续跑对象间的引用可能发生变化多标和漏标的情况就有可能发生。
               这里我们引入“三色标记”来给大家解释下把Gc roots可达性分析遍历对象过程中遇到的对象 按照“是否访问过”这个条件标记成以下三种颜色

  • 黑色 表示对象已经被垃圾收集器访问过 且这个对象的所有引用都已经扫描过黑色的对象代表已经扫描过 它是安全存活的 如果有其他对象引用指向了黑色对象无须重新扫描一遍。 黑色对象不可能直接不经过灰色对象 指向某个白色对象。
  • 灰色 表示对象已经被垃圾收集器访问过 但这个对象上至少存在一个引用还没有被扫描过
  • 白色 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段 所有的对象都是白色的 若 在分析结束的阶段 仍然是白色的对象 即代表不可达
    在这里插入图片描述

       4.5、一次完整GC流程

              这里准备用大白话来给大家来讲解哈。首先对象创建之后会存放进堆中新生代的Eden区中当Eden区满了会触发minoe GC存活的对象移动到S0区并且存活的对象GC年龄+1此时又有新建的对象存放进Eden区当Eden区满了就又会触发minior GC这次不单单回收Eden区还会回收S区此时存活的对象会移动到S1区存活的对象年龄+1反反复复这样垃圾回收当对象GC年龄大于15次时那么此时对象会存放进老年代当老年代满了之后会触发Full GCFull GC不单单回收老年代它还回收新生代Full GC 的时间会比较长大概是Minor GC的十倍左右。
              好了上面就是一次完整的GC流程大白话讲解的应该还算比较清晰。不理解的可以在评论中评论哦。

5、JVM调优

              这个JVM调优就先不给大家讲解了吧因为今天是年前上班的最后一天已经下班了我还明天中午的高铁今天晚上回去还要收拾东西今天就不讲解了等有时间了给大家补上哈
              废话少说赶紧来个结尾准备收拾东西下班回家过年
              再次提前祝大家新年快乐哦(从下写作文老师就教导我们写作文要首尾相应🐶)
              文章中有讲解不对的内容希望大家在评论区中指出哦希望大家点赞评论转发哦点赞超过三个我就更新下一篇——MySQL系列

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6