当前位置:   article > 正文

JVM高频基本面试问题整理_jvm面试题

jvm面试题

目录

1.分析JVM运行时数据区域(必考)

2.简单介绍一下Java内存模型(必考)

3.简述内存分配与回收策略

4.垃圾回收机制(必考)

5.强、软、弱、虚引用的区分?

6.Java 堆永久代的回收(该部分归属java7,大概率不会问了)

7.垃圾回收算法(必考)

8.Minor GC和Full GC触发条件

9.Minor GC 和 Full GC 有什么不一样吗?

10.简述GC中Stop the world(STW),并说出安全点

11.各垃圾回收器的特点及区别,怎么做选择?

13.G1和CMS的比较

14.CMS垃圾回收器存在的问题及解决方案(必考)

15.讲讲CMS垃圾回收器?讲讲G1垃圾回收器?(必考)

16.简述ZGC垃圾回收器(必考)

17.双亲委派模型

18.谈谈双亲委派模型的"破坏"

19.JDBC和双亲委派模型关系分析

20.JVM锁优化和锁膨胀过程(必考)

21.JVM中GC Root的选择标准是什么?相关JVM的调优参数有哪些?在工作中怎么调优的?

22.JVM性能监控有哪些?

23.一个4核8G的服务器,初步设置JVM参数,如何分配?(必考)

24.ClassLoader如何工作的?

25.对象的创建过程是什么样的?

26.Java管理内存、内存泄漏和泄漏的原因?(必考)

27.OOM说一下?怎么排查?哪些会导致OOM?(必考)

28.JVM虚拟机老年代什么情况下会发生gc,给你一个场景,一台4核8G的服务器,每隔两个小时就要出现一次老年代gc,现在有日志,怎么分析是哪里出了问题?

29.判定一个对象不可达要回收,举个实际例子来说明。有没有从代码层面去直接分析一下?

参考书籍、文献和资料


 干货分享,感谢您的阅读!大背景高频面试题基本总结回顾(含笔试高频算法整理)

1.分析JVM运行时数据区域(高频考点)

Java 内存区域和内存模型是不一样的东西:

  • 内存区域是指 Jvm 运行时将数据分区域存储,强调对内存空间的划分。
  • 内存模型(Java Memory Model,简称 JMM )是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式,如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。

主要的运行时数据如下:

JDK8 之前的内存区域图如下:

JDK8 之后的内存区域图如下:

Java 8 中 PermGen 为什么被移出 HotSpot JVM 了?两个主要原因:

  1. 由于 PermGen 内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM
  2. 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。
    根据上面的各种原因,PermGen 最终被移除,方法区移至 Metaspace,字符串常量移至 Java Heap

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器内核都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

  • 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
  • 如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。

此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

Java虚拟机栈

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。

虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

在活动线程中,只有位千栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。

  • 1. 局部变量表

局部变量表是存放方法参数和局部变量的区域。 局部变量没有准备阶段, 必须显式初始化。如果是非静态方法,则在 index[0] 位置上存储的是方法所属对象的实例引用,一个引用变量占 4 个字节,随后存储的是参数和局部变量。字节码指令中的 STORE 指令就是将操作栈中计算完成的局部变呈写回局部变量表的存储空间内。

虚拟机栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

  • 2. 操作栈

操作栈是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令往栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎, 其中的栈指的就是操作栈。字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的 stack 属性中。

i++ 和 ++i 的区别:

  1. i++:从局部变量表取出 i 并压入操作栈,然后对局部变量表中的 i 自增 1,将操作栈栈顶值取出使用,最后,使用栈顶值更新局部变量表,如此线程从操作栈读到的是自增之前的值。
  2. ++i:先对局部变量表的 i 自增 1,然后取出并压入操作栈,再将操作栈栈顶值取出使用,最后,使用栈顶值更新局部变量表,线程从操作栈读到的是自增之后的值。

之前之所以说 i++ 不是原子操作,即使使用 volatile 修饰也不是线程安全,就是因为,可能 i 被从局部变量表(内存)取出,压入操作栈(寄存器),操作栈中自增,使用栈顶值更新局部变量表(寄存器更新写入内存),其中分为 3 步,volatile 保证可见性,保证每次从局部变量表读取的都是最新的值,但可能这 3 步可能被另一个线程的 3 步打断,产生数据互相覆盖问题,从而导致 i 的值比预期的小。

  • 3. 动态链接

每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接。

  • 4.方法返回地址

方法执行时有两种退出情况:正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;异常退出。

无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:返回值压入上层调用栈帧/异常信息抛给能够处理的栈帧/PC计数器指向方法调用后的下一条指令。

本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Sun HotSpot 虚拟机直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

线程开始调用本地方法时,会进入 个不再受 JVM 约束的世界。本地方法可以通过 JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和 JVM 相同的能力和权限。 当大量本地方法出现时,势必会削弱 JVM 对系统的控制力,因为它的出错信息都比较黑盒。对内存不足的情况,本地方法栈还是会抛出 nativeheapOutOfMemory。

JNI 类本地方法最著名的应该是 System.currentTimeMillis() ,JNI使 Java 深度使用操作系统的特性功能,复用非 Java 代码。 但是在项目过程中, 如果大量使用其他语言来实现 JNI , 就会丧失跨平台特性。

Java堆

对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。

Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。

为什么要使用元空间取代永久代的实现?

  • 字符串存在永久代中,容易出现性能问题和内存溢出。
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  • 将 HotSpot 与 JRockit 合二为一。

补充内容:

  • 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

  • 直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。

在 JDK 1.4 中新加入了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。

2.简单介绍一下Java内存模型(高频考点)

Java内存模型是共享内存的并发模型,线程之间主要通过读-写共享变量(堆内存中的实例域,静态域和数组元素)来完成隐式通信。Java 内存模型(JMM)控制 Java 线程之间的通信,决定一个线程对共享变量的写入何时对另一个线程可见。

计算机高速缓存和缓存一致性

计算机在高速的 CPU 和相对低速的存储设备之间使用高速缓存,作为内存和处理器之间的缓冲。将运算需要使用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存之中。

在多处理器的系统中(或者单处理器多核的系统),每个处理器内核都有自己的高速缓存,它们有共享同一主内存(Main Memory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。

为此,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,来维护缓存的一致性。

JVM主内存与工作内存

Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。

Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这里的工作内存是 JMM 的一个抽象概念,也叫本地内存,其存储了该线程以读 / 写共享变量的副本。

就像每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存。不同线程之间无法直接访问对方工作内存中的变量,线程间的通信一般有两种方式进行,一是通过消息传递,二是共享内存。Java 线程间的通信采用的是共享内存方式,线程、主内存和工作内存的交互关系如下图所示:

这里所讲的主内存、工作内存与 Java 内存区域中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。

重排序和happens-before规则

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。java 编译器禁止处理器重排序是通过在生成指令序列的适当位置会插入内存屏障(重排序时不能把后面的指令重排序到内存屏障之前的位置)指令来实现的。

happens-before

从 JDK5 开始,java 内存模型提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

如果 A happens-before B,那么 Java 内存模型将向程序员保证—— A 操作的结果将对 B 可见,且 A 的执行顺序排在 B 之前。

重要的 happens-before 规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
  • volatile 变量规则:对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。
  • 传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。

下图是 happens-before 与 JMM 的关系

volatile关键字

volatile 可以说是 JVM 提供的最轻量级的同步机制,当一个变量定义为volatile之后,它将具备两种特性:

  • 保证此变量对所有线程的可见性。

而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。注意,volatile 虽然保证了可见性,但是 Java 里面的运算并非原子操作,导致 volatile 变量的运算在并发下一样是不安全的。而 synchronized 关键字则是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得线程安全的。

  • 禁止指令重排序优化。

普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

3.简述内存分配与回收策略

简单谈谈内存分配

对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

简单谈谈回收策略

在Java中,内存的自动分配和回收由Java虚拟机(JVM)的垃圾回收器负责。Java的垃圾回收器采用了自适应的分代垃圾回收策略,主要包括新生代(Young Generation)和老年代(Old Generation)的回收策略。详细的可以见下面的内容。

4.垃圾回收机制(高频考点)

更详细的见:Java回收垃圾的基本过程与常用算法_java垃圾回收过程_张彦峰ZYF的博客-CSDN博客

垃圾回收主要关注 Java 堆

Java 内存运行时区域中的程序计数器、虚拟机栈、本地方法栈随线程而生灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由 JIT 编译器进行一些优化),因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

