Java篇:大厂JVM虚拟机内存管理高频面试题及参考答案(小米快手理想多家面经汇总)

Java虚拟机是什么?

Java虚拟机(JVM)是Java生态系统中的一个关键组件。它是一种抽象的计算机器,能够让计算机运行Java程序。JVM具有几个重要的特性。

在其核心,JVM为Java代码提供了一个运行时环境。当你使用javac编译器编译一个Java源文件(.java)时,它会被转换为字节码(.class文件)。这种字节码是与平台无关的,可以在任何支持JVM的平台上运行。例如,你可以在Windows机器上编写一个Java程序,然后在不修改代码本身的情况下将其运行在Linux服务器上。

JVM以一种复杂的方式管理内存。它有不同的存储区域,如堆、栈、方法区和本地方法栈。堆用于动态内存分配,在程序执行期间对象被创建并存储于此。栈负责存储局部变量、方法调用和返回地址。方法区存储类相关信息,像方法代码、字段数据和常量池。

此外,JVM还负责安全事务。它执行安全策略以防止恶意代码造成危害。例如,根据安全设置,它可以限制对某些系统资源的访问。

另一个重要的方面是性能优化。现代JVM使用各种技术,如即时(JIT)编译。JIT编译在运行时将频繁执行的字节码转换为本地机器代码,这显著提高了Java程序的执行速度。

垃圾回收算法有哪些?

一般和Java特定的垃圾回收算法有多种。

引用计数算法

这是基本的垃圾回收算法之一。每个对象都有一个与之关联的引用计数。当创建一个对象时,其引用计数被设置为1。每当创建对该对象的新引用(例如,将对象赋给另一个变量)时,引用计数就递增。当移除对该对象的引用(如变量超出作用域)时,引用计数递减。如果引用计数达到0,意味着没有任何引用指向该对象,就可以考虑对其进行垃圾回收。然而,这个算法存在一些缺陷。例如,它不能很好地处理循环引用。考虑两个相互引用的对象A和B,即使没有外部对A或B的引用,它们的引用计数也永远不会达到0。

标记 - 清除算法

标记 - 清除算法分两个阶段工作。在标记阶段,垃圾收集器从根集(包括全局变量、栈变量等)出发遍历所有可达对象并将其标记为存活。在清除阶段,它遍历整个堆并回收未被标记对象占用的内存。这个算法的一个问题是,清除阶段过后,堆可能会出现碎片化,从而导致内存使用效率低下。

复制算法

该算法将堆划分为两个大小相等的区域,通常称为from - 空间和to - 空间。对象最初在from - 空间分配。当from - 空间满了时,垃圾收集器将所有存活对象从from - 空间复制到to - 空间。之后,交换这两个空间的角色。这个算法的优点是不会产生碎片化。但在最坏情况下,它需要两倍的内存空间。

在Java中,有不同的垃圾收集器基于这些算法或其组合。例如,Serial GC使用简单的标记 - 清除 - 压缩算法处理老年代,使用复制算法处理新生代。Parallel GC是Serial GC的扩展,它使用多线程来加速垃圾收集过程。CMS(Concurrent Mark - Sweep)GC旨在通过在与应用程序线程并发执行大部分垃圾收集工作来减少暂停时间。G1(Garbage - First)GC是为大堆设计的,可以在保证高吞吐量的同时控制暂停时间。

JVM垃圾识别算法有哪些?

JVM使用多种算法来识别垃圾对象。

主要的方法之一是基于可达性分析。在这种方法中,JVM从一组根对象开始。根对象包括线程栈上的局部变量、类中的静态变量和JNI(Java本地接口)引用。垃圾收集器然后遍历所有从这些根对象直接或间接可达的对象。任何无法从根集到达的对象都被视为垃圾。例如,如果你有一个对象A,它仅被一个方法的局部变量引用,并且该方法已经返回,那么A就变得不可达并且有资格被垃圾回收。

另一个方面与对象引用有关。Java中有不同类型的引用,如强引用、软引用、弱引用和虚引用。强引用是我们代码中正常使用的引用(例如,Object obj = new Object();)。只要有强引用指向一个对象,它就不会被垃圾回收。软引用用于缓存目的。仅具有软引用的对象在JVM内存不足时可能会被垃圾回收。弱引用更弱一些。仅具有弱引用的对象将在下一次垃圾收集周期中被垃圾回收。虚引用主要用于一些高级的清理操作,并且与正常的对象生命周期没有直接关系。

JVM还考虑对象的内部状态。例如,如果一个对象处于不一致或无效的状态,可能会被考虑进行垃圾回收。这种情况可能在对象部分初始化或由于某些错误而损坏时发生。

垃圾收集机制的历史是怎样的?

随着时间的推移,垃圾收集的概念有了显著的发展。

在计算的早期,内存管理主要由程序员手动进行。他们必须显式地为对象分配和释放内存。这种方法容易出错,并导致许多问题,如内存泄漏(分配了内存但没有正确释放)和悬空指针(指针指向已经释放的内存位置)。

随着编程语言的发展,自动内存管理的需求变得更加明显。引用计数是早期垃圾收集的尝试之一。然而,正如前面提到的,它存在局限性,特别是对于循环引用。

后来,标记 - 清除算法被开发出来。这些算法提供了一种更全面的方式来识别和收集垃圾对象。但它们也有自己的问题,如堆碎片化。

复制算法作为一种解决碎片化问题的方法出现了。它在某些情况下提供了一种更有效的内存管理方式。

在Java的背景下,垃圾收集机制的开发一直在持续。早期的Java虚拟机使用相对简单的垃圾收集器。随着Java变得更流行,并且在更多资源受限的环境中使用,开发了更先进的垃圾收集器。

例如,Serial GC是Java中的第一个垃圾收集器之一。它简单且适用于单处理器系统上的小型应用程序。然后,随着对更好性能的需求增加,Parallel GC被引入。它可以使用多个线程来加速垃圾收集过程,这对于多核系统特别有用。

CMS GC被开发出来以进一步减少垃圾收集期间的暂停时间。它试图在与应用程序线程并发的情况下完成大部分垃圾收集工作,这样应用程序所经历的中断就会更少。

最近,G1 GC变得流行起来。它是为大型堆设计的,可以比其前身更有效地平衡吞吐量和暂停时间。

你了解GC(垃圾收集)机制吗?请详细讲解一下

垃圾收集(GC)机制是现代编程语言(如Java)的一个基本部分。

本质上,GC机制负责自动回收程序不再需要的对象所占用的内存。在Java中,当创建对象时,它们会占用堆中的内存。随着时间的推移,这些对象中的一些可能不再被程序使用,这时候GC就发挥作用了。

GC首先需要识别哪些对象不再被使用。正如我们之前讨论的,可达性分析是一个关键方法。从根对象集开始,GC遍历所有引用并将可达对象标记为存活。未被标记的对象随后被视为垃圾。

一旦识别出垃圾对象,GC有不同的策略来回收它们占用的内存。在一些情况下,比如使用标记 - 清除算法时,它只是简单地遍历堆并回收未被标记对象占用的内存。但这可能会导致碎片化。

其他算法,如复制算法,采取不同的方法。它们将堆划分为不同的区域,并将存活对象复制到另一个区域,使原始区域为空闲以便将来分配。

在Java中,不同的垃圾收集器以各种方式实现这些算法。例如,Serial GC是一种单线程的垃圾收集器,它使用标记 - 清除和复制算法的组合来处理不同代(新生代和老年代)的对象。Parallel GC使用多个线程来加速垃圾收集过程,特别是对于大型堆。

CMS GC旨在最小化垃圾收集期间的暂停时间。它通过在与应用程序线程并发的情况下完成大部分垃圾收集工作来实现这一点。然而,它也有一些缺点,如在并发阶段会增加CPU使用率。

G1 GC是为大规模应用程序和大型堆设计的。它将堆划分为多个区域,并且可以更有针对性地收集垃圾,在有效平衡吞吐量和暂停时间方面表现更好。

GC机制也会对Java程序的性能产生影响。频繁的垃圾收集可能会导致程序执行过程中的暂停,这在实时或高性能应用程序中可能是不可取的。因此,调整GC参数通常是优化Java应用程序性能所必需的。

请介绍一下垃圾回收算法。(如引用计数法等垃圾回收算法的了解)

垃圾回收算法在Java的内存管理中起着至关重要的作用。

引用计数法(Reference Counting)

这是一种较为基础的垃圾回收算法。它的核心思想是为每个对象都维护一个引用计数器。当有新的引用指向该对象时,计数器加1;当引用失效时,计数器减1。一旦计数器的值变为0,就意味着没有任何引用指向这个对象了,那么这个对象就可以被当作垃圾回收。比如说,一个对象A被创建出来后,初始引用计数为1,如果有另一个变量引用了A,那么计数器就变为2。当这个新的引用被移除时,计数器又变回1,最后如果这个唯一的引用也不存在了,计数器为0,A就可以被回收。但是,引用计数法存在明显的缺陷,它无法处理循环引用的情况。例如有两个对象A和B,A引用B,B又引用A,即使没有其他外部引用指向它们,它们的引用计数也永远不会为0,这样就可能导致内存泄漏。

标记 - 清除算法(Mark - Sweep)

这个算法分为两个阶段。在标记阶段,垃圾回收器会从一组根对象(如全局变量、栈中的局部变量等)开始,遍历所有能够到达的对象,并将这些对象标记为存活状态。在清除阶段,就会遍历整个堆内存,把那些没有被标记为存活的对象回收掉。不过,这种算法有个比较大的问题,就是在清除之后,堆内存可能会出现碎片化现象。就好比一间屋子,东西被随意清理后,剩下的空间变得七零八落,不连续了,这样在后续分配大块内存时就可能会遇到困难。

复制算法(Copying)

此算法将堆内存划分为两个大小相等的区域,通常称为from区和to区。新创建的对象首先被分配到from区。当from区的内存快要用完时,垃圾回收器就会启动。它会将from区中所有存活的对象复制到to区,然后交换from区和to区的角色。这样做的优点是不会出现内存碎片化的问题,因为每次都是将存活对象整体复制到另一个干净的区域。但是它的缺点也很明显,需要占用双倍的堆内存空间,如果堆内存本身就很大,这无疑是一种浪费。

标记 - 整理算法(Mark - Compact)

标记 - 整理算法结合了标记 - 清除和复制算法的优点。在标记阶段,和标记 - 清除算法一样,从根对象开始标记所有存活对象。在整理阶段,它会将所有存活的对象都向一端移动,使得存活对象在内存中是连续分布的,然后直接清理掉边界以外的内存。这样既避免了碎片化,又不需要像复制算法那样占用双倍的内存空间。

JVM内存模型中各区域是如何划分的?有哪些区域?

JVM内存模型主要划分为以下几个区域:

程序计数器(Program Counter Register)

这是一个比较小的内存区域。它的主要作用是记录当前线程所执行的字节码指令的地址。在多线程环境下,每个线程都有自己的程序计数器。当线程切换时,程序计数器可以准确地记录下线程下次执行的位置。就好比一个导航仪,时刻指引着线程执行的方向。

Java虚拟机栈(Java Virtual Machine Stack)

这个区域与线程紧密相关,每个线程都有一个私有的Java虚拟机栈。它主要用于存储局部变量表、操作数栈、动态连接和方法返回地址等信息。局部变量表存放着方法参数和局部变量,操作数栈则用于执行字节码指令时的操作数运算。可以把Java虚拟机栈想象成一个栈结构的数据仓库,方法执行时就相当于在这个仓库里进进出出地存取数据。

本地方法栈(Native Method Stack)

本地方法栈与Java虚拟机栈类似,只不过它是为本地方法(即使用其他语言编写的方法,如C或C++编写的JNI方法)服务的。当调用本地方法时,相关的信息就会存储在本地方法栈中。

堆(Heap)

堆是整个JVM内存中最大的一块区域,它是所有线程共享的内存区域。堆主要用于存储对象实例和数组。在堆中分配内存是动态的,对象的创建和销毁都在这里进行。就像一个大型的储物间,各种对象都堆放在这儿。

方法区(Method Area)

方法区也是各个线程共享的内域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。可以把方法区看作是一个知识的宝库,存放着与类相关的各种重要信息。

JVM管理的内存区域分为哪些?

JVM管理的内存区域主要分为两大类:线程共享的内存区域和线程私有的内存区域。

线程共享的内存区域

堆(Heap):如前面所说,这是存储对象实例和数组的地方,是内存中最大的一块共享区域。所有线程都可以访问堆中的对象,不过要注意对象的并发访问控制,避免数据不一致等问题。

方法区(Method Area):存储类的结构信息、常量池、静态变量等内容。类的加载、链接和初始化等操作都与方法区密切相关。不同类的相关信息都存放在这里,是整个程序运行过程中非常重要的知识储备库。

线程私有的内存区域

程序计数器(Program Counter Register):每个线程都有自己的程序计数器,用于记录当前线程执行的字节码指令地址。这就像是每个线程自己的导航仪,确保线程在执行过程中不会迷失方向。

Java虚拟机栈(Java Virtual Machine Stack):每个线程都有一个私有的Java虚拟机栈,用于存储局部变量表、操作数栈等信息。线程在执行方法时,相关的操作都会在这个栈上进行,就像每个线程都有自己的工作小栈,存放着自己方法的执行信息。

本地方法栈(Native Method Stack):与Java虚拟机栈类似,但服务于本地方法。当线程调用本地方法时,相关的执行信息就会存储在本地方法栈中。

JVM 中堆 新生代 老年代的比例是多少?

在JVM中,堆内存通常被划分为新生代(Young Generation)和老年代(Old Generation)。

一般来说,默认情况下,新生代与老年代的比例是1:2。也就是说,如果堆内存的大小为X,那么新生代占1/3,老年代占2/3。不过这个比例是可以调整的,根据应用程序的具体需求,可以通过JVM参数来改变这个比例。

新生代又进一步划分为Eden区和Survivor区(通常有两个Survivor区,S0和S1)。Eden区主要用于新对象的分配,当Eden区满的时候就会触发Minor GC(新生代垃圾回收)。Survivor区则用于存放从Eden区经过垃圾回收后存活下来的对象。在新生代中,Eden区和Survivor区的比例通常是8:1:1。这意味着Eden区占新生代空间的80%,每个Survivor区各占10%。

这种比例的设计是有其合理性的。由于大多数对象的生命周期都很短,将大部分空间分配给Eden区可以提高对象分配的效率。而Survivor区的存在可以筛选出那些经过一次垃圾回收后仍然存活的对象,将它们转移到老年代之前先在Survivor区中进行多次筛选,这样可以减少老年代的垃圾回收频率。

有几种垃圾标记方法?

在JVM的垃圾回收机制中,主要有以下几种垃圾标记方法:

引用计数法标记

这是一种比较直观的标记方法。前面提到的引用计数算法就是基于这种标记思想。每个对象都有一个引用计数器,当有引用指向该对象时,计数器加1;当引用失效时,计数器减1。当计数器为0时,就标记该对象为垃圾对象。但是这种方法无法处理循环引用的情况,如两个相互引用的对象,即使没有其他外部引用,它们的引用计数也不会为0,从而不能被正确标记为垃圾。

可达性分析标记

这是一种更为常用的标记方法。它从一组根对象(如全局变量、栈中的局部变量等)开始,沿着引用链向下搜索,能够到达的对象就被标记为存活对象,而不能到达的对象则被标记为垃圾对象。可以把这个过程想象成从一个源头(根对象)出发,像水流一样蔓延到所有能够到达的地方,那些没有被水流到的地方(对象)就是垃圾。这种方法能够很好地处理循环引用的问题。

根搜索算法标记

根搜索算法其实是可达性分析的一种具体实现方式。它通过深度优先搜索或者广度优先搜索等算法,从根对象开始遍历对象图,标记所有可达的对象。在遍历过程中,需要考虑不同类型的引用(强引用、软引用、弱引用和虚引用)对对象可达性的影响。例如,强引用会使得对象肯定可达,而软引用在内存不足时可能会被回收,弱引用在下一次垃圾回收时就可能被回收,虚引用主要用于对象的清理操作,本身对对象的可达性影响较小。

标记 - 整理过程中的标记

在标记 - 整理算法中,标记阶段也是采用类似的可达性分析或者根搜索算法来标记存活对象。不过,在标记完成后,还会进行整理操作,将所有存活的对象向一端移动,使得存活对象在内存中连续分布,然后清理掉边界以外的内存。这种标记方法既保证了垃圾对象的正确标记,又解决了内存碎片化的问题。

可达性分析算法中如何判断在执行链上?

在可达性分析算法中,判断对象是否在执行链上是一个关键步骤。执行链,简单来说,就是从根对象出发,沿着引用关系形成的一条路径。要判断一个对象是否在执行链上,可以从以下几个方面来看。

首先,根对象是执行链的起点。根对象包括全局变量、栈中的局部变量以及JNI(Java Native Interface)引用等。这些根对象是整个可达性分析的基础。就好比一场寻宝游戏,根对象就是宝藏的起始点。

然后,从根对象开始,沿着强引用关系进行遍历。强引用是一种非常明确的引用关系,只要有强引用指向一个对象,这个对象就被认为是可达的,也就是在执行链上。比如说,一个对象A被变量a强引用,那么只要变量a存在,对象A就在执行链上。

但是,仅仅有强引用还不够哦。在Java中,还有软引用、弱引用和虚引用。软引用在内存不足时可能会被回收,但在内存充足时,软引用指向的对象也是在执行链上的。弱引用就比较特殊啦,下一次垃圾回收时,弱引用指向的对象就可能被回收,所以在当前垃圾回收周期内,如果一个对象只有弱引用指向它,那它可能就不在执行链上了。虚引用主要用于对象的清理操作,对对象是否在执行链上影响不大。

另外,在遍历过程中,要注意循环引用的情况。就像两个互相拥抱的小伙伴A和B,它们互相引用对方,如果没有其他外部引用指向它们,单纯从引用关系上看,它们好像在执行链上,但实际上,由于没有从根对象出发能够到达它们的路径,所以它们是不可达的。

怎么样判断对象不可达?

要判断一个对象不可达,可以从以下几个方面来考虑

从引用关系的角度来看,如果一个对象没有任何引用指向它,那肯定是不可达的。这就像一个被遗忘在角落里的小物件,没有任何东西和它有联系,那它自然就被认为是无用的,也就是不可达的啦。

在可达性分析算法中,从根对象出发,沿着引用链进行遍历。如果一个对象不能通过任何一条从根对象到它的路径到达,那这个对象就是不可达的。比如说,根对象是一棵大树的主干,其他对象就像树枝和树叶,通过各种引用关系连接到主干上。如果有一个对象,它离这棵大树的主干非常远,没有任何路径能把它和主干连接起来,那这个对象就是不可达的。

还要考虑对象的引用类型。强引用会使得对象肯定可达,但是软引用在内存不足时可能会被回收,弱引用在下一次垃圾回收时就可能被回收。如果一个对象只有软引用或者弱引用,并且在相应的条件下(内存不足或者下一次垃圾回收),那这个对象就可能变成不可达的。

