[Day11]JVM八股复习

JVM 八股

这两天因为有朋自远方来休息了好几天,休息过程中刷了三十几道算法题;今天恢复了八股文的复习;

目录

[toc]

JVM 组成

谈谈你对 JVM 的理解

JVM 的全称是 java 虚拟机; 他处于程序代码和操作系统指令之间; 也就是说只要根据不同的平台运行不同版本的 jvm 就能方便地实现代码跨平台的特性; JVM 可以自上往下分为四个部分: 类加载器、运行时数据区、执行引擎、本地方法接口; 编写的 java 源文件被编译成 class 文件后会通过类加载器加载到运行时数据区中, 类对象的创建到销毁的所有生命周期底层都会在 JVM 的运行时数据区中进行;如果对象需要执行操作系统指令就会通过执行引擎和本地方法接口实现;

说一下 JVM 的架构和组成

JVM 可以分为四个核心组成部分:类加载器、运行时数据区、本地方法接口、执行引擎

类加载器:

  • 负责将 class 文件加载到内存中;包含 加载、连接、初始化 三个阶段

运行时数据区

  • 方法区: 线程共享的存储的是类对象的元数据 (类名、字段、方法、接口)、运行时常量池、静态变量等
  • 堆: 堆空间是 JVM 中占比最大的部分是线程共享的,用于存储实例化对象和数组;为了保证这块区域的高效利用率就需要通过垃圾回收机制 gc,gc 也是 java 可以不用像 c/c++那样可以不用手动释放内存也能管理内存的重要机制
  • 虚拟机栈:是每个线程私有的,以栈桢为单位记录每个方法的执行信息;可以理解为方法调用的时候就是在这个栈中压入一个栈桢, 当被调用方法执行完成那么就弹出栈桢比回到上个栈帧的调用处
  • 本地方法栈:是每个线程调用本地 native 方法的信息存储区域
  • 程序计数器 :记录当前线程执行到的代码行号, 如果当前线程交出了 CPU 执行权, 下次再获取到 CPU 执行权后开始执行代码的位置就是有程序计数器标记的

执行引擎

  • 解释器:逐行解释执行字节码
  • 即时编译器: 将热点代码编译成本地机器指令

本地方法接口

  • 用 c c++编写的底层方法

alt

alt

介绍一下 JVM 中的方法区

  • 方法区是 JVM 中的一块共享区域;
  • 随着 JVM 的创建而创建,随着 JVM 的销毁而销毁;
  • 因为是线程共享的, 所以一般存储线程安全的不变数据;比如字符串常量池、类的元数据、运行时常量池、即时编译器编译后的代码等数据;
  • 1.8 之前是永久代作为堆的一部分;1.8 开始变成了直接内存上的一段元空间, 这改进是为了避免 OOM

介绍一下执行引擎

执行引擎由解释器和 JIT (just in time) 编译器组成; 他们对应了高级语言转为机器指令的两种方式——解释、编译 JVM 一般情况是通过解释器将指令逐行解释为符合当前机器平台的机器指令的, 但是 JVM 如果发现某段代码执行得十分频繁那么就会讲他认定为热点代码 hot spot code 然后 JIT 编译器就会将它变以为本地机器码存储在方法区中, 实现复用的效果提高效率;

方法内的局部变量是线程安全的吗?

要讨论这个问题我们可以先了解一下方法中的局部变量存储位置;他们是存储在栈空间中虚拟机栈中的, 每个方法都是一个独立的栈帧;所以局部变量时每个线程独立的是线程安全的, 并且在同个线程的不同方法中也是安全隔离的;但是如果通过参数传递的方式将变量传出了隔离范围就有可能变成线程不安全的

JVM 中栈和堆的区别是什么?

  • 作用范围不同
    • 堆是线程共享的
    • 栈是每个线程独占的
  • 回收方式不同
    • 栈是通过垃圾回收机制进行回收
    • 栈是通过方法执行结束后弹出栈帧完成回收的
  • 报错信息不同
    • 堆空间不足时报错是 OOM outOfMemory
    • 栈空间不足是报错是 stackOverFlow

什么是直接内存

直接内存并不属于 JVM, 也不由 JVM 管理;他是属于系统资源的一部分 堆外内存; JVM 可以通过直接访问他来提升某些场景下的程序执行效率,核心就是减少用户态转为内核态过程中的次数和开销来提升速度

类加载器

类加载器的分类

  • BootStrap ClassLoader 启动类加载器
  • ExtClassLoader 拓展类加载器
  • ApplicationClassLoader 应用程序类加载器
  • UserClassLoader 用户自定义类加载器