而 Java 堆不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

判断哪些对象需要被回收

有以下两种方法:

  • 引用计数法

给对象添加一引用计数器,被引用一次计数器值就加 1;当引用失效时,计数器值就减 1;计数器为 0 时,对象就是不可能再被使用的,简单高效,缺点是无法解决对象之间相互循环引用的问题。

  • 可达性分析算法

通过一系列的称为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。此算法解决了上述循环引用的问题。

在Java语言中,可作为 GC Roots 的对象包括下面几种:

  • a. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • b. 方法区中类静态属性引用的对象。
  • c. 方法区中常量引用的对象。
  • d. 本地方法栈中 JNI(Native方法)引用的对象

作为 GC Roots 的节点主要在全局性的引用与执行上下文中。要明确的是,tracing gc必须以当前存活的对象集为 Roots,因此必须选取确定存活的引用类型对象。

GC 管理的区域是 Java 堆,虚拟机栈、方法区和本地方法栈不被 GC 所管理,因此选用这些区域内引用的对象作为 GC Roots,是不会被 GC 所回收的。其中虚拟机栈和本地方法栈都是线程私有的内存区域,只要线程没有终止,就能确保它们中引用的对象的存活。而方法区中类静态属性引用的对象是显然存活的。常量引用的对象在当前可能存活,因此,也可能是 GC roots 的一部分。

可达性分析算法

不可达的对象将暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:

  • 如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。
  • 当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,直接进行第二次标记。
  • 如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。

这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,因为如果一个对象在 finalize() 方法中执行缓慢,将很可能会一直阻塞 F-Queue 队列,甚至导致整个内存回收系统崩溃。

值得注意的是,使用 finalize() 方法来“拯救”对象是不值得提倡的,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。finalize() 能做的工作,使用 try-finally 或者其它方法都更适合、及时。

5.强、软、弱、虚引用的区分?

 更详细的见:Java回收垃圾的基本过程与常用算法_java垃圾回收过程_张彦峰ZYF的博客-CSDN博客

JDK1.2 以前,一个对象只有被引用和没有被引用两种状态。后来,Java 对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用强度依次逐渐减弱。

  1. 强引用就是指在程序代码之中普遍存在的,类似"Object obj=new Object()"这类的引用,垃圾收集器永远不会回收存活的强引用对象。
  2. 软引用:还有用但并非必需的对象。在系统 将要发生内存溢出异常之前 ,将会把这些对象列进回收范围之中进行第二次回收。
  3. 弱引用也是用来描述非必需对象的,被弱引用关联的对象 只能生存到下一次垃圾收集发生之前 。当垃圾收集器工作时,无论内存是否足够,都会回收掉只被弱引用关联的对象。
  4. 虚引用是最弱的一种引用关系。 无法通过虚引用来取得一个对象实例 。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

6.Java 堆永久代的回收(该部分归属java7,大概率不会问了)

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

  1. 回收废弃常量与回收 Java 堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串"abc"已经进入了常量池中,但是当前系统没有任何一个 String 对象是叫做"abc"的,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个"abc"常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
  2. 类需要同时满足下面 3 个条件才能算是“无用的类”:
    a. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    b. 加载该类的 ClassLoader 已经被回收。
    c. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。

在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

7.垃圾回收算法(高频考点)

一共有 4 种:标记-清除算法、复制算法、标记整理算法、分代收集算法

 更详细的见:Java回收垃圾的基本过程与常用算法_java垃圾回收过程_张彦峰ZYF的博客-CSDN博客 更详细的见:Java回收垃圾的基本过程与常用算法_java垃圾回收过程_张彦峰ZYF的博客-CSDN博客

标记-清除算法

最基础的收集算法是“标记-清除”(Mark-Sweep)算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

它的主要不足有两个:

  • 效率问题,标记和清除两个过程的效率都不高;
  • 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。复制算法的执行过程如下图:

现在的商业虚拟机都采用这种算法来回收新生代,IBM 研究指出新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。

当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden:Survivor = 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(其中一块Survivor不可用),只有 10% 的内存会被“浪费”。

当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。内存的分配担保也一样,如果另外一块 Survivor 空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

标记-整理算法

复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前商业虚拟机的垃圾收器都采用“分代收集”(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块并采用不同的垃圾收集算法。

一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

8.Minor GC和Full GC触发条件

Minor GC触发条件:

  • 当Eden区满时,触发Minor GC。

Full GC触发条件:

  • System.gc()方法的调用
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

9.Minor GC 和 Full GC 有什么不一样吗?

  • 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。

  • 老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。Major GC 的速度一般会比 Minor GC 慢 10 倍以上。

10.简述GC中Stop the world(STW),并说出安全点

垃圾回收首先是要经过标记的,对象被标记后就会根据不同的区域采用不同的收集方法。垃圾回收并不会阻塞我们程序的线程,他是与当前程序并发执行的。所以问题就出在这里,当GC线程标记好了一个对象的时候,此时我们程序的线程又将该对象重新加入了“关系网”中,当执行二次标记的时候,该对象也没有重写finalize()方法,因此回收的时候就会回收这个不该回收的对象。 虚拟机的解决方法就是在一些特定指令位置设置一些“安全点”,当程序运行到这些“安全点”的时候就会暂停所有当前运行的线程(Stop The World 所以叫STW),暂停后再找到“GC Roots”进行关系的组建,进而执行标记和清除。 

这些特定的指令(安全点)位置主要在

  • 循环的末尾
  • 方法临返回前 / 调用方法的call指令后
  • 可能抛异常的位置

停顿类型就是STW,至于有GC和Full GC之分,主要是Full GC时STW的时间相对GC来说时间很长,因为Full GC针对整个堆以及永久代的,因此整个GC的范围大大增加;还有就是他的回收算法就是“标记–清除–整理”,这里也会损耗一定的时间。所以我们在优化JVM的时候,减少Full GC的次数也是经常用到的办法。 

11.各垃圾回收器的特点及区别,怎么做选择?

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。具体有Serial收集器(串行收集器)、ParNew收集器、Parallel Scavenge收集器、Serial Old 收集器、Parallel Old收集器、CMS收集器、G1收集器。

下图中7 种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。重点分析 CMS 和 G1 这两款相对复杂的收集器,了解它们的部分运作细节。

Serial收集器(串行收集器)

Serial 收集器,一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。"Stop The World"这个名字也许听起来很酷,但这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说都是难以接受的。下图示意了 Serial/Serial Old 收集器的运行过程。

实际上到现在为止,它依然是虚拟机运行在 Client 模式下的默认新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial 收集器对于运行在 Client 模式下的虚拟机来说是一个很好的选择。

ParNew收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括 Serial 收集器可用的所有控制参数(例如:-XX:SurvivorRatio-XX:PretenureSizeThreshold-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。ParNew 收集器的工作过程如下图所示。

ParNew 收集器除了多线程收集之外,其他与 Serial 收集器相比并没有太多创新之处,但它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器(并发收集器,后面有介绍)配合工作。

ParNew 收集器在单 CPU 的环境中不会有比 Serial 收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百地保证可以超越 Serial 收集器。

当然,随着可以使用的 CPU 的数量的增加,它对于 GC 时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与 CPU 的数量相同,在 CPU 非常多(如 32 个)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

注意,从 ParNew 收集器开始,后面还会接触到几款并发和并行的收集器。这里有必要先解释两个名词:并发和并行。这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们可以解释如下。

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个 CPU 上。

Parallel Scavenge收集器

Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。

所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉1分钟,那吞吐量就是99% 。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数

  • MaxGCPauseMillis参数允许的值是一个大于 0 的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不要认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集 300MB 新生代肯定比收集 500MB 快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
  • GCTimeRatio 参数的值应当是一个 0 到 100 的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为 19,那允许的最大 GC 时间就占总时间的 5%(即 1/(1+19)),默认值为 99 ,就是允许最大 1%(即 1/(1+99))的垃圾收集时间。

由于与吞吐量关系密切,Parallel Scavenge 收集器也经常称为“吞吐量优先”收集器。除上述两个参数之外,Parallel Scavenge 收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为 GC 自适应的调节策略(GC Ergonomics)。

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。如果在 Server 模式下,那么它主要还有两大用途:一种用途是在 JDK 1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途就是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。这两点都将在后面的内容中详细讲解。Serial Old 收集器的工作过程如下图所示。

Parallel Old收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在 JDK 1.6 中才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直处于比较尴尬的状态。原因是:如果新生代选择了 Parallel Scavenge 收集器,老年代除了 Serial Old(PS MarkSweep)收集器外别无选择(Parallel Scavenge 收集器无法与 CMS 收集器配合工作)。

由于老年代 Serial Old 收集器在服务端应用性能上的“拖累”,使用了 Parallel Scavenge 收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多 CPU 的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有 ParNew 加 CMS 的组合“给力”。

直到 Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。Parallel Old 收集器的工作过程如下图所示。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。从名字(包含"Mark Sweep")上就可以看出,CMS 收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中,初始标记、重新标记这两个步骤仍然需要"Stop The World"。初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,并发标记阶段就是进行 GC RootsTracing 的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

CMS 是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,但是 CMS 还远达不到完美的程度,它有以下 3 个明显的缺点:

  • 第一、导致吞吐量降低。

CMS 收集器对 CPU 资源非常敏感。其实,面向并发设计的程序都对 CPU 资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS 默认启动的回收线程数是(CPU数量+3)/4,也就是当 CPU 在4个以上时,并发回收时垃圾收集线程不少于 25% 的 CPU 资源,并且随着 CPU 数量的增加而下降。但是当 CPU 不足 4 个(譬如2个)时,CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50%,其实也让人无法接受。

  • 第二、CMS 收集器无法处理浮动垃圾(Floating Garbage),可能出现"Concurrent Mode Failure"失败而导致另一次 Full GC(新生代和老年代同时回收) 的产生。

由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此 CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。

要是 CMS 运行期间预留的内存无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败,这时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CM SInitiatingOccupancyFraction设置得太高很容易导致大量"Concurrent Mode Failure"失败,性能反而降低。

  • 第三、产生空间碎片。 

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

为了解决这个问题,CMS 收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行 FullGC 时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的 Full GC 后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。

G1收集器

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,G1 是一款面向服务端应用的垃圾收集器。HotSpot 开发团队赋予它的使命是(在比较长期的)未来可以替换掉 JDK 1.5 中发布的 CMS 收集器。

与其他 GC 收集器相比,G1 具备如下特点:

  • 并行与并发: G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
  • 分代收集: 与其他收集器一样,分代概念在 G1 中依然得以保留。虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
  • 空间整合: 与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC 。
  • 可预测的停顿: 这是 G1 相对于 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。

在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。使用 G1 收集器时,Java 堆的内存布局就与其他收集器有很大差别,它将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region (不需要连续)的集合。

G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1 在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是Garbage-First名称的来由),保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。

在 G1 收集器中,Region 之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用 Remembered Set 来避免全堆扫描的。

G1 中每个Region 都有一个与之对应的 Remembered Set,虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting and Evacuation)

