文章目录
- 前言
- 一、什么是Java 的内存模型?
- 二、什么是 JVM 的运行时数据区
- Java8 之前和之后的区别
- JVM 内存模型
- JVM 内存区域
- JVM 内存垃圾回收
- JVM如何判断哪些对象不在存活?
- JVM运行过程中如何判断哪些对象是垃圾?
- JVM 垃圾回收
- Java8 中的 jvm如何进行垃圾回收?
- Serial、Parallel、CMS、G1 的工作原理和适用场景
- Serial 收集器
- Parallel 收集器
- CMS 收集器
- G1 收集器
- Java8 默认的垃圾收集器为什么不是 G1 ?
前言
当我们提到 Java 的内存模型的时候通常会想到 JVM 运行时候的数据区域,比如包括线程私有的堆,方法区,线程共享的有本地方法栈,虚拟机栈,程序计数器。Java程序启动后,就会初始化这些内存的数据。但是这就是 Java 的内存模型了吗?
一、什么是Java 的内存模型?
Java的内存模型(Java Memory Model,JMM)是Java虚拟机(JVM)规范中定义的一种抽象概念,用于描述Java程序中各种变量的存储方式、访问规则以及线程之间的交互关系。JMM规定了Java程序中多线程并发访问共享变量时的行为规范,确保多线程程序在不同的硬件平台和操作系统上都能获得一致的行为
。
JMM主要包含以下几个关键概念:
-
主内存(Main Memory): 主内存是所有线程享的内存区域,用于存储Java对象实例、静态变量等数据。所有的变量都存储在主内存中,而且每个线程都可以访问主内存中的变量。
-
工作内存(Working Memory): 每个线程都有自己的工作内存,用于存储该线程需要使用的变量副本。线程对变量的所有操作都在工作内存中进行,而不是直接在主内存中进行。
-
内存间交互操作: Java内存模型定义了一组规则,用于描述线程如何与主内存中的变量进行交互。这些规则包括读取变量、写入变量、锁定变量等操作,确保多线程并发访问时的可见性、有序性和原子性。
-
原子性、可见性和有序性: JMM通过各种机制来保证共享变量的原子性、可见性和有序性。原子性指一个操作是不可中断的;可见性指一个线程对共享变量的修改能被其他线程立即感知到;有序性指程序执行的顺序与代码的编写顺序一致。
通过定义Java内存模型,Java程序员可以编写多线程程序而无需考虑底层硬件和操作系统的差异,确保程序的正确性和可移植性。。
二、什么是 JVM 的运行时数据区
Java8 之前和之后的区别
Java 8 之前的 JVM(Java Virtual Machine)与 Java 8 及之后的 JVM 主要在以下几个方面存在区别:
-
内存模型改进:
- 元空间(Metaspace)替代永久代(PermGen):Java 8 中,JVM 去除了永久代(Permanent Generation),并将类元数据存储转移到了一个称为元空间的新区域。元空间不在 JVM 堆内存中分配,而是使用本地内存,从而避免了永久代大小限制和 Full GC 问题。这改善了 JVM 内存管理和垃圾回收的效率。
-
字符串去重复:
- Java 8 引入了字符串去重复(String deduplication)特性,JVM 在堆中对相同的字符串字面量和字符串驻留池进行共享,减少内存占用。当大量字符串重复时,这项优化可以显著节省内存空间。
-
G1垃圾收集器成熟:
- 虽然 G1(Garbage-First)垃圾收集器在 Java 7 更新中引入,但直到 Java 8,G1 才被标记为“production-ready”,成为官方推荐的垃圾收集器之一,尤其适用于大内存服务器和需要低停顿时间的应用。G1 收集器提供了更细粒度的内存分区和更灵活的垃圾回收策略,实现了更平滑的垃圾回收过程和更好的吞吐量。
-
Lambda表达式与 invokedynamic:
- Java 8 引入了 Lambda 表达式,它改变了匿名内部类的实现方式,使用了
invokedynamic
JVM 指令。invokedynamic
是 Java 7 引入的 JVM 新特性,但在 Java 8 中得到了广泛应用。它允许在运行时动态解析和调用方法,使得 Lambda 表达式的创建和执行更加高效,同时也简化了方法和函数的处理。
- Java 8 引入了 Lambda 表达式,它改变了匿名内部类的实现方式,使用了
-
Compact Strings:
- Java 8 引入了 Compact Strings(紧凑字符串)优化,对于只包含 ASCII 字符的字符串,其内部字节数组不再使用 2 字节的
char
类型存储每个字符,而是改为使用 1 字节的byte
类型,从而减少了内存占用。
- Java 8 引入了 Compact Strings(紧凑字符串)优化,对于只包含 ASCII 字符的字符串,其内部字节数组不再使用 2 字节的
-
内存分配改进:
- Java 8 对 TLAB(Thread Local Allocation Buffers)进行了优化,提高了对象分配速度和并发性能。TLAB 是每个线程私有的内存区域,用于快速分配小对象,减少多线程环境下的锁竞争。
-
JVM监控与诊断工具增强:
- Java 8 对现有的 JVM 监控与诊断工具(如
jcmd
、jstack
、jmap
、jstat
等)进行了增强,提供了更丰富的命令选项和更详细的输出信息,便于开发者更好地分析和调试 JVM 性能问题。
- Java 8 对现有的 JVM 监控与诊断工具(如
-
新的 JVM 命令行选项:
- Java 8 引入了一些新的 JVM 命令行选项,如
-XX:+UseStringDeduplication
(启用字符串去重复)、-XX:+UnlockExperimentalVMOptions
(解锁实验性 VM 选项)等,为 JVM 调优提供了更多可能性。
- Java 8 引入了一些新的 JVM 命令行选项,如
JVM 内存模型
JVM 内存区域
Java虚拟机(JVM)内存模型是Java程序运行时的内存布局,它规定了程序在执行过程中如何分配和管理内存。JVM内存模型主要分为以下几个区域:
-
方法区(Method Area):
- 用于存储类信息、常量、静态变量等数据。
- 也是垃圾回收的主要区域之一。
-
堆(Heap):
- 存储对象实例,几乎所有的对象实例都是在这里分配内存。
- 堆是垃圾回收的主要区域。
-
栈(Stacks):
- 用于存储局部变量和部分结果,并在方法调用时用于传递参数和返回值。
- 每个线程有自己的调用栈。
-
程序计数器(Program Counter Register):
- 存储指向下一条指令的地址,即将要执行的指令代码。
- 每个线程有自己的程序计数器。
-
本地方法栈(Native Method Stacks):
- 为JVM使用到的Native方法服务,如直接调用C/C++编写的库。
-
虚拟机栈(VM Stack):
- 也称为Java栈,用于存储局部变量表、操作数栈、动态链接、方法出口等。
-
元空间(Metaspace):
- 从JDK 8开始,取代了永久代(PermGen),用于存储类元数据。
- 元空间位于本地内存(Native Memory)而不是虚拟机内存中。
-
直接内存(Direct Memory):
- 不属于虚拟机规范定义的内存区域,但也被频繁使用,如NIO中的ByteBuffer分配的内存就属于直接内存。
JVM 内存垃圾回收
JVM内存垃圾回收(Garbage Collection,GC)是指JVM自动对内存中不再使用的对象进行识别和清理的过程,以释放内存资源供其他对象使用。这一机制对于Java语言的自动内存管理至关重要。以下是JVM内存垃圾回收的一些关键点:
-
垃圾回收的目的:识别并清除不再被引用的对象,即“垃圾”,以避免内存泄漏和内存溢出。
-
垃圾回收的算法:JVM采用了多种垃圾回收算法,包括但不限于:
- 引用计数法:通过计数器记录对象被引用的次数来判定对象是否可回收,但由于难以处理循环引用问题,在Java中并不常用。
- 标记-清除(Mark-Sweep):首先标记所有需要回收的对象,然后清除这些被标记的对象。
- 复制算法(Copying):将内存分为两块,每次只使用一块,垃圾回收时将存活的对象复制到另一块,然后清除已使用的内存。
- 标记-整理(Mark-Compact):先标记可回收对象,然后将所有存活的对象压缩到内存的一端,之后清理边界外的内存。
- 分代收集算法:根据对象的生命周期将堆内存分为新生代和老年代,新生代使用复制算法,老年代使用标记-清除或标记-整理算法。
-
垃圾回收的触发条件:通常当新生代空间不足时,会触发Minor GC;当整个堆或老年代空间不足时,会触发Full GC。
-
垃圾回收的性能影响:GC过程中会暂停应用线程,这种“Stop The World”事件可能会影响应用性能。
-
垃圾回收器的选择:不同的垃圾回收器适用于不同的场景,如Serial、Parallel、CMS、G1等,开发者可以根据应用特点选择合适的垃圾回收器。
-
内存模型的变化:从JDK 7到JDK 8,永久代(PermGen)被元空间(Metaspace)取代,元空间使用的是直接内存。
-
垃圾回收的监控和调优:开发者可以通过监控GC日志和调整JVM参数来优化GC性能,如调整Eden区与Survivor区的比例、设置老年代的大小等。
-
对象的晋升:长期存活的对象或大对象可能会直接进入老年代,对象在Survivor区熬过一定次数的Minor GC后,也会晋升到老年代。
JVM如何判断哪些对象不在存活?
JVM判断对象是否存活主要依赖于“可达性分析”(Reachability Analysis)算法。这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链。如果一个对象到GC Roots没有任何引用链相连,即GC Roots到对象不可达,那么JVM就认为此对象不再存活,可以进行回收。
以下是可达性分析的具体步骤:
GC Roots的设置:在JVM中,作为GC Roots的对象包括:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
从GC Roots开始:垃圾回收器从GC Roots对象开始进行扫描。
搜索引用链:垃圾回收器会递归地跟踪GC Roots对象所引用的所有对象,以及这些对象所引用的其他对象。
标记存活对象:所有通过GC Roots直接或间接可达的对象都会被标记为存活。
确定垃圾对象:在完成引用链的遍历后,仍然没有被标记的对象被认为是垃圾,因为它们不可达,即没有任何活动线程或GC Roots引用它们。
回收垃圾对象:最后,垃圾回收器会回收那些被标记为垃圾的对象,释放它们占用的内存空间。
JVM运行过程中如何判断哪些对象是垃圾?
JVM 运行过程中,数据结构是动态变化的,因此可以使用三色标记算法来解决并发标记中一些问题。
三色标记算法是JVM中用于垃圾回收的一种算法,特别是在老年代的垃圾回收中。这种算法主要用于解决并发标记过程中的一些问题,如在并发清除过程中,对象的引用关系还在不断变化,导致清除工作不准确或遗漏。
在三色标记算法中,对象被分为三种颜色:
-
黑色:黑色对象是已经检查过的对象,它们的子对象也已经被检查过,这些对象不会被回收,并且不会被其他回收线程所改变。
-
灰色:灰色对象是已经检查过,但是其子对象还没有被检查的对象。灰色对象可能会被其他线程改变其子对象的引用关系,因此需要额外的同步措施来保证安全。
-
白色:白色对象是尚未检查的对象,也即是可能的垃圾对象。如果一个对象在完成标记过程后仍然是白色的,那么它将被认为是垃圾,可以被回收。
三色标记算法的基本步骤如下:
-
初始化:开始时,除了GC Roots直接引用的对象被标记为黑色外,所有其他对象都被认为是白色的。
-
并发标记:从GC Roots开始,遍历所有对象,将所有可达的对象标记为灰色,并检查它们的子对象。
-
重新标记:由于并发标记过程中,应用程序线程可能改变对象的引用关系,可能会产生新的灰色对象。因此,需要重新扫描这些灰色对象,以确保所有新增的子对象都被检查过。
-
清理:在所有对象都被重新标记后,所有白色的对象都可以安全地被回收。
三色标记算法通过引入灰色对象,解决了并发标记过程中对象引用关系变化的问题。然而,这种算法需要额外的同步机制来保证并发标记的安全性,这可能会影响垃圾回收的效率。
在实际的JVM实现中,如G1垃圾回收器,采用了类似的三色标记策略,但做了一些优化,以减少并发标记对应用程序性能的影响。
JVM 垃圾回收
Java8 中的 jvm如何进行垃圾回收?
Java 8中的垃圾回收(Garbage Collection,GC)机制与Java的其他版本基本相同,它依赖于垃圾回收器(Garbage Collector,GC)来自动管理对象的生命周期,回收不再使用的对象以释放内存。Java虚拟机(JVM)提供了几种不同的垃圾回收器,它们在Java 8中得到了进一步的优化和改进。
以下是Java 8中垃圾回收的一些关键概念和组件:
-
分代收集:Java的垃圾回收器通常采用分代收集的策略,将堆内存分为新生代(Young Generation)和老年代(Old Generation)。
- 新生代:新创建的对象首先分配在新生代。新生代进一步分为Eden区和两个Survivor区(S0和S1)。对象在Eden区创建,当Eden区满时,触发一次Minor GC,将存活的对象复制到Survivor区,每经过一次Minor GC,对象的年龄就会增加,当达到一定年龄后,对象会被晋升到老年代。
- 老年代:长期存活的对象以及大对象直接分配在老年代。老年代的垃圾回收不如新生代频繁,因为老年代的对象通常生命周期较长,垃圾回收成本较高。
-
垃圾回收器:Java 8提供了多种垃圾回收器,包括:
- Serial GC:单线程的垃圾回收器,适用于对延迟不敏感的小应用。
- Parallel GC:也称为吞吐量优先收集器,使用多个线程进行垃圾回收,适合多核处理器,以高吞吐量为目标。
- Concurrent Mark Sweep (CMS):以最短的停顿时间为目标,通过并发的方式进行垃圾回收,适合需要低延迟的应用。
- G1 (Garbage-First) GC:是Java 7引入的,Java 9中成为默认的垃圾回收器。G1 GC旨在替换CMS,它将堆分割成多个小块(Region),可以更灵活地进行垃圾回收,减少停顿时间。
-
堆外内存:除了堆内存,Java 8还允许使用堆外内存(Off-Heap Memory),这允许JVM管理的内存超出传统的堆限制。
注意:在Java 8中,随着元空间的引入,JVM对堆外内存的使用变得更加普遍。元空间用于存储类元数据,从而减少了对堆内存的压力,并提高了性能。然而,即使是在Java 8中,直接内存等堆外内存的使用仍然需要谨慎,以避免潜在的内存管理问题。 -
元空间(Metaspace):在Java 8中,永久代(PermGen)被元空间所取代。元空间用于存储类的元数据,而不是传统的堆内存。这减少了内存溢出的风险,因为元空间使用的是本地内存(Native Memory),理论上可以更大。
-
垃圾回收触发条件:垃圾回收通常在以下情况下触发:
- 系统空闲时。
- 堆内存使用达到一定阈值。
- 显式的调用System.gc(),尽管这只是一个建议,JVM可以决定是否执行。
-
垃圾回收日志:Java 8增强了垃圾回收日志的功能,允许开发者更详细地监控垃圾回收的行为。
-
性能监控和调优工具:Java 8提供了更强大的监控和调优工具,如JMC(Java Mission Control)和JFR(Java Flight Recorder),帮助开发者分析和优化垃圾回收性能。
Serial、Parallel、CMS、G1 的工作原理和适用场景
Serial 收集器
工作原理:
- Serial 收集器是一个单线程的收集器,它在进行垃圾收集时,必须暂停其他所有工作线程(即"Stop The World")。
- 它采用复制算法,将内存划分为两块,只使用一块,待这个内存满了,把里面的对象清除到另一块内存。
适用场景:
- 适用于单核处理器和小型应用,以及对延迟不敏感的场合。
Parallel 收集器
工作原理:
- Parallel 收集器又称为吞吐量优先收集器,是一个多线程的收集器,它在多核处理器上可以提高垃圾收集的效率。
- 同样使用复制算法进行新生代的垃圾收集,与Serial收集器相比,它可以并行地执行垃圾收集任务,减少GC造成的停顿时间。
适用场景:
- 适用于多核服务器,当吞吐量是主要考虑因素时,尤其是在后台处理系统,如批处理系统。
CMS 收集器
工作原理:
- CMS(Concurrent Mark Sweep)收集器主要用于老年代,它通过并发标记和清除来最小化GC的停顿时间。
- 它包括四个主要步骤:
初始标记、并发标记、重新标记和并发清理
,其中大部分工作可以与应用程序并发进行,从而减少了停顿时间。
适用场景:
- 适用于对响应时间和延迟敏感的应用,如Web服务器。
G1 收集器
工作原理:
- G1(Garbage-First)收集器是一个面向服务端应用的垃圾收集器,它将堆划分为多个大小相等的独立区域(Region),并发地进行垃圾回收。
- G1同时采用了复制算法和标记-整理算法,以减少内存碎片,并提供可预测的停顿时间。
适用场景:
- 适用于大堆内存,需要可预测的停顿时间的应用,如大型电子商务平台或高负载的在线服务。
在选择垃圾回收器时,需要根据应用的特点和性能要求进行选择,以确保既满足性能需求,又能有效管理内存。
Java8 默认的垃圾收集器为什么不是 G1 ?
Java 8 默认的垃圾收集器不是 G1,而是基于吞吐量优先的 Parallel GC(Parallel Scavenge 用于新生代,Parallel Old 用于老年代)。以下是几个原因:
-
吞吐量优先:Parallel GC 专注于提供高吞吐量,这意味着它允许垃圾收集线程与应用线程并行运行,从而在多核处理器上提高性能。
-
延迟性考虑:虽然 G1 垃圾收集器旨在提供可预测的低延迟GC停顿时间,但 Parallel GC 在追求最大吞吐量的同时,其停顿时间可能会更长。
-
适用场景:Parallel GC 适用于后台处理系统,如批处理系统,这些系统对延迟的容忍度较高,而更注重整体的处理能力。
-
性能测试:一些基准测试表明,在某些场景下,Parallel GC 的性能可能优于 G1,尤其是在较小的堆内存配置下。
-
社区反馈和选择:Java 社区的反馈和Oracle公司的性能测试结果导致 Parallel GC 被选为 Java 8 的默认 GC。G1 虽然是一个强大的垃圾收集器,但在当时可能还没有得到足够的认可来取代 Parallel GC 成为默认选项。
-
JDK 版本演化:随着 JDK 版本的演化,G1 逐渐获得了更多的改进和优化,最终在 JDK 9 中被提议作为服务端默认的垃圾收集器。
-
技术成熟度:在 Java 8 发布时,G1 可能还没有达到足够的成熟度,以作为所有应用场景下的默认选择。
-
避免不必要的复杂性:对于不需要特别关注 GC 停顿时间的应用,使用 Parallel GC 可以避免引入 G1 的额外复杂性。