类的加载过程

类通过类加载器加载到 JVM 可以大致分为加载-连接-初始化三个过程; 第一步:加载,将需要加载类的 Class 文件转为二进制流加载到 JVM 中的方法区中; 第二步:链接可以分为验证-准备-解析三步骤;

  • 验证就是校验需要加载的类是否符合 JVM 规范
  • 准备阶段会为类的静态变量做初始化操作
  • 解析操作会将类中的符号引用替换为直接引用 (比如一个字段的类型是 A 类型, 在加载的时候会先用一个字符比方说是 S 进行占位, 到了这一步则会将具体的直接引用地址替换符号引用) 第三步: 初始化,执行类的 init 方法;

上面三步中第一步和第三步都是可以根据用户需求进行自定义的,只有第二步是又 JVM 主导的; 比如第一步可以通过自定义用户类加载器来实现指定来源的二进制流加载类;第三步可以指定类加载后的自定义操作逻辑;

什么是双亲委派机制? 如何破坏?

因为在 JVM 加载类的过程中有这各种的类加载器;比如最上层的启动类加载器 bootstrapClassloader, 拓展加载器 extClassLoader;不同类型的类加载器有着独立的命名空间,也就是说同一个类如果通过两个不同的类加载进行加载, 那么 JVM 会认为这两个类是不相同的; 这可能引发许多的问题:

  • 可能出现恶意破坏核心类库的问题;用户可以自定义一个 java. lang. String 类那么在其他类执行的过程中就可能使用的是用户恶意篡改的类
  • 类重复加载问题;如果一个类同时被多个加载器加载那么在方法区中会浪费许多空间
  • 依赖冲突问题;如果一个类被加载了多份那么在依赖注入的场景下就无法判断应该使用哪一份 为了解决以上问题就出现了双亲委派机制

双亲委派机制用一句话概括就是,在一个类加载器想要加载一个类时会先尝试让上层加载器进行加载,上层加载器也会逐层委托直到类启动加载器,如果上层加载器成功加载那么就直接使用上层加载的类;如果上层加载器无法加载那么就由当前加载器进行加载;这样就能确保一个类不会被重复加载,并且保证下层的加载器不会覆盖上层加载器加载的类;

注意的是虽然叫做双亲委派,不能简单的说类加载器的父加载器;因为加载器之间并不是继承关系,而是组合关系

双亲委派的流程是通过当前加载器的 loadClass 进行双亲委派,如果上层加载器无法加载那么就由当前加载器通过 findClass 方法进行加载;所以想要破坏双亲委派只需要重写 loadClass 方法即可;如果想要保持双亲委派的机制只需要在定义加载器时重写 findClass 方法即可;

  • 双亲委派源码
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);

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

有哪些打破双亲委派的例子?

最常见的就是 tomcat;因为他需要实现容器隔离,就是在一个 web 容器内部署多个应用程序那么就必须要做到环境的隔离,不同的应用程序可能依赖相同类库的不同版本,但是这些类的全路径名都相同,就可以通过破坏双亲委派机制来实现;

如何自定义类加载器? 能实现什么功能?

自定义类加载器只需要创建类然后继承 ClassLoader 类然后重写 findClass 方法即可;如果想要破坏双亲委派机制那么就重写 loadClass 方法;

自定义类加载器可以实现许多个性化的功能;比如动态加载器远程类,可能类的来源不是本地的 class 文件而是远程计算机传输的二进制流;还能实现模块隔离,如果需要每个模块都需要使用相同类名的不同版本那么就可以通过打破双亲委派自定义类加载器实现;还有热加载热部署等都可以通过自定义类加载器实现;

垃圾回收

JVM 如何判断哪些对象应该被回收?

判断一个对象是否应该被回收用得最多的就两个算法:引用计数法和可达性分析算法

引用计数法通过标记一个对象被引用的次数判断是否该被回收; 如果一个对象的引用次数为 0 则说明没有对象使用它那么可以被回收;python 中就使用的这种算法;但是这个算法有个问题就是两个应该被回收的对象相互引用那么就会出现内存泄露问题;

可达性分析算法:java 中采用的算法,它通过 cg root 节点出发,不断拓展, 如果一个节点可以通过 cg root 到达那么就证明该节点依旧被使用反之则说明可以回收;

可以作为 gc root 的节点:

  • 方法区的静态属性引用对象
  • 方法区常量引用对象
  • 存活的线程
  • 虚拟机栈栈帧
  • 被 synchronized 锁定的对象