G1 的前几个步骤的运作过程和 CMS 有很多相似之处:

  • 初始标记阶段仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这阶段需要停顿线程,但耗时很短。
  • 并发标记阶段是从 GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行。
  • 最后在筛选回收阶段首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,从Sun公司透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

13.G1和CMS的比较

  • CMS收集器是获取最短回收停顿时间为目标的收集器,因为CMS工作时,GC工作线程与用户线程可以并发执行,以此来达到降低停顿时间的目的(只有初始标记和重新标记会STW)。但是CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。
  • CMS仅作用于老年代,是基于标记清除算法,所以清理的过程中会有大量的空间碎片。
  • CMS收集器无法处理浮动垃圾,由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。
  • G1是一款面向服务端应用的垃圾收集器,适用于多核处理器、大内存容量的服务端系统。G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短STW的停顿时间,它满足短时间停顿的同时达到一个高的吞吐量。
  • 从JDK 9开始,G1成为默认的垃圾回收器。当应用有以下任何一种特性时非常适合用G1:Full GC持续时间太长或者太频繁;对象的创建速率和存活率变动很大;应用不希望停顿时间长(长于0.5s甚至1s)。
  • G1将空间划分成很多块(Region),然后他们各自进行回收。堆比较大的时候可以采用复制算法,碎片化问题不严重。整体上看属于标记整理算法,局部(region之间)属于复制算法。
  • G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。所以 CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB。

14.CMS垃圾回收器存在的问题及解决方案(高频考点)

CMS是使用标记-清理算法去垃圾回收的。其中四个主要的流程分别是初始标记、并发标记、重新标记、并发清理

  • 并发消耗CPU资源

其中的并发标记和并发清理是工作线程和垃圾回收线程并发工作,这样在需要STW的时间内不会让整个系统不可用。但是在并发标记阶段,需要根据GC Roots标记出大量的存活对象,而在并发清理阶段,则需要将垃圾对象从各种随机内存位置删掉,这两个阶段都非常消耗性能,所以垃圾回收线程会占用一部分的CPU资源,导致系统的执行效率降低。

CMS默认的回收线程数是 (CPU个数+3)/4,当在CPU核数较多的时候,对系统性能的影响并不是特别大。但是如果是CPU核数较少,例如双核的时候,就会占用一个CPU去处理垃圾回收,系统的CPU资源直接降低50%,这就严重影响了效率

因为现在CPU的核数越来越多,所以这种场景基本不会对系统造成很大的影响,可以忽略不计。

  • Concurrent Mode Failure问题

并发清理阶段,工作线程和垃圾回收线程并发工作的时候,此时工作线程会不断产生新的垃圾,但是垃圾回收线程并不会去处理这些新生成的垃圾对象,需要等到下次垃圾回收的时候才会去处理,这些垃圾对象称之为:浮动垃圾 。因为有这些浮动垃圾的存在,所以老年代不能在100%使用的时候才去进行垃圾回收,否则就放不下这些浮动垃圾了。

有一个参数是“-XX:CMSInitiatingOccupancyFraction”,这个参数在jdk1.6里面默认是92%,意思是老年代使用了92%的空间就会执行垃圾回收了。但是即使预留了8%的内存去存放浮动垃圾,但是还是有可能放不下,这样就会产生Concurrent Mode Failure问题。一旦产生了Concurrent Mode Failure问题,系统会直接使用Serial Old垃圾回收器取代CMS垃圾回收器,从头开始进行GC Roots追踪对象,并清理垃圾,这样会导致整个垃圾回收的时间变得更长。

解决办法就是根据系统的需求,合理设置“-XX:CMSInitiatingOccupancyFraction”的值,如果过大,则会产生Concurrent Mode Failure问题,如果设置的过小,则会导致老年代更加频繁的垃圾回收。

  • 空间碎片问题

CMS的标记-清理算法会在并发清理的阶段产生大量的内存碎片,如果不整理的话,则会有大量不连续的内存空间存在,无法放入一些进入老年代的大对象,导致老年代频繁垃圾回收。所以CMS存在一个默认的参数 “-XX:+UseCMSCompactAtFullCollection”,意思是在Full GC之后再次STW,停止工作线程,整理内存空间,将存活的对象移到一边。还要一个参数是“-XX:+CMSFullGCsBeforeCompaction”,表示在进行多少次Full GC之后进行内存碎片整理,默认为0,即每次Full GC之后都进行内存碎片整理。

CMS虽然使用并发的方式降低了STW的时间,但是还需要配合一些CMS的参数才能完全发挥出CMS的优势,否则甚至会降低垃圾回收的效率。因此只有掌握了CMS的原理和参数的调试,才能让系统运行的更加流畅。

15.讲讲CMS垃圾回收器?讲讲G1垃圾回收器?(高频考点)

可以直接见上面的答案,当然如果想详细分析了解请见以下总结博客

CMS垃圾回收器介绍与优化分析案列整理总结_张彦峰ZYF的博客-CSDN博客

Java Hotspot G1 GC的理解总结_gc g1 hotspot_张彦峰ZYF的博客-CSDN博客

16.简述ZGC垃圾回收器(高频考点)

更加清晰和介绍的内容请直接查看垃圾回收器ZGC应用分析总结_张彦峰ZYF的博客-CSDN博客

ZGC 是一款低延迟、高吞吐的垃圾回收器,由 Oracle 公司开发。它适用于大型、多核、内存容量较大的应用程序。ZGC 的设计目标是在最大限度地减少停顿时间的同时,为大型内存提供可伸缩性,并为生产部署提供高吞吐量和稳定性。它的目标是以不到 10 毫秒的暂停时间来控制 100MB 到 4TB 的内存。此外,ZGC 还致力于避免全局 JVM 暂停,从而提高系统的可用性。简单来说,它的设计目标是在不超过 10 毫秒的暂停时间内,尽可能地回收大量的堆内存。

