Java虚拟机面试题汇锦

9 篇文章 0 订阅
3 篇文章 0 订阅

简述JVM内存模型

线程私有的运行时数据区:程序计数器、Java虚拟机栈、本地方法栈。
线程共享的运行时数据区:方法区、堆。


简述程序计数器(PC)

程序计数器表示当前线程所执行的字节码的行号指示器。
程序计数器不会 产生 StackOverflowError 和 OutOfMemoryError。

简述Java虚拟机栈

Java虚拟机栈是用来描述Java方法执行的内存模型。线程在被创建时会被分配一个栈空间,当线程结束后栈空间就会被回收。
栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作数栈、动态链接、返回地址和指向运行时常量池的引用等信息。
Java虚拟机栈会产生 StackOverflowError(线程请求的栈深度大于虚拟机允许的深度抛出时) 、 OutOfMemoryError(如果 JVM 栈容量可以动态扩展,虚拟机栈占用内存超出抛出) 两类异常、

简述本地方法栈

本地方法栈为本地方法服务(Java虚拟机栈为虚拟机执行Java方法服务),可将本地方法栈看作由native关键词修饰的函数对应的内存模型。
同样会产生 StackOverflowError 、OutOfMemoryError 两类异常。

简述Java虚拟机中的堆

Java中几乎所有的对象实例都在堆中分配内存,是内存管理中最大的一块,同时也是主要进行垃圾回收的区域。

简述方法区

方法区在Java虚拟机中用来加载类信息、常量、静态变量等数据。
Java 6 及之前使用的是永久代来实现,容易OOM;Java 7 将 字符串常量池、静态变量等从永久代移出;Java 8 开始则抛弃了永久代,使用在本地内存中实现的元空间来实现方法区,将 Java 7 中 永久代内容移到了元空间。

运行时常量池

存放常量池表,包含编译器生成的各种字面量和符号引用。除了保存Class文件中描述的符号引用外,还会把符号引用绑定的直接引用也存储在运行时常量池。
也会存放字符串常量。Java 8 之前位于方法区,大小受限于方法区,Java 8 之后开始将运行时常量池存放在堆中。

直接内存

又称堆外内存,即把内存对象分配在JVM堆外的内存区域。这部分内存区域由操作系统直接管理而不是虚拟机。
Java 通过 DriectByteBuffer 对其进行操作,避免了在 Java堆 和Native堆 间来回复制数据。


JVM的类加载过程

包含 加载、链接与初始化三个过程。

加载:查找字节流并据此创建类的过程。
1)对于数组类来说,没有对应的字节流,其是由Java虚拟机直接生成的。对于其它类,则是Java虚拟机通过类加载器来完成查找字节流的过程。
2)在Java虚拟机中,类的唯一性是由类加载器实例及类的全名共同确定的。即便是同一串字节流,若经由不同的类加载器来加载则也会得到两个不同的类(大型应用中常借此特性来运行同一个类的不同版本)。

链接:指将创建成的类合并到Java虚拟机中并使之能执行的过程。分为 验证、准备及解析三个阶段。

  • 验证:目的在于确保被加载类能够满足Java虚拟机的约束条件。
  • 准备:目的在于为被加载类的静态字段分配内存。对于静态字段的具体初始化会在初始化阶段进行。同时部分虚拟机还会实现虚方法的动态绑定方法表。
  • 解析:目的在于将符号引用转换为实际引用。

如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

初始化:为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java虚拟机会通过加锁以确保类的 < clinit > 方法仅被执行一此。

在Java代码中,如果要初始化一个静态字段,可声明是直接赋值,也可在静态代码块中对其赋值。如果直接赋值的静态字段被 final 修饰,且为基本类型或字符串时,此字段就会被编译器标记为常量值,其初始化由虚拟机完成。除此以外的直接赋值操作,以及所有静态代码块中的代码,会被编译器放于同一方法中,且命名为 < clinit > 方法。