JVM 垃圾回收算法有哪些?

垃圾回收算法可以分为标记-清除、复制、标记-整理

标记-清除:

  • 通过可达性分析算法标记出哪些需要节点需要进行清理,然后将标记的节点进行回收
  • 这个的好处是可以原地进行,并且效率高;缺点是可能产生许多的碎片化空间,让内存利用率降低

复制:

  • 将内存分成两等份,同一时刻只有一侧用于存储对象;另一侧清空;当需要进行 gc 时扫描存活的节点,将他们依次复制到空闲区域;这样做可以保证 gc 后的空间上连续的不会出现碎片化空间,但是缺点就是浪费一半的内存空间;

标记-整理:

  • 结合了标记-整理和复制的特点;不使用额外的空间,先标记需要回收的节点,然后将保留的节点整理到一起让他们连续紧凑;这样的好处是不会有额外空间的浪费并且整理后不会有碎片化空间,但是缺点就是性能很低

讲一下 JVM 中的分代回收

分代的思想我认为是从统计学和实践中得出来的方法论;比如在垃圾回收的过程中开发者发现大部分都对象生命周期很短,也就是在创建后的短时间内可能就会被销毁回收,而且新对象是频繁引用老对象,而老对象很少引用新对象;JVM 垃圾回收器的设计师就提出了分代思想,将堆内存分为新生代和老年代两部分默认是 1:2;而在新生代中分为 eden 区和两个 survivor 区,比例是 8:1:1 ;所有对象刚被创建出来都存放在 eden 区,如果经过一轮 gc 后依然存活就会通过标记-复制算法移动到一个 survivor 区中,然后清空另一个 survivor 分区和 eden 区;然后进行下次 gc 交替将每轮依旧存活的对象存储到两个 survivor 中;如果一个对象存活了多轮,达到了临界值默认是 15 次,这个指标也可以通过 jvm 调优进行指定,达到指标后就会移动到老年代;jvm 认为这些对象短时间能不会被销毁; 这样一来,每轮在新生代发生的 minor gc 都能高效清除生命周期较短的对象,并且通过巧妙的比例划分,在标记复制算法的效率和空间浪费中取得了平衡; 值得一提的是如果一个对象很大那么就会直接存储在 old 分区中;如果老年代中的空间也达到了阈值那么就会进行 full gc;

对于新生代来说 minor gc 发生得很频繁需要追求高效,所以采用高效的标记复制算法通过空间换时间的方式提高效率;而老年代一般采用整体效果更好的标记整理算法

新生代和老年代的垃圾回收器有哪些?

新生代的垃圾回收算法:

  • serial: 单线程串行清理垃圾
  • parNew : 多线程版本的垃圾清理器;
  • parallel Scavenge

老年代 :

  • serial
  • CMS
  • parallel Old

什么是 STW?

STW 是 Stop The World 的简写;是指在进行垃圾回收的过程中一个强制所有工作线程等待垃圾回收线程进行回收的动作;是为了确保垃圾回收线程的过程中不会因为工作线程的并发运行导致垃圾识别错误的问题;出现 STW 对程序的吞吐量和响应时间都有影响,所以不断迭代的垃圾回收器再不断减少 STW 的时间,常见的思路就是尽可能少的 STW 然后让工作线程和垃圾回收线程并发通知的 CMS(Concurrent Mark Sweep)

什么是三色标记算法?

没有三色标记算法的时候,每次 STW 都要通过可达性分析算法标记出哪些节点是正在活跃的节点,哪些节点上应该被回收的垃圾节点;三色标记算法就是为了缩短 STW 时间而出现的垃圾标记算法;他实现了通过短暂的 STW,让标记线程和工作线程并发执行;三色标记算法具体如下:

首先定义三种颜色:黑色、灰色、白色; 黑色:当前节点已经完成标记,并且所以直接后继节点也完成了标记; 灰色:当前节点完成了标记,但是子节点没有进行标记 白色:当然节点未被标记(暂时不可达)

执行流程如下: 第一步初始化标记状态,将所有 cg root 标记为灰色;这个 STW 时间很短 第二步并发标记,这一步不需要进行 STW,工作线程和标记线程并发执行,从灰色对象开始便利对象图,将新标记的子引用标记为灰色,标记完成后将当前节点标为黑色; 第三步重写标记,这一步会出发 STW 然后快速检查标记节点的正确性,避免并发标记过程中因为用户线程改变节点引用关系而造成标记漏标,浮动垃圾的问题; 第四步删除:当对象图中只有黑白节点时,所有白色节点就是需要被回收的节点;并发进行删除即可;