ZGC 主要有以下几个特点:

  • 低延迟:ZGC 的主要目标是最小化 GC 暂停时间。因此,ZGC 使用了基于读屏障(Read Barrier)的堆栈式(Stack-Style)替换算法,以及基于标记颜色(Mark-Color)的压缩算法,从而避免了传统 GC 中的根扫描和整理等阶段,大幅减少了 GC 暂停时间。
  • 高吞吐:虽然 ZGC 的主要目标是低延迟,但它的吞吐性能也很不错。在低延迟的基础上,ZGC 通过多线程并行处理垃圾回收任务,以及使用更大的堆空间和更高效的内存分配器等技术,提高了垃圾回收的效率和吞吐量。
  • 大堆支持:ZGC 支持的最大堆内存大小为 16TB,这使得它可以处理非常大的内存数据,例如云计算、大数据等领域的应用。
  • 透明性:ZGC 对应用程序是透明的,应用程序无需进行任何修改,即可使用 ZGC 进行垃圾回收。
  • 并发性:ZGC 是一款并发的垃圾回收器,它可以在运行应用程序的同时,进行垃圾回收操作。这使得 ZGC 可以在多核 CPU 上充分发挥并行处理能力,提高垃圾回收的效率。

总之,ZGC 是一款非常优秀的垃圾回收器,它通过独特的算法和设计,实现了低延迟、高吞吐、大堆支持、透明性和并发性等优势。

17.双亲委派模型

双亲委派模型(Parent Delegation Model)是Java类加载机制的一种重要概念。它是为了解决类的命名冲突和安全性问题而设计的一种类加载策略。

在Java中,类加载器(ClassLoader)负责将类的字节码加载到内存中,并生成对应的Class对象。双亲委派模型是一种层次化的类加载机制,它基于父子关系的思想,通过一系列的类加载器协同工作,保证类的加载和执行的安全性。

当一个类需要被加载时,双亲委派模型会按照以下方式进行:

  1. 检查类是否已经被加载过,如果是,则直接返回已加载的Class对象。
  2. 如果类没有被加载过,则委派给父类加载器进行加载。父类加载器也会按照相同的方式进行检查和委派。
  3. 如果父类加载器无法加载该类,则子类加载器会尝试加载。子类加载器首先检查自己是否已经加载过该类,如果是,则返回已加载的Class对象。
  4. 如果子类加载器无法加载该类,则子类加载器将请求委派给父类加载器的父类加载器。该过程会一直向上委派,直到达到顶层的启动类加载器(Bootstrap ClassLoader)。
  5. 如果所有的父类加载器都无法加载该类,则子类加载器会尝试自己加载该类,并生成对应的Class对象。

通过这种双亲委派模型,Java实现了类加载器的层次结构,并保证了类的唯一性和安全性。它可以避免类的重复加载,提高了类加载的效率。同时,它也能够防止恶意代码的加载和执行,通过控制类加载的权限,增强了Java程序的安全性。

双亲委派模型在Java中的典型应用是对标准库类的加载。当需要加载Java核心类库(如java.lang.String)时,由于父类加载器无法加载该类,所以会由子类加载器(如应用程序类加载器)负责加载。这样可以确保标准库的类不会被应用程序的类所替代,保证了Java平台的稳定性和一致性。

18.谈谈双亲委派模型的"破坏"

虽然双亲委派模型在Java中被广泛使用并提供了一定的安全性和稳定性,但它也存在一些破坏的情况。以下是一些可能破坏双亲委派模型的情况:

  1. 自定义类加载器:Java允许开发者自定义类加载器,通过继承ClassLoader类并重写加载方法来实现自定义的类加载逻辑。自定义类加载器可以打破双亲委派模型,例如在子类加载器加载类时,可以选择不委派给父类加载器,而是直接加载类。这样可能导致不同类加载器加载同一个类,从而破坏了双亲委派模型的唯一性。
  2. 线程上下文类加载器:线程上下文类加载器(Thread Context Class Loader)是Java中的一个特殊类加载器,用于在多线程环境中加载类。它可以通过Thread类的setContextClassLoader方法进行设置。在某些情况下,线程上下文类加载器可能会打破双亲委派模型,例如在某个线程中设置了不符合双亲委派模型的类加载器,导致该线程加载类时绕过了双亲委派模型的规则。
  3. SPI机制:Java的SPI(Service Provider Interface)机制允许通过在classpath中提供特定的配置文件来加载实现了特定接口的类。在SPI机制中,使用的是当前线程的上下文类加载器来加载配置文件中指定的类。这种机制可能会绕过双亲委派模型,因为在SPI机制中,类的加载是由应用程序自己来控制的,不受双亲委派模型的限制。

尽管双亲委派模型可能会被破坏,但这些情况通常是有意为之,并且在特定的应用场景中具有一定的合理性和灵活性。开发人员需要谨慎使用自定义类加载器、线程上下文类加载器和SPI机制,并了解它们可能对双亲委派模型产生的影响,以确保代码的正确性和安全性。

19.JDBC和双亲委派模型关系分析

JDBC(Java Database Connectivity)是Java提供的一组用于数据库访问的API。它提供了一种标准的方式来连接和操作各种类型的数据库。

JDBC的实现通常需要依赖数据库驱动程序,这些驱动程序是由数据库供应商提供的特定于数据库的实现。在使用JDBC时,开发人员需要将数据库驱动程序的相关JAR文件添加到应用程序的类路径中。

与双亲委派模型的关系如下:

  1. 双亲委派模型的类加载器层次结构:在Java中,类加载器按照双亲委派模型的规则进行类的加载。JDBC驱动程序通常由特定的类加载器加载,这些类加载器的关系也遵循双亲委派模型。例如,应用程序类加载器(Application ClassLoader)可能会委派给扩展类加载器(Extension ClassLoader),而扩展类加载器又会委派给引导类加载器(Bootstrap ClassLoader)。
  2. 类加载器隔离:由于JDBC驱动程序是由特定的类加载器加载的,不同的类加载器加载的驱动程序相互之间是隔离的。这种隔离性可以保证在同一个Java虚拟机中使用不同版本或不同供应商的JDBC驱动程序时不会发生冲突。每个类加载器只会加载它所负责的类和资源,避免了类的冲突和干扰。

总结来说,JDBC和双亲委派模型存在关系,因为JDBC驱动程序的加载遵循双亲委派模型的规则。这种关系确保了JDBC驱动程序的类加载器隔离性,允许在同一应用程序中同时使用不同版本或不同供应商的JDBC驱动程序。这对于开发使用多个数据库的应用程序非常有益,并且提供了灵活性和可扩展性。

20.JVM锁优化和锁膨胀过程(高频考点)

高效并发是JDK 1.6的一个重要主题,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁削除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

自旋锁

自选锁其实就是在拿锁时发现已经有线程拿了锁,自己如果去拿会阻塞自己,这个时候会选择进行一次忙循环尝试。也就是不停循环看是否能等到上个线程自己释放锁。这个问题是基于一个现实考量的:很多拿了锁的线程会很快释放锁。因为一般敏感的操作不会很多。当然这个是一个不能完全确定的情况,只能说总体上是一种优化。

基于这种做法的一个优化:自适应自旋锁。也就是说,第一次设置最多自旋10次,结果在自旋的过程中成功获得了锁,那么下一次就可以设置成最多自旋20次。
道理是:一个锁如果能够在自旋的过程中被释放说明很有可能下一次也会发生这种事。那么就更要给这个锁某种“便利”方便其不阻塞得锁(毕竟快了很多)。同样如果多次尝试的结果是完全不能自旋等到其释放锁,那么就说明很有可能这个临界区里面的操作比较耗时间。就减小自旋的次数,因为其可能性太小了。

锁粗化

原则上为了提高运行效率,锁的范围应该尽量小,减少同步的代码,但是这不是绝对的原则,试想有一个循环,循环里面是一些敏感操作,有的人就在循环里面写上了synchronized关键字。这样确实没错不过效率也许会很低,因为其频繁地拿锁释放锁。要知道锁的取得(假如只考虑重量级MutexLock)是需要操作系统调用的,从用户态进入内核态,开销很大。于是针对这种情况也许虚拟机发现了之后会适当扩大加锁的范围(所以叫锁粗化)以避免频繁的拿锁释放锁的过程。

