目录
9. 什么是双亲委派机制?双亲委派机制有什么好处?如何打破双亲委派机制?
32. Minor GC与Full GC分别在什么时候发生?
43. 你知道哪几种垃圾回收器,各自的优缺点,重点讲一下CMS和G1。
48. System.gc() 和 Runtime.getRuntime().gc()的区别是什么?
1. 什么是JVM?
(1)JVM指的是Java虚拟机,本质上是一个运行在计算机上的程序,它的职责是运行Java字节码文件,作用是为了支持跨平台特性。
(2)JVM的功能有三项:第一是解释执行字节码指令(解释运行);第二是管理内存中对象的分配,完成自动的垃圾回收(内存管理);第三是优化热点代码提升执行效率(即时编译)。
(3)JVM组成分为类加载子系统、运行时数据区、执行引擎、本地接口这四部分。
2. 请你介绍JVM的整体结构
JVM是运行所有Java应用程序的核心引擎,整体结构主要包括类加载器、运行时数据区、执行引擎、本地库接口和本地方法库。
(1)类加载器
负责加载.class文件到JVM中。主要分为三种类型:Bootstrap ClassLoader(启动类加载器)、Extension ClassLoader(扩展类加载器)、System ClassLoader(系统类加载器)。
(2)运行时数据区
① 方法区:存储已被加载的类信息、常量、静态变量等数据。
② 堆:Java内存管理中最大的一块区域,用于存放对象实例和数组。
③ 栈:每个线程运行时都有一个栈,用于存储局部变量、操作数栈、动态链接、方法出口等信息。
④ 程序计数器:每个线程都有一个程序计数器,用于存储指向下一条指令的地址。
⑤ 本地方法栈:为虚拟机使用到的Native方法服务。
(3)执行引擎(Execution Engine)
负责执行字节码。主要包含以下部分:
① 解释器:逐条解释字节码并执行。
② 即时编译器:将热点代码编译成机器码,提高执行效率。
③ 垃圾回收器:负责回收不再使用的对象,释放内存空间。
(4)本地库接口(Native Interface)
与本地库(如C/C++库)交互的接口,允许Java代码调用其他语言编写的本地库。
(5)本地方法库(Native Method Libraries)
存放本地方法的库,如C/C++编写的库。
3. 了解过字节码文件的组成吗?
(1)字节码文件是Java程序编译后的中间代码文件,它的扩展名为.class。字节码文件本质上是一个二进制的文件,无法直接用记事本等工具打开阅读其内容。需要通过专业的工具打开。
① 开发环境使用jclasslib插件
② 服务器环境使用javap -v命令
(2)字节码文件的组成主要包括以下几个部分:
① 魔数:所有的.class文件都是以一个魔数开始,用来标识这是一个Java编译后的字节码文件。魔数的值为0xCAFEBABE。
版本号:紧接着魔数之后的是版本号,它表示这个字节码文件是由哪个版本的Java编译器编译的。
② 常量池:常量池是.class文件中非常重要的部分,它包含着类和接口的符号信息,以及一些常量值。常量池中的每一项都有一个特定的 tag 来标识该项的类型。
③ 访问标志:访问标志用来标识类或接口的访问类型,比如public、final、abstract等。
④ 类索引、父类索引、接口索引集合:这些索引指向常量池中的相应条目,分别表示当前类的名称、父类的名称以及实现的接口列表。
⑤ 字段表:字段表包含类的所有字段的信息,包括字段的名称、类型、修饰符等。
⑥ 方法表:方法表包含类的所有方法的信息,包括方法的名称、返回值类型、参数类型、修饰符等。
⑦ 属性表:属性表包含类的各种属性信息,比如代码(Code)属性,它包含了方法体的字节码指令;还有异常处理、内部类、LineNumberTable等属性。
4. 说一下运行时数据区(介绍一下JVM内存模型)。
运行时数据区指的是JVM所管理的内存区域,其中分成两大类——线程共享:方法区、堆;线程不共享:本地方法栈、虚拟机栈、程序计数器。
(1)程序计数器
程序计数器是一块较小的内存空间,用于存储当前线程正在执行的字节码指令地址。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,因此程序计数器用于记录各个线程的执行位置,以便线程切换后能恢复正确的执行位置。
(2)虚拟机栈
每个线程在创建时都会分配一个虚拟机栈,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法在执行时会创建一个栈帧(Stack Frame)用于存储这些信息。线程结束,其对应的虚拟机栈也会被销毁。
(3)本地方法栈
本地方法栈与虚拟机栈的作用类似,但它是为虚拟机使用到的Native方法服务的。在Java中,有些方法是通过其他语言(如C/C++)实现的,这些方法称为Native方法。
(4)堆
堆是Java虚拟机管理的内存中最大的一块区域,用于存储对象实例和数组。堆是所有线程共享的内存区域,垃圾回收器的主要工作区域也是堆。
(5)方法区
方法区用于存储已被虚拟机加载的类信息、常量、静态变量等数据。方法区是所有线程共享的内存区域。在Java 8及之前版本,方法区被称为永久代(Permanent Generation),Java 8之后,方法区被元空间(Metaspace)取代。
回答:JVM内存模型主要包括以下几个部分:方法区用于存储已被加载的类的信息、常量池、静态变量、即时编译后的代码等数据。堆用于存储对象实例和数组,垃圾收集器主要管理堆内存。虚拟机栈和本地方法栈用于存储执行时所需的信息。程序计数器用于记录当前线程所执行的字节码指令的地址。运行时常量池用于存储编译期生成的各种字面量和符号引用。直接内存用于存储那些不经常在Java堆中使用的数据,如大文件、图像等。
5. 哪些区域会出现内存溢出,会有什么现象?
内存溢出指的是内存中某一块区域的使用量超过了允许使用的最大值,从而使用内存时因空间不足而失败,虚拟机一般会抛出指定的错误。
(1)堆:溢出之后会抛出OutOfMemoryError,并提示是Java heap Space导致的。
(2)栈:溢出之后会抛出StackOverflowError。
(3)方法区:溢出之后会抛出OutOfMemoryError,JDK7及之前提示永久代,JDK8及之后提示元空间。
(4)直接内存:溢出之后会抛出OutOfMemoryError。
6. 请你说说类的生命周期。
类的生命周期是指从类被加载到虚拟机(JVM)开始,到卸载出虚拟机的整个过程。以下是类在JVM中的主要生命周期阶段:
(1)加载
JVM通过类加载器读取类的字节码文件,并将其数据转换成方法区中的数据结构,同时在堆中生成一个对应的java.lang.Class对象用于封装类在方法区内的数据结构。
(2)链接
验证:确保加载的类信息符合JVM规范,没有安全方面的问题。
准备:为类变量分配内存,并设置默认初始值(如int类型默认为0,引用类型默认为null)。
解析:将类、接口、字段和方法的符号引用转换为直接引用。
(3)初始化
初始化阶段是执行类构造器<clinit>()方法的过程,该方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。此阶段将为类变量赋予正确的初始值。
(4)使用
在这个阶段,类的对象可以创建,类的方法可以被调用。
(5)卸载
当类不再被任何地方引用时,垃圾收集器可以判定这个类为不再需要的类,随后JVM将卸载此类,释放其占用的内存空间。
其中(1)~(3)为类的加载过程
7. 有几种类加载器?
类加载器(Class Loader)是Java虚拟机(JVM)的一部分,负责在运行时将Java类库中的类(.class文件)加载到JVM中,并保证这些类的正确性。类加载器在Java程序运行中扮演着非常重要的角色,因为它们决定了类何时被加载、如何被加载以及被哪个加载器加载。
(1)引导类加载器(Bootstrap Class Loader)
负责加载Java核心库(如java.*包中的类),这些库位于JVM的lib目录(如JRE的lib/rt.jar文件)。
(2)扩展类加载器(Extension Class Loader)
加载Java的扩展库,这些库位于JVM的lib/ext目录或由系统属性java.ext.dirs指定的目录中的类库。
(3)应用类加载器(Application Class Loader)
加载当前应用的类路径(Classpath)中的类库,这是程序中默认的加载器。
(4)自定义类加载器(Custom Class Loader)
用户可以自定义类加载器,以实现特殊的加载需求,如热部署、加密解密等。
8. 请描述JVM是如何加载class文件的?
JVM通过类加载器加载class文件,这个过程分为加载、链接和初始化三个阶段。在加载阶段,JVM读取class文件,并在方法区中创建对应的Class对象。链接阶段包括验证、准备和解析,确保类的正确性并为类变量分配内存。初始化阶段执行类的静态初始化代码。
类加载器通常遵循委派模型,类加载请求首先由父类加载器处理,如果父类加载器无法加载,再由子类加载器尝试。类加载器之间存在父子关系,并且类一旦被加载,就会缓存在JVM中。
9. 什么是双亲委派机制?双亲委派机制有什么好处?如何打破双亲委派机制?
(1)含义
当一个类加载器接收到加载类的任务时,会向上交给父类加载器查找是否加载过,再由顶向下进行加载。 除了顶层的引导类加载器(Bootstrap ClassLoader)外,其余的类加载器都应该有自己的父类加载器。类加载器在尝试自己加载类之前,首先委托给其父类加载器去尝试加载该类,只有当父类加载器加载失败时,子类加载器才会尝试自己加载该类。
(2)好处
① 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性。
② 保证安全性:通过双亲委派模型,Java核心API中定义的类型不会被随意替换,防止了核心API被随意篡改,确保了Java应用程序的安全性。
③ 沙箱安全机制:防止恶意代码去干扰正常的Java类库的加载。例如,用户自定义的类加载器无法加载名为java.lang.Object的类,因为即使它自定义了该类,最终也会被引导类加载器加载真正的java.lang.Object类。
打破双亲委派机制的方法:实现自定义类加载器,重写defineClass方法,将双亲委派机制的代码去除。
(3)如何打破
① 重写loadClass方法:
可以通过自定义类加载器,并重写java.lang.ClassLoader类的loadClass方法来打破双亲委派模型。在重写的方法中,可以不遵循双亲委派机制,直接尝试自己加载类。
② 线程上下文类加载器:
Java提供了线程上下文类加载器,可以通过Thread.currentThread().setContextClassLoader()来设置。它允许在运行时动态地覆盖双亲委派模型,使得Java类可以由不同于当前类的类加载器加载。
10. 请你介绍JVM的堆的结构。
JVM的堆是Java运行时数据区域中最大的一块,它是所有线程共享的内存区域,主要用来存放实例化的对象和数组。下面是堆的组成:
(1)新生代
大多数新创建的对象首先在新生代分配。新生代分为三个子区域:
Eden空间:大多数新创建的对象首先在这里分配。
S0和S空间1。当Eden空间满时,进行Minor GC(新生代垃圾回收),存活的对象会被复制到一个Survivor空间,而非存活对象则被清除。
(2)老年代
在新生代中经历了几次垃圾回收后仍然存活的对象,会被移动到老年代。老年代的空间比新生代大,通常存放长时间存活的对象。
(3)永久代/元空间
在Java 8之前,方法区被称为永久代,它是堆的一部分。从Java 8开始,永久代被移除,取而代之的是元空间,它直接在本地内存中分配,不在JVM堆内存中。
11. 请你介绍JVM的栈的结构(通常指的是虚拟机栈)。
JVM栈是线程私有的,每个线程创建时都会有自己的栈空间。栈的生命周期与线程相同,当线程结束时,其对应的栈也会被销毁。下面是虚拟机栈的组成:
(1)栈帧
每个方法调用时,都会在栈上创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。当线程调用一个方法时,JVM会在栈顶创建一个新的栈帧,用于存储该方法的状态信息。当方法执行完毕后,对应的栈帧会被销毁,操作数栈中的返回值(如果有)会被传回给上一个栈帧,然后上一个栈帧的操作数栈会弹出返回地址,线程的控制权会回到调用方法的地址继续执行。
(2)局部变量表
用于存储方法执行过程中的局部变量(包括基本数据类型、对象引用、returnAddress类型)。
(3)操作数栈
用于存放方法执行过程中的中间结果,以及调用其他方法时的参数和返回值。
(4)动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,用于支持方法调用过程中的动态链接。
(5)方法出口
记录方法正常退出或者异常退出的地址。
12. 请你介绍JVM的方法区的结构。
方法区是所有线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Java 7及之前版本,方法区是堆的一个逻辑部分,常常被称为永久代。从Java 8开始,永久代被移除,方法区的实现改为使用本地内存中的元空间。下面是方法区的组成:
(1)类信息
包括类的版本、字段、方法、接口和父类信息,以及常量池。
(2)常量池
每个类都有一个常量池,用于存储编译期生成的各种字面量和符号引用。这些字面量和符号引用在类加载后会被解析为直接引用。
(3)字段信息
包括类和实例的字段信息,如字段名、字段类型、修饰符等。
(4)方法信息
包括方法名、返回类型、参数类型、修饰符、方法字节码、异常表等。
(5)构造函数信息
包括构造函数的名称、参数类型、修饰符等。
(6)静态变量
类级别的变量,存储在方法区中。
13. 如何判断堆上的对象没有被引用?
(1)引用计数法
含义:给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。如果计数器的值为0,则表示对象没有被引用。
缺点:不能解决循环引用的问题,即两个对象相互引用,但不再被外部引用时,它们的计数器都不为0,因此不会被回收。
(2)可达性分析
这是最常用的方法。它从一系列称为GC Roots的对象开始,通过引用链遍历来确定对象是否可达。如果一个对象通过引用链从GC Roots无法到达,则认为该对象是不可达的,即没有被引用。
GC Roots包括:
① 虚拟机栈(栈帧中的本地变量表)中引用的对象。
② 方法区中类静态属性引用的对象。
③ 方法区中常量引用的对象。
④ 本地方法栈中JNI(即通常所说的Native方法)引用的对象。
14. JVM 中都有哪些引用类型?
(1)强引用
这是最常见的引用类型。如果一个对象具有强引用,那么垃圾收集器不会回收它,即在内存不足时,JVM宁愿抛出OutOfMemoryError也不会回收具有强引用的对象。
(2)软引用
软引用用来描述一些有用但非必需的对象。如果一个对象只具有软引用,当内存足够时,垃圾收集器不会回收它;但当内存不足时,垃圾收集器可能会回收软引用对象。软引用适合用来实现内存敏感的高速缓存。
(3)弱引用
弱引用也是用来描述非必需对象的,但它的强度比软引用更弱。如果一个对象只具有弱引用,那么在垃圾收集器线程扫描到它时,不管当前内存是否足够,都会被回收。弱引用适合用来实现缓存。
(4)虚引用
虚引用是最弱的引用类型。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。虚引用的唯一目的是在这个对象被垃圾收集器回收时收到一个系统通知。
(5)终结器引用
特殊的引用类型,用来实现对象的finalize方法。当对象不可达时,垃圾收集器会检查是否有终结器引用指向该对象,如果有,则会将对象放入一个F-Queue队列,稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程来执行它。
这些引用类型在java.lang.ref包中定义,并且通过相应的类(如SoftReference、WeakReference、PhantomReference)来使用。不同的引用类型允许开发者在内存管理方面有更多的控制,可以根据对象的生命周期和内存使用需求来选择合适的引用类型。
15. 有哪些常见的垃圾回收算法?
(1)标记-清除
标记阶段:遍历所有GC Roots,标记所有可达的对象。
清除阶段:遍历堆内存,回收未被标记的对象。
缺点:会产生内存碎片,影响内存使用效率。
(2)复制算法
将内存分为两个相等的部分,每次只使用其中一部分。
复制阶段:将存活的对象从旧的一侧复制到新的一侧。
清理阶段:清理旧的一侧,回收未复制的对象。
优点:没有内存碎片,效率较高。
缺点:内存使用率不高,因为一半的内存空间不能使用。
(3)标记-整理算法
标记阶段:与标记-清除算法相同。
整理阶段:将存活的对象向内存的一端移动,然后清理边界以外的内存。
优点:解决了内存碎片问题,提高了内存利用率。
缺点:实现复杂,效率可能不如复制算法。
(4)分代收集算法
将内存分为新生代和老年代。
新生代采用复制算法,老年代采用标记-整理或标记-清除算法。
优点:新生代中的对象存活率低,复制算法效率高;老年代中的对象存活率高,标记-整理或标记-清除算法效率较高。
缺点:需要管理两个不同区域的内存,增加了复杂性。
16. 请你介绍一下垃圾回收器?
(1)JDK 8及之前
ParNew + CMS:
ParNew是Serial GC的并行版本,适合于多核CPU环境。
CMS(Concurrent Mark Sweep)是一种低延迟的垃圾回收器,适用于多核CPU环境,尤其是在关注暂停时间(Pause Time)的场景。
这种组合适合于那些需要低延迟和高吞吐量的应用程序。
Parallel Scavenge + Parallel Old:
Parallel Scavenge是一个关注吞吐量的垃圾回收器,它允许用户设置吞吐量目标。
Parallel Old是Parallel Scavenge的老年代版本,使用并行算法来回收老年代。
这种组合适合于那些可以接受较长暂停时间,但需要高吞吐量性能的应用程序。
(2)JDK 9之后
从JDK 9开始,由于G1垃圾回收器日趋成熟,JDK默认的垃圾回收器已经修改为G1。
G1:
适用于多核CPU环境,特别是对于大堆。
提供了可控的停顿时间,适合于需要低延迟的应用场景。
强烈建议在生产环境中使用G1。
17. 请你介绍Java的垃圾回收机制。
(1)垃圾回收的目的
垃圾回收的主要目的是识别并回收不再使用的对象所占用的内存资源,防止内存泄漏,确保Java应用程序在长时间运行过程中内存使用的稳定性。
(2)垃圾回收的对象
在Java中,垃圾回收器主要负责回收堆空间中的对象。堆空间是Java应用程序创建对象的主要区域。
(3)垃圾回收算法
标记-清除算法:首先标记出所有需要回收的对象,然后统一回收这些对象所占用的内存。
标记-整理算法:在标记阶段完成后,将所有存活的对象压缩到内存的一端,然后清理边界以外的内存。
复制算法:将可用内存划分为两块,每次只使用其中一块。在垃圾回收时,将存活的对象复制到另一块内存区域,然后清理掉旧的内存区域。
分代收集算法:将堆空间划分为不同的代(如新生代、老年代),根据不同代的特性采用不同的垃圾回收策略。
(4)垃圾回收器
Serial GC:单线程执行的垃圾回收器,适用于小型应用。
Parallel GC:多线程执行的垃圾回收器,适用于吞吐量优先的应用。
Concurrent Mark Sweep (CMS) GC:适用于响应时间优先的应用,减少停顿时间。
Garbage-First (G1) GC:旨在提供可预测的停顿时间,同时实现高吞吐量。
ZGC(Z Garbage Collector)和Shenandoah GC:是较新的垃圾回收器,致力于实现低延迟的垃圾回收。
(5)垃圾回收触发条件
Minor GC:当新生代空间不足时,触发Minor GC,回收新生代内存。
Major GC:当老年代空间不足时,触发Major GC,回收老年代内存。
Full GC:当整个堆空间不足时,触发Full GC,回收整个堆空间。
(6)垃圾回收的影响
虽然垃圾回收机制大大简化了内存管理,但不当的垃圾回收策略可能会导致应用程序性能下降,如频繁的GC停顿(Stop-The-World,STW)会影响应用的响应时间。
18. GC Roots有哪些?
在Java虚拟机(JVM)的垃圾回收(GC)机制中,GC Roots是指那些在垃圾回收过程中不会被回收的对象,作为起点来跟踪和标记存活的对象。以下是一些常见的GC Roots对象:
(1)虚拟机栈栈帧中的本地变量中的引用对象
当方法在执行时,它的本地变量表中的引用对象都可以作为GC Roots。
(2)方法区中的静态变量
类的静态变量引用的对象也可以作为GC Roots。
(3)方法区中的常量引用
常量池中的引用对象,比如字符串常量池中的字符串引用。
(4)本地方法栈中的JNI(即通常所说的Native方法)引用的对象
即那些在本地方法执行时,引用的对象也可以作为GC Roots。
(5)系统类加载器
系统类加载器本身及其加载的类对象。
(6)被同步锁持有的对象
即当前被synchronized关键字持有的对象。
(7)JVM内部的引用
如JVM内部的类对象,一些异常对象等。
(8)反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
总的来说,GC Roots包括了一系列在垃圾回收过程中不会被回收的根对象,通过这些根对象,垃圾回收器可以确定存活对象集合并进行垃圾回收。
19. 怎么判定对象已经“死去”?
(1)可达性分析
可达性分析是从一系列称为GC Roots的对象开始,沿着对象的引用链向下搜索,如果一个对象没有任何引用链与GC Roots相连,则认为这个对象是不可达的。
(2)finalize()方法
即使对象在可达性分析中被标记为不可达,它也并非立即“死去”。一个对象在真正被回收之前,至少要经历两次标记过程:
如果对象在第一次标记后,发现它没有覆盖java.lang.Object类的finalize()方法,或者finalize()方法已经被虚拟机调用过,那么这个对象就会被判定为“确实死亡”,可以被回收。
如果对象在第一次标记后,发现它覆盖了finalize()方法且还未被虚拟机调用,那么这个对象将会被放置在一个名为F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。
在执行finalize()方法时,如果对象与引用链上的任何一个对象建立了联系,那么这个对象将被移除“即将回收”的集合,它“复活”了。
需要注意的是,finalize()方法运行代价高昂,不确定性大,且无法保证各个对象的调用顺序,因此尽量避免使用finalize()方法来拯救对象。在Java 9之后,finalize()方法已经被标记为不推荐使用(deprecated)。
20. HotSpot为什么要分为新生代和老年代?
(1)对象生命周期特性
绝大多数对象都是“朝生夕死”的,即它们在创建后很快就会变得不可达并可以被回收。
将这些短生命周期的对象放在新生代,可以使得垃圾回收更加高效,因为新生代的GC(通常是Minor GC)可以更频繁地进行,且速度较快。
(2)性能优化
通过对不同生命周期的对象进行分代,可以针对不同代的特点采用最合适的垃圾回收策略,从而优化整体的垃圾回收性能。
(3)减少Full GC频率
如果不区分新生代和老年代,那么每次垃圾回收都可能需要对整个堆进行回收,这将导致Full GC非常频繁,从而严重影响应用程序的性能。通过分代,可以尽量减少Full GC的次数。
(4)回收算法的选择
新生代通常采用复制算法,这种算法在回收时需要复制存活的对象到另一个空间,因此要求内存空间是连续的。新生代的大小可以相对较小,这使得复制操作的成本较低。
老年代则采用标记-清除(Mark-Sweep)或标记-整理(Mark-Compact)算法,这些算法适用于存活对象比例较高的区域,可以处理更大的内存空间。
(5)内存分配策略
新生代和老年代可以有不同的内存分配和回收策略。例如,新生代可以更频繁地进行垃圾回收,而老年代则可以减少回收次数,以降低应用程序的停顿时间。
(6)空间分配担保
当新生代进行垃圾回收时,如果存活对象太多,无法在新生代中容纳,那么部分对象会被晋升到老年代。这种机制称为“空间分配担保”,它确保了新生代在垃圾回收时的稳定性。
21. HotSpot GC的分类和触发条件是什么?
(1)分类
Minor GC(新生代GC)、Major GC(老年代GC)、Full GC
(2)触发条件
① Minor GC触发条件
当新生代空间不足时,即Eden区满时,会触发Minor GC。
在Minor GC过程中,如果Survivor区无法容纳存活的对象或者对象年龄达到一定阈值(默认为15),这些对象会被晋升到老年代。
② Major GC触发条件
当老年代空间不足时,会触发Major GC。
Major GC通常伴随着Minor GC,即先进行Minor GC,如果之后仍有必要,则进行Major GC。
③ Full GC触发条件
清理整个堆空间,包括新生代和老年代。
老年代空间不足。永久代或元空间空间不足。System.gc()被显式调用。
22. 什么情况下新生代对象会晋升到老年代?
(1)年龄条件
对象在新生代Survivor区经历了一定次数的Minor GC后仍然存活(默认为15次,可以通过JVM参数-XX:MaxTenuringThreshold设置),这些对象会被晋升到老年代。
(2)空间分配担保
在进行Minor GC时,如果Survivor空间不足以容纳所有存活的对象,或者存活对象的总大小超过了Survivor空间的容量,那么这些存活的对象会直接晋升到老年代。
(3)大对象分配
JVM提供了一个参数-XX:PretenureSizeThreshold(默认值为0),当创建的对象大小超过这个阈值时,对象会直接在老年代分配,而不是在新生代。这样可以避免大对象在新生代中频繁复制。
(4)晋升担保
在Minor GC之前,JVM会检查老年代是否有足够的空间来容纳新生代所有存活的对象。如果有足够的空间,Minor GC才会进行。否则,JVM会触发Full GC来清理老年代空间,以确保有足够的空间容纳晋升的对象。
23. 发生新生代GC的时候需要扫描老年代的对象吗?
在发生Young GC(也称为Minor GC)的时候,通常不需要扫描老年代的对象。Young GC的主要目标是清理新生代中的不再被引用的对象,以释放内存空间。
虽然在常规的Young GC过程中不需要扫描老年代,但是在某些特殊情况下,如晋升担保(Promotion Guarantee)失败时,JVM可能需要进行额外的检查,以确保有足够的老年代空间来接纳晋升的对象。
在使用某些垃圾回收器(如G1垃圾回收器)时,可能会进行跨代引用的扫描,但这通常是为了更复杂的垃圾回收策略,而不仅仅是Young GC。
24. 简述一下Java的内存泄漏
(1)含义
Java内存泄漏是指在Java程序中,某些对象已经不再被使用,但由于仍然被引用,导致垃圾回收器无法回收它们,随着时间的推移,可用的内存资源越来越少,最终可能导致程序运行缓慢甚至崩溃。
(2)原因
静态集合类(如HashMap、ArrayList等)中的对象无法被回收。
使用线程池或资源池时,未正确释放资源。
使用Java IO流、数据库连接等资源未关闭。
长生命周期对象持有短生命周期对象的引用,导致短生命周期对象无法被回收。
内部类和外部类相互引用,导致两者都无法被回收。
(3)预防措施
及时清除不再使用的对象引用。
使用弱引用代替强引用。
避免在长生命周期的对象中持有短生命周期对象的引用。
使用Java内存分析工具检测内存泄漏。
在代码审查时,关注内存泄漏问题。
(4)影响
内存泄漏会导致程序运行缓慢、卡顿,甚至发生OutOfMemoryError异常,影响程序稳定性。
25. 什么时候会触发Full GC?
触发Full GC的条件因Java虚拟机的实现和版本而异,但以下是一些常见的触发Full GC的情况。
(1)System.gc()调用
当Java程序中显式调用了System.gc()时,JVM会尽力去执行Full GC。
(2)老年代空间不足
当JVM在老年代中没有足够空间来进行对象分配时,会触发Full GC。
(3)永久代空间不足
在Java 8之前,如果永久代(用于存储类信息、常量池等)空间不足,也会触发Full GC。Java 8之后,永久代被元空间(Metaspace)取代,但元空间不足也可能触发Full GC。
(4)CMS GC失败
在使用CMS垃圾收集器时,如果预备的GC过程中,老年代没有足够空间容纳新晋升的对象,就会触发Full GC。
(5)JVM退出
在JVM关闭之前,通常会执行一次Full GC来清理所有内存。
26. 请你说说类的加载过程。
Java中的类加载过程包括加载、链接和初始化三个阶段。加载阶段是读取类的字节码文件,并在JVM中创建一个Class对象。链接阶段包括验证、准备和解析,确保类的正确性并为类变量分配内存。初始化阶段执行类的静态初始化代码。
至于类卸载,当一个类不再被使用,并且其ClassLoader、所有实例和Class对象都不再被引用时,JVM可能会卸载这个类以释放内存。类卸载的条件比较严格,通常发生在程序结束或使用自定义类加载器的情况下。
27. 请解释JVM中堆和栈的区别。
堆和栈是JVM中的两种不同的内存区域。栈是线程私有的,用于存储局部变量和方法调用的上下文,生命周期与线程相同,内存分配和回收是自动的,遵循FILO原则。堆是所有线程共享的,用于存储Java对象实例和数组,生命周期与JVM相同,由垃圾收集器管理。栈的大小相对较小,而堆的大小通常较大。栈存储基本数据类型和对象的引用,而堆存储对象实例和数组。此外,栈的访问速度通常比堆快,但堆需要处理多线程共享的问题。
28. 请你说说对象分配规则。
在JVM中,对象的分配遵循以下规则:首先,新创建的对象通常在新生代的Eden区尝试分配。如果Eden区空间不足,会触发Minor GC来清理空间。对于大对象,它们会直接在老年代分配,以减少内存复制。对象的年龄会随着GC次数的增加而增长,当对象的年龄达到一定阈值(默认为15)后,它们会被晋升到老年代。此外,如果Survivor区中相同年龄所有对象的总大小超过Survivor空间的一半,那么这些对象也会提前晋升到老年代。在Minor GC之前,JVM还会进行空间分配担保,以确保有足够的老年代空间来容纳可能晋升的对象。
29. 请你说说对象创建过程。
Java对象的创建过程主要包括以下几个步骤:首先,JVM检查要创建的对象的类是否已经被加载,如果没有,则进行类加载。接下来,为对象分配内存空间,并将所有字段初始化为零值。然后,JVM设置对象头,包括类的元数据、哈希码等信息。之后,执行对象的构造方法<init>()来进行实例变量的赋值和初始化块以及构造方法中的代码。最后,将对象的引用赋值给相应的变量,完成对象的创建。
例子:
// 假设有一个类Person
public class Person {
// 实例变量
private String name;
private int age;
// 构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 其他方法...
}
// 创建对象的过程
Person person = new Person("Jack", 30);
当执行new Person("Alice", 30);时,以下是发生的步骤:
① 类加载检查:检查Person类是否已经加载,如果没有,则加载它。
② 内存分配:为Person对象分配足够的内存来存储它的实例变量和对象头。
③ 初始化零值:将name和age字段设置为默认的零值(对于String是null,对于int是0)。
④ 对象头设置:设置对象头信息,如类元数据、哈希码等。
⑤ 执行初始化方法:调用Person类的构造方法,执行this.name = name;和this.age = age;。
⑥ 对象引用赋值:将新创建的Person对象的引用赋值给变量person。
30. 请解释Java类的生命周期。
Java类的生命周期包括以下几个阶段:首先是加载阶段,类加载器读取类的二进制数据,并在方法区中创建相应的数据结构。接着是链接阶段,它包括验证、准备和解析三个子阶段,确保类信息正确无误并为类变量分配内存。然后是初始化阶段,执行类构造器<clinit>()方法,完成类变量的赋值和静态代码块的执行。在使用阶段,类被程序使用,创建对象,调用方法等。最后是卸载阶段,当类不再被使用,并且其类加载器、所有实例和Class对象都不再被引用时,类可能会被JVM卸载,释放内存资源。
31. 在JVM中,永久代会发生垃圾回收吗?
是的,在JVM中,永久代确实会发生垃圾回收。永久代主要存储类的元数据、常量池等信息,当这些信息不再被使用时,它们所占用的空间可以在Full GC期间被回收。不过,从Java 8开始,永久代已经被移除,取而代之的是元空间,它位于本地内存中,其垃圾回收机制与永久代相似,但更加灵活和高效。
32. Minor GC与Full GC分别在什么时候发生?
Minor GC主要发生在新生代,当Eden区或Survivor区空间满了时,或者在Minor GC后Survivor区仍然满了时。Full GC主要发生在老年代,当老年代空间满了,或者在执行Minor GC之前空间分配担保失败时。Minor GC和Full GC的主要区别在于,Minor GC只会清理新生代,而Full GC会清理新生代和老年代,并且需要暂停应用程序的所有用户线程。
33. 对象一定分配在堆中吗?有没有了解逃逸分析技术?
不是所有对象都一定分配在堆中。在Java中,对象默认分配在堆上,因为堆是Java内存模型中最大的内存区域,可以被所有线程共享。但是,如果对象在方法内部创建且只在该方法内部使用,那么它可以被分配在栈上,而不是堆上。这种技术被称为逃逸分析。逃逸分析是一种编译期分析技术,它可以帮助JVM确定一个对象是否在方法执行结束时仍然被外部引用。如果逃逸分析确定一个对象不会在方法执行结束时被外部引用,那么这个对象就可以在栈上分配,而不是在堆上分配。这样,对象的生命周期仅限于当前方法执行结束时,方法执行结束后,这个对象所占用的栈空间就会被回收,减少了GC的压力。
34. 什么是TLAB?
Java虚拟机中,TLAB称为线程本地分配缓冲区。它为每个线程提供了一个小内存区域,用于优化对象创建的性能。当对象创建请求到达时,JVM会首先检查当前线程的TLAB是否还有足够的空间来分配新的对象。如果有,那么对象就在TLAB中分配内存;如果没有,JVM会从堆中分配内存。TLAB的使用可以减少多线程竞争堆内存导致的性能下降,尤其是在对象创建非常频繁且分配的内存量很小时。
35. 什么是STW?什么是OopMap?什么是安全点?
Stop The World(STW)是指在某些垃圾收集器工作期间,所有用户线程都必须暂停执行,直到垃圾收集完成。这是因为垃圾收集器需要暂停用户线程来确保它们不会访问已经被标记为垃圾的对象。
OopMap是一种数据结构,它记录了Java方法执行时每个字节码指令对应的栈帧中对象的引用,用于快速确定对象引用,以便在垃圾收集过程中快速访问和标记对象。
安全点是JVM在执行时设置的检查点,在这个点上,所有线程的状态都是安全的,可以被垃圾收集器访问。安全点通常是方法执行的某个位置,在这个位置上,所有线程的状态都已经更新,可以被垃圾收集器访问。
36. 什么是指针碰撞?
指针碰撞是Java虚拟机中的一种优化技术,用于提高对象的分配速度。在TLAB(Thread Local Allocation Buffer)中,内存被划分为多个小的内存块,每个线程有自己的TLAB。当一个线程需要创建一个新对象时,它会尝试在TLAB中找到一个空闲的内存块来分配。如果找到了空闲块,对象就可以直接分配到这个块中,这种分配方式被称为‘指针碰撞’。指针碰撞技术可以显著提高对象创建的速度,尤其是在对象创建非常频繁的情况下。
37. 什么是空闲列表?
空闲列表是Java虚拟机中一种用于管理内存碎片的技术。当对象被垃圾收集器回收后,堆内存中会留下许多小块的空闲内存,这些小块内存称为内存碎片。为了提高内存的利用率,垃圾收集器会维护一个空闲列表,用于记录可用的内存块。空闲列表的作用是记录可用的内存块,并在内存分配时优先从空闲列表中查找合适的内存块,以减少内存碎片的产生,提高内存的使用效率。
38. 为什么JVM中新生代有两个Survivor区?
JVM中新生代有两个Survivor区,即From Survivor区和To Survivor区,是为了提高垃圾收集的效率。在JVM的垃圾收集过程中,新生代中的对象首先在Eden区创建。当Eden区满了,会触发Minor GC,这时Eden区和From Survivor区中的存活对象会被复制到To Survivor区。在下一个Minor GC之前,对象在To Survivor区中继续存活,这些对象会被复制回From Survivor区。这种设计减少了对象的复制次数,从而提高了性能。同时,两个Survivor区的设计有助于解决内存碎片问题,使得Survivor区可以更有效地存储新的对象。
39. 请你说说Eden和Survior的比例分配。
Eden区可能占据新生代空间的80%,而两个Survivor区各占据10%。这个比例分配不是固定不变的,JVM的配置文件(如java.conf或-Xmx、-Xms等参数)可以调整Eden区和Survivor区的比例,以适应不同的应用场景和性能需求。
40. 为什么要有新生代和老年代?
新生代和老年代的设计是为了优化垃圾回收效率。新生代用于存放新创建的对象,由于这些对象生命周期短,回收频繁,采用复制算法进行垃圾回收,能够快速清理无用的对象。而老年代则存放生命周期较长的对象,采用标记-清除或标记-整理算法,减少回收频率,提高回收效率。这种分代策略能够有效降低垃圾回收的开销,提升JVM的整体性能。
41. 新生代中为什么要分为Eden和Survivor?
新生代之所以分为Eden和Survivor,主要是为了优化垃圾回收效率和内存分配策略。Eden区用于存放新创建的对象,这些对象大多数生命周期较短,便于快速回收。而Survivor区分为两块,采用复制算法,可以在垃圾回收时将存活的对象从一块Survivor区复制到另一块,同时清除不再使用的对象,这样既避免了内存碎片化,又能通过年龄判断机制将长期存活的对象晋升到老年代,从而提高内存利用率和垃圾回收效率。
42. 对象在JVM中是怎么存储的?
在JVM中,对象的存储主要分为三个部分:对象头、实例数据和对齐填充。对象头包括两部分,一是标记字段,用于存储对象的运行时数据,如哈希码、GC分代年龄等;二是类型指针,指向对象的类元信息。实例数据部分存储的是对象真正的内容,即各个字段的值。对齐填充部分不是必须的,仅为了保证对象的大小是8字节的整数倍,便于内存分配和访问效率。此外,对象在堆内存中分配,并通过垃圾回收器进行管理。
43. 你知道哪几种垃圾回收器,各自的优缺点,重点讲一下CMS和G1。
在JVM中,CMS和G1是两种常见的垃圾回收器。CMS回收器以其低停顿时间著称,适用于对响应时间敏感的应用,但它可能会因为内存碎片和无法处理浮动垃圾而导致性能波动。G1回收器则通过将堆划分为多个区域并优先回收价值最大的区域,旨在提供可预测的停顿时间,同时保持较高的吞吐量。然而,G1的内存占用较高,且在某些情况下,停顿时间可能不如预期稳定。总体来说,CMS适合停顿时间要求严格的环境,而G1则在停顿时间和吞吐量之间寻求平衡。
44. GC是什么?为什么要有GC?
垃圾回收是JVM自动管理内存的一种机制,它负责监控和回收不再使用的对象所占用的内存资源。GC的存在使得Java开发者无需手动管理内存分配和释放,从而减少了内存泄漏和悬挂指针等内存管理错误,提高了开发效率和程序的稳定性。GC的必要性在于它能够确保程序的持续运行不会因内存耗尽而中断,同时优化内存使用,降低系统资源消耗。
45. 说说分代收集算法?
分代收集算法是JVM中常用的垃圾回收策略,它基于对象存活周期的不同将堆内存划分为年轻代和老年代。年轻代由于对象生命周期短、死亡率高,采用复制算法,通过Minor GC快速清理不存活的对象,并将存活对象复制到Survivor区或晋升到老年代。老年代则针对对象存活率高的特点,通常采用标记-清除或标记-整理算法,以减少内存碎片或提高空间利用率。分代收集算法通过优化不同生命周期的对象回收方式,提高了垃圾回收的效率,并减少了应用程序的停顿时间。
46. 常见的垃圾回收器算法有哪些,各有什么优劣?
常见的垃圾回收器算法主要包括标记-清除(Mark-Sweep)、标记-整理(Mark-Compact)、复制(Copying)和分代收集。
标记-清除算法简单但会产生内存碎片;标记-整理算法解决了内存碎片问题,但可能增加额外的时间开销;复制算法适用于存活率低的场景,能够快速回收且无内存碎片,但会浪费一定内存空间。分代收集结合了以上算法,通过对不同生命周期的对象采用不同的回收策略,提高了回收效率,但实现复杂且需要额外的内存空间来维护代际信息。每种算法都有其适用的场景,选择合适的算法能够有效提升JVM的性能。
47. 说说Java GC机制?GC Roots有哪些?
Java垃圾回收机制,它负责自动管理内存,回收不再使用的对象所占用的内存资源。Java GC的基本原理是通过可达性分析算法来判定对象是否存活,即哪些对象是垃圾,需要被回收。GC过程中,垃圾收集器会从一组称为GC Roots的对象开始,递归地识别并标记所有存活的对象,最后统一回收未被标记的对象所占用的内存空间。
GC Roots主要包括以下几类对象:
首先,虚拟机栈(栈帧中的本地变量表)中引用的对象是GC Roots之一;
其次,方法区中类静态属性引用的对象也是GC Roots;
再者,方法区中常量引用的对象同样被视为GC Roots;
最后,本地方法栈中JNI(即通常所说的Native方法)引用的对象也会被作为GC Roots。这些GC Roots是判定对象是否存活的起始点,只有当没有任何GC Roots引用一个对象时,该对象才被认为是可回收的。
48. System.gc() 和 Runtime.getRuntime().gc()的区别是什么?
System.gc() 和 Runtime.getRuntime().gc() 在Java中都是用来建议JVM执行垃圾回收的,但实际上它们之间存在细微的差别。System.gc() 是一个静态方法,它直接调用了运行时环境中的垃圾回收器,而 Runtime.getRuntime().gc() 是通过获取当前运行时的实例,然后调用其实例方法来进行垃圾回收。从功能上来说,两者几乎等效,都是发起一个垃圾回收的请求,但并不保证垃圾回收一定会被执行。