面试复习-JVM

发布于 2023-04-18  808 次阅读


JVM


logo

JVM组成 new的在堆

JVM由三部分组成:类加载器、运行时数据区和执行引擎。

  1. 类加载器:类加载器(ClassLoader)负责加载Java字节码文件并将其转换为JVM可运行的代码,同时根据需要解决类依赖问题。JVM内置了三种类加载器,分别是 启动类加载器、扩展类加载器和应用程序类加载器 。这些类加载器按照不同的顺序从不同位置加载类,并将其放入运行时数据区中,以进行进一步的解析和执行。

findclass loadclass 双亲委派模型

线程上下文加载器是Java中一个重要的概念,它可以解决在复杂应用程序中的类加载问题。每个线程都有一个线程上下文加载器,它用于搜索类,如果找不到,则委托给父类加载器来搜索类,直至委托到Bootstrap Class Loader为止

  1. 运行时数据区:Java虚拟机在内存中开辟了运行时数据区,用于存放各种数据和对象实例。其中包括方法区、堆、栈、本地方法栈和程序计数器等五个部分。

img

  • 方法区:方法区(Method Area)主要用于存放类信息、常量、静态变量和编译后的代码等数据。它是所有线程共享的内存区域,一般不进行垃圾回收。
  • 堆:堆(Heap)主要用于存放对象实例。由于 Java 采用了自动内存分配和垃圾回收机制,所以堆是动态分配和释放的。堆是所有线程共享的内存区域,一般通过GC进行垃圾回收。
  • 栈:栈(Stack)用于存储线程执行方法的局部变量、操作数栈等数据。每个线程都有一个独立的栈空间,它们之间互相独立,互不干扰。栈是 LIFO 的,每入栈和出栈都伴随着方法调用和返回操作。
  • 本地方法栈:本地方法栈(Native Method Stack)与栈类似,但其是为本地方法服务的。本地方法是使用本地语言(如 C/C++)编写的方法,可以调用系统底层库或者操作系统提供的功能。本地方法栈也与线程相关,也是 LIFO 的。
  • 程序计数器:程序计数器(Program Counter Register)主要用于记录线程下一条要执行的指令地址。因为线程切换时需要知道线程当前的执行状态,所以程序计数器也是线程私有的。
  1. 执行引擎:执行引擎(Execution Engine)根据字节码指令执行程序动作,并将结果存储到运行时数据区中。执行引擎支持解释执行和即时编译执行两种模式,它们各自的优势和局限性都不同。通常情况下,解释执行比较慢但是灵活,而即时编译执行比较快但是占用内存较多。

这就是 JVM 的主要组成部分,它们协同工作,使 Java 应用程序得以运行。JVM 负责管理 Java 应用程序运行时所需的各种资源,也是 Java 技术实现跨平台的重要原因之一。

类加载器主要任务

  1. 加载:根据类的全限定名(包含包名和类名),解析运行时数据区,即instanceKlass实例,存放在方法区;在堆区生成该类的Class对象,即instanceMirrorKlass对象。
  2. 链接:将类的二进制数据合并到JVM的运行状态之中,包括验证、准备和解析三个阶段。
  • 验证:确保被加载的类的正确性和安全性,包括文件格式验证元数据验证、字节码验证和符号引用验证等。
  • 准备:为类变量分配内存。 final 直接赋值 基础类型 0 引用 null
  • 解析:将符号引用转化为直接引用,如将类、方法和字段等从符号引用转变为直接引用。字符串->地址
  1. 初始化:为类的静态变量赋值。在JVM规范中,一个类只有在需要使用时才会进行初始化,例如创建类实例、调用类的静态方法或访问类的静态变量时。类初始阶段,JVM底层会加锁,解决并发问题

    cinit 静态成员变量复制 收集所有类初始化代码和 static {} 域的代码,收集在一起成为 () 方法;

    子类初始化时会首先调用父类的 () 方法;

    img

线程私有与共享问题

私有的:

image-20230409171145210

栈 栈帧 (局部变量)

局部变量在栈

