在Java开发领域,JVM面试一直是一个热门话题。作为一名优秀的开发者,你是否已经准备好迎接这场挑战了呢?今天,我们就来深度解析一下JVM面试的热点问题,帮助你更好地应对面试,一举拿下offer!
1、说一下 JVM 的主要组成部分及其作用?
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
- Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到 Runtime data area中的method area。
- Execution engine(执行引擎):执行classes中的指令。
- Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
- Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
作用:
在Java程序的执行过程中,首先会通过编译器将Java源代码转换成一种叫做字节码(Bytecode)的中间形式。这种字节码是一种平台无关的二进制代码,它比Java源代码更接近于机器语言,但仍然包含了一些用于定位和操作数据结构的指令。
然后,类加载器(ClassLoader)会负责将这种字节码加载到内存中。这个过程通常是动态进行的,也就是说,当程序需要使用某个类的时候,类加载器就会将这个类的字节码加载到内存中。这个过程通常发生在Java虚拟机(JVM)的运行时数据区(Runtime Data Area)的方法区内。
需要注意的是,虽然字节码文件是JVM的一套指令集规范,但它并不能直接交给底层操作系统去执行。这是因为不同的操作系统可能对同一组字节码有不同的解释方式,因此需要一个专门的命令解析器执行引擎(Execution Engine)来将字节码翻译成底层系统可以执行的指令。
最后,Java程序中还可以调用其他语言编写的本地库接口(Native Interface),进行一些系统调用或者C函数调用。这种方式可以使Java程序更好地利用底层系统的功能,提高程序的性能和效率。
下面是ava程序运行机制详细说明
Java程序运行机制步骤:
- 首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
- 再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;
- 运行字节码的工作是由解释器(java命令)来完成的。
从上图中可以观察到,Java文件经过编译器的处理后,被转换成了对应的.class文件。接下来,类加载器会负责将这些.class文件加载到Java虚拟机(JVM)中。
实际上,类的加载过程可以用一句话来概括:类的加载是将类的二进制数据从.class文件中读取出来,并将其放入内存中的运行时数据区的方法区内。然后,在堆区创建一个java.lang.Class对象,用于封装类在方法区内的数据结构。
通过类加载器的作用,JVM能够动态地加载和卸载类,使得程序可以在运行时动态地获取和使用类。这种动态性是Java语言的一大特点,它赋予了程序更高的灵活性和扩展性。
2、说一下JVM运行时数据区?
Java虚拟机在执行Java程序的过程中,会将其管理的内存区域划分为多个不同的数据区域。每个数据区域都有特定的用途,并且它们的创建和销毁时间也不同。
首先,Java虚拟机将内存区域划分为堆(Heap)、方法区(Method Area)和栈(Stack)。
-
堆:堆是Java虚拟机所管理的最大的数据区域。它用于存储对象实例以及数组。当创建一个新的对象时,会在堆上分配相应的内存空间。而当一个对象不再被引用时,垃圾回收器会负责将其所占用的内存释放回堆中。堆的大小可以通过JVM的参数进行设置,例如-Xmx和-Xms分别表示最大堆大小和初始堆大小。
-
方法区:方法区是用于存储类信息、常量池、静态变量等数据的一块内存区域。它与Java类相关联,因为每个Java类都包含方法区的一份拷贝。方法区的生命周期与Java虚拟机的生命周期一致,因此随着虚拟机进程的启动而存在。
-
栈:栈是Java虚拟机用来支持函数调用和方法执行的数据结构。每个线程在创建时都会创建一个独立的栈,其中存储着局部变量、操作数栈和返回地址等信息。栈的特点是后进先出(LIFO),即最后进入栈的元素会最先被取出。栈的生命周期与线程的生命周期一致,因此依赖于线程的启动和结束来建立和销毁。
除了这些主要的数据区域外,Java虚拟机还可能划分其他一些辅助的区域,例如程序计数器(Program Counter Register)和本地方法栈(Native Method Stack)等。这些区域的具体用途和生命周期取决于Java虚拟机的实现和运行环境。
- 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
- 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
3、什么是堆内存?
以Hotspot为例,堆内存(HEAP)主要由GC模块进行分配和管理, 可分为以下部分:
- 新生代(伊甸园区+幸存者区)
- 老年代
我们在jvm参数中只要使用-Xms,-Xmx等参数就可以设置堆的大小和最大值,理解jvm的堆还需要知道下面这个公式:
堆内内存 = 新生代+老年代。如下面的图所示:
在使用堆内内存(on-heap memory)的时候,完全遵守JVM虚拟机的内存管理机制,采用垃圾回收器(GC)统一进行内存管理,GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,GC会对所有分配的堆内内存进行扫描,在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World。
常见的垃圾回收算法主要有:
- 引用计数器法(Reference Counting)
- 标记清除法(Mark-Sweep)
- 复制算法(Coping)
- 标记压缩法(Mark-Compact)
- 分代算法(Generational Collecting)
4、什么是堆外内存?
堆外内存,也常被称为直接内存,是Java虚拟机管理内存的一种方式。与Java虚拟机的堆内存相对应,堆外内存是将内存对象分配在Java虚拟机的堆以外的内存区域。这部分内存并不受Java虚拟机的管理,而是直接由操作系统进行操作和管理。
这种设计的主要优点是能够在一定程度上减少垃圾回收对应用程序造成的影响。因为堆外内存的分配和释放不依赖于Java虚拟机的垃圾回收机制,所以它可以更快速地进行内存的分配和释放,从而提高应用程序的性能。
作为Java开发者,我们经常使用java.nio.DirectByteBuffer类来管理堆外内存。这个类的实例会在对象创建的时候自动分配堆外内存。DirectByteBuffer类提供了一种在Java堆外分配内存的方式,它主要是通过其成员变量unsafe来进行操作的。
5、使用堆外内存的优点
- 减少了垃圾回收, 因为垃圾回收会暂停其他的工作。
- 加快了复制的速度,堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。
6、简述Java垃圾回收机制
在Java编程中,程序员并不需要显式地释放对象的内存,这是由Java虚拟机(JVM)自动完成的。当一个对象不再被任何变量引用时,它就会被视为“垃圾”,并被标记为可回收的内存。然后,JVM会定期运行垃圾回收线程来检查这些垃圾对象,并将它们从内存中清除。
这个垃圾回收线程在JVM中的优先级是低的,这意味着它不会在程序运行过程中频繁地执行。相反,它会在JVM认为合适的时候执行。例如,当JVM的空闲时间超过一定阈值时,或者当堆内存的使用率达到一定限制时,垃圾回收线程就会被触发。
垃圾回收线程的工作过程是这样的:首先,它会扫描所有的对象,找出那些没有被任何其他变量引用的对象。这些对象就被称为“垃圾”。然后,它将这些垃圾对象添加到一个称为“垃圾回收集合”的数据结构中。最后,它会从内存中彻底清除这些垃圾对象,释放它们占用的内存空间。
总的来说,Java程序员不需要担心内存管理问题,因为JVM会自动处理这些问题。这大大简化了编程的复杂性,使得程序员可以更专注于实现具体的功能,而不是管理内存。
7、哪些对象会被存放到老年代?
- 新生代对象每次经历⼀次minor gc,年龄会加1,当达到年龄阈值(默认为15岁)会直接进⼊老年代;
- 大对象直接进⼊老年代;
- 新生代复制算法需要⼀个survivor区进行轮换备份,如果出现大量对象在minor gc后仍然存活的情况时,就需要老年代进行分配担保,让survivor⽆法容纳的对象直接进⼊老年代;
- 如果在Survivor空间中相同年龄所有对象大⼩的总和大于Survivor空间的⼀半,年龄大于或等于该年龄的对象就可以直接进⼊年⽼代。
8、什么时候触发full gc?
- 调用System.gc时,系统建议执行Full GC,但是不必然执行
- 老年代空间不⾜
- 方法区空间不⾜
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小