【面试题】JVM常见面试题(二)

JVM 的几个主要组成部分?

主要由 4 个部分组成:

  • 运行时数据区域:就是我们常说的JVM的内存。
  • 执行引擎:执行 class 字节码文件中的指令。
  • 类加载系统:根据给定的全限定名类名(如:java.lang.Object)来装载 .class 文件到运行时数据区中的方法区中。
  • 本地接口:与本地方法库交互,是其它编程语言交互的接口。

虚拟机栈和堆的区别?

① 物理地址方面的区别:

  • 的物理地址分配对对象是不连续的。因此性能慢些。
  • 虚拟机栈 使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。

② 内存分配方面的区别:

  • 因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于虚拟机栈。
  • 虚拟机栈 是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。

③ 存放的内容方面的区别:

  • 存放的是对象的实例和数组。因此该区更关注的是数据的存储。
  • 虚拟机栈 存放的是局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。

:静态变量放在方法区,而静态的对象还是放在堆。

④ 线程共享方面的区别:

  • 对于整个应用程序都是共享、可见的。
  • 虚拟机栈 只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。

为什么要把堆和栈区分出来呢?

可以从这几个方面考虑:

  • 软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想
  • 堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如︰共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间
  • 栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能相应栈中只需记录堆中的一个地址即可

垃圾回收算法?

标记-清除

在这里插入图片描述

定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识清除相应的内容,给堆内存腾出相应的空间

  • 这里的腾出内存空间并不是将内存空间的字节清0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存

缺点容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢

标记-整理

标记 - 整理算法在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后整理剩余的对象,将可用的对象移动到一起,使内存更加紧凑,连续的空间就更多。可以有效避免因内存碎片而导致的问题,但是因为整体需要消耗一定的时间,所以效率较低

在这里插入图片描述

复制算法

将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间

  • 当需要回收对象时,先将 GC Root 直接引用的的对象(不需要回收的对象)从 FROM 放入 TO

在这里插入图片描述

  • 然后清除FROM中的需要回收的对象:

在这里插入图片描述

  • 最后交换 FROMTO 的位置:(FROM 换成 TO,TO 换成 FROM )

在这里插入图片描述

分代收集

分代收集算法:这种算法是把 Java 堆分为新生代和老年代,新生代默认的空间占比总空间的1/3,老生代的默认占比是2/3

新生代里有 3 个分区:伊甸园、To 幸存区、From 幸存区,它们的默认占比是 8:1:1。即-XX:SurvivorRatio=8,其中Survivor分为From Survivor和ToSurvivor,因此Eden此时占新生代空间的80%

在这里插入图片描述

新创建的对象都被放在了新生代的伊甸园
在这里插入图片描述
当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC

Minor GC 会将伊甸园和幸存区FROM存活的对象复制到 幸存区 TO中, 并让其寿命加1,再交换两个幸存区

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1

在这里插入图片描述

如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代
在这里插入图片描述

如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收

大对象处理策略?

当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代

线程内存溢出

某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行

这是因为当一个线程抛出OOM异常后它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常

根据不同年代的特点采用最适当的收集算法?

老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法

  • 新生代使用:复制算法
  • 老年代使用:标记 - 清除 或者 标记 - 整理 算法

分代收集算法流程小结

  • 新创建的对象首先会被分配在伊甸园区域。
  • 新生代空间不足时,触发 Minor GC,伊甸园和 FROM 幸存区需要存活的对象会被 COPY 到 TO 幸存区中,存活的对象寿命+1,并且交换 FROMTO
  • Young GC 会引发 Stop The World:暂停其他用户的线程,等待垃圾回收结束后,用户线程才可以恢复执行。
  • 当对象寿命超过阈值15时,会晋升至老年代。
  • 如果新生代、老年代中的内存都满了,就会先触发 Minor GC,再触发 Full GC,扫描新生代和老年代中所有不再使用的对象并回收

CMS 垃圾收集器?

Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器

特点:基于标记-清除算法实现。并发收集、低停顿

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务

CMS收集器的运行过程分为下列4步:

  • 初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题

  • 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行,不需要停顿

  • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题

  • 并发清除:对标记的对象进行清除回收,不需要停顿

主要缺点有:吞吐量低,导致 CPU 利用率不够高、无法处理浮动垃圾、它使用的回收算法“标记-清除”算法会导致收集结束时会有大量空间碎片产生

CMS的问题

  1. 并发回收导致cpu资源紧张

    在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数+3)/4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。

  2. 无法清理浮动垃圾:

    在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为"浮动垃圾”。

  3. 并发失败(Concurrent Mode Failure):

    由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了92%的空间后就会触发CMS垃圾回收,这个值可以通过-XX:CMSInitiatingOccupancyFraction参数来设置。

    这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次"并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启用SerialOld来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。

  4. 内存碎片问题:

    CMS是一款基于"标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于在FullGC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS在执行过若干次不整理空间的FullGC之后,下一次进入FullGC前会先进行碎片整理(默认值为0,表示每次进入FullGC时都进行碎片整理)。

