JVM-JMM

<陈乔乔>整理

对于Java程序员来说,不需要在为每一个new操作去写配对的delete/free,不容易出现内容泄漏和内存溢出错误,看起来由JVM管理内存一切都很美好。不过,也正是因为Java程序员把内存控制的权力交给了JVM,一旦出现泄漏和溢出,如果不了解JVM是怎样使用内存的,那排查错误将会是一件非常困难的事情。学习JVM的内存,首先得先了解一下JVM内部的组成部分,看看JVM内存在整个JVM中地位和作用。

一、JVM内部结构

       在网上找到一张描述JVM内部结构的图,这张图很清楚形象的描绘了整个JVM的内部结构,以及各个部分之间的交互和作用。

JVM-JMM

 JVM我们可以划分为这么几个部分:Class Loader、运行时数据区、执行引擎、本地方法接口。网上有文章有将这4部分称为两个子系统和两个组件,两个子系统是Class loader子系统和Execution engine(执行引擎) 子系统,两个组件为Runtime data area (运行时数据区域)组件和Native interface(本地接口)组件。叫法不重要,我们先具体看看这4部分都处于虚拟机中的什么地位吧。

1. Class Loader(类加载器)就是将Class文件加载到内存,再说的详细一点就是,把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是类加载器的作用。

2. 运行时数据区 就是我们常说的JVM管理的内存了,也是我们这里主要讨论的部分。运行数据区是整个JVM的重点。我们所有写的程序都被加载到这里,之后才开始运行。这部分也是我们这里将要讨论的重点。

3.Execution engine(执行引擎) 是Java虚拟机最核心的组成部分之一。执行引擎用于执行指令,不同的java虚拟机内部实现中,执行引擎在执行Java代码的时候可能有解释执行(解释器执行)和编译执行(通过即时编译器产生本地代码执行,例如BEA JRockit),也有可能两者兼备。任何JVM specification实现(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好坏主要就取决于他们各自实现的Execution engine的好坏。

4.Native interface 与native libraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现JVM无法控制的native heap OutOfMemory。

二、JVM内存结构

        Java虚拟机在执行Java程序的过程中会把它所管理的内存换分为若干个不同的数据区域。JVM执行Java程序的过程中,会使用到各种数据区域,这些区域有各自的用途、创建和销毁时间。根据《Java虚拟机规范(第二版)》(下文称VM Spec)的规定,JVM包括下列几个运行时数据区域(如上图所示):

数据区 描述

PROGEAM

COUNTER

REGISTER

程序计数器,线程私有、指向下一条要很执行的指令。 

如果线程正在执行的是JAVA方法,记录的是正在执行的虚拟机字节码指令的地址;正在执行的是Native方法为空(Undefined) 

JAVA

STACK

Java虚拟机栈,线程私有,生命周期与线程相同。 

描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口 

NATIVE METHOD STACK
为虚拟机使用到的Native 方法服务 
HEAP 
线程共享,由于现在收集器基本采用的分代收集算法,所以Java堆中还可以细分:新生代和老生代;更细致一点的有Eden空间、From Survivor空间、To Survivor空间等 

所有的对象实例以及数组都要在堆上分配,垃圾收集器管理的主要区域 
MEATHOD AREA
线程共享的内存区域,别名叫做非堆(Non-Heap),目的是与Java堆区分开来,存储类信息、常量、静态变量、即时编译器编译后的代码。 

方法区存放的信息包括 
A、类的基本信息: 
       1.每个类的全限定名 
       2.每个类的直接超类的全限定名(可约束类型转换) 
       3.该类是类还是接口 
       4.该类型的访问修饰符 
       5.直接超接口的全限定名的有序列表 
B、已装载类的详细信息 
       1.运行时常量池:在方法区中,每个类型都对应一个常量池,存放该类型所用到的所有常量,常量池中存储了诸如文字字符串、final变量值、类名和方法名常量。它们以数组形式通过索引被访 问,是外部调用与类联系及类型对象化的桥梁。(存的可能是个普通的字符串,然后经过常量池解析,则变成指向某个类的引用) 
       2.字段信息:字段信息存放类中声明的每一个字段的信息,包括字段的名、类型、修饰符。字段名称指的是类或接口的实例变量或类变量,字段的描述符是一个指示字段的类型的字符串,如private A a=null;则a为字段名,A为描述符,private为修饰符。 
       3.方法信息:类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。(在编译的时候,就已经将方法的局部变量、操作数栈大小等确定并存放在字节码中,在装载的时候,随着类一起装入方法区。) 
       4.静态变量:就是类变量,类的所有实例都共享,我们只需知道,在方法区有个静态区,静态区专门存放静态变量和静态块。 
       5.到类classloader的引用:到该类的类装载器的引用。 
       6.到类class 的引用:jvm为每个加载的类型(译者:包括类和接口)都创建一个java.lang.Class的实例。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。

除了上述5个大的部分,这里还要介绍另外一种----直接内存。直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。

        这里下面的这幅图从另外一个角度描绘了运行时数据区域,这个图更形象的描绘了真实jvm内存中的样子:

JVM-JMM

三、Java内存模型

        Java内存模型(Java Memory Model,JMM)JMM主要是为了规定了线程和内存之间的一些关系。对Java程序员来说只需负责用synchronized同步要害字,其它诸如与线程/内存之间进行数据交换/同步等繁琐工作均由虚拟机负责完成。如图1所示:根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

JVM-JMM

线程若要对某变量进行操作,必须经过一系列步骤:首先从主存复制/刷新数据到工作内存,然后执行代码,进行引用/赋值操作,最后把变量内容写回Main Memory。Java内存模型中定义了以下8种操作:

操作 描述
lock
将主内存中的变量锁定,为一个线程所独占
unclock
将lock加的锁定解除,此时其它的线程可以有机会访问此变量
read
将主内存中的变量值读到工作内存当中
load
将read读取的值保存到工作内存中的变量副本中。
use
将值传递给线程的代码执行引擎
assign
将执行引擎处理返回的值重新赋值给变量副本
store
将变量副本的值存储到主内存中。
write
将store存储的值写入到主内存的共享变量当中。

我们可以看到,要保证数据的同步,lock和unlock定义了一个线程访问一次共享内存的界限,有lock操作也必须有unlock操作,另外一些操作也必须要成对出现才可以,像是read和load、store和write需要成对出现,如果单一指令出现,那么就会造成数据不一致的问题。Java内存模型也针对这些操作指定了必须满足的规则:
(1) read和load、store和write必须要成对出现,不允许单一的操作,否则会造成从主内存读取的值,工作内存不接受或者工作内存发起的写入操作而主内存无法接受的现象。
(2) 在线程中使用了assign操作改变了变量副本,那么就必须把这个副本通过store-write同步回主内存中。如果线程中没有发生assign操作,那么也不允许使用store-write同步到主内存。
(3) 在对一个变量实行use和store操作之前,必须实行过load和assign操作。
(4) 变量在同一时刻只允许一个线程对其进行lock,有多少次lock操作,就必须有多少次unlock操作。在lock操作之后会清空此变量在工作内存中原先的副本,需要再次从主内存read-load新的值。在执行unlock操作前,需要把改变的副本同步回主存。


你可能感兴趣的:(jvm,JMM)