写点什么

JVM 课程作业

作者:追随哆咪
  • 2023-02-25
    山东
  • 本文字数:4668 字

    阅读完需:约 15 分钟

题目 01- 请你用自己的语言向我介绍 Java 运行时数据区(内存区域)

堆、虚拟机栈、本地方法栈、方法区(永久代、元空间)、运行时常量池(字符串常量池)、直接内存

答:

堆:

是运行时数据区中的一部分,而且是占比最大的一块区域,属于线程共享的所以需要进行垃圾回收。它分为年轻代和老年代(jdk1.8),双方占比是 1:2。年轻代还区分成了 Eden 和 2 个 Survivor 区,占比为 8:1。

之所以要划分年代是基于分代假说理论的基础上实现的,为了提升 GC 的效率和内存空间使用率考虑的。之所以是 2 个 Survivor 区,是因为这块区域用的是复制算法进行垃圾对象计算的,也是为了提高效率。JDK1.8 之后,老年代有了 Humongous 区,是在对象创建的时候如果 Eden 区放不下的话,直接会进入到 Humongous 区的。

虚拟机栈:

是运行时数据区中的一部分,属于线程独享,默认值是 1M,负责执行 Java 编写的方法,它是一种 LIFO 的数据结构,类似于弹夹一样。

每个线程都会创建自己的虚拟机栈,里面主要存储的是栈帧,栈帧是一种数据结构,主要存储的是局部变量表、操作数栈、动态链接和方法返回地址等信息,每个方法从被调用到执行完毕的过程,都对应着一个栈帧的入栈和出栈。当方法被执行的时候,会创建一个栈帧,当方法结束返回或者异常返回的时候,栈帧都会自动结束。

虚拟机栈会发生 2 种错误,一个是栈帧的个数超过虚拟机栈的大小的时候,会报 StackOverflowError;另一个是当开启一个新的线程创建虚拟机栈的时候,如果 jvm 内存不够无法创建栈的时候,会报一个 OutofMemoryError 的错误。

本地方法栈:

是运行时数据区中的一部分,属于线程独享。结构和虚拟机栈一样,区别在于本地方法被调用的时候,创建的是本地方法栈。

方法区(永久代、元空间)

是运行时数据区中的一部分,需要线程共享,需要进行垃圾回收的。方法区主要存储的是编译之后的代码、类的元数据、静态数据以及运行时常量池的。

它的实现方式分为 JDK1.7 之前的永久代和 JDK1.8 之后的元空间,主要的演变过程是 JDK1.6 叫永久代,放在 JVM 中的运行时数据区中;JDK1.7 的时候将其中的静态数据和运行时常量池放入了堆中,类的元数据和编译之后的代码依然存在永久代中;在 JDK1.8 的时候改名成了元空间,并且将这部分从 JVM 中挪了出来到外面的物理机内存上了。1.8 的这种方式可以提升 GC 计算和回收垃圾对象的效率,而且因为类的元数据、方法等信息不太好计算空间大小,所以在永久代中指定大小比较困难,小了容易报 PermGen Space 的错误,大了就容易导致老年代溢出。字符串存在永久代中,也容易出现性能问题和永久代内存溢出。

运行时常量池:

属于方法区的一部分数据,运行时常量池主要存储字面量和符号引用。一个字节码文件有一个字节码的常量池,一个字节码常量池有一个运行时常量池,但是为了避免在每一个运行时常量池中存在重复的字符串,节省内存空间,并且为了提高读取的效率,因而产生了字符串常量池这种特殊的常量池。

字符串常量池是共享的,全局只有一个,他是 StringTable 的数据结构,类似 HashTable 这种结构,底层是数组+链表来实现的,每个子项是个 Entry 的元素,使用哈希函数来生成 Hashcode,再与数组的长度取余计算出索引位置,在读取上是 O(1)的速度。

因为哈希函数会有哈希冲突的情况,所以它还会采用哈希碰撞来确认字符串是否相同。具体查找对象的方式是:先根据字符串的字面量生成 hashcode,找到对应的 entry;如果之前没有发生过冲突,则可能只是一个 entry;如果之前发生过冲突,那么他可能是个链表,然后 Java 会遍历链表的每个 entry,匹配引用对应的字符串;如果找到字符串,就返回字符串的引用;如果没有找到字符串,再使用 intern()方法的时候,会将 intern()方法调用者的引用放入到 stringtable

直接内存

