面试系列重启:JVM 篇
时间:2021-10-20 16:36:30
手机看文章
扫描二维码
随时随地手机看文章
[导读]今天跟大家聊聊JVM的面试。但是其实我不知道这种直接问题答案的形式大家喜欢点,还是喜欢我最开始俏皮的面试系列那种风格?6000字,发车!什么是跨平台性,已经在Java中是如何实现的?平台可以指OS硬件,所谓跨平台性,是指语言编写的程序,可以在多个系统平台上运行。字节码是通过Jav...
今天跟大家聊聊 JVM 的面试。但是其实我不知道这种直接问题答案的形式大家喜欢点,还是喜欢我最开始俏皮的面试系列那种风格?6000 字,发车!
什么是跨平台性,已经在Java中是如何实现的?
平台可以指OS 硬件,所谓跨平台性,是指语言编写的程序,可以在多个系统平台上运行。字节码是通过Java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序编译后的字节码文件了,即一次编译,到处运行。JVM是跨平台的吗?
不是的,Java 平台的核心是执行字节码的"虚拟机器"的概念。无论程序运行在哪硬件或操作系统下,此字节码都是一样的。虽然 Java 程序是独立于平台的,但执行这些程序的 Java 虚拟机代码并非如此。每个操作系统或者硬件上都有不同的虚拟机。JVM是如何工作的?
用户创建 aobing.java 文件Java 编译器(javac)把文件编译到 aobing.class文件,这个地方会有一些编译期优化。Java 虚拟机器加载类,并由解释器逐条翻译或即时编译器将其编译为机器代码。转换后的机器代码是由 CPU 直接执行,主流的虚拟机都是架设在操作系统之上的。什么时即时编译器?
即时编译器是 JRE 的一部分,全称 Just-In-Time Compiler ,一般称之为 JIT ,它可显著的提高 Java 应用程序在运行时间的性能。Java 编译成字节码后,这些代码可以通过 JVM 在许多不同的计算机架构上进行运行。在开始运行时,JVM 中的解释器首先开始工作,逐行的将字节码解释为本地机器码,意味着 Java 应用程序的执行速度比本地语言的应用程序慢,这是背景。为了提高效率,虚拟机引入了 JIT 技术,通过将热点代码编译成本机代码,以提高 Java 程序的性能。JIT 编译后的代码存放在方法区中。虚拟机是怎么识别出热点代码的?
目前热点代码的探测有两种方式:采样计数器HotSpot使用的是计数器的方式,它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)回边计数器(Back EdgeCounter)。这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。JDK、JRE和JVM
JDK:JDK 是 (SDK) 软件开发套件的扩展子集,包括用于开发、调试和监控 Java 应用程序的工具。JRE:JRE 称为 Java 运行时间环境是 JDK 的一部分,是开发 Java 应用程序的一组编程工具。Java 运行时间环境为执行 Java 应用程序提供了最低要求,并且它由 Java 虚拟机器 (JVM) 核心类和核心类库组成。JVM:JVM是一种可以执行字节码的虚拟机器。它是 Java 平台的代码执行组件。什么是虚引用?
虚引用是虚拟机中定义的"非强"引用的级别之一。4种引用的级别由高到低依次为强引用、软引用、弱引用和虚引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。虚引用的对象,它是如此脆弱以至于我们通过虚引用甚至无法获取到被引用的对象,它存在的唯一作用就是当它指向的对象被回收后,虚引用本身会被加入到引用队列中,用作记录它指向的对象已被回收。什么是Java的内存结构、内存模型和对象模型内存结构是和运行时数据区有关。内存模型是指用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,是虚拟机的规范之一。对象模型是指java对象在内存中真正的存储(表示)形式有关。运行时数据区包括哪几部分?
存放实例对象的堆。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器生成代码的方法区。为了支持多线程的程序计数器。用于支持方法的运行的虚拟机栈和本地方法栈。面向对象的优点
模型和真实世界中的对象类似,理解起来更容易,又因为面向对象有封装、继承、多态的特性,可以设计出低耦合高内聚的系统,易维护、易复用、易扩展。什么是多态?
面向对象的第三大特性之一。是指同一个行为具有多个不同表现形式或形态的能力。Java作为面向对象的语言,同样可以描述一个事物的多种形态。如Student类继承了Person类,一个Student的对象便既是Student,又是Person。final finally finalize区别
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用。说一下JVM加载一个类的过程
JVM 中类的装载是由类加载器,也就是ClassLoader,和它的子类来实现的,Java 中的类加载器是一个重要的 Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。由于 Java 的跨平台性, 经过编译的 Java 源程序并不是一个可执行程序, 而是一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接( 验证、 准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应的 Class 对象。加载完成后, Class 对象还不完整, 所以此时的类还不可用。当类被加载后就进入连接阶段, 这一阶段包括验证、准备( 为静态变量分配内存并设置默认的初始值) 和解析( 将符号引用替换为直接引用) 三个步骤。最后 JVM 对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句, 就依次执行这些初始化语句。类加载器有哪些,各有什么作用?
从JDK 1.2开始, 类加载过程采取了双亲委派机制。更好的保证了 Java 平台的安全性,在该机制中,JVM 自带的 Bootstrap 是根加载器, 其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载。JVM 不会向 Java 程序提供对 Bootstrap 的引用。- 根加载器(BootStrap)一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar)。
- 扩展加载器( ExtClassLoader)从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap。
- 系统加载器( AppClassLoader) 又叫应用类加载器,其父类是 Extension。它是应用最广泛的类加载器。它从环境变量 classpath 或者系统属性 java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。
- 用户自定义类加载器 ( java.lang.ClassLoader 的子类)父类是AppClassLoader。
error和exception有什么区别?
error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。exception表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况。System.out.println(),System是什么,out是什么,println又是什么?
System是捆绑在java.lang包中的最终类。out是打印流类的参考,它是系统类的静态成员。println是一种打印流类的方法,它捆绑在 java.io 包中打印输出。方法区、永久区和元数据区它们之间是什么关系?
方法区是jvm规范里要求的,永久区是Hotspot虚拟机对方法区的具体实现,前者是规范,后者是实现方式。jdk1.8作了改变。说说GC是什么,以及为什么要有GC?
GC 是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃。Java 虚拟机提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。Java 程序员不用担心内存管理, 因为垃圾收集器会自动进行管理。GC中STW是什么?
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程(除了垃圾收集)都被挂起。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互。如何识别出垃圾?
常用有两种方式:引用计数法,这种难以解决对象之间的循环引用的问题。可达性分析算法,主流的JVM采用的是这种方式。简单聊聊垃圾回收算法
- 标记-清除算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
- 复制算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。缺点是浪费空间,优点是回收速度快,没碎片。
- 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,完成碎片整理。
- 分代收集算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,我觉得它更像是一种思想,而不是算法。
你知道有哪些垃圾回收器?
- Serial 收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
- ParNew 收集器,ParNew 收集器其实就是 Serial 收集器的多线程版本。
- Parallel 收集器,Parallel Scavenge 收集器类似 ParNew 收集器,Parallel 收集器更关注系统的吞吐量。
- Parallel Old 收集器,Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法
- CMS 收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
- G1 收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征
基于栈和寄存器的指令集架构是什么?
前者的指令运行过程,需要借助栈这个数据结构来完成,主要的优点就是可移植,缺点是执行速度慢。后者的寄存器指令由硬件直接提供,速度很快,但是缺点是和硬件强绑定。我们主流的java虚拟机采用的都是基于栈的指令集架构。虚拟机栈是什么?
JVM规范让每个Java线程拥有自己的独立的JVM栈,也就是Java方法的调用栈。当方法调用的时候,会生成一个栈帧。方法的调用返回过程,其实就是栈帧的入栈出栈。栈帧是保存在虚拟机栈中的,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。程序计数器为什么是私有的?
虚拟机是支持多线程并发的,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。需要注意的是,如果执行的是 native 修饰的本地方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。SafePoint 是什么?
比如 GC 的时候必须要等到 Java 线程都进入到 safepoint 的时候 VMThread 才能开始执行 GC 1.循环的末尾 (防止大循环的时候一直不进入 safepoint,而其他线程在等待它进入 safepoint) 2.方法返回前 3.调用方法的 call 之后 4.抛出异常的位置Java对象的创建过程清楚吗?
- JVM 遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲)
- 为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)”
- 将除对象头外的对象内存空间初始化为 0
- 对对象头进行必要设置
Java 对象结构了解过吗?
Java 对象由三个部分组成:对象头、实例数据、对齐填充。- 对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC 分代年龄、锁标识状态、线程持有的锁、偏向线程 ID(一般占 32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
- 实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
- 对齐填充:JVM 要求对象起始地址必须是 8 字节的整数倍(8 字节对齐)
Java 对象的定位方式你清楚吗?
句柄池、直接指针。方法区和永久代有什么区别
永久代又叫 Perm 区,只存在于 HotSpot JVM 中,并且只存在于 JDK 1.7 和之前的版本中,JDK 1.8 中已经彻底移除了永久代,JDK 1.8 中引入了一个新的内存区域叫 metaspace。- 并不是所有的 JVM 中都有永久代,IBM 的 9,Oracle 的 JRocket 都没有永久代。
- 永久代是实现层面的东西。
- 永久代里面存的东西基本上就是方法区规定的那些东西。
为什么要用 metaspace 替换 permspace 呢
主要有如下几点:- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- 移除永久代是为融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,不需要配置永久代。