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:配置并行收集器的线程数,即同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
相关推荐
草字几秒前
uniapp 如果进入页面输入框自动聚焦,此时快速返回页面或者跳转到下一个页面,输入法顶上来的页面出现半屏的黑屏问题。
java·前端·uni-app
SiYuanFeng7 分钟前
【问题未解决-寻求帮助】VS Code 中使用 Conda 环境,运行 Python 后 PowerShell 终端输出内容立即消失
开发语言·python·conda
博主逸尘8 分钟前
uniApp实战六:Echart图表集成
java·uni-app·php
我是ed.22 分钟前
cocos Js 使用 webview 通过 postMessage 进行通信
开发语言·javascript·ecmascript
段ヤシ.27 分钟前
Windows环境下安装Python和PyCharm
开发语言·python·pycharm
大萌神Nagato32 分钟前
如何修改VM虚拟机中的ip
linux·开发语言·ip·虚拟机·静态ip
陈煜的博客1 小时前
elasticSearch 增删改查 java api
java·大数据·elasticsearch
hweiyu001 小时前
Scala实用编程(附电子书资料)
开发语言·后端·scala
mftang1 小时前
C 标准库 <time.h> 函数详解
c语言·开发语言
lly2024061 小时前
SVG 在线编辑器
开发语言