03_JVM面试

在这里插入图片描述

1.说一下 JVM 的主要组成部分及其作用?

JVM(Java虚拟机)是Java编程语言的核心组件,它是Java程序在计算机上执行的运行环境。JVM的主要组成部分及其作用如下:

  1. 类加载器(Class Loader):类加载器负责将Java字节码加载到JVM中。它将类文件从文件系统、网络等位置加载到内存,并进行验证、准备和解析。类加载器还负责解决类之间的依赖关系。
  2. 执行引擎(Execution Engine):执行引擎是JVM的核心组件,负责执行已加载的字节码指令。它将字节码解释为具体的机器指令或者通过即时编译器(Just-In-Time Compiler,JIT)将字节码编译成本地机器码进行执行,以提高执行效率。
  3. 运行时数据区(Runtime Data Area):运行时数据区是JVM用于存储程序运行期间所需的数据的区域。主要包括方法区、堆、栈、程序计数器和本地方法栈等。
    • 方法区(Method Area):用于存储类的结构信息、常量、静态变量等。在Java 8及之前的版本中,方法区被实现为永久代(Permanent Generation)。从Java 8开始,永久代被元数据区(Metaspace)所取代。
    • 堆(Heap):用于存储Java对象实例。所有通过关键字new创建的对象都会在堆中分配内存。
    • 栈(Stack):用于存储方法调用和局部变量。每个线程在运行时都会创建一个栈帧,用于存储方法的局部变量、操作数栈、动态链接等信息。
    • 程序计数器(Program Counter):存储当前线程正在执行的字节码指令的地址。
    • 本地方法栈(Native Method Stack):用于支持本地(非Java)方法的执行。
  4. JIT编译器(Just-In-Time Compiler):JIT编译器是JVM的一部分,用于将热点代码(经常执行的代码)进行即时编译成本地机器码。这种编译方式能够提高执行效率,因为本地机器码的执行速度通常比解释执行字节码快。
  5. 垃圾收集器(Garbage Collector):垃圾收集器是JVM的一部分,负责自动管理内存。它会自动检测和回收不再使用的对象,并释放它们所占用的内存空间。垃圾收集器可以减轻开发人员手动进行内存管理的负担,提高程序的可靠性和性能。

这些组成部分共同协作,使得Java程序能够在不同的操作系统和硬件平台上运行,实现了Java的“一次编写,到处运行”的特性。

2. Java程序运行机制详细说明

Java程序的运行机制如下:

  1. 编写Java源代码:开发人员使用Java编程语言编写源代码,源代码文件以.java为扩展名。
  2. 编译Java源代码:使用Java编译器(javac命令)将源代码编译成字节码文件。字节码是一种中间形式的二进制代码,以.class为扩展名。
  3. 类加载:JVM的类加载器负责将字节码文件加载到内存中。它会按需加载所需的类文件,并解析类的结构信息。加载的类会存储在运行时数据区的方法区中。
  4. 字节码验证:JVM会对加载的字节码进行验证,确保它们符合Java语言规范和安全约束。这个过程包括类型检查、访问权限验证、数据流分析等。
  5. 字节码解释/编译执行:JVM的执行引擎负责执行字节码指令。一种常见的执行方式是解释执行,即逐条解释字节码指令并执行对应的操作。另一种方式是即时编译执行(JIT编译),它将热点代码(经常执行的代码)编译成本地机器码,并直接执行机器码以提高性能。
  6. 运行时数据区管理:JVM会在运行时为程序分配一些内存空间,用于存储程序执行期间所需的数据。这些数据包括对象实例、方法区、栈帧、程序计数器等。
    • 堆:用于存储对象实例。所有通过关键字new创建的对象都会在堆中分配内存。堆是JVM中最大的一块内存区域。
    • 方法区:用于存储类的结构信息、常量、静态变量等。方法区在Java 8及之前的版本中被实现为永久代(Permanent Generation),从Java 8开始被元数据区(Metaspace)所取代。
    • 栈:每个线程在运行时都会创建一个栈帧,用于存储方法的局部变量、操作数栈、动态链接等信息。栈是线程私有的,每个方法的调用都会创建一个栈帧,方法调用结束后,栈帧会被销毁。
    • 程序计数器:存储当前线程正在执行的字节码指令的地址。
    • 本地方法栈:用于支持本地(非Java)方法的执行。
  7. 垃圾收集:JVM的垃圾收集器负责自动管理内存。它会周期性地检测和回收不再使用的对象,并释放它们所占用的内存空间。垃圾收集器减轻了开发人员手动进行内存管理的负担,提高了程序的可靠性和性能。
  8. 程序结束:当程序执行完毕或者遇到异常时,JVM会终止程序的执行,并释放占用的内存资源。

通过这个运行机制,Java程序实现了跨平台的能力,即一次编写的Java代码可以在不同的操作系统和硬件平台上运行,只需要在相应的平台上安装JVM即可。这种特性使得Java成为一种广泛应用于跨平台开发的编程语言。

3.说一下 JVM 运行时数据区

