Java八股文背诵 第四天JVM

JVM

  1. 堆和栈的区别

      • 运行时确定内存大小。
      • 由 JVM 管理。
      • 是一块一块的内存。
      • 线程共享。
      • 编译时即可确定内存大小。
      • 线程私有。
      • 实现方式采用数据结构中的栈实现,具有先进后出的顺序特点。
      • 在分配速度上比堆快,分配一块栈内存不过是简单的移动一个指针。
  2. JVM 主要组成部分

    • 组成部分
      • 类加载器(ClassLoader
      • 运行时数据区(Runtime Data Area
      • 执行引擎(Execution Engine
      • 本地库接口(Native Interface
    • 作用
      • 首先通过类加载器(ClassLoader)将 Java 代码转为字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中。而字节码文件只是 JVM 的一套指令规范,并不能直接交给底层操作系统去执行,因此需要特定命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交给 CPU 执行,而这个过程中需要调用其他语言的本地接口(Native Interface)来实现整个程序的功能。
  3. JVM 内存结构

    • 程序计数器(Program Counter Register
      • 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。在多线程环境下,每个线程都有自己的独立的程序计数器。当线程执行 Java 方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址。
    • Java 虚拟机栈(Java Virtual Machine Stacks
      • 每个 Java 线程都有一个私有的 Java 虚拟机栈,与线程同时创建。每个方法在执行时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。栈帧在方法调用时入栈,方法返回时出栈。
    • 本地方法栈(Native Method Stack
      • 本地方法栈与 Java 虚拟机栈类似,但它为本地方法(Native Method)服务。本地方法是用其他编程语言(如 C、C++)编写的,通过 Java Native Interface(JNI)与 Java 代码进行交互。
    • Java 堆(Java Heap
      • Java 堆是 Java 虚拟机中最大的一块内存区域,用于存储对象实例。所有的对象实例和数组都在堆上分配内存。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆可以分为新生代(Young Generation)和老年代(Old Generation)等不同的区域,其中新生代又包括 Eden 空间、Survivor 空间(From 和 To)。
    • 方法区(Method Area
      • 方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 HotSpot 虚拟机中,方法区也被称为永久代(Permanent Generation),但在较新的 JVM 版本中,永久代被元空间(Metaspace)所取代。
    • 运行时常量池(Runtime Constant Pool
      • 运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。与 class 文件中的常量池不同,运行时常量池可以动态改变。
    • 字符串常量池
      • 字符串常量池是 JVM 为了提升性能和减少内存消耗针对字符串(String)专门开辟的一块区域,主要是为了避免字符串的重复创建。
    • 直接内存(Direct Memory
      • 直接内存不是 JVM 的一部分,但也与内存管理有关。Java NIO 库允许直接分配堆外内存,这些内存不受 Java 堆大小的限制,也不受垃圾回收器管理。直接内存通常是通过 ByteBuffer 类来使用的。
  4. 四种引用:强引用、软引用、弱引用、虚引用分别介绍

    • 强引用(Strong Reference

      • 强引用是 Java 中最常见的引用类型。如果一个对象具有强引用,即使系统内存不足,垃圾回收器也不会回收这个对象,只有在不再有任何强引用指向这个对象时,才会被回收。
    • 软引用(Soft Reference

      • 软引用用于描述一些还有用但非必需的对象。如果一个对象只有软引用指向它,那么在系统内存不足时,垃圾回收器会尝试回收这些对象。软引用通常用于实现内存敏感的缓存,可以在内存不足时释放缓存中的对象。
    • 弱引用(Weak Reference

      • 弱引用比软引用的生命周期更短暂。如果一个对象只有弱引用指向它,在进行下一次垃圾回收时,不论系统内存是否充足,这些对象都会被回收。弱引用通常用于实现对象缓存,但不希望缓存的对象影响垃圾回收的情况。
    • 虚引用(Phantom Reference

      • 虚引用是 Java 中最弱的引用类型。如果一个对象只有虚引用指向它,那么无论何时都可能被垃圾回收器回收,但在对象被回收之前,虚引用会被放入一个队列中,供程序员进行处理。虚引用主要用于跟踪对象被垃圾回收的时机,进行一些必要的清理或记录。
    • 示例代码

      java 复制代码
      Object obj = new Object(); // 强引用
      SoftReference<Object> softRef = new SoftReference<>(new Object()); // 软引用
      WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用
      ReferenceQueue<Object> queue = new ReferenceQueue<>();
      PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue); // 虚引用
  5. 怎么判断对象是否可以被回收

    • 引用计数法
      • 给对象中添加一个引用计数器。每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1。任何时候计数器为 0 的对象就是不可能再被使用的。但是这样有一个问题,不能解决循环引用的问题。
    • 可达性分析算法
      • 这个算法的基本思想是通过一系列的称为 "GC Roots" 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,即证明此对象是不可用的,即可被回收。
  6. 垃圾回收算法、垃圾回收机制

    • 垃圾回收算法
      • 标记 - 清除算法
        • 标记 - 清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
        • 适用场景:存活对象较多的情况下比较高效,适用于年老代(即旧生代)。
        • 缺点:
          • 容易产生内存碎片,当再来一个比较大的对象时,可能会因为找不到足够大的连续空间而提前触发垃圾回收。
          • 需要扫描整个空间两次:第一次标记存活对象,第二次清除没有标记的对象。
      • 复制算法
        • 从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到另一块新的内存上,之后将原来那一块内存全部回收掉。
        • 现在的商业虚拟机都采用这种收集算法来回收新生代。
        • 适用场景:存活对象较少的情况下比较高效。
        • 优点:
          • 只需要扫描整个空间一次,标记存活对象并复制移动。
        • 缺点:
          • 需要两块内存空间。
          • 需要复制移动对象。
          • 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下。这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
      • 标记 - 整理算法
        • 标记 - 整理算法是一种老年代的回收算法,它在标记 - 清除算法的基础上做了一些优化。首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
      • 分代收集算法
        • 分代收集算法就是目前虚拟机使用的回收算法,它解决了标记 - 整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation)。在不同年代使用不同的算法,从而使用最合适的算法。新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以只能使用标记 - 清除或者标记 - 整理算法。
    • 垃圾回收机制
      • 年轻代
        • 年轻代分为 Eden 区和 Survivor 区(两块:FromTo),且 Eden:From:To = 8:1:1
        • 新产生的对象优先分配在 Eden 区(除非配置了 -XX:PretenureSizeThreshold,大于该值的对象会直接进入老年代)。
        • Eden 区满了或放不下了,这时候其中存活的对象会复制到 From 区。这里需要注意的是,如果存活下来的对象 From 区都放不下,则这些存活下来的对象全部进入老年代。之后 Eden 区的内存全部回收掉。
        • 之后产生的对象继续分配在 Eden 区,当 Eden 区又满了或放不下了,这时候将会把 Eden 区和 From 区存活下来的对象复制到 To 区(同理,如果存活下来的对象 To 区都放不下,则这些存活下来的对象全部进入老年代),之后回收掉 Eden 区和 From 区的所有内存。
        • 如上这样,会有许多对象会被复制很多次(每复制一次,对象的年龄就 +1),默认情况下,当对象被复制了 15 次(这个次数可以通过 -XX:MaxTenuringThreshold 来配置),就会进入老年代。
        • 当老年代满了或者存放不下将要进入老年代的存活对象的时候,就会发生一次 Full GC(这也是我们最需要减少的,因为耗时很严重)。
      • 垃圾回收的两种类型
        • Minor GC:对新生代进行回收,不会影响到老年代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。
        • Full GC :也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于 Full GC 需要对整个堆进行回收,所以比 Minor GC 要慢,因此应该尽可能减少 Full GC 的次数。导致 Full GC 的原因包括:老年代被写满、永久代(Perm)被写满和 System.gc() 被显式调用等。
      • 总结
        • 年轻代:复制算法。所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速地收集掉那些生命周期短的对象。
        • 老年代:标记 - 清除或标记 - 整理。在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
        • 每一种算法都会有很多不同的垃圾回收器去实现,在实际使用中,根据自己的业务特点做出选择就好。
  7. 垃圾回收器

    • 常见的垃圾收集器
      • 新生代的收集器
        • Serial :串行收集器 - 复制算法
          • Serial 收集器是新生代单线程收集器,优点是简单高效,是最基本、发展历史最悠久的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。
          • Serial 收集器依然是虚拟机运行在 Client 模式下默认新生代收集器,对于运行在 Client 模式下的虚拟机来说是一个很好的选择。
        • ParNew :并行收集器 - 复制算法
          • ParNew 收集器是新生代并行收集器,其实就是 Serial 收集器的多线程版本。
          • 除了使用多线程进行垃圾收集之外,其余行为包括 Serial 收集器可用的所有控制参数、收集算法、对象分配规则、收集策略等都与 Serial 收集器完全一样。
        • Parallel Scavenge :并行收集器 - 复制算法
          • Parallel Scavenge 收集器是新生代并行收集器,追求高吞吐量,高效利用 CPU。
          • 该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
      • 老年代的收集器
        • Serial Old :串行收集器 - 标记整理算法
          • Serial OldSerial 收集器的老年代版本,它同样是一个单线程的串行收集器,使用标记整理算法。
          • 这个收集器的主要意义也在于给 Client 模式下的虚拟机使用。如果在 Server 模式下,主要有两大用途:
            • 在 JDK 1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用。
            • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
        • Parallel Old :并行收集器 - 标记整理算法
          • Parallel OldParallel Scavenge 收集器的老年代版本,使用多线程和 "标记 - 整理" 算法。这个收集器在 JDK 6 中才开始提供。
        • CMS :并发标记清除收集器 - 标记清除算法
          • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
          • 前很大一部分的 Java 应用集中在互联网网站或者 B/S 系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。
          • CMS 收集器是基于 "标记 - 清除" 算法实现的,它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为 4 个步骤:
            • 初始标记
            • 并发标记
            • 重新标记
            • 并发清除
          • 其中,初始标记、重新标记这两个步骤仍然需要 "Stop The World"。
          • CMS 收集器的主要优点:
            • 并发收集
            • 低停顿
          • CMS 收集器的三个明显的缺点:
            • CMS 收集器对 CPU 资源非常敏感。CPU 个数少于 4 个时,CMS 对用户程序的影响就可能变得很大,为了应付这种情况,虚拟机提供了一种称为 "增量式并发收集器" 的 CMS 收集器变种。
            • CMS 收集器无法处理浮动垃圾,可能会出现 "Concurrent Mode Failure" 失败而导致另一次 Full GC 的产生。在 JDK 1.5 的默认设置下,CMS 收集器当老年代使用了 68% 的空间后就会被激活。
            • CMS 是基于 "标记 - 清除" 算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发 Full GC
      • 新生代和老年代垃圾收集器
        • G1 收集器 :标记整理算法
          • JDK 1.7 之后全新的回收器,用于取代 CMS 收集器。
          • G1 收集器的优势:
            • 独特的分代垃圾收集器,分代 GC:分代收集器,同时兼顾年轻代和老年代。
            • 使用分区算法,不要求 Eden、年轻代或老年代的空间都连续。
            • 并行性:收集期间,可由多个线程同时工作,有效利用多核 CPU 资源。
            • 空间整理:收集过程中,会进行适当对象移动,减少空间碎片。
            • 可预见性:G1 可选取部分区域进行收集,可以缩小收集范围,减少全局停顿。
          • G1 收集器的阶段分为以下几个步骤:
            • 初始标记:它标记从 GC Roots 开始直接可达的对象。
            • 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,找出存活对象。
            • 最终标记:标记那些在并发标记阶段发生变化的对象,将被回收。
            • 筛选回收:首先对各个 Region 的回收价值和成本进行排序,根据用户所期待的 GC 停顿时间指定回收计划,回收一部分 Region。
    • 总结
      • 年轻代:复制算法。所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速地收集掉那些生命周期短的对象。
      • 老年代:标记 - 清除或标记 - 整理。在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
      • 每一种算法都会有很多不同的垃圾回收器去实现,在实际使用中,根据自己的业务特点做出选择就好。
  8. JVM 类加载机制

    • 类加载机制是 Java 虚拟机(JVM)在运行 Java 程序时负责将类加载到内存中的过程。它包括以下几个步骤:

      • 加载(Loading)
        • 在此阶段,类加载器负责查找类的字节码文件,并将其加载到内存中。字节码可以来自文件系统、网络等位置。加载阶段不会执行类中的静态初始化代码。
      • 连接(Linking)
        • 连接阶段包括三个子阶段:
          • 验证(Verification):确保加载的类文件格式正确,并且不包含不安全的构造。
          • 准备(Preparation) :在内存中为类的静态变量分配内存空间,并设置默认初始值。这些变量在此阶段被初始化为默认值,比如数值类型为 0,引用类型为 null
          • 解析(Resolution):将类、接口、字段和方法的符号引用解析为直接引用,即内存地址。这一步骤可能包括将常量池中的符号引用解析为直接引用。
      • 初始化(Initialization)
        • 在此阶段,执行类的静态初始化代码,包括静态字段的赋值和静态代码块的执行。静态初始化在类的首次使用时进行,可以是创建实例、访问静态字段或调用静态方法。
    • 示例代码

      java 复制代码
      public class ClassLoadingExample {
          public static void main(String[] args) {
              // 步骤 1:加载
              MyClass myClass = new MyClass();
      
              // 步骤 3:初始化
              System.out.println(MyClass.staticField);
      
              // 使用类中的方法
              myClass.printMessage(); // 输出:Initialized static method
          }
      }
      
      class MyClass {
          // 步骤 2:连接 - 准备
          public static String staticField = "Initialized static field";
      
          // 步骤 3:初始化
          static {
              System.out.println("IntializedInitialized static method");
          }
      
          public void printMessage() {
              System.out.println("Instance method called");
          }
      }
  9. 双亲委派机制是什么

    • 双亲委派机制是 Java 类加载器中采用的一种类加载策略。该机制的核心思想是:如果一个类加载器收到了类加载请求,默认先将该请求委托给其父类加载器处理。只有当父级加载器无法加载该类时,才会尝试自行加载。
    • JVM 中的类加载器
      • 启动类加载器(Bootstrap ClassLoader :负责加载 %JAVA_HOME%/lib 目录下的核心类库(如 rt.jar)。
      • 扩展类加载器(Extension ClassLoader :负责加载 %JAVA_HOME%/lib/ext 目录下的扩展类库。
      • 系统类加载器(System ClassLoader :负责加载用户类路径(classpath)下的应用程序类。
    • 加载机制
      • 它们之间的关系是父子关系,启动类加载器是最顶层的类加载器,扩展类加载器是启动类加载器的子加载器,系统类加载器又是扩展类加载器的子加载器。
      • 当类加载器接收到类加载的请求时,首先检查该类是否已经被当前类加载器加载。
      • 若该类未被加载过,当前类加载器会将加载请求委托给父类加载器去完成。
      • 若当前类加载器的父类加载器为 null,会委托启动类加载器完成加载。
      • 若父类加载器无法完成类的加载,当前类加载器才会去尝试加载该类。
    • 双亲委派机制的优缺点
      • 优点
        • 避免重复加载:由于类加载器直接从父类加载器那里加载类,避免了类的重复加载。
        • 提高安全性 :通过双亲委派模型,Java 标准库中的核心类库(如 java.lang.*)由启动类加载器加载,这样能保证这些核心类库不会被恶意代码篡改或替换,从而提高程序的安全性。
        • 保持类加载的一致性:这种方式确保了同一个类的加载由同一个类加载器完成,从而在运行时保证了类型的唯一性和相同性。这也有助于减轻类加载器在处理相互关联的类时的复杂性。
      • 缺点
        • 灵活性降低:由于类加载的过程需要不断地委托给父类加载器,这种机制可能导致实际应用中类加载的灵活性降低。
        • 增加了类加载时间:在类加载的过程中,需要不断地查询并委托父类加载器,这意味着类加载所需要的时间可能会增加。在类数量庞大或类加载器层次比较深的情况下,这种时间延迟可能会变得更加明显。
    • 如何破坏双亲委派机制
      • Tomcat 的 Web 应用加载 :Tomcat 使用 URLClassLoader,会优先加载指定的类路径。
      • Java Agent:通过 Java Agent 技术,可以在 JVM 启动时加载特定的类,从而绕过双亲委派机制。
  10. JVM 参数配置

    • 堆大小
      • -Xms<size>:设置 JVM 堆的初始大小。
      • -Xmx<size>:设置 JVM 堆的最大大小。
    • 新生代大小
      • -Xmn<size>:设置新生代的大小。
    • 新生代最小/最大空间
      • -XX:NewSize<size>:设置新生代最小空间大小。
      • -XX:MaxNewSize<size>:设置新生代最大空间大小。
    • 永久代/元空间大小
      • -XX:MaxPermSize<size>:设置永久代的最大大小。
      • -XX:MaxMetaspaceSize<size>:设置元空间的最大大小。
    • 线程栈大小
      • -Xss<size>:设置每个线程的堆栈大小。
    • 垃圾回收器配置
      • -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效,即年轻代使用并发收集,而老年代仍旧使用串行收集。
      • -XX:ParallelGCThreads=20:配置并行收集器的线程数,即同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
相关推荐
weifexie20 分钟前
ruby可变参数
开发语言·前端·ruby
王磊鑫21 分钟前
重返JAVA之路-初识JAVA
java·开发语言
千野竹之卫21 分钟前
3D珠宝渲染用什么软件比较好?渲染100邀请码1a12
开发语言·前端·javascript·3d·3dsmax
半兽先生42 分钟前
WebRtc 视频流卡顿黑屏解决方案
java·前端·webrtc
liuluyang5301 小时前
C语言C11支持的结构体嵌套的用法
c语言·开发语言·算法·编译·c11
凌叁儿1 小时前
python保留关键字详解
开发语言·python
南星沐2 小时前
Spring Boot 常用依赖介绍
java·前端·spring boot
明飞19872 小时前
C_内存 内存地址概念
c语言·开发语言
代码不停2 小时前
Java中的异常
java·开发语言
兮兮能吃能睡2 小时前
Python中的eval()函数详解
开发语言·python