栈帧 :包含了一个方法的局部变量表、操作数栈、动态链接、方法出口等信息。栈帧通常具有固定的大小

img

image-20230409170851379

堆(对象实例) 静态代码>构造方法 父>子

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。

方法区(共享)

方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。方法区(Method Area)主要用于存放类信息、常量、静态变量和编译后的代码等数据。它是所有线程共享的内存区域,一般不进行垃圾回收。

具体来说,方法区主要包含以下内容:

  1. 类型信息:虚拟机加载的每个类型信息,如类和接口的全限定名、父类和接口列表等;

  2. 字段和方法信息:每个类或接口中定义的所有字段和方法信息,包括常量和静态变量等;

  3. 运行时常量池:每个类或接口所对应的常量池,包括字面量和符号引用等;

  4. 字符串常量池StringTable 本质上就是一个HashSet<String> ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置)。

    StringTable 中保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象。

    "abc" 0 new("abc") 1 x.intern把串池返回

    image-20230409200104537
    image-20230409193510120

程序计数器

程序计数器(Program Counter Register)主要用于记录线程下一条要执行的指令地址。因为线程切换时需要知道线程当前的执行状态,所以程序计数器也是线程私有的。

  1. 如果线程正在执行的是Java 方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址
  2. 如果正在执行的是Native 方法,则这个技术器值为空(Undefined)
  3. 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
  4. 指令指针寄存器 IP(Instruction Pointer Register)。累加器 段寄存器: cs ss es

重点的 JVM 参数

  1. -Xmx:指定最大堆内存大小,用于设置 JVM 可以使用的最大堆内存大小。语法为 -Xmx<size>[g|G|m|M],其中 size 表示内存大小,单位可以是 GB、MB 等。
  2. -Xms:指定初始化堆内存大小,用于设置 JVM 初始化时可用的堆内存大小。语法为 -Xms<size>[g|G|m|M],其中 size 表示内存大小,单位可以是 GB、MB 等。
  3. -XX:PermSize-XX:MaxPermSize(JDK 8 之前)或者 -XX:MetaspaceSize-XX:MaxMetaspaceSize(JDK 8 之后):这两个参数用于设置永久代(Permanent Generation)或元空间(Metaspace)的初始大小和最大大小,对于运行时生成大量动态类的应用程序来说很重要。
  4. -XX:+UseConcMarkSweepGC-XX:+UseG1GC:这两个参数用于选择垃圾收集器,前者表示使用 CMS 垃圾收集器,后者表示使用 G1 垃圾收集器。
  5. -XX:NewRatio-XX:SurvivorRatio:这两个参数用于调整新生代(Young Generation)和老年代(Old Generation)的比例,以达到更好的性能和内存利用率。
  6. -XX:+HeapDumpOnOutOfMemoryError:这个参数用于在 JVM 发生 OutOfMemoryError 错误时自动生成堆转储文件(Heap Dump File),方便程序员进行调试和分析。

垃圾回收

四种引用

img

可达性分析

GC Roots 是指一组特殊的引用,包括虚拟机栈中引用的对象、类静态属性引用的对象以及常量引用的对象等,这些根节点对应的对象都是活跃的,即正在被程序使用

从 GC Roots 开始遍历整个对象图,所有被引用到的对象都被认为是活跃的,没有被引用到的对象则被认为是无用的。这些无用的对象将会被 JVM 标记为 "待回收" 的对象,然后在下一次垃圾回收时被回收释放。

在三色标记算法中,所有待回收的对象可以划分为三个颜色:白色、灰色和黑色。

  • 白色对象:表示对象尚未被遍历过,即还没有进行可达性分析。
  • 灰色对象:表示对象已经被遍历过,但其引用的其他对象还未被遍历。
  • 黑色对象:表示对象及其引用的其他对象都已经被遍历过,并且是活跃的。