JVM(Java虚拟机)的运行时数据区是JVM在运行Java程序时用来存储数据的区域。它主要包括以下几个部分:

  1. 堆(Heap):堆是JVM中最大的一块内存区域,用于存储对象实例。所有通过关键字new创建的对象都会在堆中分配内存。堆是被所有线程共享的,它在JVM启动时被创建,并且会被自动进行垃圾收集。堆被划分为不同的区域,如新生代(Young Generation)和老年代(Old Generation)等,以支持不同类型对象的分配和回收。
  2. 方法区(Method Area):方法区用于存储类的结构信息、常量、静态变量等。在Java 8及之前的版本中,方法区被实现为永久代(Permanent Generation)。从Java 8开始,永久代被元数据区(Metaspace)所取代。方法区也是被所有线程共享的。
  3. 栈(Stack):栈用于存储线程的执行环境和局部变量。每个线程在运行时都会创建一个栈帧(Stack Frame),栈帧中包含了方法的参数、局部变量、操作数栈、动态链接等信息。每个方法调用都会创建一个新的栈帧,并将其压入栈中。栈是线程私有的,每个线程都拥有自己独立的栈。
  4. 程序计数器(Program Counter):程序计数器是一个小的内存区域,它存储当前线程正在执行的字节码指令的地址。每个线程都有自己独立的程序计数器。
  5. 本地方法栈(Native Method Stack):本地方法栈用于支持本地(非Java)方法的执行。它类似于Java栈,但用于执行本地代码而不是Java字节码。本地方法栈也是线程私有的。

这些运行时数据区协同工作,提供了Java程序执行所需的内存和数据结构。它们在JVM启动时被创建,并随着程序的执行进行动态调整和管理。垃圾收集器会定期检测并回收不再使用的对象,释放占用的内存空间,确保运行时数据区的有效利用。

4.说一下深拷贝和浅拷贝

深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是在编程中常用的两种对象复制方式,它们有不同的特点和应用场景。

浅拷贝是一种简单的复制方式,它创建一个新对象,并将原始对象的成员变量的值复制到新对象中。新对象和原始对象共享相同的引用类型成员变量,因此对其中一个对象进行修改会影响到另一个对象。换句话说,浅拷贝仅复制了对象的引用,而不复制对象本身。

深拷贝则是一种更为复杂的复制方式,它不仅创建一个新对象,还会递归复制原始对象的所有引用类型成员变量所引用的对象。深拷贝会生成一份完全独立的副本,新对象和原始对象之间没有任何关联。因此,对其中一个对象进行修改不会对另一个对象产生影响。

下面是深拷贝和浅拷贝的主要特点和使用场景:

浅拷贝的特点:

  • 创建一个新对象,复制原始对象的值(包括引用类型的成员变量)。
  • 新对象和原始对象共享相同的引用类型成员变量。
  • 修改新对象或原始对象的引用类型成员变量会影响到另一个对象。
  • 浅拷贝适用于成员变量类型简单、互相独立、不包含引用关系的对象。

深拷贝的特点:

  • 创建一个新对象,递归复制原始对象的值及其引用类型成员变量所引用的对象。
  • 新对象和原始对象之间没有任何关联,是完全独立的。
  • 修改新对象或原始对象的引用类型成员变量不会对另一个对象产生影响。
  • 深拷贝适用于成员变量类型复杂、包含引用关系的对象,希望实现彻底独立的复制。

需要注意的是,深拷贝可能会涉及到对象图的递归遍历和复制,因此在实现深拷贝时需要考虑对象的可复制性、循环引用等因素,以避免无限递归和其他潜在的问题。在某些情况下,可以通过实现Cloneable接口或使用序列化/反序列化等方式来实现深拷贝。

5.说一下堆栈的区别?

堆(Heap)和栈(Stack)是在计算机内存中用于存储数据的两个主要区域,它们具有不同的特点和使用方式。

  1. 堆(Heap):
    • 功能:堆用于存储动态分配的内存,主要用于存储对象实例。
    • 特点:
      • 大小:堆是JVM中最大的一块内存区域。
      • 共享:堆是被所有线程共享的,所有创建的对象实例都存储在堆中。
      • 对象生命周期:堆中的对象由垃圾收集器负责自动回收,无需手动释放。
      • 分配和释放:通过Java关键字new创建对象时,在堆上分配内存;当对象不再被引用时,垃圾收集器会自动释放堆上的内存。
  2. 栈(Stack):
    • 功能:栈用于存储方法调用和局部变量等信息。
    • 特点:
      • 大小:栈相对于堆来说比较小。
      • 线程私有:每个线程在运行时都会创建一个栈,用于存储方法调用和局部变量。每个线程都拥有自己独立的栈。
      • 后进先出:栈是一种后进先出(LIFO)的数据结构,每个方法的调用会创建一个栈帧,方法调用结束后,栈帧会被销毁。
      • 快速分配和释放:栈的分配和释放非常高效,仅需移动栈指针即可实现。
      • 局部变量存储:栈用于存储方法的局部变量,包括基本数据类型和对象的引用。

堆和栈在存储和管理数据方面有着不同的角色和机制。堆用于存储对象实例,对象在堆上分配和回收,而栈用于存储方法调用和局部变量。堆和栈在内存分配、大小、生命周期、共享性等方面都存在显著的差异,了解它们的区别有助于编写高效和可靠的程序。

6.队列和栈是什么?有什么区别?

队列(Queue)和栈(Stack)是常见的数据结构,用于存储和操作数据,它们有以下主要区别:

队列(Queue):

  • 特点:队列是一种先进先出(First-In-First-Out,FIFO)的数据结构,类似于现实生活中排队的概念。
  • 操作规则:元素按照插入的顺序排列,最先插入的元素先被访问和移除(出队),最后插入的元素最后被访问和移除。
  • 操作方法:
    • 入队(Enqueue):在队列的末尾插入元素。
    • 出队(Dequeue):从队列的开头移除元素。
  • 应用场景:适用于需要按照顺序处理元素,例如任务调度、消息传递、广度优先搜索等。

栈(Stack):

  • 特点:栈是一种后进先出(Last-In-First-Out,LIFO)的数据结构,类似于现实生活中的一摞盘子。
  • 操作规则:元素按照插入的顺序排列,最后插入的元素先被访问和移除(弹出),最先插入的元素最后被访问和移除。
  • 操作方法:
    • 入栈(Push):将元素压入栈的顶部。
    • 出栈(Pop):从栈的顶部弹出元素。
  • 应用场景:适用于需要临时存储和后进先出访问数据的场景,例如函数调用、表达式求值、深度优先搜索等。

主要区别:

  • 数据顺序:队列按照先进先出的顺序进行操作,而栈按照后进先出的顺序进行操作。
  • 操作方法:队列支持在队尾插入元素和在队头移除元素,而栈支持在栈顶压入元素和从栈顶弹出元素。
  • 元素访问顺序:在队列中,最先插入的元素最先被访问和移除;在栈中,最后插入的元素最先被访问和移除。
  • 应用场景:队列适合处理按顺序排列的任务,栈适合处理需要后进先出访问的场景。、

7.HotSpot虚拟机对象探秘

HotSpot虚拟机是Oracle官方推出的Java虚拟机(JVM)的一种实现,它是目前最常用和广泛采用的JVM实现之一。在HotSpot虚拟机中,对象的创建、内存布局和垃圾回收等都经过精心设计和优化,以提供高性能和有效的内存管理。

下面是HotSpot虚拟机中对象的一些关键方面:

  1. 对象的创建:当使用new关键字创建一个对象时,HotSpot虚拟机会在堆内存中分配一块连续的内存空间来存储对象的实例数据。它使用了指针碰撞(Bump the Pointer)或空闲列表(Free List)的方式来分配内存。
  2. 对象的内存布局:HotSpot虚拟机中的对象内存布局通常由对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)组成。对象头包含一些必要的元数据,如锁信息、GC标记信息等。实例数据包含对象的成员变量。对齐填充用于保证对象的起始地址是对齐的,提高访问效率。
  3. 垃圾回收:HotSpot虚拟机通过垃圾回收器来回收不再使用的对象内存。它使用分代垃圾回收算法,将堆内存划分为不同的代(Generation),如新生代(Young Generation)和老年代(Old Generation)。新生代使用复制算法进行垃圾回收,而老年代使用标记-清除-整理算法。HotSpot虚拟机还使用了并行、并发和基于写屏障的技术来提高垃圾回收的效率。
  4. 对象的访问和操作:HotSpot虚拟机使用句柄(Handle)和直接指针(Direct Pointer)两种方式来访问对象。句柄是一个稳定的句柄池,其中的句柄指向实际对象数据的地址。直接指针直接指向对象的内存地址。HotSpot虚拟机可以根据具体情况选择句柄访问或直接指针访问,以在访问效率和内存占用之间进行权衡。
  5. 其他优化技术:HotSpot虚拟机还使用了一系列优化技术来提升对象的创建和访问效率,如逃逸分析(Escape Analysis)、即时编译(Just-In-Time Compilation)和栈上分配(Stack Allocation)等。这些技术通过静态和动态分析,以及对程序运行时行为的观察,来优化对象的生命周期和内存管理。

HotSpot虚拟机作为Java开发的主要平台,通过对对象的创建、内存布局和垃圾回收等方面的优化,提供了高性能和高效的内存管理机制。这些优化和技术使得Java应用程序能够在不同的硬件和操作系统上以高效的方式运行。

8.HotSpot对象分配内存

类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:

  • 指针碰撞:如果Java堆的内存是**规整,**即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
  • 空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。

选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

9.HotSpot对象的访问定位

在HotSpot虚拟机中,对象的访问定位主要涉及两种方式:使用句柄(Handle)和直接指针(Direct Pointer)。

  1. 句柄访问方式:
    • 对象存储:在堆内存中,HotSpot虚拟机维护了一个稳定的句柄池(Handle Pool)。对象的实际数据存储在句柄池之外,而句柄则包含了指向对象数据的引用。
    • 句柄结构:句柄由两部分组成,一个指向对象数据的指针和一个指向对象类型的指针。
    • 访问过程:当通过句柄访问对象时,首先根据对象的引用在句柄池中查找到对象的实际地址,然后再通过实际地址访问对象的数据。
  2. 直接指针访问方式:
    • 对象存储:对象的实际数据直接存储在堆内存中,而不需要额外的句柄。
    • 访问过程:通过直接指针可以直接访问对象的实际数据,无需经过句柄的查找。

HotSpot虚拟机可以根据具体情况选择句柄访问方式或直接指针访问方式。句柄访问方式的优点是对象的实际数据位置可以变化而不影响句柄的引用,从而简化了对象移动和垃圾回收的操作。而直接指针访问方式则避免了额外的句柄查找操作,直接访问对象的实际数据,提高了访问效率。

需要注意的是,具体的对象访问定位方式可能会受到编译器优化、运行时环境和虚拟机的配置等因素的影响。HotSpot虚拟机在运行时会根据程序的执行情况和性能需求来动态选择适当的对象访问方式。

10.JVM内存溢出异常

JVM内存溢出异常(OutOfMemoryError)是指在Java应用程序中,当JVM无法分配足够的内存来满足对象的创建和存储需求时抛出的异常。当JVM耗尽了堆内存、栈内存或者永久代/元空间的内存时,就会发生内存溢出异常。

常见的JVM内存溢出异常包括:

  1. Java堆内存溢出:当应用程序需要创建的对象超出了堆内存的限制时,会抛出Java堆内存溢出异常(OutOfMemoryError: Java Heap Space)。这通常发生在长时间运行的应用程序中,或者应用程序使用了大量的对象并且没有及时进行垃圾回收。
  2. Java栈溢出:每个线程在JVM中都有自己的栈空间,用于存储方法调用和局部变量等信息。当方法调用的层次过深,或者方法中使用了过多的局部变量时,会导致Java栈溢出异常(OutOfMemoryError: Stack Overflow)。
  3. 方法区/永久代/元空间溢出:方法区在旧版的JVM中用于存储类的元数据信息,包括类的结构、常量池、方法信息等。在较新的JVM版本中,方法区被替换为永久代(Permanent Generation)或元空间(Metaspace)。当加载的类过多、常量池或静态变量占用过多内存时,会导致方法区/永久代/元空间溢出异常(OutOfMemoryError: PermGen Space 或 OutOfMemoryError: Metaspace)。

避免JVM内存溢出异常的一些常见做法包括:

  1. 增加JVM内存限制:通过调整JVM的启动参数,增加堆内存、栈内存或永久代/元空间的大小,以提供更多的可用内存。
  2. 优化内存使用:检查应用程序的内存使用情况,释放不再使用的对象,避免内存泄漏和不必要的对象创建。
  3. 使用合适的数据结构和算法:选择合适的数据结构和算法,以减少内存的占用和优化性能。
  4. 优化垃圾回收:根据应用程序的需求和特点,选择适当的垃圾回收器和回收策略,以减少垃圾回收对内存的影响。
  5. 分析和监控内存使用:使用内存分析工具和监控工具,定期检查和分析应用程序的内存使用情况,及时发现和解决潜在的内存问题。

11.简述Java垃圾回收机制

Java的垃圾回收机制(Garbage Collection)是自动管理内存的机制,它负责在运行时自动识别和回收不再使用的对象,释放其占用的内存资源。Java的垃圾回收机制可以减轻开发人员手动释放内存的负担,提高程序的可靠性和开发效率。

Java的垃圾回收机制基于以下几个基本概念:

  1. 引用:在Java中,对象通过引用进行访问和操作。引用是指向对象的指针或引用变量。只有当对象没有被任何引用指向时,才可以被判定为不再使用。
  2. 垃圾收集器:垃圾收集器是负责执行垃圾回收的组件。它会周期性地扫描内存,找出不再使用的对象,并回收它们占用的内存。
  3. 可达性分析:Java的垃圾回收机制使用可达性分析算法来判断对象是否可达。可达性分析从一组称为"根"(Root)的对象开始,递归地遍历对象引用链,标记所有被引用的对象为可达对象。未被标记的对象即为不可达对象,可以被垃圾回收器回收。
  4. 垃圾回收算法:Java的垃圾回收机制使用了不同的垃圾回收算法,包括标记-清除算法(Mark and Sweep)、复制算法(Copying)、标记-整理算法(Mark and Compact)等。这些算法根据对象的特点和内存分配的方式来选择合适的回收策略。
  5. 分代回收:Java的垃圾回收机制将堆内存划分为不同的代(Generation),如新生代(Young Generation)和老年代(Old Generation)。新生代通常使用复制算法进行回收,而老年代使用标记-整理算法或标记-清除算法。

Java的垃圾回收机制具有以下优点:

  1. 自动管理内存:开发人员无需手动释放内存,减少了内存管理的复杂性。
  2. 避免内存泄漏:垃圾回收机制可以自动识别不再使用的对象,并回收其占用的内存,避免了内存泄漏问题。
  3. 提高开发效率:减少了手动内存管理的工作量,使开发人员可以更专注于业务逻辑的实现。
  4. 提高程序可靠性:自动内存管理减少了潜在的内存错误,提高了程序的稳定性和可靠性。

尽管Java的垃圾回收机制带来了很多好处,但也需要注意垃圾回收可能会对程序的性能产生一定的影响。因此,在开发Java应用程序时,需要合理设计对象的生命周期和内存使用方式,以最大程度地优化垃圾回收的性能。

12.GC是什么?为什么要GC

GC是垃圾回收(Garbage Collection)的缩写。它是一种自动化的内存管理机制,在编程语言中用于自动回收不再使用的内存资源。GC的主要目的是减轻开发人员手动释放内存的负担,提高程序的可靠性和开发效率。

GC的存在有以下几个主要原因:

  1. 动态内存分配:在许多编程语言中,如Java和C#,对象的内存分配是动态的,即在运行时根据需要创建对象。这种动态内存分配使得手动管理内存变得复杂和容易出错。
  2. 避免内存泄漏:手动管理内存时,如果忘记释放一个不再使用的对象,就会导致内存泄漏。内存泄漏会逐渐消耗可用内存,最终导致程序的性能下降甚至崩溃。GC通过自动识别和回收不再使用的对象,避免了内存泄漏问题。
  3. 提高开发效率:手动管理内存需要开发人员花费大量的时间和精力来追踪和管理对象的生命周期。而GC机制可以自动处理内存管理,减少了开发人员的工作量,提高了开发效率。
  4. 简化代码逻辑:通过使用GC,开发人员可以将更多的精力集中在业务逻辑的实现上,而不需要过多关注内存管理的细节。这样可以简化代码逻辑,降低程序的复杂性。
  5. 避免悬垂指针和野指针:手动管理内存时,如果引用了已被释放的内存块,会导致悬垂指针或野指针的问题。这些问题可能导致程序崩溃或产生不可预测的行为。GC机制通过自动回收不再使用的对象,可以避免这些问题的发生。

总之,GC机制使得内存管理变得更加方便和安全,减轻了开发人员的负担,提高了程序的可靠性和开发效率。它是现代编程语言中不可或缺的一部分。

13.垃圾回收的优点和原理。并考虑2种回收机制

垃圾回收(Garbage Collection)的优点和原理如下:

优点:

  1. 自动化内存管理:垃圾回收机制自动管理内存,减轻了开发人员手动释放内存的负担,简化了内存管理的复杂性。
  2. 避免内存泄漏:垃圾回收机制能够自动识别不再使用的对象,并回收其占用的内存,避免了内存泄漏问题。
  3. 提高开发效率:开发人员无需过多关注内存管理的细节,可以将更多精力投入到业务逻辑的实现上,提高了开发效率。
  4. 增加程序可靠性:垃圾回收机制能够自动处理内存管理,减少了悬垂指针、野指针等内存相关问题的发生,提高了程序的可靠性。

原理: 垃圾回收的基本原理是通过可达性分析(Reachability Analysis)来确定不再被引用的对象,从而标记为垃圾对象并回收其占用的内存。

  1. 引用链分析:垃圾回收器从一组称为"根"(Root)的对象开始,递归地遍历对象引用链,标记所有被引用的对象为可达对象。
  2. 标记阶段:通过遍历对象引用链,垃圾回收器标记所有可达对象,并将其标记为存活对象。
  3. 清除阶段:垃圾回收器对未被标记的对象进行清除,释放其占用的内存空间。
  4. 压缩(可选):在清除阶段之后,垃圾回收器可能会对内存空间进行压缩,以减少内存碎片并提高内存利用率。

不同的垃圾回收机制有不同的实现方式,其中常见的两种回收机制是:

  1. 标记-清除算法(Mark and Sweep):该算法通过标记可达对象和清除未被标记的对象来进行垃圾回收。它的主要缺点是会产生内存碎片,导致内存利用率降低。
  2. 复制算法(Copying):该算法将内存空间划分为两个区域,通常是将堆分为相等大小的两部分。在对象存活期间,垃圾回收器将存活对象复制到另一个区域,然后清除当前区域中的所有对象。这样可以避免内存碎片问题,但需要额外的空间来存储复制的对象。

以上是垃圾回收的一般优点和原理,具体的垃圾回收实现可能会有不同的策略和算法。不同的垃圾回收器可以根据应用场景和需求选择合适的回收机制。

14.垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。

通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。

可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

15.Java 中都有哪些引用类型?

  • 强引用:发生 gc 的时候不会被回收。
  • 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
  • 弱引用:有用但不是必须的对象,在下一次GC时会被回收。
  • 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

16.怎么判断对象是否可以被回收?

垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。

一般有两种方法来判断:

  • 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
  • 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

17.在Java中,对象什么时候可以被垃圾回收

当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。 垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。

18.说一下 JVM 有哪些垃圾回收算法?

JVM(Java虚拟机)中有几种常见的垃圾回收算法,它们是:

  1. 标记-清除算法(Mark and Sweep):标记-清除算法是最基本的垃圾回收算法之一。它分为两个阶段,首先标记所有活动对象,然后清除未被标记的对象。该算法简单直观,但容易产生内存碎片。
  2. 复制算法(Copying):复制算法将内存划分为两个区域,通常是将堆分为相等大小的两部分。在对象存活期间,活动对象会被复制到另一个区域,然后清除当前区域中的所有对象。这样可以避免内存碎片问题,但需要额外的空间来存储复制的对象。
  3. 标记-整理算法(Mark and Compact):标记-整理算法在标记阶段与标记-清除算法类似,但在清除阶段会将存活对象向一端移动,然后清理掉边界外的内存。这样可以消除内存碎片,但会增加移动对象的成本。
  4. 分代回收算法(Generational Collection):分代回收算法根据对象的生命周期将堆内存划分为不同的代。一般将对象分为新生代(Young Generation)和老年代(Old Generation)。新生代使用复制算法进行频繁的回收,而老年代使用标记-整理算法或标记-清除算法进行回收。这样可以根据对象的特点和生命周期选择合适的回收策略,提高回收效率。
  5. 并发标记-清除算法(Concurrent Mark and Sweep):并发标记-清除算法允许垃圾回收与应用程序并发执行,减少停顿时间。它通过在垃圾回收过程中与应用程序并发工作来实现。并发标记-清除算法的引入可以降低长时间停顿对应用程序的影响,但会增加回收器的复杂性。

这些垃圾回收算法在不同的场景下有不同的优势和限制。JVM通常根据应用程序的需求和运行环境选择合适的垃圾回收算法。另外,随着JVM的不断演进和发展,可能会引入新的垃圾回收算法或对现有算法进行改进和优化。

19.说一下 JVM 有哪些垃圾回收器?

JVM(Java虚拟机)提供了多种垃圾回收器(Garbage Collector)的实现,每个回收器都有自己的特点和适用场景。以下是一些常见的垃圾回收器:

  1. Serial回收器(Serial Collector):Serial回收器是一种单线程的垃圾回收器,它会停止应用程序的所有线程来进行垃圾回收。它适用于单核处理器或小规模的应用程序,因为它的回收效率较低。
  2. Parallel回收器(Parallel Collector):Parallel回收器是Serial回收器的多线程版本,它能够利用多个处理器或多核处理器进行并行回收,提高回收效率。它适用于多核处理器或需要高吞吐量的应用程序。
  3. CMS回收器(Concurrent Mark Sweep Collector):CMS回收器是一种并发的垃圾回收器,它在应用程序运行的同时执行垃圾回收操作,减少了停顿时间。它适用于对停顿时间敏感的应用程序,但可能会降低吞吐量。
  4. G1回收器(Garbage-First Collector):G1回收器是一种面向大堆(大于6GB)的垃圾回收器,它采用了分代回收和并发回收的策略。它能够根据应用程序的需求动态调整回收区域,并根据垃圾产生的情况选择性地回收,以达到更好的吞吐量和更低的停顿时间。

除了以上列举的回收器,还有其他一些特定用途的回收器,如ZGC回收器(用于大内存和低延迟的应用)、Shenandoah回收器(用于超大堆和低停顿时间的应用)等。这些回收器都有自己的特点和适用场景,开发人员可以根据应用程序的需求和性能要求选择合适的垃圾回收器。此外,JVM还提供了参数配置选项,可以根据具体情况对垃圾回收器进行调优和配置。

20.详细介绍一下 CMS 垃圾回收器?

CMS(Concurrent Mark Sweep)垃圾回收器是一种并发的垃圾回收器,旨在减少垃圾回收过程对应用程序的停顿时间。它适用于对停顿时间敏感的应用程序,其中较短的停顿时间是至关重要的。

CMS回收器的工作过程可以分为以下几个阶段:

  1. 初始标记(Initial Mark):在这个阶段,CMS回收器会暂停应用程序的线程,并标记所有从根对象直接可达的对象。这个阶段的停顿时间相对较短,只标记了少量的对象。
  2. 并发标记(Concurrent Mark):在初始标记之后,CMS回收器会与应用程序并发运行,同时标记从根对象开始的可达对象。这个阶段不会暂停应用程序的线程,因此应用程序可以继续执行。并发标记的过程中,可能会有新的对象被创建,这些新创建的对象需要通过特殊的方式标记。
  3. 重新标记(Remark):在并发标记阶段结束后,CMS回收器会再次暂停应用程序的线程,重新标记那些在并发标记期间发生变化的对象。这个阶段的停顿时间相对较长,但通常会比初始标记阶段短。重新标记确保所有的变化都被正确标记,以避免漏标对象。
  4. 并发清除(Concurrent Sweep):在重新标记之后,CMS回收器会与应用程序并发运行,同时清理未标记的对象。这个阶段不会暂停应用程序的线程,因此应用程序可以继续执行。

CMS回收器的优点是它通过并发标记和并发清除的方式,减少了停顿时间。这对于对响应性要求高的应用程序非常重要。然而,由于CMS回收器与应用程序并发运行,可能会导致以下一些缺点:

  1. CPU资源竞争:并发标记和并发清除会与应用程序竞争CPU资源,可能会导致应用程序的吞吐量降低。
  2. 内存碎片:CMS回收器采用标记-清除算法,可能会导致内存碎片问题,降低了内存利用率。
  3. 需要更多的内存:CMS回收器在回收过程中需要维护一些附加的数据结构,可能会占用更多的内存。

因此,CMS回收器适用于对停顿时间要求较高的应用程序,但在高并发、大堆、长时间运行的情况下,可能会出现一些性能问题。在最新的JDK版本中,一些新的垃圾回收器如G1(Garbage-First)回收器已经取代了CMS回收器,以提供更好的性能和更低的停顿时间。

21.新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?

在JVM(Java虚拟机)中,新生代和老年代是堆内存的两个主要分区,分别用于存放不同生命周期的对象。针对这两个分区,JVM提供了不同的垃圾回收器。下面是常见的新生代和老年代垃圾回收器:

新生代垃圾回收器:

  1. Serial回收器:它是最基本的垃圾回收器,采用复制算法。它是单线程的,会暂停应用程序的所有线程进行垃圾回收。适用于单核处理器或小规模应用程序。
  2. ParNew回收器:它是Serial回收器的多线程版本,也采用复制算法。与Serial回收器类似,但可以利用多个处理器或多核处理器进行并行垃圾回收,提高回收效率。
  3. Parallel Scavenge回收器:它是并行的新生代垃圾回收器,采用复制算法。它注重吞吐量,通过并行处理来实现高吞吐量的垃圾回收,适用于追求最大吞吐量的应用程序。

老年代垃圾回收器:

  1. Serial Old回收器:它是Serial回收器的老年代版本,采用标记-整理算法。与Serial回收器类似,是单线程的。适用于单核处理器或小规模应用程序。
  2. Parallel Old回收器:它是Parallel Scavenge回收器的老年代版本,采用标记-整理算法。与Parallel Scavenge回收器类似,是并行的,通过并行处理来提高回收效率。
  3. CMS回收器(Concurrent Mark Sweep):它是并发的老年代垃圾回收器,采用标记-清除算法。与并行回收器不同,CMS回收器可以与应用程序并发运行,减少停顿时间,适用于对停顿时间敏感的应用程序。
  4. G1回收器(Garbage-First):它是一种面向大堆的垃圾回收器,同时兼顾新生代和老年代的回收。G1回收器使用分代回收和并发回收的策略,能够根据应用程序的需求动态调整回收区域,并选择性地回收垃圾对象。

区别:

  • 新生代垃圾回收器主要处理生命周期较短的对象,使用复制算法,追求较高的回收效率和较低的停顿时间。而老年代垃圾回收器主要处理生命周期较长的对象,使用标记-整理或标记-清除算法,注重回收效率和整理内存空间。
  • 新生代垃圾回收器通常在堆内存中较小的区域进行回收,而老年代垃圾回收器在堆内存中较大的区域进行回收。
  • 新生代垃圾回收器通常采用复制算法,将存活的对象复制到另一个区域,然后清空原始区域。而老年代垃圾回收器采用标记-整理或标记-清除算法,将存活的对象整理或清除后,保留连续的内存空间。
  • 新生代垃圾回收器通常以提高吞吐量为目标,适用于追求最大吞吐量的应用程序。而老年代垃圾回收器通常以减少停顿时间为目标,适用于对停顿时间敏感的应用程序。
  • JVM的垃圾回收器选择和使用通常是根据应用程序的需求和性能要求来进行调优和配置。可以根据应用程序的内存使用情况、对象生命周期、吞吐量和停顿时间等因素来选择适合的垃圾回收器组合。

22.简述分代垃圾回收器是怎么工作的?

分代垃圾回收器是一种垃圾回收策略,根据对象的生命周期将堆内存划分为不同的代(Generation),并对不同代的对象采用不同的回收策略。通常将堆内存划分为新生代(Young Generation)和老年代(Old Generation)两个主要部分。

  1. 新生代(Young Generation):
    • 新生代是存放新创建的对象的区域,通常包括Eden区和两个Survivor区(通常是一个From区和一个To区)。
    • 当对象被创建时,它们首先被分配到Eden区。
    • 当Eden区满时,会触发一次新生代的垃圾回收,这个过程称为Minor GC(Minor Garbage Collection)。
    • 在Minor GC中,垃圾回收器将清理无用的对象,并将存活的对象复制到Survivor区,同时对Survivor区进行年龄计数。
    • 在多次Minor GC后,仍然存活的对象会被移到老年代。
  2. 老年代(Old Generation):
    • 老年代是存放生命周期较长的对象的区域。
    • 当对象在新生代经过多次垃圾回收后仍然存活,它们会被晋升到老年代。
    • 当老年代空间不足时,会触发一次老年代的垃圾回收,这个过程称为Major GC(Major Garbage Collection)或Full GC(Full Garbage Collection)。
    • 在Major GC中,垃圾回收器会对整个堆内存进行回收,清理无用的对象,整理内存空间。

分代垃圾回收器利用了对象的特性:大多数对象在创建后不久就变得不可达,只有一小部分对象具有长生命周期。通过将堆内存划分为新生代和老年代,并对不同代采用不同的回收策略,可以针对不同生命周期的对象进行更有效的垃圾回收。新生代采用复制算法以提高回收效率,而老年代采用标记-清理或标记-整理算法以整理内存空间。

分代垃圾回收器的优点是可以针对不同对象的生命周期进行优化,提高垃圾回收效率。通过频繁回收新生代,可以快速释放短生命周期对象占用的内存空间,减少了老年代的压力。而对老年代的回收相对较少,减少了全堆垃圾回收的频率,降低了停顿时间。这样可以在提高吞吐量的同时,尽量减少应用程序的停顿时间,提供更好的性能和响应性。

23.内存分配策略

简述java内存分配与回收策率以及Minor GC和Major GC

所谓自动内存管理,最终要解决的也就是内存分配和内存回收两个问题。前面我们介绍了内存回收,这里我们再来聊聊内存分配。

对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,某些场景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取决于哪一种垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵循以下几种「普世」规则:

对象优先在 Eden 区分配

多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。

这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现 Major GC/Full GC。

  • Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
  • Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。
大对象直接进入老年代

所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。

前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。

长期存活对象将进入老年代

虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。

23.简述java类加载机制?

Java类加载机制是Java虚拟机(JVM)将类加载到内存并执行的过程。当Java程序运行时,JVM会按需加载和链接类,以及对类进行初始化。Java类加载机制由以下三个步骤组成:

  1. 加载(Loading):
    • 类加载的第一步是查找和加载类的字节码文件。类的字节码可以从本地文件系统、网络或其他来源获取。
    • 类加载器负责查找类的字节码文件,并将其加载到内存中。JVM提供了三个内置的类加载器:Bootstrap ClassLoader、Extension ClassLoader和Application ClassLoader。
  2. 链接(Linking):
    • 链接阶段将加载的类的字节码进行验证、准备和解析。
    • 验证(Verification):验证确保加载的类的字节码符合Java虚拟机规范,并且不会引发安全问题。
    • 准备(Preparation):准备阶段为类的静态变量分配内存,并设置默认初始值。
    • 解析(Resolution):解析阶段将符号引用转换为直接引用,建立对其他类的引用关系。
  3. 初始化(Initialization):
    • 初始化阶段对类进行初始化,为静态变量赋予初始值,执行静态代码块。
    • 初始化阶段是类加载的最后一步,会按照特定的顺序执行初始化操作。如果一个类具有父类,则会首先初始化父类,然后再初始化子类。
    • 类的初始化是在首次使用该类的时候进行的,包括创建实例、访问静态变量或调用静态方法等。