初始化完成以后类才正式处于可执行状态。


简述Java创建对象的过程

1)检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。
2)通过检查后虚拟机将为新生对象分配内存。
3)完成内存分配后虚拟机将成员变量设置为零值。
4)设置对象头,包括哈希码、GC信息、锁信息、对象所属类元信息等。
5)执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

有哪些类加载器

1)BootStrapClassLoader(启动类加载器)

  • 负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。
  • 启动类加载器是由 C++ 实现的,没有对应的 Java 对象,因此在 Java 中只能用 null 来指代。
  • 其他的类加载器都是 java.lang.ClassLoader 的子类,因此有对应的 Java 对象。这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至 Java 虚拟机中,方能执行类加载。

2) ExtensionClassLoader(扩展类加载器)

  • 负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。
  • 扩展类加载器的父类加载器是启动类加载器。

3)AppClassLoader(应用类加载器)

  • 负责加载应用程序路径下的类(指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径),默认情况下,应用程序中包含的类便是由应用类加载器加载的。
  • 应用类加载器的父类加载器则是扩展类加载器。

4)自定义类加载器

Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。

双亲委派机制

一个类加载器收到类加载请求后,会首先判断当前类是否被加载过,若被加载过则会直接返回,若未被加载过则会首先将类加载请求转发给父类加载器,一直转发到启动类加载器,只有当前父类加载器无法完成时才会自己尝试加载。

  • 加载类顺序:BootstrapClassLoader->ExtensionClassLoader->AppClassLoader->CustomClassLoader
  • 检查类是否加载顺序:CustomClassLoader->AppClassLoader->ExtensionClassLoader->BootstrapClassLoader

优点:

  • 避免类的重复加载。相同的类被不同的类加载器加载会产生不同的类,双亲委派机制保证了Java程序的稳定运行。
  • 保证核心API不被修改。

如何避开双亲委派机制:

  • 重载 loadClass() 方法,即自定义类加载器。

如何自定义类加载器:

  • 新建自定义类继承 java.lang.ClassLoader。
  • 重写 findclass、loadClass、defineClass 方法。

JVM的对象内存分配策略

  • 指针碰撞:在内存中放一个指针作为分界指示器将使用过的内存放在一边,空闲的放在另一半,通过挪动指针完成分配。
  • 空闲列表:对于Java堆内存不规整的情况,虚拟机维护了一个列表记录那些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。

Java对象的内存分配怎么保证线程安全

  • 对分配内存空间采用CAS机制,配合失败重试的方式保证操作的原子性,但该方法效率低。
  • 常用策略是对每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存时直接在这块内存中分配。

Java 对象的内存布局

对象在堆内存的存储布局分为 对象头、实例数据和对齐填充

对象头:包含 MarkWord、类型指针。

  • MarkWord:存储哈希码、GC分代年龄、锁状态标志位、线程持有的锁、偏向线程ID等信息。
  • 类型指针:对象指向其类的元数据的的指针,若对象是数组,则有一块用于记录数组长度的数据。

实例数据:代码中所定义的各种类型的字段信息。
对齐填充:起占位作用。HotSpot 虚拟机要求对象的起始地址必须是 8 的整数倍,因此需要对齐填充(涉及到压缩指针概念)。

常见内存分配策略

1)多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够的空间时将发起一次 Minor GC。
对于大对象的内存分配,将直接进入老年代分配。
2)如果对象在经历 Minor GC 后还存活且能被 survivor 容纳,则会将该对象移动到 survivor 中并将年龄设置为 1,且每经过一次 Minor GC 都会将年龄 +1 。当年龄增到到一定程度(默认 15)就会被晋升到老年代。
3)如果在 survivor 中相同年龄的所有对象的大小的总合大于 survivor 的一半,则所有不小于该年龄的对象可直接进入老年代。

空间分配担保

