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虚拟机结构与机制
相关文章
|
2月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
421 1
|
15天前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型
|
1月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
2月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
2月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
27 3
|
2月前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
59 1
|
2月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
2月前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
27 1
|
3月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
49 4
|
3月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
113 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS