Java面试被问到JVM知识,有关这方面的都有什么?

我参加一个Java岗位的面试, 面试官 问到关于JVM(Java虚拟机)的知识。虽然我在日常开发中接触过JVM,但对其内部机制的了解还不够深入

请先 登录 后评论

1 个回答

牧心

 1. JVM的体系结构

 类加载器(ClassLoader)

 作用:负责加载字节码文件(.class文件)到内存中。它是JVM执行类加载机制的基础组件,将类的字节码数据加载到*区,在堆中创建对应的Class对象作为*区中类数据的访问入口。

 分类:主要包括启动类加载器(Bootstrap ClassLoader),它负责加载Java核心类库(如java.lang包中的类),是由C++实现的,是JVM的一部分;扩展类加载器(Ex* ClassLoader),用于加载Java的扩展库(位于jre/lib/ext目录下);应用程序类加载器(Application ClassLoader),也称为系统类加载器,负责加载用户类路径(classpath)下的类。

 双亲委派模型:这是类加载器的一种工作机制。当一个类加载器收到类加载请求时,它首先会把请求委派给父类加载器。只有当父类加载器无法完成该加载任务时(它的搜索范围中没有找到所需的类),子加载器才会尝试自己加载。这种模型可以避免类的重复加载,并且保证了Java核心类库的安全性,例如,用户自定义的java.lang.Object类不会被加载,因为启动类加载器已经加载了系统的java.lang.Object类。

 运行时数据区(Runtime Data Areas)

 程序计数器(Program Counter Register):它是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。它是线程私有的,每个线程都有自己独立的程序计数器,这样可以保证各个线程按自己的执行顺序执行字节码。

 Java虚拟机栈(Java Virtual Machine Stacks):它也是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java*执行的内存模型,每个*在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、*出口等信息。当一个*被调用时,一个新的栈帧就会被压入栈中;当*执行完成后,栈帧就会从栈中弹出。如果栈的深度超过了虚拟机允许的范围,就会抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,但是在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

 本地*栈(Native Method Stacks):与Java虚拟机栈类似,不过它是为本地(Native)*服务的。本地*是指用非Java语言(如C或C++)编写的,并且被Java代码调用的*。它的具体实现方式和内存分配方式可能因JVM的不同而有所差异,在某些JVM实现中,本地*栈和Java虚拟机栈是合二为一的。同样,本地*栈也会出现StackOverflowError和OutOfMemoryError异常。

 堆(Heap):它是JVM管理的内存中*的一块,是被所有线程共享的一块内存区域。几乎所有的对象实例和数组都在堆上分配内存。堆的内存空间是不连续的,它主要分为新生代(Young Generation)和老年代(Old Generation)。新生代又可以细分为Eden空间、From Survivor空间和To Survivor空间。垃圾收集器主要就是针对堆内存进行回收操作,以释放那些不再被引用的对象所占用的空间。因为堆是共享的,并且需要频繁地进行对象的创建和销毁,所以它也是最容易出现OutOfMemoryError异常的区域。

 *区(Method Area):它也是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息(包括类的版本、字段、*、接口等信息)、常量、静态变量、即时编译器编译后的代码等数据。在Java 8之前,*区是通过*代(PermGen)实现的,*代有固定的大小限制,容易出现OutOfMemoryError异常。在Java 8及以后,*区被元空间(Met*ace)取代,元空间使用本地内存,理论上它的大小只受限于本地内存的大小,不过也需要合理配置参数,否则也可能出现内存问题。

2. 垃圾回收(Garbage Collection,GC)

 垃圾回收的基本原理

 引用计数法(Reference Counting):这是一种简单的垃圾回收算法。每个对象都有一个引用计数器,当有一个地方引用这个对象时,计数器就加1;当引用失效时,计数器就减1。当计数器的值为0时,就表示这个对象可以被回收了。但是这种*无法解决循环引用的问题,例如,对象A引用对象B,对象B又引用对象A,此时它们的引用计数都不为0,但实际上这两个对象可能已经没有其他有效的外部引用了,应该被回收。

 可达性分析算法(Reachability *ysis):这是目前主流JVM使用的垃圾回收算法。它以一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到GC Roots没有任何引用链相连(即不可达)时,则证明此对象是可以被回收的。GC Roots对象包括虚拟机栈(栈帧中的本地变量表)中引用的对象、本地*栈中JNI(Java Native Inte*ce)引用的对象、*区中类静态属性引用的对象、*区中常量引用的对象等。

 垃圾收集器(Garbage Collector)

 Serial收集器:这是最基本、历史最悠久的收集器。它是一个单线程收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,直到收集结束。它的优点是简单高效,对于限定单个CPU的环境来说,由于没有线程交互的开销,专心做垃圾收集可以获得*的单线程收集效率。

 ParNew收集器:它是Serial收集器的多线程版本。除了使用多线程进行垃圾收集外,其余行为包括收集算法、Stop

 The

 World机制等都和Serial收集器一样。它是许多运行在Server模式下的JVM虚拟机*的新生代收集器,因为它能与CMS收集器(老年代收集器)很好地配合工作。

 Parallel Scavenge收集器:它也是一个新生代收集器,采用复制算法。它的特点是关注的是吞吐量(Throughput),即CPU用于运行用户代码的时间与CPU总消耗时间的比值。它提供了两个参数用于*控制吞吐量,如

 XX:MaxGCPauseMillis(控制*垃圾收集停顿时间)和

 XX:GCTimeRatio(直接设置吞吐量大小)。

 CMS收集器(Concurrent Mark Sweep):这是一种以获取最短回收停顿时间为目标的老年代收集器。它的工作过程比较复杂,主要分为四个阶段:初始标记(Initial Mark)、并发标记(Concurrent Mark)、重新标记(Re

 Mark)和并发清除(Concurrent Sweep)。其中初始标记和重新标记这两个阶段需要暂停所有用户线程(Stop

 The

 World),但时间比较短;并发标记和并发清除阶段是与用户线程同时进行的,这样就可以在一定程度上减少垃圾收集时对用户线程的影响,从而提高应用程序的响应速度。不过,CMS收集器也有一些缺点,比如它对CPU资源比较敏感,在并发阶段会占用一部分CPU资源,导致应用程序的性能下降;而且它会产生大量的空间碎片,需要定期进行碎片整理。

 Garbage

 First(G1)收集器:它是一款面向服务端应用的垃圾收集器,主要应用于多处理器和大容量内存环境。G1收集器在收集过程中不会产生空间碎片,它把堆内存划分成多个大小相等的独立区域(Region),在进行垃圾回收时,会优先回收垃圾最多的区域。它采用了标记

 整理(Mark

 Compact)和复制(Copy)算法相结合的方式。G1收集器可以*地控制停顿时间,通过设置

 XX:MaxGCPauseMillis参数来指定目标停顿时间,它会尽量在这个时间范围内完成垃圾收集工作。 ### 3. JVM性能调优

 性能指标

 响应时间(Resp*e Time):指从用户发出请求到收到响应的时间间隔。在JVM性能调优中,需要关注*执行时间、线程阻塞时间等因素对响应时间的影响。例如,一个Web应用程序,用户点击一个按钮后,等待服务器返回数据的时间就是响应时间。如果响应时间过长,用户体验就会很差。

 吞吐量(Throughput):是指单位时间内系统处理的请求数量。对于一个处理大量并发请求的服务器来说,吞吐量是一个重要的性能指标。例如,一个每秒能够处理100个HTTP请求的Web服务器,其吞吐量就是100个请求/秒。在调优过程中,需要平衡吞吐量和响应时间之间的关系。

 内存占用(Memory Footprint):指JVM进程占用的内存大小。包括堆内存、栈内存、*区内存等各个部分的占用情况。如果内存占用过高,可能会导致系统频繁地进行垃圾回收,甚至出现OutOfMemoryError异常。例如,一个Java应用程序在处理大量数据时,需要合理配置堆内存大小,以避免内存溢出。

 调优工具

 JDK自带的工具

 jc*ole:它是一个基于JMX(Java Management Extensi*)的可视化监控工具,可以用来监控Java应用程序的运行时状态,包括内存使用情况、线程状态、类加载情况等。通过jc*ole,可以直观地看到堆内存的使用量、各个线程的状态(如运行、阻塞、等待等),并且可以检测到死锁等问题。

 jvisualvm:它是一个功能更强大的多合一工具,不仅可以监控Java应用程序的性能,还可以进行性能分析和故障排查。它可以生成详细的性能报告,包括*的执行时间、对象的分配情况等。例如,可以通过jvisualvm来分析一个应用程序中哪个*占用了大量的时间,从而对其进行优化。

 第三方工具

 YourKit Java Profiler:这是一款商业的Java性能分析工具,它提供了非常详细的性能分析功能,包括CPU使用率分析、内存泄漏检测、线程性能分析等。它可以帮助开发人员深入了解应用程序的性能瓶颈,并且提供了多种可视化的图表来展示分析结果。

 调优策略

 调整堆内存大小:根据应用程序的实际需求,合理配置堆内存的大小。如果应用程序需要处理大量的对象,并且内存占用比较高,可以适当增加堆内存的大小。但是,过大的堆内存也可能会导致垃圾回收时间过长。例如,对于一个内存密集型的应用程序,可以通过设置

 Xmx(*堆内存)和

 Xms(初始堆内存)参数来调整堆内存大小。

 选择合适的垃圾收集器:根据应用程序的性能要求和特点,选择合适的垃圾收集器。例如,如果应用程序对响应时间比较敏感,要求尽量减少垃圾收集时的停顿时间,可以选择CMS收集器或者G1收集器;如果应用程序对吞吐量要求比较高,对停顿时间不是特别敏感,可以选择Parallel Scavenge收集器。

 优化代码层面:在代码层面进行优化也是提高JVM性能的重要手段。例如,尽量减少对象的创建和销毁,避免在循环中创建大量的临时对象;合理使用缓存,减少重复计算;及时释放资源,避免资源泄漏等。 ### 4. JVM字节码和指令集

 字节码(Bytecode)

 概念:Java源代码经过编译器编译后生成的中间形式的代码就是字节码。字节码是一种二进制格式的代码,它不依赖于具体的硬件平台和操作系统,具有良好的可移植性。字节码文件(.class文件)的结构是按照JVM规范定义的,它包含了类的各种信息,如常量池、类的访问标志、字段和*的信息等。

 示例:以一个简单的Java类为例,如`public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }`,这个类经过编译后会生成一个字节码文件。通过反编译工具(如javap)可以查看字节码的内容,字节码中包含了很多指令,如`ldc`(将常量池中的常量加载到操作数栈)、`invokevirtual`(调用实例*)等,这些指令是JVM执行的最小单位。

 指令集(Instruction Set)

 概念:JVM指令集是JVM能够识别和执行的一套指令规范。它包括操作码(Opcode)和操作数(Operand)两部分。操作码用于指定要执行的操作类型,如加载、存储、运算、跳转等;操作数则是操作的对象或者数据。不同的JVM实现可能会对指令集有一些细微的差异,但都必须遵循JVM规范。

 示例:在JVM指令集中,`aload_0`指令用于将*个引用类型本地变量加载到操作数栈顶。如果在一个*中有一个本地变量是一个对象引用,就可以使用这个指令将其加载到操作数栈,以便后续进行*调用或者其他操作。

请先 登录 后评论