比如像这样的代码:

  1. synchronized{
  2. 做一些事情
  3. }
  4. synchronized{
  5. 做另外一些事情
  6. }

就会被粗化成:

  1. synchronized{
  2. 做一些事情
  3. 做另外一些事情
  4. }

锁消除

通过逃逸分析发现其实根本就没有别的线程产生竞争的可能(别的线程没有临界量的引用),或者同步块内进行的是原子操作,而“自作多情”地给自己加上了锁。有可能虚拟机会直接去掉这个锁。

偏向锁

在大多数的情况下,锁不仅不存在多线程的竞争,而且总是由同一个线程获得。因此为了让线程获得锁的代价更低引入了偏向锁的概念。偏向锁的意思是如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入或者退出同一个同步代码块,不需要再次进行抢占锁和释放锁的操作。偏向锁可以通过 -XX:+UseBiasedLocking开启或者关闭

偏向锁的获取:偏向锁的获取过程非常简单,当一个线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,表示哪个线程获得了偏向锁,结合Mark Word来分析一下偏向锁的获取逻辑

  • 首先获取目标对象的Mark Word,根据锁的标识为和epoch去判断当前是否处于可偏向的状态
  • 如果为可偏向状态,则通过CAS操作将自己的线程ID写入到MarkWord,如果CAS操作成功,则表示当前线程成功获取到偏向锁,继续执行同步代码块
  • 如果是已偏向状态,先检测MarkWord中存储的threadID和当前访问的线程的threadID是否相等,如果相等,表示当前线程已经获得了偏向锁,则不需要再获得锁直接执行同步代码;如果不相等,则证明当前锁偏向于其他线程,需要撤销偏向锁。

偏向锁的撤销:当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,撤销偏向锁的过程需要等待一个全局安全点(所有工作线程都停止字节码的执行)。

  • 首先,暂停拥有偏向锁的线程,然后检查偏向锁的线程是否为存活状态
  • 如果线程已经死了,直接把对象头设置为无锁状态
  • 如果还活着,当达到全局安全点时获得偏向锁的线程会被挂起,接着偏向锁升级为轻量级锁,然后唤醒被阻塞在全局安全点的线程继续往下执行同步代码

轻量级锁

当存在超过一个线程在竞争同一个同步代码块时,会发生偏向锁的撤销。当前线程会尝试使用CAS来获取锁,当自旋超过指定次数(可以自定义)时仍然无法获得锁,此时锁会膨胀升级为重量级锁。

当存在超过一个线程在竞争同一个同步代码块时,会发生偏向锁的撤销。偏向锁撤销以后对象会可能会处于两种状态

  • 一种是不可偏向的无锁状态,简单来说就是已经获得偏向锁的线程已经退出了同步代码块,那么这个时候会撤销偏向锁,并升级为轻量级锁
  • 一种是不可偏向的已锁状态,简单来说就是已经获得偏向锁的线程正在执行同步代码块,那么这个时候会升级到轻量级锁并且被原持有锁的线程获得锁

那么升级到轻量级锁以后的加锁过程和解锁过程是怎么样的呢?

轻量级锁加锁

  • JVM会先在当前线程的栈帧中创建用于存储锁记录的空间(LockRecord)
  • 将对象头中的Mark Word复制到锁记录中,称为Displaced Mark Word.
  • 线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针
  • 如果替换成功,表示当前线程获得轻量级锁,如果失败,表示存在其他线程竞争锁,那么当前线程会尝试使用CAS来获取锁, 当自旋超过指定次数(可以自定义)时仍然无法获得锁,此时锁会膨胀升级为重量级锁

轻量级锁解锁

  • 尝试CAS操作将所记录中的Mark Word替换回到对象头中
  • 如果成功,表示没有竞争发生
  • 如果失败,表示当前锁存在竞争,锁会膨胀成重量级锁

重量级锁

重量级锁依赖对象内部的monitor锁来实现,而monitor又依赖操作系统的MutexLock(互斥锁)

大家如果对MutexLock有兴趣,可以抽时间去了解,假设Mutex变量的值为1,表示互斥锁空闲,这个时候某个线程调用lock可以获得锁,而Mutex的值为0表示互斥锁已经被其他线程获得,其他线程调用lock只能挂起等待

为什么重量级锁的开销比较大呢?
原因是当系统检查到是重量级锁之后,会把等待想要获取锁的线程阻塞,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程,都需要通过操作系统来实现,也就是相当于从用户态转化到内核态,而转化状态是需要消耗时间的

锁的膨胀过程

首先简单说下先偏向锁、轻量级锁、重量级锁三者各自的应用场景:

  • 偏向锁: 只有一个线程进入临界区;
  • 轻量级锁: 多个线程交替进入临界区;
  • 重量级锁: 多个线程同时进入临界区。

首先它们的关系是:最高效的是偏向锁,尽量使用偏向锁,如果不能(发生了竞争)就膨胀为轻量级锁,最后是重量级锁。

21.JVM中GC Root的选择标准是什么?相关JVM的调优参数有哪些?在工作中怎么调优的?

在Java语言中,可作为 GC Roots 的对象包括下面几种:

  • a. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • b. 方法区中类静态属性引用的对象。
  • c. 方法区中常量引用的对象。
  • d. 本地方法栈中 JNI(Native方法)引用的对象

作为 GC Roots 的节点主要在全局性的引用与执行上下文中。要明确的是,tracing gc必须以当前存活的对象集为 Roots,因此必须选取确定存活的引用类型对象。

在JVM中进行调优时,可以使用以下一些相关的调优参数:

  1. 堆大小调优参数:如-Xms(初始堆大小)和-Xmx(最大堆大小)可以调整堆内存的大小,以满足应用程序的需求。
  2. 垃圾回收器选择参数:如-XX:+UseSerialGC(使用串行垃圾回收器)、-XX:+UseParallelGC(使用并行垃圾回收器)、-XX:+UseConcMarkSweepGC(使用CMS垃圾回收器)等可以选择不同的垃圾回收器。
  3. 垃圾回收器调优参数:如-XX:NewRatio(新生代与老年代的比例)、-XX:SurvivorRatio(Eden区与Survivor区的比例)、-XX:MaxTenuringThreshold(对象晋升老年代的年龄阈值)等可以调整垃圾回收器的行为和效率。
  4. 内存分配参数:如-XX:NewSize(新生代的初始大小)、-XX:MaxNewSize(新生代的最大大小)、-XX:MetaspaceSize(元空间大小)等可以调整内存分配的相关参数。

JVM常见的调优参数讲解:

  • -Xmx:指定java程序的最大堆内存, 使用java -Xmx5000M -version判断当前系统能分配的最大堆内存
  • -Xms:指定最小堆内存, 通常设置成跟最大堆内存一样,减少GC
  • -Xmn:设置年轻代大小。整个堆大小=年轻代大小 + 年老代大小。所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
  • -Xss:指定线程的最大栈空间,此参数决定了java函数调用的深度,,值越大调用深度越深,,若值太小则容易出栈溢出错误(StackOverflowError)
  • -XX:PermSize:指定方法区(永久区)的初始值,默认是物理内存的1/64, 在Java8永久区移除, 代之的是元数据区, 由-XX:MetaspaceSize指定
  • -XX:MaxPermSize:指定方法区的最大值, 默认是物理内存的1/4, 在java8中由-XX:MaxMetaspaceSize指定元数据区的大小
  • -XX:NewRatio=n:年老代与年轻代的比值,-XX:NewRatio=2, 表示年老代与年轻代的比值为2:1
  • -XX:SurvivorRatio=n:Eden区与Survivor区的大小比值,-XX:SurvivorRatio=8表示Eden区与Survivor区的大小比值是8:1:1,因为Survivor区有两个(from, to)

在工作中进行JVM调优时,可以采取以下步骤:

  1. 监控和分析应用程序的内存使用情况、垃圾回收行为和性能指标,例如使用JVM监控工具、日志分析工具等。
  2. 根据监控数据和分析结果,识别潜在的内存泄漏、频繁的垃圾回收、内存不足等问题。确定哪些对象是不必要的,是否存在循环引用导致的内存泄漏等情况。
  3. 针对具体问题采取相应的优化策略。例如,如果存在内存泄漏,需要检查对象的生命周期管理,确保适时释放不再使用的对象;如果频繁进行垃圾回收,可能需要调整垃圾回收器参数、内存分配参数,或考虑优化算法和数据结构等。
  4. 调整JVM参数以优化内存使用和垃圾回收性能。根据应用程序的需求和特点,调整堆大小、垃圾回收器的选择和调优参数,以提升性能和减少内存占用。
  5. 进行性能测试和验证,观察调优后的效果。使用合适的工具和方法,进行压力测试、内存分析等,评估调优的效果和稳定性,根据测试结果进行迭代优化。
  6. 持续监控和优化。JVM调优是一个持续的过程,需要定期监控应用程序的性能和内存使用情况,及时发现和解决新的问题,保持应用程序的稳定和高效运行。