算法初始时,所有对象都被标记为白色对象。从根节点出发,对所有的可达对象进行遍历,将其标记为灰色对象。在遍历过程中,如果发现某个灰色对象指向一个白色对象,则将白色对象标记为灰色对象,然后将其加入到一个称之为 "灰色对象列表" 的队列中。当灰色对象列表为空时,表示所有的可达对象已经被遍历完毕,此时所有未被标记为灰色或黑色的对象就是待回收的对象,也就是无用对象。

垃圾回收算法

  1. 标记-清除算法(Mark-Sweep)

    标记-清除算法分为两个阶段:首先从根节点出发,遍历所有可达对象并标记;然后对于所有未标记的对象进行清除。该算法存在明显的缺点,即容易产生内存碎片问题。

  2. 复制算法(Copying)

    复制算法将可用内存空间分为相等的两部分,每次只使用其中一部分存储对象,当这部分内存空间用完之后,将其中存活的对象复制到另外一块空闲的内存空间中,然后对原来的内存空间进行清除。复制算法消耗更多的内存空间,但能够有效地解决内存碎片问题,适用于短期存活的对象。

  3. 标记-整理算法(Mark-Compact)

    标记-整理算法先标记所有存活的对象,然后将所有存活的对象移动到内存空间的一端,最后清除不再存活的对象。相比于标记-清除算法,它能够有效地解决内存碎片问题。但是开销较大,因为需要将存活的对象移动到一端。

  4. 分代算法(Generational)

    分代算法认为对象的生命周期可以分为不同的阶段,不同生命周期的对象被放置在不同的内存区域中,以便更加高效地进行垃圾回收。通常将 Java 堆分为新生代和老年代两个区域,对于新生代的对象采用复制算法,而老年代则采用标记-整理算法。通过这种方式可以减少全堆扫描的次数,提高垃圾回收效率。

分代垃圾回收

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

在什么情况下会触发对象进入老年代呢?有以下几种情况:

  1. 达到对象年龄阈值:对象在新生代的存活时间超过一定阈值,则会晋升到老年代。这个阈值可以通过 JVM 参数 -XX:MaxTenuringThreshold 来控制,默认为 15。
  2. 新生代的 Survivor 区不足以容纳存活对象:当新生代中的 Survivor 区空间不足以容纳所有存活对象时,会采用担保机制将存活对象直接晋升到老年代。具体实现方式是将 Eden 区和一个 Survivor 区中的存活对象复制到另一个 Survivor 区中,然后清空 Eden 和原来的 Survivor 区。
  3. 大对象直接进入老年代:如果对象的大小超过了 JVM 参数 -XX:PretenureSizeThreshold 指定的值,则会直接在老年代中分配内存空间。

部分收集 (Partial GC)

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。

整堆收集 (Full GC):收集整个 Java 堆和方法区。

垃圾回收器

常用的垃圾回收器以及它们的工作过程:

  1. Serial 收集器:是最基础的单线程收集器。新生代采用标记-复制算法,老年代采用标记-整理算法。

  2. Parallel 收集器:是 Serial 收集器的多线程版本。

  3. CMS 收集器(Concurrent Mark Sweep):是一种基于“标记-清除”算法实现的垃圾回收器。它主要特点是高并发、低停顿。low

    初始标记:仅仅标记GC ROOTS的直接关联对象,并且世界暂停

    并发标记:使用GC ROOTS TRACING算法,进行跟踪标记,世界不暂停

    重新标记,因为之前并发标记,其他用户线程不暂停,可能产生了新垃圾,所以重新标记,世界暂停 ,建议先清理

    image-20230409202552115

  4. G1 收集器:G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。

    它采用分代收集和增量整理的方式,能够在较短时间内完成垃圾回收,并且可以有效地避免内存碎片问题。

    G1 收集器的工作过程分为多个阶段:初始标记、根区域扫描、并发标记、重新标记、清理和重置。其中,初始标记和重新标记需要停顿整个应用程序,但时间较短;其他阶段可以在不停顿应用程序的情况下执行。G1 收集器可以根据应用程序的实际情况来设置最大停顿时间,以达到更好的性能和响应速度。G1回收的范围是整个Java堆(包括新⽣代,⽼年代),