Minor GC 前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,若满足则表示此次 Minor GC 确定安全。若不满足,JVM则会查看 HandlePromotionFailure 参数是否允许担保失败,若允许则会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足则允许 Minor GC ,否则将进行 Full GC。


对象是否是垃圾的判断

  • 引用计数法:设置引用计数器,对象被引用则+1,引用失效则 -1,如果计数器为 0 则被标记为垃圾。但可能存在循环引用情况,一般不使用这种方法。
  • 可达性分析:通过 GC Roots 的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,如果某个对象没被搜到,则被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。

Java 的四种引用类型

https://blog.csdn.net/qq_39694454/article/details/116530091

  • 强引用:强引用关联的对象不会被回收,使用new方法新建的对象一般都为强引用。
  • 软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。一般采用 SoftReference 类来创建软引用。
  • 弱引用:垃圾收集器碰到即回收,也就是说它只能存活到下一次垃圾回收发生之前。一般采用
    WeakReference 类来创建弱引用。
  • 虚引用:无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知(提供一种在对象被finalize之后能做某些事情的机制)。虚引用必须与引用队列联合使用。

主要的垃圾清除算法

  • 标记清除算法:先标记需要清楚的对象,然后进行统一回收。效率不高,且会产生大量不连续的碎片。
  • 标记整理算法:先标记存活的对象,然后将所有存活对象向一端移动,之后清理边界外的内存。
  • 标记复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间使用完后,就将存活对象复制到另一块,再将已使用的内存空间一次性清理掉。

什么是分代收集算法

依据对象在内存的存活周期将对象划分为几块,并采取不同的收集算法。
一般划分为新生代与老年代,新生代使用标记复制算法,老年代使用标记清除或标记整理算法。


垃圾收集器

Serial垃圾收集器

单线程串行收集器。进行垃圾回收时,必须暂停其它线程。(Stop the world)
新生代使用标记复制算法,老年代使用标记整理算法。简单高效。

ParNew 垃圾收集器

可认为是 Serial 垃圾收集器的多线程版本。

Parallel Scavenge 垃圾收集器

注重吞吐量的垃圾收集器,CPU代码运行时间/CPU总消耗时间(代码运行时间+垃圾回收时间)。
新生代使用标记复制算法,老年代使用标记整理算法。

CMS 垃圾收集器

注重最短时间停顿。
CMS垃圾收集器为最早提出的并发收集器,垃圾收集线程与用户线程同时并行。采用标记清除算法。

收集步骤:

  • 初始标记:暂停其它线程(Stop the world),标记与 GC roots 直接关联的对象。
  • 并发标记:可达性分析过程(不会停顿)。
  • 并发预清理:查找指向并发标记阶段从 新生代 晋升 老年代 的对象,重新标记,暂停虚拟机(Stop the world)扫描CMS堆中剩余对象。
  • 并发清除:清理垃圾对象,程序不会停顿。
  • 并发重置:重置CMS垃圾收集器的数据结构。
G1 垃圾收集器

G1 垃圾收集器将堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。每个小空间可以单独进行垃圾回收。
收集步骤:

  • 初始标记:标记与GC roots直接关联的对象。
  • 并发标记:可达性分析。
  • 最终标记:将并发标记过程中,用户修改的对象再次标记。
  • 筛选回收:对各个 Region 的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间制定回收计划并回收。

Minor GC

指发生在新生代的垃圾回收,因为大多数对象存活时间很短,所以 Minor GC 发生非常频繁,一般回收速度也很快

Full GC

指清理整个堆内存空间,包括老年代与新生代。
调用 System.GC()、老年代空间不足、空间分配担保失败等都会引发 Full GC。


常见 JVM 调优参数

  • -Xms 初始堆大小
  • -Xmx 最大堆大小
  • -XX:NewSize 新生代大小
  • -XX:MaxNewSize 新生代最大值
  • -XX:PermSize 永久代初始值
  • -XX:MaxPermSize 永久代最大值
  • -XX:NewRatio 新生代与老年代比例
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值