不属于 JVM 的一部分,它是为了避免在 Java 堆和 native 堆中来复制数据而增加的一种内存。JDK1.4 之后加入了一个 NIO 类,引入了一种基于了通道和缓冲区的 I/O 方式,它是使用原生 native 函数直接分配堆外内存的然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作的。直接内存的分配不受 Java 虚拟机大小的影响,只跟物理机总内存大小有关系。直接内存创建的效率不如堆内存高效,但是他的读取数据的效率比堆内存高效。


为什么堆内存要分年轻代和老年代?

答:因为按照三大分代假说,弱分代假说是指绝大多数对象是朝生夕灭的,强分代假说是指越难被垃圾回收器回收的对象 越不容易消亡,所以最好将对象区分成容易回收的和不容易回收的。对于这两种对象区别对待,容易被回收的放在一起,不容易被回收的放在一起,这样可以提高垃圾收集的效率和内存空间的使用率。


题目 02- 描述一个 Java 对象的生命周期

  • 解释一个对象的创建过程

答:

对象的创建过程分为三部分,加载—>链接—>初始化。

  1. 加载:是指查找字节流,并且据此创建类的过程。

  2. 链接:是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。

  3. 验证:在于确保被加载类能够满足 Java 虚拟机的约束条件。

  4. 准备:则是为被加载类的静态字段分配内存。

  5. 解析:正是将这些符号引用解析成为实际引用。

  6. 初始化:则是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。

举例:

  • 通过 new 指令来触发初始化类,先判断类是否被加载过了。

  • 如果没有加载过需要先到类加载子系统里面进行加载(启动类加载器、扩展类加载器、应用类加载器、自定义加载器,该过程会遵循双亲委派机制,自下至上检查类是否被加载,自上而下加载类)。

  • 如果加载了,需要开始分配内存空间了,有两种分配方式,分别是:

  • 指针碰撞:垃圾收集器不带压缩功能(Serial,parNew 等)。主要在年轻代使用,内存地址是连续的。但是这种方式会在内存分配的时候产生内存分配安全问题(线程安全),解决方案是 TLAB 本地线程分配缓存和 CAS 乐观锁。

  • 空间列表:垃圾收集器带压缩功能(CMS)。主要在老年代使用,内存空间不连续。

  • 然后进行内存空间的初始化为零值,并且进行必要内容的设置,然后执行<clinit>方法对其他内容进行初始化。


  • 解释一个对象的内存分配

答:

  • 当对象申请内存空间的时候,会默认进入到 Eden 区申请内存

  • 如果 Eden 区放得下,则在 Young 区申请成功

  • 如果 Eden 区放不下,则进行 YoungGC,会将 Eden 区的存活对象移植到 Survivor 某个区中

  • 如果经过 YoungGC 之后,Eden 区或者 S0/S1 区能放得下,则在 Young 区申请成功

  • 如果经过 YoungGC 之后,放不下,则会到 Old 区判断对方是否能放下

  • 如果 Old 区能放下,则在 Old 区申请成功

  • 如果 Old 区放不下,则需要进行 FullGC

  • 如果 FullGC 之后能放下,则申请成功

  • 如果 FullGC 之后放不下,会再重复两次(一共三次 FullGC),再判断是否能放下,如果能则申请成功;如果依然不能,则报 OOM 的错


  • 解释一个对象的销毁过程

答:

判断一个对象是否可以被销毁,主要有两种方式:

  • 引用计数算法:顾名思义通过对象引用计数的方式来判断对象是否可以被回收,如果对象被引用则引用数+1,如果引用消失,则引用数-1,当引用数变为 0 的时候,此时对象就是垃圾对象需要被回收。效率不稳定,因为要遍历全部对象,而且对于循环引用的对象无法识别。

  • 根可达算法:根可达算法是指通过一系列叫 GCRoot 的对象作为起点,从这些起点开始向下搜索,搜索走过的路径叫引用链,当一个对象到 GCRoot 没有任何引用链相连时,则这个对象是不可用的,可以视为垃圾对象。根可达算法可以解决循环引用的问题,而且效率高,因为他只需要从起点开始遍历,对于没有遍历到的对象,自动视为垃圾对象。

对于不可达的对象不能在一开始就认为是垃圾对象,要进行一次标记,然后会进行筛选,筛选条件是对象是否需要调用 Finalize()方法,如果需要调用 Finalize()方法,则会看是否会在 Finalize()方法中对该对象进行重新与引用链建立关系,如果没有,将会被进行第二次的标记。第二次标记成功的对象将真的会被回收,如果失败则继续存活。


  • 对象的 2 种访问方式是什么?

答:

  1. 句柄:稳定,对象被移动只要修改句柄中的地址

  2. 直接指针:访问速度快,节省了一次指针定位的开销


  • 为什么需要内存担保?

答:

内存担保的主要原因是:尽量让新创建的对象的内存在 Young 区进行创建,因为新创建的对象可能很快就消亡了,而且在 Young 区进行 MinorGC 比较频繁,所有可以很快回收内存,所有如果 Young 区内存不够的话,先把之前在 Young 区存活的老对象移植到 Old 区,因为这部分对象可能比较难以销毁,所以尽早进入 Old 区比较好。


题目 03- 垃圾收集算法有哪些?垃圾收集器有哪些?他们的特点是什么?

答:

算法:

  • 标记-清除算法:最基本的算法,主要分为标记和清除 2 个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。缺点是:效率不高,而且会产生大量内存碎片化,大对象无法分配,提前进行 GC。

  • 复制算法:它将可用内存按容量划分为相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。JVM 中的 Young 区就采用这种方式。这种方式没有碎片化内存空间,但是会存在一定程度的内存浪费。

  • 标记-整理算法:比标记-清除算法多了一个整理的过程,在标记和清除之后,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。JVM 中的 Old 区采用这种方式。确定是性能较低,因为要 copy 对象并且进行空间的压缩。


垃圾收集器:

  • 串行:

  • Serial——Young 区回收器

  • SerialOld——Old 区回收器

  • 并行:

  • ParNew——Young 区回收器

  • Parallel Scavenge——Young 区回收器

  • Parallel Old——Old 区回收器

  • CMS——Old 区回收器

  • 堆回收器:

  • G1

  • ZGC


  • Serial 收集器

答:属于年轻代的垃圾收集器,单线程执行,采用复制算法,进行垃圾回收的时候,必须暂停其他所有的工作线程


  • SerialOld 收集器

答:属于老年代的垃圾收集器,单线程执行,采用标记-整理算法,进行垃圾回收的时候,必须暂停其他所有的工作线程


  • ParNew 收集器

答:

  • 年轻代使用并行回收收集器,采用复制算法,进行垃圾回收的时候,必须暂停其他所有的工作线程

  • 老年代使用串行收集器,采用标记-整理算法,进行垃圾回收的时候,必须暂停其他所有的工作线程

  • Serial 收集器的多线程版本,单 CPU 性能并不如 Serial,因为存在线程交互的开销


  • ParallelScavenge 收集器

答:着重的是达到一个可控制的吞吐量

  • 吞吐量优先收集器(吞吐量 = 运行用户代码时间 /(运行用户代码时间+垃圾收集时间))

  • 年轻代使用并行回收收集器,采用复制算法

  • 老年代使用串行收集器,采用标记-整理算法


  • ParallelOld 收集器

答:老年代使用并行收集器,采用标记-整理算法。在 CPU 敏感,并且注重吞吐量的场合,可以采用 ParallelScavenge 和 ParallelOld 组合的方式的收集器。


  • CMS 收集器

答:

  • 低延迟:减少 STW 对用户体验的影响【低延迟要求高】

  • 并发收集:可以同时执行用户线程

  • CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率

  • 达到某一阈值时,便开始进行回收

  • CMS 收集器的垃圾收集算法采用的是标记-清除算法

  • 会产生内存碎片,导致并发清除后,用户线程可用的空间不足

  • CMS 收集器对 CPU 资源非常敏感。


  • G1 收集器

答:

  • 并行与并发:充分利用多核环境下的硬件优势

  • 多代收集:不需要其他收集器配合就能独立管理整个 GC 堆

  • 空间整合:“标记-整理”算法实现的收集器,局部上基于“复制”算法不会产生内存空间碎片

  • 可预测的停顿:能让使用者明确指定消耗在垃圾收集上的时间。当然,更短的 GC 时间的代价是回

  • 收空间的效率降低


  • ZGC 收集器

答:

  • 并发

  • 基于 region

  • 压缩

  • NUMA 感知

  • 使用彩色指针

  • 使用负载屏障

用户头像

追随哆咪

关注

还未添加个人签名 2018-02-10 加入

还未添加个人简介

评论

发布
暂无评论
JVM课程作业_追随哆咪_InfoQ写作社区