需要注意的是,JVM调优是一项复杂的任务,需要深入了解JVM的工作原理和相关参数,结合具体的应用场景和需求进行调优。同时,调优过程中需要谨慎操作,避免过度优化或调整不当导致的问题。建议在进行JVM调优时,充分了解应用程序的特点和需求,同时参考官方文档和专业资料,或者寻求专业的JVM调优工具和咨询服务的支持。

22.JVM性能监控有哪些?

在监控JVM性能时,可以使用多种工具和技术来获取关于应用程序运行时的各种指标和数据。以下是一些常用的JVM性能监控工具和技术:

  1. JMX(Java Management Extensions):JMX是Java平台提供的一种监控和管理Java应用程序的标准接口。通过JMX,可以获取JVM内部的各种信息,如内存使用情况、垃圾回收统计、线程信息等。可以使用JMX客户端工具或编写自定义的JMX监控程序来收集和分析这些信息。
  2. JVM自带的工具:JVM提供了一些自带的工具,如jstat、jstack、jmap、jconsole等,用于监控和诊断JVM性能问题。这些工具可以提供关于垃圾回收、线程状态、堆转储、类加载等方面的信息。
  3. 垃圾回收器日志:JVM可以生成垃圾回收器日志,记录垃圾回收的详细信息。可以通过配置垃圾回收器日志的参数,如-Xloggc,-XX:+PrintGCDetails等,将日志输出到文件,并使用分析工具来分析垃圾回收的行为和效果。
  4. 第三方监控工具:有许多第三方的监控工具可用于JVM性能监控,如VisualVM、Grafana、Prometheus等。这些工具提供了更强大的功能和可视化界面,可以监控和展示更多的指标和数据,并提供实时监控、报警和分析功能。
  5. APM(Application Performance Monitoring)工具:APM工具可以监控整个应用程序的性能,包括JVM的性能。这些工具可以提供实时的性能指标、事务跟踪、错误分析等功能,帮助定位和解决性能问题。常见的APM工具包括New Relic、AppDynamics、Dynatrace等。

通过以上工具和技术,可以获得关于JVM的内存使用情况、垃圾回收统计、线程状态、类加载信息等各种指标和数据,从而对JVM的性能进行监控、诊断和调优。选择合适的监控工具和技术,取决于应用程序的需求和复杂性,以及运维团队的技术栈和经验。

当涉及到JVM性能监控的可视化工具时,以下是一些常用的工具:

  1. VisualVM:VisualVM是一款强大的可视化工具,由JDK自带,用于监控和分析Java应用程序的性能。它提供了实时的CPU、内存、线程、垃圾回收等方面的监控数据,并能够生成图表和报告,帮助开发人员进行性能分析和问题排查。
  2. Grafana:Grafana是一种开源的数据可视化和监控工具,它支持各种数据源和插件,包括用于监控JVM性能的插件。通过配置Grafana,可以创建自定义的仪表盘,用于展示JVM的性能指标和数据,并提供实时监控、报警和分析功能。
  3. Prometheus:Prometheus是一种开源的系统监控和警报工具,它可以收集各种指标数据,并提供可视化的查询和展示功能。结合Prometheus的JVM客户端库,可以收集JVM的性能指标,并使用Prometheus的查询语言和图形面板展示和分析这些数据。
  4. Datadog:Datadog是一种全功能的监控和分析平台,支持监控JVM的性能和应用程序的其他方面。它提供了可视化的仪表盘、实时监控、警报和日志分析等功能,可以帮助开发人员和运维团队监控和优化JVM的性能。

这些工具提供了可视化界面和交互式的操作,使监控数据更加易于理解和分析。它们能够生成图表、报告和警报,帮助开发人员和运维团队快速识别和解决性能问题,优化应用程序的性能。根据实际需求和个人偏好,选择适合的可视化工具进行JVM性能监控和调优。

JDK的命令行工具回顾

  • jps(虚拟机进程状况工具):jps可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称 以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)。
  • jstat(虚拟机统计信息监视工具):jstat是用于监视虚拟机各种运行状态信息的命令行工 具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jinfo(Java配置信息工具):jinfo的作用是实时地查看和调整虚拟机各项参数。
  • jmap(Java内存映像工具):命令用于生成堆转储快照(一般称为heapdump或dump文 件)。如果不使用jmap命令,要想获取Java堆转储快照,还有一些比较“暴力”的手段:譬如 在第2章中用过的-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常出 现之后自动生成dump文件。jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永 久代的详细信息,如空间使用率、当前用的是哪种收集器等。
  • jhat(虚拟机堆转储快照分析工具):jhat命令与jmap搭配使用,来分析jmap生成的堆 转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在 浏览器中查看。
  • jstack(Java堆栈跟踪工具):jstack命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈 的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循 环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿 的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些 什么事情,或者等待着什么资源。

23.一个4核8G的服务器,初步设置JVM参数,如何分配?(高频考点)

在为一个4核8G的服务器设置JVM参数时,以下是一个初步的分配建议。请注意,这仅是一个起点,具体的参数设置可能需要根据应用程序的需求和特性进行微调。

  1. 确定堆内存大小:
    • 对于堆内存的分配,一般建议将总内存的一部分分配给堆。通常,可以将总内存的50%-70%作为堆内存的初始大小(-Xms参数),并将总内存的70%-80%作为堆内存的最大大小(-Xmx参数)
    • 对于8G的服务器,可以设置初始堆大小为4G(-Xms4g),最大堆大小为6G或7G(-Xmx6g或-Xmx7g)。这取决于应用程序的内存需求和可用的内存资源
  2. 设置垃圾回收器:
    • 根据应用程序的特性和性能需求,选择合适的垃圾回收器。如果应用程序对吞吐量要求较高且对延迟要求不敏感,可以选择并行垃圾回收器(-XX:+UseParallelGC)或G1垃圾回收器(-XX:+UseG1GC)。
    • 例如,可以使用以下参数配置并行垃圾回收器:-XX:+UseParallelGC,-XX:+UseParallelOldGC,-XX:ParallelGCThreads=<线程数>
  3. 设置线程数量:
    • 根据服务器的核心数量和应用程序的负载情况,配置合适的线程池和线程数。这包括设置并行垃圾回收的线程数和应用程序自身的线程池大小
    • 例如,可以设置并行垃圾回收的线程数为与核心数相同:-XX:ParallelGCThreads=4
  4. 其他参数:
    • 根据具体情况,可以进一步调整其他参数来优化JVM性能。例如,可以调整新生代和老年代的比例(-XX:NewRatio参数)、调整Survivor区的大小(-XX:SurvivorRatio参数)等。
    • 根据应用程序的特性和需求,还可以使用其他参数来调整垃圾回收策略和内存分配等方面的行为。

在设置JVM参数之前,建议先进行性能测试和基准测试,监控应用程序的性能和资源消耗情况,以便进行适当的调整和优化。同时,建议参考官方文档和专业资料,以获得更详细和针对性的参数配置建议。

24.ClassLoader如何工作的?

ClassLoader 顾名思义就是类加载器,ClassLoader 作用

  • 负责将 Class 加载到 JVM 中
  • 审查每个类由谁加载(父优先的等级加载机制)
  • 将 Class 字节码重新解析成 JVM 统一要求的对象格式

类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。

  • 加载:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构在内存中生成一个代表这个类的Class对象,作为方法去这个类的各种数据的访问入口
  • 验证:验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟自身的安全。
  • 准备:准备阶段是正式为类变量分配内存设置类变量初始值的阶段,这些变量所使用的内存都将在方法去中进行分配。这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
  • 解析:解析阶段是虚拟机将常量池内的符号(Class文件内的符号)引用替换为直接引用(指针)的过程。
  • 初始化:初始化阶段是类加载过程的最后一步,开始执行类中定义的Java程序代码(字节码)

25.对象的创建过程是什么样的?

  • 类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程

  • 分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

  • 初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  • 设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

  • 执行init方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

