JVM内存区域与多线程

简介: Java并发的机制的背后是Java虚拟机(JVM)的工作机制,本文从几个关于并发和多线程的疑问开始,引出Java内存区域的介绍,希望能帮助大家更好的理解Java并发机制。

Java并发的机制的背后是Java虚拟机(JVM)的工作机制,本文从几个关于并发和多线程的疑问开始,引出Java内存区域的介绍,希望能帮助大家更好的理解Java并发机制。

1. 线程创建和切换的代价——JVM的内存区域

在《从任务到线程:Java结构化并发应用程序》和《尝试Java加锁新思路:原子变量和非阻塞同步算法》中,曾经分别介绍过,创建线程和线程间切换对于性能和资源的消耗是不容忽视的,无限制地创建线程会消耗过多的内存资源并不可取,过多的线程间上下文切换也会降低多线程并发的性能。但是线程创建和切换的代价到底是怎么产生的呢?这就不得不提到Java的运行时数据区了。

1.1 JVM运行时数据区

JVM运行时数据区

根据《Java虚拟机规范》 JVM 将所管理的内存区域划分为 Method Area方法区),Heap),Program Counter Register程序计数器), VM Stack(虚拟机栈),Native Method Stack本地方法栈),其中Method Area和Heap是线程共享的,VM Stack,Native Method Stack 和Program Counter Register是线程隔离的。

如果读者对于JVM运行时数据区不是很了解,由于篇幅有限,请参看JVM初探 -JVM内存模型浅析Java虚拟机结构与机制,这里不再展开说明,只提供一份思维导图,帮助大家梳理内容:

Java内存区域.png

概括地说来,JVM初始运行的时候都会分配好Method Area(方法区)和Heap(堆),而JVM 每遇到一个线程,就为其分配一个Program Counter Register(程序计数器), VM Stack(虚拟机栈)和Native Method Stack (本地方法栈),当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。

1.2 线程创建的内存代价

每当有线程被创建的时候,JVM就需要为其在内存中分配虚拟机栈和本地方法栈来记录调用方法的内容,分配程序计数器记录指令执行的位置,这样的内存消耗就是创建线程的内存代价。

内存作为有限的资源,如果JVM创建了过多的线程,必然会导致资源的耗尽。因此,使用Executor架构复用线程可以节省内存资源,是十分必要的。

1.3 线程切换的性能代价

JVM的并发是通过线程切换并分配时间片执行来实现的. 在任何一个时刻, 一个处理器内核只会执行一条线程中的指令。因此, 为了线程切换后能恢复到正确的执行位置, JVM需要先保存被挂起线程的上下文环境:将线程执行位置保存到程序计数器中,将调用方法的信息保存在栈中;同时将待执行线程的程序计数器和栈中的信息写入到处理器中,完成线程的上下文切换。维护线程隔离数据区中的内容在处理器中的导入导出,就是线程切换的性能代价。

控制线程上下文切换次数的方法有很多:

  1. 使用基于CAS的非拥塞算法,详见尝试Java加锁新思路:原子变量和非阻塞同步算法
  2. 无锁并发编程,尽量使用线程封闭(ThreadLocal)或者不变量,而不是用锁,详见对象共享:Java并发环境中的烦心事
  3. 使用线程池+等待队列的方式,控制线程数目,详见从任务到线程:Java结构化并发应用程序

2. 对象访问的定位 VS 内存可见性

根据JVM运行时的内存模式,在一个方法中使用某个变量,JVM要现在栈中找到该变量的引用,然后通过引用找到该对象在堆中保存到实例数据,如下图所示:


对象访问的定位

但是这个过程为什么会出现多线程间的内存可见性的问题呢?

这个过程从单个线程的角度来说,是没有问题的,线程可以找到自己需要的变量,但是得到的变量不一定是最新的,这是由于主存和缓存内容不一致造成的。

JVM不光有内存区域的划分,还有内存模式(JMM)来控制Java线程见的通信,其决定了一个线程的共享变量的写入合适对另一个线程可见。在Java中通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域主存(main memory),而每个线程又单独的有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交由线程处理(操作码+操作数),数据写入时,先被写入到工作缓存中,JMM选择合适的时机将其同步到主存中,以此提高访问效率。


JMM中的主存和缓存

工作缓存其实是一个抽象的概念,Java的内存分区中并没有专门的一块区域叫线程的工作缓存,其实际上是缓存、对读写缓冲区、寄存器以及其他硬件和编译器优化的统称。

因此虚拟栈到Java堆中寻找实例数据和JMM并不矛盾,二者是JVM不通角度的阐述。

3. 对象的创建——静态区域加载的多线程安全性

为了保证多线程安全性,一些必要的操作都需要加锁来保证其原子性和可见性,但是类中静态区的代码是不需要加锁就能保证多线程安全性。这是因为什么呢?

答案在于JVM类加载过程的保护机制。和普通类的实例被分配在Java堆上不同,类的静态属性都保存在方法区,其创建收到类加载过程的影响。


方法区,又名非堆

类的加载过程大题分为:加载(Loading),连接(Linking),初始化(Initialization),使用(Using)和卸载(UnLoading)五个步骤。这里和静态属性有关的主要是连接和初始化:在连接步骤的准备阶段,静态属性会分配内存;在初始化步骤,JVM会生成一个特别的方法——<clinit>方法来专门执行静态代码块和静态变量初始化。

<clinit>方法执行的过程中,JVM会对类加锁,保证在多线程环境下,只有一个线程能成功执行<clinit>方法,其他线程都将被拥塞,并且<clinit>方法只能被执行一次,被拥塞的线程被唤醒之后也不会再去执行<clinit>方法。如果类有继承关系,JVM还会保证父类的<clinit>方法将先于子类的<clinit>方法执行。

由此可见,静态代码块的多线程安全性是由JVM为其加锁实现的,这也是延迟初始化占位类模式的安全性基础,详见《从Java内存模型角度理解安全初始化》。

参考文献

  1. 深入理解JVM之JVM内存区域与内存分配
  2. 深入理解JVM—JVM内存模型
  3. JVM 内存初学 (堆(heap)、栈(stack)和方法区(method) )
  4. Java常量池详解之一道比较蛋疼的面试题
  5. JVM中锁优化简介
  6. JVM初探 -JVM内存模型
  7. 浅析Java虚拟机结构与机制
相关文章
|
22天前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
33 4
|
22天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
45 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
12天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
37 10
|
9天前
|
监控 Java 数据库连接
线程池在高并发下如何防止内存泄漏?
线程池在高并发下如何防止内存泄漏?
|
11天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
11天前
|
监控 Java 数据库连接
使用线程池时,如何避免内存泄漏的问题?
使用线程池时,如何避免内存泄漏的问题?
|
19天前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
32 2
|
21天前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
45 2
|
22天前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
43 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
11天前
|
监控 数据可视化 Java
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?

相关实验场景

更多