【并发编程的艺术】JVM体系与内存模型

简介: JVM体系结构图介绍

内容参考来源:《Java并发编程的艺术》,有需要可评论获取资料。

一 JVM体系结构

先看一张JVM体系结构图:

通过上图可见,JVM由类加载器运行时数据区执行引擎三个子系统组成。

简单介绍下三个子系统的功能,以便后续介绍中有更明确的理解:

1.1 类加载器

类加载器的功能,是处理类的动态加载(Loading)链接(Linking),并且在第一次引用类时进行初始化(Initialization)

Loading->Linking->Initialization 也是通常类的加载过程。

1.2 运行时数据区

运行时数据区约定了在运行时程序代码的数据比如变量、参数等等的存储位置,包括:

  • PC 寄存器(程序计数器):保存正在执行的字节码指令的地址;
  • :在方法调用时,创建一个叫栈帧的数据结构,用于存储局部变量和部分过程的结果
  • :存储类实例对象和数组对象,垃圾回收的主要区域
  • 方法区:也被称为元空间,还有个别名 non-heap(非堆),使用本地内存存储 class meta-data 元数据(运行时常量池,字段和方法的数据,构造函数和方法的字节码等),在 JDK 8 中,把 interned String 和类静态变量移动到了 Java 堆
  • 运行时常量池:存储类或接口中的数值字面量字符串字面量以及所有方法或字段的引用,基本上涉及到方法或字段,JVM 就会在运行时常量池中搜索其具体的内存地址
  • 本地方法栈:与 JVM 栈类似,只不过服务于 Native 方法

1.3 执行引擎

运行时数据区存储着要执行的字节码,执行引擎将会读取并逐个执行。包括:

解释器(Interpreter),JIT编译器(JIT Compiler),垃圾收集器(Garbage Collector。

此外,还有执行引擎所需的本地库(Native Method Libraries)和与其交互的 JNI 接口(Java Native Interface)

感兴趣想继续深入研究的朋友可以查询JVM相关文档,或等待后续文章中对此进行详细描述。

二 关于内存结构与内存模型

   提起内存结构 和 内存模型,可能很多人会搞混。这里再明确一下。

2.1 内存结构

   描述的是内存被划分为多个数据区域,各区域都有对应的功能,重点是组成结构;简单来说,就是大家都了解过的下面这张图(来自《深入理解Java虚拟机(第2版)》):

以及堆内存的分代结构(Jvm1.8以前,1.8后永久代改为元数据区,以下仅用于示例):

2.2 内存模型

   内存模型是一个比较复杂的概念,基于Java Memory Model and Thread Specification(JSR-133)的描述,Java内存模型(Java Memory Model-JMM)与线程规范紧密相关,通过下面的内容目录,我们可以看到其涵盖了锁(Locks),可见性(Visibility),顺序(Ordering)、原子性(Atomicity)、顺序一致性(Sequential Consistency)、Final字段(Final Fields)等一系列我们熟知的概念。

   简单来说,Java内存模型描述了一组规范,来解决Java多线程对共享内存进行操作的时候,会出现的一些如可见性、原子性和顺序性的问题。

三 并发的典型场景与分析

3.1 并发代价

首先提几个经典的问题:

1、多线程一定比单线程快吗? 或者,并发一定比串行快吗?

答案:不一定,并发执行可能会比串行慢。原因?线程有创建和上下文切换的开销。

那么由此带来的直接问题,如何减少上下文切换?

无锁并发CAS使用最少线程和使用协程

其中,无锁并发和CAS都是从“锁”的角度来减少开销。

无锁并发编程:多线程竞争锁时,会引起上下文切换,所以考虑通过避免使用锁的方式。例如根据数据id,做hash算法取模后分段,不同线程处理不同的段来避免争用;

CAS:Java提供Atomic包,使用CAS来更新数据,而不需要加锁。(这里实际是不显示使用锁,根据Linux x86架构下的cas源码,仍然有LOCK_IF_MP)。

使用最少线程:避免创建过多线程,这会导致造成大量线程处于等待状态。

协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

3.2 资源限制

   执行程序时,通常需要考虑的资源包括:网络带宽、磁盘(大小&iops性能)、内存、cpu,这些可归类为硬件资源,此外还有软件资源,例如数据库连接数,socket连接数等。

   使程序跑的更快,在资源的角度可以考虑两个方向,一是考虑资源扩充(扩容):单机->集群,并行执行程序,软件资源限制,考虑池化方式来实现资源复用;另一个方向,在固定的资源限制下,并发编程,尽可能对并行度调优。例如下载文件任务,主要依赖带宽和硬盘读写速度两个资源;涉及数据库读写操作时,连接数需要考虑;如果SQL执行很快且线程数比数据库连接数大很多,那么某些线程会被阻塞,等待数据库连接,我们就需要调整线程数来避免这种情况。

附:CAS底层实现

   程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

intel的手册对lock前缀的说明如下:

  1. 确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
  2. 禁止该指令与之前和之后的读和写指令重排序。
  3. 把写缓冲区中的所有数据刷新到内存中。
相关文章
|
4月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
411 55
|
5月前
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
390 6
|
8月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
880 166
|
10月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
1689 1
|
6月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
304 29
JVM简介—1.Java内存区域
|
9月前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
122 0
|
6月前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
6月前
|
存储 设计模式 监控
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
143 0
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
|
7月前
|
存储 算法 Java
JVM: 内存、类与垃圾
分代收集算法将内存分为新生代和老年代,分别使用不同的垃圾回收算法。新生代对象使用复制算法,老年代对象使用标记-清除或标记-整理算法。
89 6
|
9月前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型
205 10