当我们提到 Java 的内存模型的时候通常会想到 JVM 运行时候的数据区域,比如包括线程私有的堆,方法区,线程共享的有本地方法栈,虚拟机栈,程序计数器。Java程序启动后,就会初始化这些内存的数据。但是这就是 Java 的内存模型了吗?
Java的内存模型(Java Memory Model,JMM)是Java虚拟机(JVM)规范中定义的一种抽象概念,用于描述Java程序中各种变量的存储方式、访问规则以及线程之间的交互关系。JMM规定了Java程序中多线程并发访问共享变量时的行为规范,确保多线程程序在不同的硬件平台和操作系统上都能获得一致的行为
。
JMM主要包含以下几个关键概念:
主内存(Main Memory): 主内存是所有线程享的内存区域,用于存储Java对象实例、静态变量等数据。所有的变量都存储在主内存中,而且每个线程都可以访问主内存中的变量。
工作内存(Working Memory): 每个线程都有自己的工作内存,用于存储该线程需要使用的变量副本。线程对变量的所有操作都在工作内存中进行,而不是直接在主内存中进行。
内存间交互操作: Java内存模型定义了一组规则,用于描述线程如何与主内存中的变量进行交互。这些规则包括读取变量、写入变量、锁定变量等操作,确保多线程并发访问时的可见性、有序性和原子性。
原子性、可见性和有序性: JMM通过各种机制来保证共享变量的原子性、可见性和有序性。原子性指一个操作是不可中断的;可见性指一个线程对共享变量的修改能被其他线程立即感知到;有序性指程序执行的顺序与代码的编写顺序一致。
通过定义Java内存模型,Java程序员可以编写多线程程序而无需考虑底层硬件和操作系统的差异,确保程序的正确性和可移植性。。
Java 8 之前的 JVM(Java Virtual Machine)与 Java 8 及之后的 JVM 主要在以下几个方面存在区别:
内存模型改进:
字符串去重复:
G1垃圾收集器成熟:
Lambda表达式与 invokedynamic:
invokedynamic
JVM 指令。invokedynamic
是 Java 7 引入的 JVM 新特性,但在 Java 8 中得到了广泛应用。它允许在运行时动态解析和调用方法,使得 Lambda 表达式的创建和执行更加高效,同时也简化了方法和函数的处理。Compact Strings:
char
类型存储每个字符,而是改为使用 1 字节的 byte
类型,从而减少了内存占用。内存分配改进:
JVM监控与诊断工具增强:
jcmd
、jstack
、jmap
、jstat
等)进行了增强,提供了更丰富的命令选项和更详细的输出信息,便于开发者更好地分析和调试 JVM 性能问题。新的 JVM 命令行选项:
-XX:+UseStringDeduplication
(启用字符串去重复)、-XX:+UnlockExperimentalVMOptions
(解锁实验性 VM 选项)等,为 JVM 调优提供了更多可能性。Java虚拟机(JVM)内存模型是Java程序运行时的内存布局,它规定了程序在执行过程中如何分配和管理内存。JVM内存模型主要分为以下几个区域:
方法区(Method Area):
堆(Heap):
栈(Stacks):
程序计数器(Program Counter Register):
本地方法栈(Native Method Stacks):
虚拟机栈(VM Stack):
元空间(Metaspace):
直接内存(Direct Memory):
JVM内存垃圾回收(Garbage Collection,GC)是指JVM自动对内存中不再使用的对象进行识别和清理的过程,以释放内存资源供其他对象使用。这一机制对于Java语言的自动内存管理至关重要。以下是JVM内存垃圾回收的一些关键点:
垃圾回收的目的:识别并清除不再被引用的对象,即“垃圾”,以避免内存泄漏和内存溢出。
垃圾回收的算法:JVM采用了多种垃圾回收算法,包括但不限于:
垃圾回收的触发条件:通常当新生代空间不足时,会触发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判断对象是否存活主要依赖于“可达性分析”(Reachability Analysis)算法。这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链。如果一个对象到GC Roots没有任何引用链相连,即GC Roots到对象不可达,那么JVM就认为此对象不再存活,可以进行回收。
以下是可达性分析的具体步骤:
GC Roots的设置:在JVM中,作为GC Roots的对象包括:
从GC Roots开始:垃圾回收器从GC Roots对象开始进行扫描。
搜索引用链:垃圾回收器会递归地跟踪GC Roots对象所引用的所有对象,以及这些对象所引用的其他对象。
标记存活对象:所有通过GC Roots直接或间接可达的对象都会被标记为存活。
确定垃圾对象:在完成引用链的遍历后,仍然没有被标记的对象被认为是垃圾,因为它们不可达,即没有任何活动线程或GC Roots引用它们。
回收垃圾对象:最后,垃圾回收器会回收那些被标记为垃圾的对象,释放它们占用的内存空间。
JVM 运行过程中,数据结构是动态变化的,因此可以使用三色标记算法来解决并发标记中一些问题。
三色标记算法是JVM中用于垃圾回收的一种算法,特别是在老年代的垃圾回收中。这种算法主要用于解决并发标记过程中的一些问题,如在并发清除过程中,对象的引用关系还在不断变化,导致清除工作不准确或遗漏。
在三色标记算法中,对象被分为三种颜色:
黑色:黑色对象是已经检查过的对象,它们的子对象也已经被检查过,这些对象不会被回收,并且不会被其他回收线程所改变。
灰色:灰色对象是已经检查过,但是其子对象还没有被检查的对象。灰色对象可能会被其他线程改变其子对象的引用关系,因此需要额外的同步措施来保证安全。
白色:白色对象是尚未检查的对象,也即是可能的垃圾对象。如果一个对象在完成标记过程后仍然是白色的,那么它将被认为是垃圾,可以被回收。
三色标记算法的基本步骤如下:
初始化:开始时,除了GC Roots直接引用的对象被标记为黑色外,所有其他对象都被认为是白色的。
并发标记:从GC Roots开始,遍历所有对象,将所有可达的对象标记为灰色,并检查它们的子对象。
重新标记:由于并发标记过程中,应用程序线程可能改变对象的引用关系,可能会产生新的灰色对象。因此,需要重新扫描这些灰色对象,以确保所有新增的子对象都被检查过。
清理:在所有对象都被重新标记后,所有白色的对象都可以安全地被回收。
三色标记算法通过引入灰色对象,解决了并发标记过程中对象引用关系变化的问题。然而,这种算法需要额外的同步机制来保证并发标记的安全性,这可能会影响垃圾回收的效率。
在实际的JVM实现中,如G1垃圾回收器,采用了类似的三色标记策略,但做了一些优化,以减少并发标记对应用程序性能的影响。
Java 8中的垃圾回收(Garbage Collection,GC)机制与Java的其他版本基本相同,它依赖于垃圾回收器(Garbage Collector,GC)来自动管理对象的生命周期,回收不再使用的对象以释放内存。Java虚拟机(JVM)提供了几种不同的垃圾回收器,它们在Java 8中得到了进一步的优化和改进。
以下是Java 8中垃圾回收的一些关键概念和组件:
分代收集:Java的垃圾回收器通常采用分代收集的策略,将堆内存分为新生代(Young Generation)和老年代(Old Generation)。
垃圾回收器:Java 8提供了多种垃圾回收器,包括:
堆外内存:除了堆内存,Java 8还允许使用堆外内存(Off-Heap Memory),这允许JVM管理的内存超出传统的堆限制。
注意:在Java 8中,随着元空间的引入,JVM对堆外内存的使用变得更加普遍。元空间用于存储类元数据,从而减少了对堆内存的压力,并提高了性能。然而,即使是在Java 8中,直接内存等堆外内存的使用仍然需要谨慎,以避免潜在的内存管理问题。
元空间(Metaspace):在Java 8中,永久代(PermGen)被元空间所取代。元空间用于存储类的元数据,而不是传统的堆内存。这减少了内存溢出的风险,因为元空间使用的是本地内存(Native Memory),理论上可以更大。
垃圾回收触发条件:垃圾回收通常在以下情况下触发:
垃圾回收日志:Java 8增强了垃圾回收日志的功能,允许开发者更详细地监控垃圾回收的行为。
性能监控和调优工具:Java 8提供了更强大的监控和调优工具,如JMC(Java Mission Control)和JFR(Java Flight Recorder),帮助开发者分析和优化垃圾回收性能。
工作原理:
适用场景:
工作原理:
适用场景:
工作原理:
初始标记、并发标记、重新标记和并发清理
,其中大部分工作可以与应用程序并发进行,从而减少了停顿时间。适用场景:
工作原理:
适用场景:
在选择垃圾回收器时,需要根据应用的特点和性能要求进行选择,以确保既满足性能需求,又能有效管理内存。
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 的额外复杂性。