面试中的JVM:结合经典书籍的深度解读
写在前面
🔥我把后端Java面试题做了一个汇总,有兴趣大家可以看看!这里👉
⭐️在无数次的复习巩固中,我逐渐意识到一个问题:面对同样的面试题目,不同的资料来源往往给出了五花八门的解释,这不仅增加了学习的难度,还容易导致概念上的混淆。特别是当这些信息来自不同博主的文章或是视频教程时,它们之间可能存在的差异性使得原本清晰的概念变得模糊不清。更糟糕的是,许多总结性的面试经验谈要么过于繁复难以记忆,要么就是过于简略,对关键知识点一带而过,常常在提及某项技术时,又引出了更多未经解释的相关术语和实例,例如,在讨论ReentrantLock时,经常会提到这是一个可重入锁,并存在公平与非公平两种实现方式,但对于这两种锁机制背后的原理以及使用场景往往语焉不详。
⭐️正是基于这样的困扰与思考,我决定亲自上阵,撰写一份与众不同的面试指南。这份指南不仅仅是对现有资源的简单汇总,更重要的是,它融入了我的个人理解和解读。我力求回归技术书籍本身,以一种层层递进的方式剖析复杂的技术概念,让那些看似枯燥乏味的知识点变得生动起来,并在我的脑海中构建起一套完整的知识体系。我希望通过这种方式,不仅能帮助自己在未来的技术面试中更加从容不迫,也能为同行们提供一份有价值的参考资料,使大家都能在这个过程中有所收获。
JVM相关面试题
1 JVM组成
面试官:JVM由那些部分组成,运行流程是什么?
候选人:
在JVM中共有四大部分,分别是ClassLoader(类加载器)、Runtime Data Area(运行时数据区,内存分区)、Execution Engine(执行引擎)、Native Method Library(本地库接口)
它们的运行流程是:
第一,类加载器(ClassLoader)把Java代码转换为字节码。
第二,运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行
第三,执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口(Native Method Library)来实现整个程序的功能。
JDK1.6:(图为《深入理解Java虚拟机》第三版)
JDK1.8:(图为JavaGuide面试笔记)
面试官:能简单说一下 JVM 运行时数据区吗?
候选人:
运行时数据区包含了堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器这几部分,每个功能作用不一样。
- 堆解决的是对象实例存储的问题,垃圾回收器管理的主要区域。
- 方法区可以认为是线程共享区域,用于存储已被虚拟机加载的信息,常量、静态变量、即时编译器编译后的代码。
- 虚拟机栈为虚拟机执行Java方法(也就是字节码服务),不需要执行GC。
- 本地方法栈执行的是本地方法,不需要执行GC。
- 程序计数器中存放的是当前线程所执行的字节码的行数。JVM工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。
面试官:详细介绍一下程序计数器的作用?
候选人:(源自《深入理解Java虚拟机》第三版 2.2.1节)
1)程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器⼯作时通过改变这个计数器的值来选取下⼀条需要执⾏的字节码指令,分⽀、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
2)另外,为了线程切换后能恢复到正确的执⾏位置,每条线程都需要有⼀个独⽴的程序计数器,各线程之间计数器互不影响,独⽴存储,我们称这类内存区域为“线程私有”的内存。
3)如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。
4)此内存区域是唯一一个在《 Java 虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。
通过一个简单的例子来说明**程序计数器(Program Counter Register)**的行为。
假设我们有以下Java代码:
public class Example {public static void main(String[] args) {System.out.println("Hello, World!");nativeMethod();}public static native void nativeMethod(); }
这段代码首先打印了一条消息到控制台,然后调用了一个本地方法
nativeMethod()
。在这个过程中,程序计数器的行为如下:
- 执行Java方法时:
- 当
main
方法开始执行时,程序计数器会指向Example
类的main
方法的第一条字节码指令。- 随着
System.out.println("Hello, World!");
语句的执行,程序计数器会依次指向该语句对应的字节码指令。- 比如,可能有一条字节码指令用于创建
String
对象,另一条用于调用println
方法等。- 执行本地方法时:
- 当
main
方法调用nativeMethod()
时,程序计数器的值会变成Undefined
,因为一旦进入本地方法的执行,控制权就从JVM转移到了本地代码上,而本地代码的执行不是通过字节码指令来进行的,因此程序计数器无法记录这些指令的位置。当
nativeMethod()
返回后,程序计数器会恢复到调用nativeMethod()
之后的下一条字节码指令处,继续执行剩下的main
方法中的代码。
面试官:什么是虚拟机栈
候选人:(源自《深入理解Java虚拟机》第三版 2.2.2节)
1)与程序计数器⼀样,Java 虚拟机栈也是线程私有的,它的⽣命周期和线程相同。虚拟机栈描述的是 Java⽅法执⾏的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
经常有人把 Java 内存区域笼统地划分为
堆内存(Heap)
和栈内存(Stack)
,这种划分方式直接继承自传统的 C、C++程序的内存布局结构,在Java语言里就显得有些粗糙了,实际的内存区域划分要比这更复杂。不过这种划分方式的流行也间接说明了程序员最关注的、与对象内存分配关系最密切的区域是“堆”和“栈”两块。其中,“堆”在稍后笔者会专门讲述,而“栈”通常就是指这里讲的虚拟机栈
,或者更多的情况下只是指虚拟机栈中局部变量表部分。
2)局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引⽤(reference 类型,它不同于对象本身,可能是⼀个指向对象起始地址的引⽤指针,也可能是指向⼀个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
3)在《 Java 虚拟机规范》中,对这个内存区域规定了两类异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
- 如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。
栈内存溢出的两种可能:
- 栈帧过多导致栈内存溢出(递归调用)
- 栈帧过大导致栈内存溢出
栈内存分配越大越好吗?
未必,默认栈内存通常1024K,栈帧过大会导致线程数变少。
面试官:能不能解释一下本地方法栈?
候选人:(源自《深入理解Java虚拟机》第三版 2.2.3节)
和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java ⽅法 (也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 在 HotSpot 虚拟机中, Java虚拟机栈和本地方法栈合⼆为⼀。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常。
面试官:你能给我详细的介绍Java堆吗?
候选人:(源自《深入理解Java虚拟机》第三版 2.2.4节)
1)Java 虚拟机所管理的内存中最⼤的⼀块,Java 堆是所有线程共享的⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例以及数组都在这⾥分配内存。
Java世界中“⼏乎”所有的对象都在堆中分配,但是,随着JIT编译器(即时编译技术)的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致⼀些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从JDK 1.7开始已经默认开启逃逸分析,如果某些⽅法中的对象引⽤没有被返回或者未被外⾯使⽤(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
2)Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。从垃圾回收的⻆度,由于现在收集器基本都采⽤分代垃圾收集算法,所以 Java 堆还可以细分为:新⽣代和⽼年代;再细致⼀点有:Eden 空间、From Survivor、To Survivor 空间等。进⼀步划分的⽬的是更好地回收内存,或者更快地分配内存。
3)如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出 OutOfMemoryError 异常。
在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下⾯三部分:
- 新⽣代内存(Young Generation)
- ⽼⽣代(Old Generation)
- 永⽣代(Permanent Generation)
JDK 8 版本之后⽅法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取⽽代之是元空间,元空间使⽤的是直接内存。
面试官:你听过方法区吗?
候选人:(源自《深入理解Java虚拟机》第三版 2.2.5节)
1)⽅法区(也叫堆外内存)与 Java 堆⼀样,是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2)根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。
⽅法区和永久代的关系
《Java 虚拟机规范》只是规定了有⽅法区这么个概念和它的作⽤,并没有规定如何去实现它。那么,在不同的 JVM 上⽅法区的实现肯定是不同的了。 ⽅法区和永久代的关系很像Java 中接⼝和类的关系,类实现了接⼝,⽽永久代就是 HotSpot 虚拟机对虚拟机规范中⽅法区的⼀种实现⽅式。 也就是说,永久代是 HotSpot 的概念,⽅法区是 Java 虚拟机规范中的定义,是⼀种规范,⽽永久代是⼀种实现,⼀个是标准⼀个是实现,其他的虚拟机实现并没有永久代这⼀说法。
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
整个永久代有⼀个 JVM 本身设置固定⼤⼩上限,⽆法进⾏调整,很容易造成OOM,⽽元空间使⽤的是直接内存,受本机可⽤内存的限制,不会进行GC,也因此提升了性能。
面试官: 说下直接内存?
候选人:(源自《深入理解Java虚拟机》第三版 2.2.7节)
1)直接内存并不是虚拟机运⾏时数据区的⼀部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使⽤。⽽且也可能导致 OutOfMemoryError 错误出现。
2)JDK1.4 中新加⼊的 NIO(New Input/Output) 类,引⼊了⼀种基于通道(Channel) 与缓存区(Buffer) 的 I/O ⽅式,它可以直接使⽤ Native 函数库直接分配堆外内存,然后通过⼀个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引⽤进⾏操作。这样就能在⼀些场景中显著提⾼性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
3)本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存⼤⼩以及处理器寻址空间的限制。
面试官:能说一下堆栈的区别是什么吗?
候选人:
-
栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。
-
堆会GC垃圾回收,而栈不会。
-
栈内存是线程私有的,而堆内存是线程共有的。
-
两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。
面试官:方法中定义的局部变量是否是线程安全的?
候选人:
这个得分情况讨论:
public class LocalVariableThreadSafe {// s1的声明方式是线程安全的,因为线程私有,不会被其他线程调用public static void method1(){StringBuilder s1 = new StringBuilder();s1.append("a");s1.append("b");}// 线程不安全,因为stringBuilder是外面传进来的,有可能被多个线程调用public static void method2(StringBuilder stringBuilder){stringBuilder.append("a");stringBuilder.append("b");}// 线程不安全,因为返回了一个StringBuilder对象,有可能被其他线程共享public static StringBuilder method3(){StringBuilder builder = new StringBuilder();builder.append("a");builder.append("b");return builder;}// 线程安全,返回的是stringBuilder.toString()相当于new了一个String对象(可以去看源码)// 没有被其他线程共享的可能public static String method4(){StringBuilder stringBuilder = new StringBuilder();stringBuilder.append("a");stringBuilder.append("b");return stringBuilder.toString();}
}
2 类加载器
面试官:什么是类加载器,类加载器有哪些?
候选人:
Java 虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。
常见的类加载器有4个:
-
启动类加载器(BootStrap Class Loader):由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。加载Java中最核心的类。
-
扩展类加载器(Extension Class Loader):主要加载JAVA_HOME/jre/lib/ext目录中的类库。允许扩展Java中比较通用的类。在JDK9之后变成平台类加载器。
-
应用类加载器(Application Class Loader):主要用于加载用户类路径(ClassPath)上所有的类库,也就是加载开发者自己编写的类。
-
自定义类加载器:开发者自定义类继承ClassLoader类,实现自定义类加载规则。
面试官:说一下类装载的执行过程?
候选人:(源自《深入理解Java虚拟机》第三版 7.2节)
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。这七个阶段的发生顺序如图 7-1 所示。
1.加载:通过一个类的全限定名来获取定义此类的二进制字节流,加载完类之后,JVM将字节码信息保存到方法区中,生成一个InstanceClass对象
,保存类的所有信息,最后在堆区生成一个代表这个类的java.lang.Class
对象。
Tips:为什么要在堆区生成一个java.lang.Class对象,不直接使用方法区呢?
虽然类的信息主要保存在方法区,但是为了提供一个可访问的接口、支持反射操作、便于垃圾回收以及增强安全性,Java设计者选择在堆区生成一个
java.lang.Class
对象。这种方法使得类的信息更加易于管理和访问,同时也保持了方法区的纯粹性,使其专注于存储类的元数据。
2.验证:保证加载类的准确性。如验证字节流是否以魔数0xCAFEBABE
开头;主、次版本号是否在当前Java虚拟机接受范围之内等。
3.准备:为静态变量分配内存并设置类变量初始值。
关于准备阶段,还有两个容易产生混淆的概念需要着重强调,首先是这时候进内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象起分配在 Java 堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设个类变量的定义为:
public static int value = 123;
那变量 value在准备阶段过后的初始值为0而不是123,因为这时,尚未开始执行任何Java方法,而把 value 赋值为 123 的
putstatic
指令是程序被编译后,存放于类构造器<clinit>()
方法之中,所以把 value 赋值为 123 的动作要到类的初始化阶段才会被执行。
Tips:为什么要赋初始值?
如果不赋初始值,当类初始化时得到的就是系统残留的随机值,不友好。
4.解析:把类中的符号引用转换为直接引用。
补充:符号引用和直接引用
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
解析的目的在于让JVM能够正确地调用类的方法、访问类的字段等。只有当符号引用被解析成了直接引用,JVM才能知道在内存中定位到具体的地址,并执行相应的操作。
5.初始化:执行静态代码块,为静态变量赋值。(初始化阶段就是执行类构造器<clinit>()
方法的过程)
6.使用:JVM 开始从入口方法开始执行用户的程序代码。
7.卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。
面试官:什么是双亲委派模型?
候选人:
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器返回自己无法完成这个加载请求(它的搜索返回中没有找到所需的类)时,子类加载器才会尝试自己去加载。
双亲委派模型的实现:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {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.c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}}
这段代码的逻辑清晰易懂:先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的 loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,抛出 ClassNotFoundException 异常的话,才调用自己的 findClass()方法尝试进行加载。
面试官:JVM为什么采用双亲委派机制
候选人:
主要有两个原因。
第一、通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后子类无需重复加载,保证唯一性。
第二、为了安全,保证类库API不会被修改,保证安全性。
在工程中新建java.lang包,接着在该包下新建String类,并定义main函数
public class String {public static void main(String[] args) {System.out.println("demo info");} }
此时无法执行main函数,会出现异常,在类 java.lang.String 中找不到 main 方法。出现该信息是因为由双亲委派的机制,java.lang.String的在启动类加载器(Bootstrap classLoader)得到加载,因为在核心jre库中有其相同名字的类文件,但该类中并没有main方法。这样就能防止恶意篡改核心API库。
面试官:怎样破坏双亲委派机制?
候选人:
- 自定义类加载器并且重写loadClass方法,就可将双亲委派机制破坏。
- JNDI服务使用线程上下文类加载器去加载所需的SPI服务代码。这是一种父类加载器去请求子类加载器完成类加载的行为。
- Osgi框架的类加载器。
3 垃圾回收
前提知识:
并行与并发:
- 并行(Parallel):并行说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
- 并发(Concurrent):并发说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求。(有可能对象引用被修改)
内存溢出与内存泄漏:
- 内存泄漏(memory leak):在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。大多数都是由堆内存泄漏引起的。
- 少量的内存泄漏可以容忍,但是如果发生持续的内存泄漏,就像滚雪球雪球越滚越大,不管有多大的内存迟早会被消耗完,最终导致的结果就是内存溢出。但是产生内存溢出并不是只有内存泄漏这一种原因。
面试官:简述Java垃圾回收机制?(GC是什么?为什么要GC)
候选人:
为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC(Garbage Collection)。
有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。
引入垃圾回收机制的主要目的是为了减轻程序员的负担,提高程序的健壮性和效率,同时减少内存泄漏等问题。
面试官:强引用、软引用、弱引用、虚引用的区别?
候选人:(源自《深入理解Java虚拟机》第三版 3.2.3节)
在JDK1.2版之前,Java 里面的引用是很传统的定义:一个对象在这种定义下只有**“被引用”或者“未被引用”两种状态,对于描述一些“食之无味,弃之可惜”的对象就显得无能为力。譬如我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象——很多系统的缓存功能**都符合这样的应用场景。
在 JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(WeakReference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
1)强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似Object obj=new Object()
这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
2)软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2 版之后提供了SoftReference类
来实现软引用。下面举个例子说明:
首先限制其最大堆内存:-Xmx2M -Xms1M
import java.lang.ref.SoftReference;public class SoftReferenceExample {public static void main(String[] args) {VeryLargeObject object = new VeryLargeObject();SoftReference<VeryLargeObject> softRef = new SoftReference<>(object);object = null; // 移除强引用// 创建多个大对象来模拟内存压力createMemoryPressure();// 尝试让垃圾收集器工作System.gc();// 检查软引用是否仍然有效if (softRef.get() == null) {System.out.println("Soft reference was cleared.");} else {System.out.println("Soft reference is still valid.");}}private static class VeryLargeObject {private byte[] largeByteArray = new byte[1024 * 1024]; // 占用1MB内存}private static void createMemoryPressure() {for (int i = 0; i < 5000; i++) { // 创建更多大对象new VeryLargeObject();}}
}// 输出结果: Soft reference was cleared.
// 如果把堆大小设置很大时,就会输出:Soft reference is still valid.
3)弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够 ,都只会回收掉被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类
来实现弱引用。下面举个例子说明:
import java.lang.ref.WeakReference;public class WeakReferenceExample {public static void main(String[] args) {Object object = new VeryLargeObject();WeakReference<Object> weakRef = new WeakReference<>(object);object = null; // 移除强引用// 只要执行了 System.gc(); 那么都会输出 Weak reference was cleared.System.gc();if (weakRef.get() == null) {System.out.println("Weak reference was cleared.");} else {System.out.println("Weak reference is still valid.");}}private static class VeryLargeObject {private byte[] largeByteArray = new byte[1024 * 1024 * 10]; // 占用10MB内存}
}
4)虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。**为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。**虚引⽤必须和引⽤队列(ReferenceQueue)联合使⽤。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;public class PhantomReferenceExample {public static void main(String[] args) {ReferenceQueue<Object> queue = new ReferenceQueue<>();Object object = new Object();PhantomReference<Object> phantomRef = new PhantomReference<>(object, queue);object = null; // 移除强引用// 触发垃圾回收后,等待虚引用被加入队列,表明对象已被回收。System.gc();// 检查队列是否有新的引用对象while (queue.poll() == null) {// 等待直到垃圾收集完成}System.out.println("Phantom reference was enqueued in the queue.");}
}
面试官:如何判断对象是否存活?
候选人:(源自《深入理解Java虚拟机》第三版 3.2.1 3.2.2节)
一共可以有两种方法判定:
-
引用计数法
:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。客观地说,引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。JVM却没有选用引用计数法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
举个简单的例子,对象 objA 和 objB 都有字段instance,赋值令 objA.instance =objB 及 objB.instance =objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
-
可达性分析算法
:这个算法的基本思路就是通过一系列称为**“GCRoots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain)**,如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的,它们将会被判定为可回收的对象。
面试官:如何判断对象是否真正死亡?fnalize()与System.gc()
候选人:(源自《深入理解Java虚拟机》第三版 3.2.4节)
要真正宣告一个对象死亡,最多会经历两次标记过程:
- 如果对象在进行达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行
fnalize()
方法。假如对象没有覆盖finalize()
方法,或者finalize()
方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。 - 如果这个对象被判定为确有必要执行
finalize()
方法,那么该对象将会被放置在一个名为F-Queue
的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer
线程去执行它们的finalize()
方法。finalize()
方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue
中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
一次对象自我拯救的演示:
public class FinalizeEscapeGC {public static FinalizeEscapeGC SAVE_INSTANCE = null;public void isAlive(){System.out.println("I am still alive");}@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("finalize method executed");FinalizeEscapeGC.SAVE_INSTANCE = this;}public static void main(String[] args) throws InterruptedException {SAVE_INSTANCE = new FinalizeEscapeGC();// 对象第一次成功拯救自己SAVE_INSTANCE = null;System.gc();// 因为Finalizer方法优先级很低,暂停1秒,以等待它Thread.sleep(1000);if(SAVE_INSTANCE != null){SAVE_INSTANCE.isAlive();}else{System.out.println("no, i am dead :(");}// 下面这段代码与上面完全相同,但是这次自救却失败了SAVE_INSTANCE = null;System.gc();// 因为Finalizer方法优先级很低,暂停1秒,以等待它Thread.sleep(1000);if(SAVE_INSTANCE != null){SAVE_INSTANCE.isAlive();}else{System.out.println("no, i am dead :(");}} }
运行结果:
finalize method executed I am still alive no, i am dead :(
值得一提的是,代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败了。这是因为任何一个对象的 finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。
面试官: (回收方法区)如何判断⼀个常量是废弃常量?如何判断⼀个类是⽆⽤的类?
候选人:(源自《深入理解Java虚拟机》第三版 3.2.4节)
1)方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类。
2)回收废弃常量与回收 Java堆中的对象非常类似。举个常量池中字面量回收的例子,假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。
3)判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
-
该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
-
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
-
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
Java 虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了一些参数进行控制。
面试官: JVM 垃圾回收算法有哪些?
候选人:(源自《深入理解Java虚拟机》第三版 3.3节)
我记得一共有四种,分别是标记—清除算法(最早出现)、标记—复制算法(第二代)、标记—整理算法(第三代)、分代回收(最新出现)。
-
标记—清除算法(最早出现)
1)如它的名字一样,算法分为**“标记”和“清除”**两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收掉所有未被标记的对象。
2)它的主要缺点有两个:第一个是执行效率不稳定,如果 Java 堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长面降低;第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
-
标记—复制算法(第二代)
1)为了解决标记—清除算法面对大量可回收对象时执行效率低的问题。1969年Fenichel提出了一种称为“半区复制”的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
2)这样实现简单,运行高效,不会发生碎片化,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内在缩小为了原来的一半,空问浪费未免太多了一点。
-
标记—整理算法(第三代)
1)1974年 Edward Lueders 提出了另外一种有针对性的 “标记-整理”(Mark-Compact) 算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。
2)优点:内存使用效率高,不会发生碎片化。缺点:整体阶段的效率不高。
面试官: 你能详细聊一下分代回收吗?
候选人:
关于分代回收是这样的:
在java8时,堆被分为了两份:新生代和老年代,它们默认空间占用比例是1:2
对于新生代,内部又被分为了三个区域。Eden区,From Survivor区,To Survivor区。默认空间占用比例是8:1:1
具体的工作机制:
1)当创建一个对象的时候,那么这个对象会被分配在新生代的Eden区。当Eden区要满了时候,触发YoungGC。
2)当进行YoungGC后,此时在Eden区存活的对象被移动到From区,并且当前对象的年龄会加1,清空Eden区。
3)当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和From区中的对象,移动到To区中,这些对象的年龄会加1,清空Eden区和From区。
4)当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和To中的对象,移动到From区中,这些对象的年龄会加1,清空Eden区和To区。
5)对象的年龄达到了某一个限定的值(默认15岁),那么这个对象就会进入到老年代中。
当然也有特殊情况,如果进入Eden区的是一个大对象,在触发YoungGC的时候,会直接存放到老年代。
当老年代满了之后,触发FullGC。FullGC同时回收新生代和老年代,当前只会存在一个FullGC的线程进行执行,其他的线程全部会被挂起。 我们在程序中要尽量避免FullGC的出现。
补充:那些比例为什么要这么划分呢?
- 大部分对象都是短暂存在的,而少数长期存活的对象则需要更多的空间。通过合理分配空间给新生代和老年代,可以最大化垃圾回收的效率并最小化对应用程序性能的影响。(1:2)
- 这种比例设计是为了优化新生代内的对象管理和垃圾回收,使系统能够在处理大量短寿命对象的同时,也能有效地管理那些存活时间较长的对象。通过这种方式,可以提高整体的内存使用效率和垃圾回收效率。(8:1:1)
面试官:讲一下新生代、老年代、永久代的区别?
候选人:
新生代主要用来存放新生的对象。
老年代主要存放应用中生命周期长的内存对象。
永久代指的是永久保存区域。主要存放Class和Meta(元数据)的信息。在Java8中,永久代已经被移除,取而代之的是一个称之为“元数据区”(元空间)的区域。元空间和永久代类似,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存的限制。
面试官:说一下 JVM 有哪些垃圾回收器?
候选人:
在JVM中,实现了多种垃圾收集器,包括:串行垃圾收集器(Serial)、并行垃圾收集器(Parallel,JDK8默认)、CMS垃圾收集器、G1垃圾收集器(JDK9默认)、ZGC。衡量垃圾收集器的三项最重要的指标是:内存占用、吞吐量和延迟,三者共同构成了一个“不可能三角”。一款优秀的收集器通常最多可以同时达成其中的两项。
垃圾收集器 | 分类 | 作用位置 | 使用算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行运行 | 作用于新生代 | 复制算法 | 响应速度优先 | 适用于单CPU环境下的client模式 |
ParNew | 并行运行 | 作用于新生代 | 复制算法 | 响应速度优先 | 多CPU环境Server模式下与CMS配合使 |
Parallel | 并行运行 | 作用于新生代 | 复制算法 | 吞吐量优先 | 适用于后台运算而不需太多交互的场 |
Serial Old | 串行运行 | 作用于老年代 | 标记-整理算法 | 响应速度优先 | 适用于单CPU环境下的Client模式 |
Parallel Old | 并行运行 | 作用于老年代 | 标记-整理算法 | 吞吐量优先 | 适用于后台运算而不需太多交互的场 |
CMS | 并发运行 | 作用于老年代 | 标记-清除算法 | 低延迟 | 适用于互联网或B/S业务 |
G1 | 并发、并行 | 作用于新生代、老年代 | 标记-整理算法、复制 | 吞吐量优先 | 面向服务端应用 |
ZGC | 并发、并行 | 全堆 | 标记-整理算法 | 极低延迟、高吞吐量 | 适用于需要极高吞吐量和低延迟的 |
GC发展阶段: Serial => Parallel(并行)=> CMS(并发)=> G1 => ZGC
面试官: 你能解释一下三色标记、增量更新算法和原始快照(STAB)算法吗?
候选人:
三色标记是一种用于垃圾收集过程中对象标记的技术。它将对象分为三种颜色:白色表示尚未被访问的对象,灰色表示部分访问的对象,黑色表示完全访问的对象。通过这种标记方法,垃圾收集器可以从根节点开始逐步标记对象,并最终清除白色对象。
如果用户线程此时是冻结的,只有收集器线程在工作,那不会有任何问题。但如果用户线程与收集器是并发工作的,收集器在对象图上标记颜色,同时用户线程在修改引用关系,这样就会可能出现两种后果:一种是把原本消亡的对象错误标记为存活,这不是好事,但其实是可以容忍的,只不过产生了点逃过本次收集的浮动垃圾而已,下次收集清理掉就好。另一种是把原本存活的对象错误标记为已消亡,这就是非常致命的后果了。当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本是黑色的对象被误标为白色:
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
因此,我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(STAB)。
增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
面试官:说说 CMS 垃圾回收器?
候选人:(源自《深入理解Java虚拟机》第三版 3.5.6节)
1)CMS(Concurent Mark sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。从名字(包含“Mark sweep”)上就可以看出 CMS 收集器是基于标记-清除算法。现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
2)其中初始标记、重新标记这两个步骤仍然需要“Stop The World”(STW,停顿)。初始标记
仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快;并发标记
阶段就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记
阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段通过写屏障记录在并发标记期间所有修改对象引用的操作,CMS基于增量更新来做并发标记;最后是并发清除
阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
3)CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集、低停顿,一些官方公开文档里面也称之为“并发低停顿收集器”(Concurrent Low Pause Collector)。但至少也有以下三个明显的缺点:
- CMS 收集器无法处理“浮动垃圾”(Floating Garbage)。在 CMS 的
并发标记
和并发清理
阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。 - 退化问题:要是CMS 运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”,这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用 SerialOld收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
- CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
面试官:说说 G1(Garbage First) 垃圾回收器?
候选人:(源自《深入理解Java虚拟机》第三版 3.5.7节)
1)G1是一款主要面向服务端应用的垃圾收集器。JDK9 发布之日,G1宣告取代 Parallel Scavenge + Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用的收集器。
2)在 G1 收集器出现之前的所有其他收集器,包括 CMS在内,垃圾收集的目标范围要么是整个新生代(MinorGC),要么就是整个老年代(Major GC),再要么就是整个 Java 堆(Full GC)。而 G1 跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称 CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是 G1收集器的 Mixed GC 模式。
3)作为CMS收集器的替代者和继承人,设计者们希望做出一款能够建立起**“停顿预测模型”** 的收集器。
停顿预测模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标,这几乎已经是实时 Java(RTSJ)的中软实时垃圾收集器特征了。
G1收集器之所以能建立可预测的停顿时间模型,是因为G1收集器通过将Java堆分成多个大小相等的独立区域(Region),让 G1 收集器去跟踪各个 Region里面的垃圾堆积的“价值”大小,然后后台维护一个优先级列表,并根据用户设定的最大停顿时间,优先回收最有价值的Region,从而实现可控的停顿时间和高效垃圾回收。
4)G1 收集器的运作过程大致可划分为以下四个步骤:
-
初始标记(Initial Marking):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改
TAMS指针
的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。这个阶段需要停顿线程,但耗时很短。G1为每一个 Region 设计了两个名为
TAMS(Top at Mark Start)的指针
,把 Region 中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。 -
并发标记(Concurrent Marking):从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序
并发
执行。当对象图扫描完成以后,还要重新处理SATB记录
下的在并发时有引用变动的对象。(这里并不是全堆作为GC Roots扫描,而是使用记忆集用于记录哪些Region可能包含指向其他Region的对象。这样,在并发标记阶段,只需要扫描这些可能包含跨Region引用的Region即可,而不需要扫描整个堆。) -
最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的
SATB 记录
。这个阶段通过写屏障记录在并发标记期间所有修改对象引用的操作,G1 基于STAB记录来做并发标记。 -
筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部Region 的存活对象制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程
并行
完成的。
从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。
5)优点:允许用户指定最大停顿时间目标(使用-XX:MaxGCPauseMillis),减少垃圾回收过程中应用程序的暂停时间;支持并发并行;可以预测垃圾收集的停顿时间;G1通过将堆划分为多个大小固定的Region,并且可以灵活地选择哪些Region进行回收,从而提高了内存利用率。
缺点:相对于其他垃圾收集器,G1的设计更加复杂,这使得其理解和调优变得更加困难。为了支持Region划分及相应的管理信息,G1可能会消耗额外的内存资源来存储这些元数据。
G1 收集器关键细节问题解决:
1)将Java堆分成多个独立Region后,Region里面存在的跨Region引用对象如何解决?
解决思路:使用记忆集避免全堆作为 GC Roots 扫描,但在 G1 收集器上记忆集的应用其实要复杂很多,它的每个Region 都维护有自己的记忆集,这些记忆集会记录下别的 Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是种哈希表,Key是别的 Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构 (卡表是“我指向谁”、这种结构还记录了“谁指向我”) 比原来的卡表实现起来更复杂,同时由于 Region数量比传统收集器的分代数量明显要多得多,因此 G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。
2)在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
解决办法:CMS 收集器采用增量更新算法实现,而 G1收集器则是通过原始快照(SATB)算法来实现的。
面试官:说说 G1 和 CMS 垃圾回收器的区别?(记忆集、卡表、写屏障)
候选人:(源自《深入理解Java虚拟机》第三版 3.5.7节)
- 相比 CMS,G1的优点有很多,暂且不论可以指定最大停顿时间、分 Region 的内存布局、按收益动态确定回收集这些创新性设计带来的红利,单从最传统的算法理论上看,G1也更有发展潜力。与CMS 的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个 Region 之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着 G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。当然比起CMS,G1的弱项也可以列举出不少,如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比 CMS 要高。
- 就内存占用来说,虽然 G1 和 CMS 都使用卡表来处理跨代指针,但 G1 的卡表实现更为复杂,而且堆中每个 Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致 G1 的记忆集(和其他内存消耗)可能会占更多的内存空间;相比起来 CMS 的卡表就相当简单,只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的。
- 在执行负载的角度上,它们都使用到写屏障。CMS用写后屏障来更新维护卡表;而 G1 除了使用写后屏障来进行同样的(由于 G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。
拓展:记忆集与卡表、写屏障
记忆集:记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
卡表:卡表就是记忆集的一种具体体现,它定义了记忆集的记录精度、与堆内存的映射关系等。
写屏障:在HotSpot虚拟机里是通过写屏障技术维护卡表状态的。写屏障可以看作是在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形通知。在赋值前的部分的写屏障叫做写前屏障,在赋值后的则叫作写后屏障。
void oop_field_store(oop* field, oop new_val) {// 引用字段赋值操作*field = new_val;// 写后屏障,在这里完成卡表状态更新post_write_barrier(field, new_val); }
面试官:说说你对ZGC(Z Garbage Collector) 收集器的理解?
候选人:(源自《深入理解Java虚拟机》第三版 3.6.2节)
1)ZGC收集器是一款基于 Region 内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
染色指针是一种直接将少量额外的信息存储在指针上的技术,ZGC的染色指针是最直接的、最纯粹的,他直接把标记信息记在引用对象的指针上。
染色指针可以使得一旦某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用掉,而不必等待整个堆中所有指向该 Region 的引用都被修正后才能清理。
染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。
染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。
2)ZGC的运作过程大致可划分为以下四个大的阶段。全部四个阶段都是可以并发执行的,仅是两个阶段中间会存在短暂的停顿小阶段。ZGC几乎在所有地方都并发执行的,除了初始标记是STW的。
-
并发标记(Concurrent Mark):与G1 一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于 G1的初始标记、最终标记(尽管 ZGC 中的名字不叫这些)的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与 G1不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的标志位。
-
并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。(统计需要清理的Region,并组成重分配集)
重分配集与 G1 收集器的回收集(Collection Set)还是有区别的。ZGC 划分 Region的目的并非为了像 G1那样做收益优先的增量回收。相反,ZGC每次回收都会扫描所有的 Region,用范围更大的扫描成本换取省去 G1中记忆集的维护成本。
-
并发重分配(Concurrent Relocate):重分配是 ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region上,并为重分配集中的每个 Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC 收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,一旦重分配集中某个 Region 的存活对象都复制完毕后,这个 Region就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉)。(对象复制到新的Region上,并维护一个转发表,复制完毕后,旧Region立即释放)
假设有两个Region(A 和 B),它们属于重分配集,需要被清理。每个Region包含若干对象,我们需要将存活的对象复制到新的位置,并更新指向这些对象的所有引用。
// 确定 Region A 和 Region B 为重分配集。 Region A: [Obj1, Obj2, Obj3] Region B: [Obj4, Obj5] // 其他对象的引用 Obj6 -> Obj1 Obj7 -> Obj4
// Obj1 和 Obj2 是存活对象,复制到新的 Region。 // Obj4 是存活对象,复制到新的 Region。 // Obj3 和 Obj5 不再存活,将被丢弃。 Region A (New): [Obj1', Obj2'] Region B (New): [Obj4']
创建转发表记录旧对象到新对象的映射关系。
Forward Table: Obj1 -> Obj1' Obj2 -> Obj2' Obj4 -> Obj4'
旧的 Region A 和 Region B 可以被标记为可用,但转发表暂时保留。
Region A (Old): [Obj1, Obj2, Obj3] (已标记为可用) Region B (Old): [Obj4, Obj5] (已标记为可用)
-
并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,使它们指向新位置的对象。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。(修正旧对象引用,与并发标记阶段合并,释放转发表)
更新所有指向旧对象的引用。一旦所有引用被修正,转发表可以被释放。
Obj6 -> Obj1' (更新前:Obj6 -> Obj1) Obj7 -> Obj4' (更新前:Obj7 -> Obj4)
3)ZGC完全没有使用记忆集(ZGC每次垃圾回收都会扫描整个堆),它甚至连分代都没有,连像 CMS 中那样只记录新生代和老年代间引用的卡表也不需要,因而完全没有用到写屏障**(通过染色指针替代),所以给用户线程带来的运行负担也要小得多。可是,必定要有优有劣才会称作权衡,ZGC的这种选择也限制了它能承受的对象分配速率不会太高**。当应用程序频繁地创建和销毁大量短期生存的对象时,ZGC需要频繁地进行对象的复制和引用更新操作。这会导致更高的CPU负载,因为需要不断更新染色指针,并且在重分配阶段进行大量的对象复制操作。虽然ZGC的目标是降低暂停时间,但如果对象分配速率非常高,那么即使是短暂的暂停也会变得频繁,从而影响整体性能。
拓展:恐怖的测试结果
- 在ZGC的“弱项”吞吐量方面,以低延迟为首要目标的ZGC已经达到了以高吞吐量为目标的Parallel Scavenge的99%,直接超越了G1。(见图1,源自《深入理解Java虚拟机》第三版 P120)
- 而在ZGC的“强项”停顿时间测试上,它毫不留情地与Parallel Scavenge、G1拉开了两个数量级的差距。(见图2,源自《深入理解Java虚拟机》第三版 P120)
面试官:Minor GC、Major GC、Mixed GC、Full GC是什么?
候选人:(源自《深入理解Java虚拟机》第三版 3.3.1节)
- 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS 收集器会有单独收集老年代的行为。
- 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1收集器会有这种行为。
- 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集(尽量避免)。
4 JVM实践(调优)
面试官:JVM 调优的参数可以在哪里设置参数值?
候选人:
我们当时的项目是springboot项目,可以在项目启动的时候,java -jar中加入参数就行了。
java -jar -Xms256m -Xmx512m -XX:+UseConcMarkSweepGC your-app.jar
java -jar -Xms256m -Xmx512m -XX:+UseG1GC your-app.jar
这里的 -Xms256m
和 -Xmx512m
分别设置了初始堆大小和最大堆大小,-XX:+UseConcMarkSweepGC
设置了使用的垃圾收集器为CMS。-XX:+UseG1GC
设置了使用的垃圾收集器为G1。
面试官:用的 JVM 调优的参数都有哪些?
候选人:
对于JVM调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型。
堆内存管理参数
- 初始堆内存大小 (
-Xms<size>
)。示例:-Xms256m
表示初始堆内存为256MB。 - 最大堆内存大小 (
-Xmx<size>
)。示例:-Xmx512m
表示最大堆内存为512MB
一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutofMemoryError通常会将 -Xms 和 -Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
年轻代管理参数
- 年轻代大小 (
-Xmn<size>
)。示例:-Xmn128m
表示年轻代大小为128MB。 - Eden区与Survivor区的比例 (
-XX:SurvivorRatio=<value>
)。示例:-XX:SurvivorRatio=8
表示Eden区与两个Survivor区的比例为8:1:1。
垃圾收集器选择参数
- 使用串行垃圾收集器 (
-XX:+UseSerialGC
) - 使用并行垃圾收集器 (
-XX:+UseParallelGC
) - 使用CMS垃圾收集器 (
-XX:+UseConcMarkSweepGC
) - 使用G1垃圾收集器 (
-XX:+UseG1GC
)
垃圾收集器优化参数
- 最大暂停时间 (
-XX:MaxGCPauseMillis=<value>
) - 并行垃圾收集线程数 (
-XX:ParallelGCThreads=<value>
) - 并发标记线程数 (
-XX:ConcGCThreads=<value>
)
日志和调试参数
- 开启GC日志 (
-Xloggc:./path/gc.log
) - 详细GC信息 (
-XX:+PrintGCDetails
) - GC时间戳 (
-XX:+PrintGCTimeStamps
) - 在内存溢出时导出堆转储 (
-XX:+HeapDumpOnOutOfMemoryError
) - 堆转储文件路径 (
-XX:HeapDumpPath=<path>
)
以下是一个包含常用参数的示例命令:
java -Xms256m -Xmx512m -Xmn128m -XX:SurvivorRatio=8 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=4 -XX:ConcGCThreads=2 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -jar your-app.jar
也可以直接在IDEA中设置VM参数:
面试官:平时调试 JVM都用了哪些工具呢?
候选人:
嗯,我们一般都是使用jdk自带的一些工具,比如:
-
jps 输出JVM中运行的进程状态信息。
-
jstack 查看java进程内线程的堆栈信息。
-
jmap 用于生成堆转存快照(dump文件)。
-
jstat 用于JVM统计监测工具。
-
jinfo 实时查看和修改JVM参数(有时我们可以查看默认JVM参数设置)。
**代码案例:**首先,我们创建一个简单的Java应用程序,用于演示如何使用这些工具。
// 这个简单的应用程序启动了一个无限循环的任务,每隔一秒输出一条消息。 public class SimpleApp {public static void main(String[] args) {System.out.println("SimpleApp started.");Runnable task = () -> {while (true) {try {Thread.sleep(1000);System.out.println("Running task...");} catch (InterruptedException e) {e.printStackTrace();}}};Thread thread = new Thread(task);thread.start();} }
先启动程序,然后使用
jps
查看正在运行中的Java进程:jps
输出可能类似于:
16304 691 Bootstrap 36648 Launcher 36649 SimpleApp 36654 Jps
这里
36649
是进程ID。
jstack
用来查看Java进程中各个线程的堆栈信息。jstack <36649> thread_dump.txt
输出可能类似于:
"main" #1 prio=5 os_prio=0 tid=0x00007f8d5c000800 nid=0x1b00 waiting on condition [0x00007f8d5c200000] java.lang.Thread.State: WAITING (parking)at sun.misc.Unsafe.park(Native Method)- parking to wait for <0x0000000725b45508> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)at java.lang.Thread.run(Thread.java:748)
jmap
用于生成堆转存快照(heap dump),可以用来分析内存使用情况(我们一般直接在参数中配置好了)。jmap -dump:format=b,file=dump.bin 36649
jstat
用来监控JVM的运行时统计数据,如垃圾收集情况、内存使用情况等。jstat -gc 36649
输出可能类似于:
S0C S1C S0U S1U EC EU OC OU MC MU CCS Count CCS Time0.00 0.00 0.00 0.00 3072.00 534.00 104448.00 33920.00 0.00 0.00 16 0.00
还有一些可视化工具,像Jconsole、VisualVM和JProfiler等。
面试官:假如项目中产生了Java内存泄露(OOM),你说一下你的排查思路?
候选人:
详情见:https://blog.csdn.net/weixin_74199893/article/details/142657909?spm=1001.2014.3001.5501
结合模拟场景案例进行分析…
面试官:你的GC调优经验?
候选人:
B站直达:尚硅谷JVM精讲与GC调优教程 P60-P72
CSDN直达:大师级GC调优:剖析高并发系统的垃圾回收优化实战
调优案例一:调整堆大小提升服务的吞吐量
调优案例二:逃逸分析之栈上分配、标量替换、锁清除
调优案例二:合理配置堆内存
调优案例三:CPU占用很高的排查方案
调优案例四:G1并发GC线程数对性能的影响
调优案例五:调整垃圾回收器对吞吐量的影响
调优案例六:日均百万订单如何设置JVM参数
相关文章:
面试中的JVM:结合经典书籍的深度解读
写在前面 🔥我把后端Java面试题做了一个汇总,有兴趣大家可以看看!这里👉 ⭐️在无数次的复习巩固中,我逐渐意识到一个问题:面对同样的面试题目,不同的资料来源往往给出了五花八门的解释&#…...
使用语音模块的开发智能家居产品(使用雷龙LSYT201B 语音模块)
在这篇博客中,我们将探讨如何使用 LSYT201B 语音模块 进行智能设备的语音交互开发。通过这个模块,我们可以实现智能设备的语音识别和控制功能,为用户带来更为便捷和现代的交互体验。 1. 语音模块介绍 LSYT201B 是一个基于“芯片算法”的语音…...
深入理解支持向量机:从基本原理到实际应用
第6章 支持向量机 在本章中,我们将深入探讨支持向量机(SVM)这一强大的分类算法。SVM在模式识别和机器学习领域广泛应用,尤其在处理高维数据时表现出色。我们将依次讨论间隔与支持向量、对偶问题、核函数、间隔与正则化、支持向量…...
每天一题:洛谷P2041分裂游戏
题目描述 有一个无限大的棋盘,棋盘左下角有一个大小为 n 的阶梯形区域,其中最左下角的那个格子里有一枚棋子。你每次可以把一枚棋子“分裂”成两枚棋子,分别放在原位置的上边一格和右边一格。(但如果目标位置已有棋子,…...
简单的 curl HTTP的POSTGET请求以及ip port连通性测试
简单的 curl HTTP的POST&GET请求以及ip port连通性测试 1. 需求 我们公司有一个演示项目,需要到客户那边进行项目部署,项目部署完成后我们需要进行项目后端接口的测试功能,但是由于客户那边么有条件安装类似于postman这种的测试工具&am…...
ubuntu下快捷键启动程序
背景:公司自开发的软件,经常需要启动,每次去找目录启动很麻烦,所以想快捷启动 方法1: 通过编辑.baserc启动 例如启动程序是toolA, 放在/home/user/software/目录下,那么在~/.baserc里面加入一行代码 al…...
Yii2 init 初始化脚本分析
脚本目的: init 脚本主要的作用是:从 environments 目录中复制配置文件,确保应用适配不同环境(例如开发、生产环境等)。 工作流程: 获取 $_SERVER 的 argv 参数 加载 environments/index.php 文件&#…...
深入理解gPTP时间同步过程
泛化精确时间协议(gPTP)是一个用于实现精确时间同步的协议,特别适用于分布式系统中需要高度协调的操作,比如汽车电子、工业自动化等。 gPTP通过同步主节点(Time Master)和从节点(Time Slave)的时钟,实现全局一致的时间参考。 以下是gPTP实现主从时间同步的详细过程:…...
基于阿里云服务的移动应用日志管理方案—日志的上传、下载、存档等
前言 如题,基于阿里云服务(ECS、OSS)实现 APP 的用户日志上传以及日志下载的功能,提高用户反馈问题到研发去分析、定位、解决问题的整个工作流的效率。 术语 ECS: 云服务器ECS(Elastic Compute Service)…...
Python浪漫之画星星
效果图(动态的哦!): 完整代码(上教程): import turtle import random import time # 导入time模块# 创建一个画布 screen turtle.Screen() screen.bgcolor("red")# 创建一个海龟&a…...
Android使用协程实现自定义Toast弹框
Android使用协程实现自定义Toast弹框 最近有个消息提示需要显示10s,刚开始使用协程写了一个shoowToast方法,传入消息内容、显示时间和toast显示类型即可,以为能满足需求,结果测试说只有5s,查看日志和源码发现Android系统中Toa…...
git diff命令详解
git diff 是 Git 中非常常用的命令,用于比较不同版本的文件改动。可以比较工作区、暂存区、或者提交之间的差异。下面是对 git diff 常用场景的详细解释: 1. git diff 当你执行 git diff 时,它会显示工作区与暂存区之间的差异,也…...
Vue 插槽:组件通信的“隐形通道”
在 Vue 中,插槽(slot)是实现组件内容分发的机制,允许我们将子组件的内容传递给父组件,从而提升组件的可复用性和灵活性。插槽的本质是通过将父组件内容传递到子组件指定的插槽位置,使得子组件在渲染时可以动…...
react1816中的setState同步还是异步的深层分析
setState 是 react 中更新 UI 的唯一方法,其内部实现原理如下: 调用 setState 函数时,React 将传入的参数对象加入到组件的更新队列中。React 会调度一次更新(reconciliation),在调度过程中,Re…...
【UE5】将2D切片图渲染为体积纹理,最终实现使用RT实时绘制体积纹理【第七篇-体积纹理绘制】
我们前几篇已经完成了渲染部分,现在终于开始做动态绘制功能了 之前使用的是这样一个体积雾的切片图,那么现在要做的就是动态编辑它 首先,让我们简单了解一下它是如何运作的: 开始绘制画布以渲染目标,并将材质绘制到画…...
Linux的环境搭建
目录 1、linux的简单介绍 2、搭建linux环境 2.1 linux的环境安装 2.2 使用Xshell远程登入linux 2.2.1 Xshell免密登入 2.3 windows与Xshell与linux云服务器的关系 1、linux的简单介绍 linux操作系统 为 部分汇编 C语言编写 的操作系统 源代码公开(开源),官…...
WPF+Mvvm案例实战(五)- 自定义雷达图实现
文章目录 1、项目准备1、创建文件2、用户控件库 2、功能实现1、用户控件库1、控件样式实现2、数据模型实现 2、应用程序代码实现1.UI层代码实现2、数据后台代码实现3、主界面菜单添加1、后台按钮方法改造:2、按钮添加:3、依赖注入 3、运行效果4、源代码获…...
网络爬虫-Python网络爬虫和C#网络爬虫
爬虫是一种从互联网抓取数据信息的自动化程序,通过 HTTP 协议向网站发送请求,获取网页内容,并通过分析网页内容来抓取和存储网页数据。爬虫可以在抓取过程中进行各种异常处理、错误重试等操作,确保爬取持续高效地运行 1、Python网…...
如何有效解除TikTok账号间的IP关联
在当今社交媒体环境中,TikTok凭借其独特的短视频形式吸引了数以亿计的用户。对许多内容创作者而言,运营多个账号是获取更大曝光和丰富内容的有效策略。然而,如何避免这些账号之间的IP关联,以防止被平台识别并封禁,成为…...
Python自省机制
Python 自省机制 Python 自省(Introspection)是一种动态检查对象的能力,使得开发者可以在运行时获取对象的相关信息,比如属性、方法、类型等。自省机制让 Python 具备了更强的动态性和灵活性,便于调试和开发。 自省&…...
wgan-gp 对连续变量 训练,6万条数据,训练结果不错,但是到局部的时候,拟合不好,是否可以对局部数据也进行计算呢
Wasserstein GAN with Gradient Penalty (WGAN-GP) 是一种改进的生成对抗网络(GAN),它通过引入梯度惩罚来改进训练过程,从而提高生成模型的稳定性和质量。如果你在使用WGAN-GP对连续变量进行训练时,发现整体训练结果不…...
python 制作 发货单 (生成 html, pdf)
起因, 目的: 某个小店,想做个发货单。 过程: 先写一个 html 模板。准备数据, 一般是从数据库读取,也可以是 json 格式,或是 python 字典。总之,是数据内容。使用 jinja2 来渲染模板。最终的结果可以是 h…...
GeoWebCache1.26调用ArcGIS切片
常用网址: GeoServer GeoWebCache (osgeo.org) GeoServer 用户手册 — GeoServer 2.20.x 用户手册 一、版本需要适配:Geoserver与GeoWebCache、jdk等的版本适配对照 查看来源 二、准备工作 1、数据:Arcgis标准的切片,通过…...
深度学习-卷积神经网络-基于VGG16模型, 实现猫狗二分类(文末附带数据集下载链接, 长期有效)
简介: 1.基于VGG16模型进行特征提取, 结合mlp实现猫狗二分类 2.训练数据--"dog_cat_class\training_set" 3.模型训练流程 1.对图像数据进行导入和预处理 2.搭建模型, 导入VGG16模型, 去除mlp层, 将经过VGG16训练后的数据作为输入, 输入到自建的mlp层中进行训练, 要…...
计算Java集合占用的空间【详解】
以ArrayList为例,假设集合元素类型是Person类型,假设集合容量为10,目前有两个person对象{name:“Jack”,age12} {name:“Tom”,age14} public class Person{private String name;private int age; }估算Person对象占用的大小: 对…...
仕考网:关于中级经济师考试的介绍
中级经济师考试是一种职称考试,每年举办一次,报名时间在7-8月,考试时间在10-11月 报名入口:中guo人事考试网 报名条件: 1.高中毕业并取得初级经济专业技术资格,从事相关专业工作满10年; 2.具备大学专科…...
SYN590RL 300MHz至450MHz ASK接收机芯片IC
一般描述 SYN590RL是赛诺克全新开发设计的一款宽电压范围,低功耗,高性能,无需外置AGC电容,灵敏度达到典型-110dBm,300MHz”450MHz 频率范围应用的单芯片ASK或OOK射频接收器。 SYN59ORL是一款典型的即插即用型单片高集成度无线接收器&…...
15分钟学 Go 第 20 天:Go的错误处理
第20天:Go的错误处理 目标 学习如何处理错误,以确保Go程序的健壮性和可维护性。 1. 错误处理的重要性 在开发中,错误处理至关重要。程序在运行时可能会出现各种问题,例如文件未找到、网络连接失败等。正确的错误处理能帮助我们…...
C++——string的模拟实现(上)
目录 引言 成员变量 1.基本框架 成员函数 1.构造函数和析构函数 2.拷贝构造函数 3.容量操作函数 3.1 有效长度和容量大小 3.2 容量操作 3.3 访问操作 (1)operator[]函数 (2)iterator迭代器 3.4 修改操作 (1)push_back()和append() (2)operator函数 引言 在 C—…...
JavaCV 之均值滤波:图像降噪与模糊的权衡之道
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,…...
石家庄商城网站建设/湖北疫情最新消息
金木水火土命查询表五行相生的顺序是:水生木,木生火,火生土,土生金,金生水; 五行相克的顺序是:金克木,木克土,土克水,水克火,火克金.金木水火土命查询表(2009-03-30)土命 金命 水命 查询表 金木 甲子年生海中金命(1924,1984) 乙丑年生海中金…...
上海青浦区网站建设公司/谷歌下载
random库是使用随机数的python标准库。 伪随机数:采用梅森旋转算法生产的伪随机数列中元素 random库主要用于生成随机数基本随机数函数 随机数种子相同的种子生成的随机数是相同的,可以复现结果。 扩展随机数函数例 圆周率的计算 蒙特卡洛方法from rando…...
网站做好了怎么做后台/独立站
最近在整理之前工作的文件,发现大概有50个小时的专家call & 会议录音啥的,于是就研究了一下如何批量把长语音转成格式优美的文字文档。 当然做事情之前先来知乎搜了搜有没有现成的解决方案可用,于是发现了这个问题,但一楼说的…...
玉溪网站制作/上海百度公司地址
docker搭建npm仓库(verdaccio) 文章目录docker搭建npm仓库(verdaccio)拉去镜像设置存储目录创建目录结构配置文件内容运行拉去镜像 docker pull verdaccio/verdaccio 设置存储目录 mkdir -p ~/data/verdaccio/volume 创建目录…...
贵阳网站建设蜜蜂/torrentkitty磁力猫引擎
https://www.cnblogs.com/liyasong/p/saoma.html...
建设德育网站的意义/广告推广系统
转载于:https://blog.51cto.com/ceshi/167991...