Java 内存区域有哪些部分

Java的内存区域主要分为以下几个部分:

  1. 程序计数器:程序计数器是一块较小的内存空间,每个线程都有自己独立的程序计数器。当线程执行Java方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址。
  2. Java虚拟机栈:每个Java线程都有一个私有的Java虚拟机栈,与线程同时创建。每个方法在执行时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。栈帧在方法调用时入栈,方法返回时出栈。
  3. 本地方法栈: 本地方法栈与Java虚拟机栈类似,但它为本地方法服务。本地方法是用其他编程语言(如C/C++)编写的,通过JNI与Java代码进行交互。
  4. :堆是被所有线程共享的一块最大的内存区域,在虚拟机启动时创建;所有的对象实例和数组都在堆上分配内存。
  5. 方法区: 方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  6. 运行时常量池:是方法区的一部分,用于存储编译期间生成的类、方法和常量等信息。
  7. 字符串常量池:字符串常量池是 JVM 为了提升性能和减少内存消耗针对字符串专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
  8. 直接内存:不是Java虚拟机运行时数据区的一部分,但Java可以通过NIO操作直接内存,提高IO性能。

介绍一下什么是强引用、软引用、弱引用、虚引用

这四种引用决定了对象的生命周期以及垃圾回收器(Garbage Collector, GC)如何收集垃圾。

  1. 强引用不回收,**最常见的引用类型(99%)**。如果一个对象具有强引用,那么垃圾收集器绝不会回收它。
  2. 软引用内存不足即回收,软引用用于描述一些还有用但非必需的对象。如果一个对象只有软引用指向它,那么在系统内存不足时,垃圾回收器会尝试回收这些对象。软引用通常用于实现内存敏感的缓存,可以在内存不足时释放缓存中的对象。
  3. 弱引用发现即回收,弱引用比软引用的生命周期更短暂。如果一个对象只有弱引用指向它,在进行下一次垃圾回收时,不论系统内存是否充足,这些对象都会被回收。弱引用通常用于实现对象缓存,但不希望缓存的对象影响垃圾回收的情况。
  4. 虚引用跟踪对象回收,虚引用是Java中最弱的引用类型。如果一个对象只有虚引用指向它,那么无论何时都可能被垃圾回收器回收,但在对象被回收之前,虚引用会被放入一个队列中,供程序员进行处理。虚引用主要用于跟踪对象被垃圾回收的时机,进行一些必要的清理或记录。

有哪些垃圾回收算法

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

​ 标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。适用场合:

  • 存活对象较多的情况下比较高效
  • 适用于年老代(即旧生代)

复制算法(Copying)

​ 通过根节点(GC Roots),标记所有从根节点开始的对象,并将这些存活的对象复制到一块儿新的内存上去,之后将原来的那一块儿内存全部回收掉,现在的商业虚拟机都采用这种收集算法来回收新生代。适用场合:

  • 存活对象较少的情况下比较高效
  • 扫描了整个空间一次(标记存活对象并复制移动)
  • 适用于年轻代(即新生代):基本上98%的对象是”朝生夕死”的,存活下来的会很少

缺点:

  • 需要两倍的内存空间
  • 需要复制移动对象
  • 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。

标记-压缩(Mark-Compact)

​ 标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。 首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

分代收集算法

​ 分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于新年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以只能使用标记-清除或者标记-压缩算法。

有哪些垃圾回收器(GC)

新冰川呀!

新生代、并发、串行、压缩

  1. 按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。

​ 串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。

  • 在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client模式下的JVM中
  • 在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器。和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“Stop-the-world”机制。
  1. 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
  • 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
  • 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
  1. 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器。
  • 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
  • 非压缩式的垃圾回收器不进行这步操作。
  1. 按工作的内存区间分,又可分为新生代垃圾回收器和老年代(旧生代)垃圾回收器。

类加载机制

类的生命周期等于类的加载机制加上使用和卸载。

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

  1. 加载:加载是一个读取Class文件,将其转化为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户调用的java.lang.Class类型的对象的过程。

  2. 连接:连接阶段包括三个子阶段:

    1. 验证(Verify):确保加载的类文件格式正确(二进制文件开头 CA FE BA BE),并且不包含不安全的构造。
    2. 准备(Prepare):在方法区中为类的静态变量分配内存空间,并设置默认初始值。比如数值类型为0,引用类型为null。
    3. 解析(Resolve):将类、接口、字段和方法的符号引用解析为直接引用,即内存地址。
  3. 初始化:在此阶段,执行类的静态初始化代码,包括静态字段的赋值和静态代码块的执行。

双亲委派机制

​ 双亲委派机制是Java类加载器中的一种设计模式,用于确定类的加载方式和顺序。这个机制确保了Java核心库的安全性和一致性。该机制的核心思想是:如果一个类加载器收到了类加载请求,默认先将该请求委托给其父类加载器处理。只有当父级加载器无法加载该类时,才会尝试自行加载。

​ 双亲委派机制能够提高安全性,防止核心库的类被篡改。因为所有的类最终都会通过顶层的启动类加载器进行加载。另外由于类加载器直接从父类加载器那里加载类,也避免了类的重复加载。

JDK8新特性

  1. Lambda 表达式:允许以更简洁的语法编写匿名函数。
  2. Stream API : Stream API提供了一种声明式的方式来对集合进行操作。它支持如过滤、映射、排序、归约等多种操作。
  3. 新的日期和时间 API:Java 8 引入了 java.time 包,提供了全新的日期和时间 API。它解决了旧的java.util.Date和 java.util.Calendar (日历)的诸多问题,并提供了更加清晰和易用的日期和时间处理方式。
  4. 函数式接口:Java 8 提供了一系列函数式接口,如 Consumer(消费者)、Predicate、Function等。
  5. 方法引用:方法引用提供了一种更简洁的方式,即传递方法作为参数(如 System.out::println)来引用它,而不是执行该方法。

JVM的组成