类加载机制具有以下特点:

  • 延迟加载:JVM采用按需加载的策略,只有在需要使用某个类时才会加载该类。
  • 双亲委派模型:类加载器遵循双亲委派模型,即先委托父类加载器尝试加载类,只有在父类加载器无法加载时才由子类加载器尝试加载。
  • 缓存加载:一旦类被加载到内存中,通常会在JVM的缓存中保留,以便后续的类加载请求可以直接使用已加载的类。

类加载机制使得Java具有动态性和灵活性,可以在运行时动态加载类,实现动态扩展和插件化等功能。

24.描述一下JVM加载Class文件的原理机制

JVM加载Class文件的原理机制可以概括为以下几个步骤:

  1. 定位Class文件:当Java程序需要使用某个类时,JVM会通过类的全限定名(包括包名和类名)来定位对应的Class文件。通常,JVM会使用类加载器来根据类的名称和路径规则查找Class文件。
  2. 读取Class文件:一旦Class文件被定位,JVM会使用类加载器将Class文件从磁盘或其他位置读取到内存中。读取过程将二进制数据转换为JVM内部可以理解的数据结构。
  3. 解析Class文件:在读取Class文件后,JVM会对其进行解析。解析阶段会验证Class文件的格式是否正确,并将符号引用转换为直接引用,以便在运行时可以准确地定位和访问相关的类、字段和方法。
  4. 创建类的数据结构:在解析阶段之后,JVM会为加载的类创建相应的数据结构,包括运行时常量池、字段和方法的数据结构等。这些数据结构将在类的实例化和方法调用过程中使用。
  5. 分配内存空间:在类的数据结构创建后,JVM会为类的静态变量分配内存空间,并设置默认的初始值。此外,还会为类的实例化对象分配内存空间,用于存储对象的实例字段。
  6. 进行初始化:最后,JVM会执行类的初始化操作。类的初始化阶段会为静态变量赋予初始值,并执行静态代码块中的代码。类的初始化是在首次使用该类的时候进行的,确保类在使用之前已经被正确地初始化。

JVM加载Class文件的过程是按需进行的,即当程序需要使用某个类时才会加载对应的Class文件。这种机制可以避免不必要的类加载,提高运行效率。此外,JVM采用双亲委派模型来加载类,先委托父类加载器尝试加载类,只有在父类加载器无法加载时才由子类加载器尝试加载,确保类的加载过程是有序的和可控的。

25.什么是类加载器,类加载器有哪些?

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。

主要有一下四种类加载器:

  1. **启动类加载器(Bootstrap ClassLoader)**用来加载java核心类库,无法被java程序直接引用。
  2. **扩展类加载器(extensions class loader)😗*它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  3. **系统类加载器(system class loader):**它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
  4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

26.说一下类装载的执行过程?

类装载分为以下 5 个步骤:

  • 加载:根据查找路径找到相应的 class 文件然后导入;
  • 验证:检查加载的 class 文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作。

27.什么是双亲委派模型?

**双亲委派模型(Parent Delegation Model)**是Java类加载机制中的一种设计模式,用于解决类加载的安全性和隔离性问题。它是由Java的创始人之一,Sun Microsystems公司在JDK 1.2版本引入的。

根据双亲委派模型,当类加载器接收到加载类的请求时,它首先将该请求委托给父类加载器进行处理,只有当父类加载器无法加载该类时,才由当前类加载器尝试加载。这个过程会递归地向上委托,直到最顶层的启动类加载器(Bootstrap ClassLoader)。

具体来说,当需要加载一个类时,类加载器会按照以下顺序进行委派:

  1. 当前类加载器首先检查是否已经加载过该类,如果已经加载则直接返回已加载的类。
  2. 如果尚未加载该类,则将加载请求委托给父类加载器。
  3. 父类加载器依次重复步骤1和步骤2,直到顶层的启动类加载器。
  4. 如果启动类加载器无法加载该类,则会回溯到当前类加载器,由当前类加载器尝试加载类。
  5. 如果当前类加载器也无法加载该类,则抛出ClassNotFoundException异常。

通过双亲委派模型,Java类加载器可以按照一定的层次结构进行加载,并确保类的加载是有序和一致的。它的主要优势在于保证类的唯一性和安全性,避免了类的重复加载和冲突。同时,它也实现了类加载的隔离性,不同的类加载器可以加载各自的类,从而实现了应用程序的隔离和模块化。这种模型在Java中的应用广泛,例如在Web应用程序中的不同Web应用之间的类加载隔离就是基于双亲委派模型实现的。

28.说一下 JVM 调优的工具?

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

  • jconsole:用于对 JVM 中的内存、线程和类等进行监控;
  • jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

29.常用的 JVM 调优的参数都有哪些?

  • -Xms2g:初始化推大小为 2g;
  • -Xmx2g:堆最大内存为 2g;
  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • -XX:+PrintGC:开启打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 详细信息。
  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是二次元穿越来的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值