Java基础技术JVM面试【笔记】
JVM
JVM 对 java 类的使用总体上可以分为两部分:一是把静态的 class 文件加载到 JVM 内存,二是在 JVM 内存中进行 Java 类的生命周期管理
JVM 内存结构是什么样的?
JVM 内存的主要分为五个区:
方法区(Method Area)
虚拟机栈(VM Stack)
本地方法栈(Native method stack)
堆(Heap)
程序计数器(Program Counter Register)
堆(Heap)
堆会在虚拟机启动时进行创建,可以说,几乎所有的对象实例都在这里创建,而且这里是垃圾收集器管理的主要区域
其是线程共享的
方法区(Method Area)
方法区主要是用来存储 JVM 加载的类信息,其中就包括类的方法(如类的接口以及父类等)、常量、静态变量、即时编译器编译后的代码等数据,其还包括运行时常量池,用于存放静态编译产生的字面量和符号引用
其很少发生 GC(Garbage Collection,垃圾回收),偶尔发生的 GC 主要是对常量池回收和类型的卸载
其是线程共享的
虚拟机栈(VM Stack)
虚拟机栈又被称为栈内存,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息,那么,每一个方法被调用直至执行完成的过程,就对应着一个栈桢在虚拟机栈中从入栈到出栈的过程
其是线程私有的
本地方法栈(Native method stack)
本地方法栈有点类似于虚拟机栈,不过本地方法栈为 Native 方法服务,而虚拟机栈为 java 方法服务
其是线程私有的
程序计数器(Program Counter Register)
在内存空间小,字节码解释器工作时通过改变程序计数器的计数值来选取下一条需要执行的字节码指令,像是分支、循环、跳转、异常处理和线程恢复等功能都需要依赖程序计数器来完成,该内存区域是唯一一个 java 虚拟机规范没有规定任何 OOM 情况的区域
其是线程私有的
各个区域会抛出的异常:
堆的异常:如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常
方法区的异常:当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常
虚拟机栈以及本地方法栈的异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常,对于支持动态扩展的虚拟机,当扩展无法申请到足够的内存时会抛出 OutOfMemory 异常
程序计数器的异常:此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域
JVM这五大模块的示意图如下,其中蓝色的(方法区以及堆)是线程共享的,紫色的(虚拟机栈,本地方法栈以及程序计数器)是线程私有的
java程序计数器的作用?
记录当前线程锁执行的字节码的行号
1.程序计数器是一块较小的内存空间
2.处于线程独占区
3.执行java方法时,它记录正在执行的虚拟机字节码指令地址。执行native方法,它的值为undefined
4.该区域是唯一一个没有规定任何OutOfMemoryError的区域
Java的内存结构和内存模型?
java 内存结构(或者说 java 内存分布)就是上文提到的五大区块的划分
java 内存模型则主要是由 JSR-133: JavaTM Memory Model and Thread Specification这个文档来描述,它是为了屏蔽各种硬件和操作系统差异,保证 Java 程序在各种平台下对内存的访问都能保证效果一致而提出的一套规范,其主要解决问题的手段是限制处理器优化和使用内存屏障
JVM 的类加载机制是什么样的?有几类加载器?
JVM 是通过双亲委派模型来进行类的加载,即当某个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回,只有父类加载器无法完成此加载任务时,才会自己去加载
Java 有 3 类加载器:
启动类加载器 (Bootstrap ClassLoader)
扩展类加载器 (Extension ClassLoader)
应用程序类加载器 (Application ClassLoader)
启动类加载器 (Bootstrap ClassLoader):负责加载 JAVA_HOMElib 目录中的,或通过 - Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录也不会被加载)的类。启动类加载器无法被 Java 程序直接引用
扩展类加载器 (Extension ClassLoader):负责加载 JAVA_HOMEjrelibext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库
应用程序类加载器 (Application ClassLoader):负责加载用户路径(classpath)上的类库
除了上述三种加载器外,还可以通过继承 java.lang.ClassLoader 类实现自己的类加载器(主要是重写 findClass 方法),通过一个类的全限定名来获取描述此类的二进制字节流,这个动作的代码模块被称为类加载器
Java平台无关性?
类加载器和字节码是 java 平台无关性的基石,通过一个类的全限定名来获取描述此类的二进制字节流,这个动作的代码模块被称为类加载器,而此类的二进制字节流即是程序存储格式的字节码
对于任意一个类,都需要由加载它的类加载器和这个类本身 (字节码进行描述)一同确立其在 Java 虚拟机中的唯一性
双亲委派模型解决了什么问题?
其解决了两个问题
一个是基础类的统一加载问题(越基础的类由越上层的加载器进行加载),如类 java.lang.String,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,所以在程序的各种类加载器环境中都是同一个类
另一个就是提高 java 代码的安全性,比如说用户自定义了一个与系统库里同名的 java.lang.String 类,那么这个类就不会被加载,因为最顶层的类加载器会首先加载系统的 java.lang.String 类,而不会加载自定义的 String 类,防止了恶意代码的注入
类加载的过程?
一个类的生命周期可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。
其中前五个阶段即类加载,这五个阶段(验证、准备、解析这三个阶段又统称为连接)的主要作用如下
加载:通过类的全限定名来获取定义此类的二进制字节流,同时还可以将字节流所代表的静态存储结构转化为方法区的运行时的数据结构,而且,在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口
验证:四个作用,分别是文件格式验证(验证字节流是不是符合class文件格式的规范),无数据验证(对类的元数据进行语义校验,保证不存在不符合java语言规范的元数据信息),字节码验证(对类的方法体进行检验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为),符号引用验证(对类自身以外,比如常量池中的各类符号引用,其中的信息进行匹配性的校验,确保解析动作能正常执行)
准备:正式为类变量分配内存并设置变量初始值(变量初始值通常为零值)的阶段,这些内存都将在方法区中进行分配
解析:将常量池的符号引用替换为直接引用的过程,包括类或接口的解析,字段解析,类方法解析,接口方法解析
初始化:根据程序去初始化类变量和其他资源
JVM 中的 GC 是什么?为什么要有 GC?
JVM 中的 GC(Garbage Collection)是垃圾收集的意思,它是将 java 的无用的对象进行清理,释放内存,以免发生内存泄露
GC 是 Java 语言的一大特征,因为内存处理是容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃。Java 语言认为这么重要的地方不应该交给程序员来处理,所以提供 GC 用于自动监测对象是否超过作用域,从而达到自动回收内存的目的
如何判断对象是否死去?
通过引用计数法或者根搜索算法都可以判断对象是不是死去了
引用计数法
给对象添加一个引用计数器,每当有一个地方引用它,计数器就 + 1,当引用失效时,计数器 就 - 1,在任何时刻,计数器都为 0 的对象就是不能再被使用的
引用计数法的缺点:
1、每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗
2、难以解决对象之间的循环引用问题
根搜索算法
又称可达性分析算法。基本思路就是从一系列称之为 “GC Roots” 的对象开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(即图论中的所说的从 GC Roots 到这个对象不可达)时,则证明此对象是可回收的
简单来说就是,假设有十个对象,其中九个都有引用链接,有一个没有引用链接,对象和对象之间没有,GC引用遍历也没有引用链相连,这个时候就可以认为这个对象可以被回收了,主流的Java虚拟机一般使用根搜索算法来管理内存
Java 中的 GC Root 对象有哪些?
虚拟机栈 (栈帧中的本地方法表) 中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈 JNI(即一般说的 Native 方法)的引用对象
垃圾回收算法有哪些?
有四种,复制,标记-清除,标记-压缩,分代收集
复制(Coping)算法
将可用内存按容量划分为相等的两部分,每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清除第一块内存,再将第二块上的对象复制到第一块,不过现在广泛应用的是改进型复制算法
实现方便,运行高效,不用考虑内存碎片,但是内存利用率只有一半
标记 - 清除(Mark-Sweep)算法
分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象
算法简单,但是有两个缺点:
1、效率不高,标记和清除的效率都很低
2、空间问题,会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次 GC 动作
标记 - 压缩(Mark-Compact)算法
又称标记 - 整理算法,标记过程仍然与 “标记 - 清除” 算法一样,但不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存,形成一版连续的内存区域
解决标记 - 清除算法产生的大量内存碎片问题;当对象存活率较高时,也解决了复制算法的空间效率问题,不过它本身也存在时间效率方面的问题
分代收集(Generational Collection)算法
根据对象的生存周期,将堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法
在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法了,老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记 - 整理 或者 标记 - 清除
严格地说,这并非是一种算法,而是一种思想,或者说是一种复合算法
垃圾收集器?
新生代的垃圾回收器是 ParNewGC,老年代的回收器是 ConcMarkSweepGC(CMS,并发标记清除 GC)
ParNewGC+CMS 是目前比较常用的 GC 配置。此外,常见的垃圾回收器还有:串行收集器 (Serial Copying)、并行回收 GC (Parallel Scavenge) 等,算法及原理如下:
ParNewGC 特点,cms 的工作过程?
ParNew 是串行收集器(Serial GC)的多线程版本,会使用多个 CPU 和线程完成垃圾收集工作,默认使用的线程数和 CPU 数相同
CMS 是一种以最短回收停顿时间为目标的收集器,基于标记 - 清除(Mark-Sweep)算法实现的,主要针对老年代进行回收,在GC日志上可以发现其处理过程有七个步骤:
第一步,初始标记 (CMS-initial-mark) ,从 GC Roots 开始,扫描和 GC Roots 直接关联的对象并标记。该步骤会导致 STW(虚拟机暂停正在执行的任务)
第二步,并发标记 (CMS-concurrent-mark),从步骤 1 中标记过的对象出发,所有可到达的对象都在本阶段中标记, 该阶段与用户线程同时运行
第三步,并发预清理(CMS-concurrent-preclean),标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。通过重新扫描,减少后面第 5 步 "重新标记" 的工作,该阶段与用户线程同时运行
第四步,可被终止的预清理(CMS-concurrent-abortable-preclean),这个阶段会尽量尝试着承担 STW 的 Final Remark 阶段的工作。其持续的时间依赖因素较多(通常持续时间较长),因为它是重复做相同的事情直到发生 aboart 的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止,该阶段与用户线程同时运行
第五步,重新标记 (CMS-remark) ,收集器线程扫描在 CMS 堆中剩余的对象并进行标记, 是第二个并且是最后一个 STW 的阶段
第六步,并发清除 (CMS-concurrent-sweep),清理垃圾对象,与用户线程同时运行
第七步,并发重置 (CMS-concurrent-reset),这个阶段,重置 CMS 收集器的数据结构,等待下一次垃圾回收, 与用户线程同时运行
CMS的缺点?
1.会产生空间碎片。原因是是 CMS 回收器采用的基础算法是标记 - 清除算法(Mark-Sweep),因此不会整理、压缩堆空间,可以调整
2.消耗更多的 CPU 资源。CMS 的七个步骤中有五个步骤是需要和用户线程并发执行的,需要更多的 CPU 资源进行支撑
3.堆空间利用率降低。一是在老年代使用到一定程度(而非全部使用完时)就会开始触发 CMS,二是在 CMS 标记阶段应用程序的线程是并发执行的,因此需要预留一部分空间用于应用程序的空间分配
CMS的 concurrent-mode-failure 异常,会导致什么样的问题,以及需要怎么处理?简要介绍一下 promotion failed 异常?
concurrent-mode-failure 异常的现象说明:在 CMS GC 过程中,如果在并行清理的过程中老年代的空间不足以容纳应用产生的垃圾,则会抛出 “concurrent mode failure”
concurrent-mode-failure 异常的影响:老年代的 CMS GC 会转入 STW 的串行,所有应用线程被暂停,停顿时间变长
concurrent-mode-failure 异常可能的原因及解决方案:
1.老年代使用太多时才触发 CMS GC,可以调整 - XX:CMSInitiatingOccupancyFraction=N,告诉虚拟机当 old 区域的空间上升到 N% 的时候就开启 CMS
2.CMS GC 后空间碎片太多,可以加上 - XX:+UseCMSCompactAtFullCollection 和 -XX:CMSFullGCsBeforeCompaction=n 参数,表示经过 n 次 CMS GC 后做一次碎片整理
3.垃圾产生速度超过清理速度(比如说新生代晋升到老年代的阈值过小、Survivor 空间过小、存在大对象等),可以通过调整对应的参数或者关注程序代码来解决
promotion failed 异常:通常是由于 Minor GC 后, Survivor 空间容纳不了剩余对象,将要放入老年代,而此时老年代也放不下
JVM的参数?
GC 优化?如何优化?
GC 优化的一般步骤:
1. 评估现状及设定目标。评估是否需要调优及调优的目标优先级。比如说降低 Full GC 的的执行时间,降低 Young GC 的执行时间等等
2. 调优。根据 gc 日志等找到优化空间,比如说 Full GC 执行时间太长可能是因为老年代太大了,看能否调整为并行 GC 或者增加并行 GC 的线程数或者减少老年代大小等
3. 评估效果。根据 gc 日志、jstat 等命令、Mat/Visual VM 等工具来监控调优效果
4. 细微调整。 根据评估效果来进一步调整相关参数
新的垃圾回收机制?
除了常规的 Serial/Parallel/CMS 等垃圾回收器外,目前比较新的垃圾回收器还有:
1、G1(Garbage First)GC,JDK7 开始引入,JDK9 以后的默认配置
(1) 场景: 响应速度优先,面向服务端应用
(2) 目标:尽量缩短处理超大堆(大于 4GB)时产生的停顿、解决 CMS 的内存碎片问题,替换 CMS
(3) 特点:
A、分区收集,将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分 Region(不需要连续)的集合
B、可预测的停顿,根据各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。在回收时采用部分内存回收(在 YGC 时会回收所有新生代分区,在混合回收时会回收所有的新生代分区和部分老生代分区)
2、ZGC,JDK11 开始引入,目前尚处于实验阶段
(1) 场景:解决 G1 GC 大内存支持不友好、内存利用率不高等问题
(2) 目标:支持 TB 级内存、停顿时间控制在 10ms 之内、降低对整体应用性能的影响(对吞吐量的影响低于 15%)
(3) 特点:
A、不分代的垃圾回收器,即垃圾回收时对全量内存进行标记,以 page 为单位进行对象的分配和回收,但是回收时仅针对部分内存回收,优先回收垃圾比较多的 page
B、当前只支持 Linux 的 64 位系统
3、Shenandoah GC,JDK12 开始引入,也是一款实验性质的垃圾回收器
(1) 目标: 最小化垃圾回收对用户代码造成的停顿(降至毫秒级)、支持 TB 级内存
(2) 特点: 不分代的垃圾回收器