G1?

Garbage First 收集器

JDK 9以后默认使用,而且替代了CMS 收集器

img

适用场景

  • 同时注重吞吐量和低延迟(响应时间)
  • 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域
  • 整体上是标记-整理算法,两个区域之间是复制算法

相关参数:JDK8 并不是默认开启的,所需要参数开启

img

G1回收过程,G1回收器的运作过程大致可分为四个步骤:

  1. 初始标记(会STW):仅仅只是标记一下GCRoots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行MinorGC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。

  2. 并发标记:从GCRoots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。

  3. 最终标记(会STW):对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。

  4. 清理阶段(会STW):更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的。

什么情况下会触发Full GC?

  • 调用 System.gc()

    只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行不建议使用这种方式,而是让虚拟机管理内存

  • 老年代空间不足

    老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等

    这种情况如何尽量避免?

    • 应当尽量不要创建过大的对象以及数组。

    • 可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。

    • 还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间

  • 空间分配担保失败

    使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC

  • JDK 1.7 及以前的永久代空间不足
    在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的
    当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,此时也会触发Full GC

    这种情况怎么避免?

    为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间转为使用 CMS GC

  • Concurrent Mode Failure

    执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC

什么是内存泄漏?

内存泄漏(Memory Leak)是指本来无用的对象却继续占用内存,没有再恰当的时机释放占用的内存。 不使用的内存,却没有被释放,称为内存泄漏。 也就是该释放的没释放,该回收的没回收,比较典型的场景是: 每一个请求进来,或者每一次操作处理,都分配了内存,却有一部分不能回收(或未释放),那么随着处理的请求越来越多,内存泄漏也就越来越严重。

在Java中一般是指无用的对象却因为错误的引用关系,不能被GC回收清理。

避免内存泄漏的方法?

  • 尽量不要使用 static 成员变量,减少生命周期;
  • 及时关闭无用的资源
  • 不用的对象,可以手动设置为 null

JVM中有哪些类加载器?

以 JDK 8 为例:

名称 加载哪的类 说明
Bootstrap ClassLoader(启动类加载器) JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader(扩展类加载器) JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader(应用程序类加载器) classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

类加载器的优先级(由高到低):启动类加载器 -> 扩展类加载器 -> 应用程序类加载器 -> 自定义类加载器

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在 JAVA_HOME/jre/lib 目录中的,或者被-Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。用来加载java核心类库,无法被java程序直接引用
  • 扩展类加载器(Extension ClassLoader):这个加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载JAVA_HOME/jre/lib/ext目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。用来加载java的扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类;
  • 应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher$AppClassLoader 实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。它根据java的类路径来加载类,一般来说,java应用的类都是通过它来加载的
  • 自定义类加载器:用户自定义的类加载器,继承自ClassLoader

在这里插入图片描述

类的加载的过程?

在这里插入图片描述

类加载的过程包括:加载、验证、准备、解析、初始化。其中验证、准备、解析统称为连接

加载验证准备初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)

  • 加载:通过一个类的全限定名来获取定义此类的二进制字节流,在内存中生成一个代表这个类的java.lang.Class对象。
  • 验证:确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。确保被加载的类的正确性
  • 准备:为静态变量分配内存并设置静态变量初始值,这里所说的初始值“通常情况”下是数据类型的零值。
  • 解析:将常量池内的符号引用替换为直接引用。
  • 初始化:到了初始化阶段,才真正开始执行类中定义的 Java 初始化程序代码。主要是静态变量赋值动作和静态语句块(static{})中的语句。

什么是双亲委派模型?

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

为什么要使用双亲委派模型呢?(好处)

避免重复加载 + 避免核心类篡改

  • 采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父加载器已经加载了该类时,就没有必要子加载器再加载一次。
  • 其次是考虑到安全因素,java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class,这样便可以防止核心API库被随意篡改。

怎么打破双亲委派模型?

自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法。

常用的JVM启动参数有哪些?

1 # JVM启动参数不换行 
2 # 设置堆内存
3 ‐Xmx4g ‐Xms4g 
4 # 指定GC算法 
5 ‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=50 
6 # 指定GC并行线程数 
7 ‐XX:ParallelGCThreads=4 
8 # 打印GC日志 
9 ‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps 
10 # 指定GC日志文件 
11 ‐Xloggc:gc.log 
12 # 指定Meta区的最大值 
13 ‐XX:MaxMetaspaceSize=2g 
14 # 设置单个线程栈的大小 
15 ‐Xss1m 
16 # 指定堆内存溢出时自动进行Dump 
17 ‐XX:+HeapDumpOnOutOfMemoryError 
18 ‐XX:HeapDumpPath=/usr/local/

参考
https://csp1999.blog.csdn.net/article/details/117318685
https://blog.csdn.net/qq_45966440/article/details/121308864
https://blog.csdn.net/yanpenglei/article/details/119684369

猜你喜欢

转载自blog.csdn.net/qq_51998352/article/details/121587435