26.Java管理内存、内存泄漏和泄漏的原因?(高频考点)

Java是如何管理内存

Java的内存管理就是对象的分配和释放问题。

在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由 GC 决定和执行的。在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是 Java 程序运行速度较慢的原因之一。因为,GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。 以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。

Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。

总之,Java使用自动内存管理(Garbage Collection)来管理内存。它提供了垃圾回收器(Garbage Collector)来自动回收不再使用的对象,使开发人员无需显式释放内存。这种自动内存管理机制减轻了开发人员的负担,但也可能导致内存泄漏的问题。

内存泄漏是指应用程序中分配的内存资源无法被回收和释放,导致内存占用不断增加,最终可能导致系统性能下降或崩溃。以下是一些常见的导致内存泄漏的原因:

  1. 对象引用未及时释放:当一个对象不再被使用时,如果仍然存在对该对象的引用,垃圾回收器无法回收该对象占用的内存。这可能发生在代码中未及时将对象引用置为null或对象的生命周期管理不当的情况下。
  2. 集合类的使用不当:在使用集合类(如List、Map等)时,如果没有正确地从集合中移除不再需要的元素,这些元素将继续占用内存,导致内存泄漏。
  3. 资源未正确关闭:当使用一些需要手动关闭的资源,如文件、数据库连接、网络连接等时,如果没有及时关闭这些资源,会导致资源的泄漏,进而造成内存泄漏。
  4. 单例模式的使用不当:如果单例模式中的对象没有正确释放,那么对象的引用将一直存在,导致内存泄漏。
  5. 内部类的使用不当:在某些情况下,内部类会持有外部类的引用,如果内部类的实例没有被妥善处理,将导致外部类的引用无法被回收,造成内存泄漏。

为避免内存泄漏,可以采取以下措施:

  1. 及时释放对象引用:在不再使用对象时,确保将对象的引用置为null,使得垃圾回收器可以回收该对象的内存。
  2. 正确使用集合类和资源:在使用集合类时,及时从集合中移除不再需要的元素。对于需要手动关闭的资源,确保在使用完毕后及时关闭。
  3. 注意单例模式和内部类:确保单例对象的生命周期管理正确,及时释放不再使用的单例对象。对于内部类,注意处理好内外部类之间的引用关系,避免导致外部类的引用无法释放。
  4. 进行内存泄漏检测和性能测试:使用内存分析工具来检测潜在的内存泄漏问题,并进行性能测试来评估应用程序的内存使用情况和性能表现。通过及时发现和解决内存泄漏问题,可以确保应用程序的稳定性和性能。

此外,可以采用以下方法来减少内存泄漏的可能性:

  • 使用弱引用或软引用:在某些情况下,可以使用弱引用(Weak Reference)或软引用(Soft Reference)来引用对象,这样当内存不足时,垃圾回收器可以更容易地回收这些对象,避免内存泄漏。
  • 定期执行垃圾回收:尽管Java的垃圾回收机制是自动执行的,但有时可以通过手动触发垃圾回收(System.gc())来加速回收不再使用的对象,减少内存占用。
  • 使用内存分析工具:使用专业的内存分析工具(如Eclipse Memory Analyzer、VisualVM等)来检测和分析内存泄漏问题。这些工具可以帮助定位内存泄漏的源头,并提供详细的报告和建议。
  • 定期进行代码审查:进行定期的代码审查,特别关注对象的生命周期管理和资源的释放,以发现和修复潜在的内存泄漏问题。

综上所述,通过正确释放对象引用、合理使用集合类和资源、注意单例模式和内部类的使用,以及使用内存分析工具和定期的代码审查,可以减少内存泄漏的发生,提高应用程序的内存管理和性能。

27.OOM说一下?怎么排查?哪些会导致OOM?(高频考点)

OOM(Out of Memory)指的是Java应用程序在申请内存时无法满足需求,导致内存耗尽的错误。当发生OOM时,应用程序无法再分配新的对象,并抛出OutOfMemoryError异常。

要排查OOM错误,可以采取以下步骤:

  1. 查看错误信息:在捕获到OOM异常时,查看异常信息,通常会显示错误的原因和导致OOM的位置。错误信息中可能包含堆栈跟踪和异常消息,这对于定位问题非常有帮助。
  2. 分析内存转储文件(Heap Dump):当应用程序发生OOM时,可以生成一个内存转储文件,它包含了应用程序在发生OOM之前的内存状态。使用工具(如Eclipse Memory Analyzer、VisualVM等)分析这个内存转储文件可以查看内存中的对象、其引用关系和占用空间情况,以确定可能导致OOM的对象或内存泄漏问题。
  3. 检查内存使用情况:监控和分析应用程序的内存使用情况,包括堆内存、非堆内存和内存池的使用情况。通过观察内存使用的趋势和峰值,可以判断是否存在内存泄漏或者内存分配过于频繁的情况。
  4. 检查代码:审查代码,特别关注对象的生命周期管理和资源的释放。检查是否存在未及时释放对象引用、未关闭资源、未正确处理集合类等情况,这些都有可能导致内存泄漏和OOM问题。
  5. 调整内存配置:根据应用程序的需求和资源情况,适当调整JVM的内存参数,如堆内存大小(-Xmx和-Xms)、非堆内存大小(-XX:MaxPermSize或-XX:MaxMetaspaceSize)等。调整这些参数可能需要根据实际情况进行实验和调优。

导致OOM的常见原因包括:

  • 内存泄漏:未释放不再使用的对象或资源,导致内存占用不断增加,最终耗尽内存。
  • 内存溢出:应用程序需要的内存超过了可用的内存资源。
  • 高并发或大数据量:并发访问或处理大量数据时,可能导致内存使用剧增,超过了可用的内存大小。
  • 不合理的内存配置:如果分配的内存不足以支持应用程序的需求,也会导致OOM。

通过仔细分析错误信息、内存转储文件,检查内存使用情况,审查代码,并适当调整内存配置,可以帮助排查和解决OOM问题。

无以上工具时手动简单排查OOM问题如下

A 确认是不是内存本身就分配过小         方法:jmap -heap pid  查看对应的jvm的堆内存空间,比如eden区和old是否被占用等

B 找到最耗内存的对象                           方法:jmap -histo:live pid | more

jmap 命令将对文件导出----》jstack pid导出线程信息----》利用分析工具对dump文件进行分析,加入文件过大可以用eclipse分析插件memory Alyse还是啥----》打开插件的monitor-tree,找到占用空间最多的线程。找到线程名,在jstack导出的文件下查看线程对应的类,用代码分析
Linux命令行工具

  • 查看进程创建的线程数,以及网络连接数,如果资源耗尽,也可能出现OOM     pstree    netstat
  • 进程打开的句柄数和线程数      ll /proc/${PID}/fd | wc -l            ll /proc/${PID}/task | wc -l (效果等同pstree -p | wc -l)

28.JVM虚拟机老年代什么情况下会发生gc,给你一个场景,一台4核8G的服务器,每隔两个小时就要出现一次老年代gc,现在有日志,怎么分析是哪里出了问题?

在一台4核8G的服务器上,如果每隔两个小时发生一次老年代垃圾回收(GC),说明应用程序的内存使用量较大,导致老年代的内存空间被占满,需要进行垃圾回收来释放内存。要分析是哪里出了问题,可以通过以下步骤进行调查:

  1. 检查GC日志:查看应用程序的GC日志,分析GC发生的频率、持续时间和原因。GC日志会提供有关GC类型(如Full GC或Minor GC)、GC时间、回收对象数量等信息,这些信息有助于确定GC发生的原因和GC类型的比例。
  2. 分析堆转储文件:如果有可用的堆转储文件(heap dump),使用内存分析工具(如Eclipse Memory Analyzer、VisualVM等)加载堆转储文件进行分析。分析堆转储文件可以查看内存中的对象、其引用关系和占用空间情况,有助于确定内存中占用较多空间的对象类型和数量。
  3. 监控内存使用情况:使用监控工具(如JVisualVM、Grafana等)监控服务器的内存使用情况。观察内存的使用趋势、峰值和持续增长的情况,确定内存占用是否在一直增长导致频繁的老年代GC。
  4. 定位内存泄漏:使用内存分析工具对应用程序的内存进行检查,特别关注内存泄漏问题。检查是否存在对象未及时释放、未关闭资源、集合类使用不当等情况,这些都可能导致内存泄漏和频繁的GC。
  5. 分析应用程序逻辑和代码:审查应用程序的逻辑和代码,检查是否存在资源泄漏、缓存未合理管理、不必要的对象创建等问题。定位出可能导致内存占用过大的代码段,并进行优化和改进。
  6. 调整JVM参数和内存配置:根据应用程序的需求和服务器资源情况,适当调整JVM的参数和内存配置。可以调整堆内存大小、新生代和老年代的比例、垃圾回收器的选择等,以达到更好的性能和内存利用率。