另外,循环引用也会影响对象的可达性判断。如果两个或多个对象互相引用,但是没有其他外部引用指向它们,从可达性分析的角度看,它们是不可达的。就好比两个手拉手的小伙伴,但是他们被困在一个孤岛上,没有船或者其他方式能让他们和外界联系,那他们就是不可达的。

堆中的区域是怎么划分的?

堆是Java虚拟机中非常重要的一块内存区域,它的划分可是很有讲究的呢。

堆通常被划分为新生代(Young Generation)和老年代(Old Generation)。这就像是把一个大仓库分成了两个不同的区域,一个放新到的货物(新生代),一个放存放时间比较久的货物(老年代)。

新生代又进一步被划分为Eden区和Survivor区。Eden区就像是一个新货物的接收区,新创建的对象一般都会先分配到Eden区。Survivor区有两个,通常称为S0和S1,它们就像是从Eden区筛选出来的“优质货物”的临时存放区。当Eden区满了的时候,就会触发一次Minor GC(新生代垃圾回收),把Eden区和其中一个Survivor区(比如S0)中存活的对象复制到另一个Survivor区(S1),然后清空Eden区和S0区。经过多次这样的筛选后,仍然存活的对象就会被移到老年代。

老年代则是存放那些经过多次垃圾回收后仍然存活的对象。这些对象就像是大仓库里长期存放的重要货物,它们的存在时间比较长。

有些JVM实现还可能会在堆中划分出永久代(Permanent Generation)或者元空间(Metaspace)。永久代主要用于存储类的元数据信息,比如类的结构、常量池等。不过在Java 8及以后的版本中,永久代逐渐被元空间所取代。元空间使用的是本地内存,而不是堆内存,这样可以避免一些内存溢出的问题。

堆中不同区域的垃圾收集算法有哪些?

堆的不同区域由于其特点不同,采用的垃圾收集算法也有所差异哦。

新生代(Young Generation)

复制算法(Copying Algorithm):这是新生代最常用的垃圾收集算法。如前面所说,新生代被划分为Eden区和Survivor区。当Eden区满时,就会触发Minor GC。在Minor GC过程中,会将Eden区和其中一个Survivor区(假设为S0)中存活的对象复制到另一个Survivor区(S1),然后清空Eden区和S0区。这种算法的优点是不会产生内存碎片化,因为每次都是将存活对象整体复制到另一个区域。但是它的缺点是需要占用双倍的Survivor区空间,如果对象存活率比较高,就会有较多的空间浪费。

老年代(Old Generation)

标记 - 清除算法(Mark - Sweep):这个算法分为标记和清除两个阶段。在标记阶段,垃圾收集器会从根对象出发,遍历所有能够到达的对象,并将这些对象标记为存活。在清除阶段,就会遍历整个老年代,把那些没有被标记为存活的对象回收掉。这种算法实现起来比

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

17年+码农经历了很多次面试,多次作为面试官面试别人,多次大数据面试和面试别人,深知哪些面试题是会被经常问到。 在多家企业从0到1开发过离线数仓实时数仓等多个大型项目,详细介绍项目架构等企业内部秘不外传的资料,介绍踩过的坑和开发干货,分享多个拿来即用的大数据ETL工具,让小白用户快速入门并精通,指导如何入职后快速上手。 计划更新内容100篇以上,包括一些企业内部秘不外宣的干货,欢迎订阅!

全部评论

相关推荐

不像现在的我,已经是虚伪的社会人了。
真烦好烦真烦:好有个性的一段话,导师没有让你修改吗
点赞 评论 收藏
分享
05-12 17:28
已编辑
门头沟学院 硬件开发
ldf李鑫:不说公司名祝你以后天天遇到这样的公司
点赞 评论 收藏
分享
评论
4
15
分享

创作者周榜

更多
牛客网
牛客企业服务