三色标记算法的问题

在常规的是三色标记算法中会出现浮动垃圾和漏标的问题; 浮动垃圾:

  • 当一个节点被标记为不是垃圾,但是用户线程并发修改了引用关系,将这个节点的引用删除了,但是标记线程无法感知,那么就会让应该删除的节点被标记保留下来了;这种问题会产生浮动垃圾,并不严重,只需要在下一次标记的时候就能进行回收;不会造成内存泄露

漏标:

  • 一个已经被标记成黑色的黑点被用户线程新增了一个指向白色节点的引用;但是标记线程无法感知;就会导致应该被标记的白色节点漏标;这样极有可能出现指针异常,这样的错误是严重的,不能容忍的;

CMS 解决的方式是通过插入写屏障解决的;具体来说就是用户线程所有为黑色节点指向白色节点的操作都记录在一张表中 card table 然后遍历这张表;并且在最后删除之前通过 remark 进行再次的校验;

介绍一下 CMS

CMS 全称 Concurrent Mark Sweep 是作用在老年代的垃圾回收器;标记算法采用的是三色标记算法;垃圾清除算法采用的是标记-清除;具体的工作流程如下:

  1. 初始化标记:进行短暂的 STW,将所有的 gc root 标记为灰色;
  2. 并发标记阶段:这个阶段不需要 STW,标记线程和工作线程并发执行;标记线程会遍历所有灰色节点,将新到达的子节点标记为灰色,所有子节点标记完成后将当前黑点标记为灰色;直到图中的所有点只剩下黑色和白色;在这个过程中如果用户线程将黑色节点新增指向白色节点的引用,那么就要记录在一张卡表中,这是为了解决三色标记算法可能出现的漏标问题;
  3. 重新标记阶段:这个阶段会重新扫描卡表 card table 中的节点,并且重新检查所有标记节点的正确性;这一步也会出发 STW,这一步的 STW 是 CMS 算法的瓶颈;
  4. 并发删除:工作线程和 CMS 并发执行;

CMS 的优点是让老年代的 GC 速度快速响应,缺点就是因为采用了标记清除算法会产生大量的碎片化空间,只能通过 fullgc 解决;其次就是 remark 阶段的 STW 可能时间会很长;

介绍一下 G1

G1 垃圾回收器从堆内存设计上采用的并不是分代而是采用的逻辑分区的方式,每个区域都可以根据需要分配到 eden、survivor、old、humongous 角色; humongous 主要是存储体积较大的对象; G1 的优点是可以预测和控制 STW 时间;G1 会根据用户设置的最大 STW 时间来决策该回收哪些区域;执行流程如下:

  1. young GC: 进行 STW,将 eden 中存活的对象通过标记复制移动到 survivor 分区中,如果存活达到阈值则移动到 old 分区中;因为每个年轻代的 region 分区很小所以这个 STW 时间很短
  2. 并发标记周期:这一步的流程和 CMS 类似;不同的是他们解决三色标记算法漏标问题的方式不同,G1 采用的是 SATB,简单来说就是将所有被删除引用的节点放入缓冲区中,在最终标记节点将这些点标记成灰色再次扫描
  3. 混合回收:根据用户指定的 STW 时间判断选择回收比例更高的 region 进行标记复制回收

什么是强软弱虚引用?

简单来说, 强软弱虚引用描述的是一个对象的被回收级别, 引用越强, 证明越不能被轻易回收; 越低说明更容易被回收

  • 强引用
    • 能通过 CG Root 直接找到的引用, 只要依然可达就不会被垃圾回收
  • 软引用
    • 通过 SoftReferencr 使用, 当进行多次垃圾回收都依然空间不足时进行回收
  • 弱引用
    • 通过 WeakReference 使用, 当进行垃圾回收时就会将弱引用对象进行回收
    • 比如 ThreadLocal 中就通过将 ThreadLocalMap 中的 key 指定为弱引用确保在 GC 时进行回收来解决一部分内存泄露问题
  • 虚引用对象
    • 必须配合引用队列使用, 被引用对象回收时, 会将虚引用入队, 然后由 Reference Handler 线程进行对虚引用相关外部资源进行释放
    • 提供了当对象被释放时灵活的处理, 是对象被收回释放时的一种通知机制
#java##面试#
全部评论
淦,大佬我以为你考研去了
点赞 回复 分享
发布于 03-13 17:59 湖北

相关推荐

评论
4
7
分享

创作者周榜

更多
牛客网
牛客企业服务