通过以上步骤的分析,可以确定是否存在内存泄漏或者内存使用过大的问题,并定位问题出现的位置。根据分析结果,进行相应的优化和调整,以减少频繁的老年代GC和提高应用程序的性能。

简单的GC日志查看分析

阅读 GC 日志是处理 Java 虚拟机内存问题的基础技能,它只是一些人为确定的规则,没有太多技术含量。

每一种收集器的日志形式都是由它们自身的实现所决定的,换而言之,每个收集器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性,例如以下两段典型的 GC 日志:

  1. 33.125:[GC[DefNew:3324K->152K(3712K),0.0025925 secs]3324K->152K(11904K),0.0031680 secs]
  2. 100.667:[Full GC[Tenured:0 K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007

最前面的数字33.125: 和 100.667: 代表了 GC 发生的时间,这个数字的含义是从 Java 虚拟机启动以来经过的秒数。

GC 日志开头的 [GC 和 [Full GC 说明了这次垃圾收集的停顿类型,而不是用来区分新生代 GC 还是老年代 GC 的。

如果有 Full ,说明这次 GC 是发生了 Stop-The-World 的,例如下面这段新生代收集器 ParNew 的日志也会出现 [Full GC(这一般是因为出现了分配担保失败之类的问题,所以才导致 STW)。如果是调用 System.gc() 方法所触发的收集,那么在这里将显示 [Full GC(System)

[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]

接下来的 [DefNew[Tenured[Perm 表示 GC 发生的区域,这里显示的区域名称与使用的 GC 收集器是密切相关的,例如上面样例所使用的 Serial 收集器中的新生代名为 "Default New Generation",所以显示的是 [DefNew。如果是 ParNew 收集器,新生代名称就会变为 [ParNew,意为 "Parallel New Generation"。如果采用 Parallel Scavenge 收集器,那它配套的新生代称为 PSYoungGen,老年代和永久代同理,名称也是由收集器决定的。

后面方括号内部的 3324K->152K(3712K)含义是GC前该内存区域已使用容量 -> GC后该内存区域已使用容量 (该内存区域总容量)。而在方括号之外的 3324K->152K(11904K) 表示 GC前Java堆已使用容量 -> GC后Java 堆已使用容量 (Java 堆总容量)

再往后,0.0025925 secs 表示该内存区域 GC 所占用的时间,单位是秒。有的收集器会给出更具体的时间数据,如 [Times:user=0.01 sys=0.00,real=0.02 secs] ,这里面的 user、sys 和 real 与 Linux 的 time 命令所输出的时间含义一致,分别代表用户态消耗的 CPU 时间、内核态消耗的 CPU 事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。

CPU 时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘 I/O、等待线程阻塞,而 CPU 时间不包括这些耗时,但当系统有多 CPU 或者多核的话,多线程操作会叠加这些 CPU 时间,所以读者看到 user 或 sys 时间超过 real 时间是完全正常的。

29.判定一个对象不可达要回收,举个实际例子来说明。有没有从代码层面去直接分析一下?

JVM 主要通过引用计数法可达性分析算法来判断对象是否可达:

  1. 引用计数法:每个对象有一个引用计数器,当有引用指向该对象时,计数器加1;当引用失效时,计数器减1。计数器为0的对象即为不可达。
  2. 可达性分析算法:从根对象(GC Roots)开始向下搜索,所有能通过引用链到达的对象都是可达的,反之为不可达。

举个实际例子来说明

假设有一个简单的社交媒体应用程序,用户可以创建帖子,并在帖子下方评论。每个评论都有一个指向帖子的引用,而帖子也有一个指向用户的引用,这样就形成了一个引用链。当用户删除帖子时,这个引用链会发生变化。

在删除帖子后,如果垃圾回收系统检测到帖子对象不再被用户代码所引用,也没有其他对象通过其他引用链与之相连,那么这个帖子对象就会被判定为不可达。换句话说,垃圾回收器无法从程序的根对象(如全局变量或活动的线程栈)访问到这个帖子对象。

在这种情况下,垃圾回收器会将这个不可达的帖子对象标记为待回收,然后在合适的时机进行垃圾回收,将其内存释放,并将其回收用于后续的对象分配。这样,系统就能有效地管理内存,避免出现内存泄漏问题,同时保持程序的稳定性和性能。

代码层面分析说明

  1. public class GarbageCollectionExample {
  2. static class User {
  3. String name;
  4. Post post;
  5. User(String name) {
  6. this.name = name;
  7. }
  8. }
  9. static class Post {
  10. String content;
  11. Comment comment;
  12. Post(String content) {
  13. this.content = content;
  14. }
  15. }
  16. static class Comment {
  17. String text;
  18. Comment(String text) {
  19. this.text = text;
  20. }
  21. }
  22. public static void main(String[] args) {
  23. // 创建用户和帖子对象,并建立引用链
  24. //创建了一个 User 对象,一个 Post 对象和一个 Comment 对象
  25. User user = new User("Alice");
  26. Post post = new Post("Hello World");
  27. Comment comment = new Comment("Nice post!");
  28. //并且通过 user.post 和 post.comment 建立了引用链。
  29. //此时,这些对象都是可达的,因为有从根对象 user 到达它们的路径。
  30. user.post = post;
  31. post.comment = comment;
  32. // 删除用户对帖子的引用
  33. //当我们删除用户对帖子的引用时,即将 user.post 设为 null
  34. //现在 user 不再引用 post 对象。
  35. user.post = null;
  36. // 触发垃圾回收
  37. System.gc();
  38. // 睡眠一段时间,确保垃圾回收发生
  39. //虽然调用了 System.gc(),但这只是一个建议,JVM 可能不会立即执行垃圾回收。
  40. //为了确保垃圾回收发生,我们加了一个 Thread.sleep(1000) 来等待一段时间。
  41. try {
  42. Thread.sleep(1000);
  43. } catch (InterruptedException e) {
  44. e.printStackTrace();
  45. }
  46. }
  47. }

详细的代码解释可见注释,但这个来说:

  • 可达对象:在 user.post = null; 之前,所有对象 user, post, comment 都是可达的。
  • 不可达对象:在 user.post = null; 之后,postcomment 对象变得不可达,因为没有其他活动对象引用它们。user 对象仍然是可达的。

当我们将 user.post 设为 null 后,post 对象和它引用的 comment 对象都变得不可达,因为没有其他对象持有它们的引用。这意味着这些对象不再能从任何活动的根对象(例如活动的线程栈变量或静态变量)到达,因此它们可以被垃圾回收器回收。

在执行 System.gc(); 后,垃圾回收器会在合适的时机回收 postcomment 对象,释放它们占用的内存。即使垃圾回收器没有立即执行,这些对象最终会被回收,因为它们已经不可达了。

从代码层面分析对象的可达性,主要通过观察对象之间的引用关系来判断。当一个对象不再被任何活动对象引用时,该对象就变得不可达,垃圾回收器可以回收它

参考书籍、文献和资料

1.https://www.cnblogs.com/czwbig/p/11127124.html

2.https://www.cnblogs.com/czwbig/p/11127159.html

3.JVM进阶(八)——Stop The World(停顿类型STW)_jvm stw 是怎么停止线程_ladymorgana的博客-CSDN博客

4.百度安全验证

5.JAVA锁优化和膨胀过程_LYuanZhuo的博客-CSDN博客

6.https://www.koofun.com/pro/queanswers?proquestionId=1672

7.深入理解ClassLoader工作机制(jdk1.8)_zthgreat的博客-CSDN博客

8.https://www.cnblogs.com/ffaiss/p/11510839.html

9.OOM问题排查及原因解析_小山沟里的程序猿的博客-CSDN博客

10.新浪博客

11.Java内存溢出(OOM)异常排查指南_pbuzhidaol的博客-CSDN博客

12.CMS垃圾回收器存在的问题及解决方案_霁云HYY的博客-CSDN博客

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/869826
推荐阅读
相关标签
  

闽ICP备14008679号