java内存的划分_Java内存区域划分

Java运行时数据区:

Java虚拟机在执行Java程序的过程中会将其管理的内存划分为若干个不同的数据区域,这些区域有各自的用途、创建和销毁的时间,有些区域随虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束来建立和销毁。

下面描述了一个java 文件被 JVM 加载到内存中的过程:

1. HelloWorld.java 文件首先需要经过编译器编译,生成 HelloWorld.class 字节码文件。

2. Java 程序中访问HelloWorld这个类时,需要通过 ClassLoader(类加载器)将HelloWorld.class 加载到 JVM 的内存中。

3. JVM 中的内存可以划分为若干个不同的数据区域,主要分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区。

6b5c5a1ae0831a2f21c2a025d6eda4ff.png

1. 程序计数器(Program Counter Register)

Java 程序是多线程的,CPU 可以在多个线程中分配执行时间片段。当某一个线程被 CPU 挂起时,需要记录代码已经执行到的位置,方便 CPU 重新执行此线程时,知道从哪行指令开始执行。这就是程序计数器的作用。“程序计数器”是虚拟机中一块较小的内存空间,主要用于记录当前线程执行的位置。

关于程序计数器还有几点需要格外注意:

(1) 在 Java 虚拟机规范中,对程序计数器这一区域没有规定任何 OutOfMemoryError 情况。

(2) 线程私有的,每条线程内部都有一个私有程序计数器。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

(3) 当一个线程正在执行一个 Java 方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。

2.虚拟机栈

虚拟机栈也是线程私有的,与线程的生命周期同步。在 Java 虚拟机规范中,对这个区域规定了两种异常状况:

(1) StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。

(2) OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出。

在我们学习 Java 虚拟机的的过程当中,经常会看到一句话:JVM 是基于栈的解释器执行的,DVM 是基于寄存器解释器执行的。

“基于栈”指的就是虚拟机栈。虚拟机栈的初衷是用来描述 Java 方法执行的内存模型,每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧,接下来看下这个栈帧是什么。

栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧。我们可以这样理解:一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址等。如下图所示:

ac7c68271acab4d5753f7a132f13fc3f.png

局部变量表

局部变量表是变量值的存储空间,我们调用方法时传递的参数,以及在方法内部创建的局部变量都保存在局部变量表中。在 Java 编译成 class 文件的时候,就会在方法的 Code 属性表中的 max_locals 数据项中,确定该方法需要分配的最大局部变量表的容量。

操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入方法的Code属性表中的max_stacks数据项中。栈中的元素可以是任意Java数据类型,包括long和double。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的。在方法执行的过程中,会有各种字节码指令被压入和弹出操作数栈(比如:iadd指令就是将操作数栈中栈顶的两个元素弹出,执行加法运算,并将结果重新压回到操作数栈中)。

动态链接

动态链接的主要目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。

在一个 class 文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中。

Java 虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的就是为了支持方法调用过程中的动态连接(Dynamic Linking)。具体过程会在后续的字节码执行课时中介绍。

返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法:

正常退出:指方法中的代码正常完成,或者遇到任意一个方法返回的字节码指令(如return)并退出,没有抛出任何异常。

异常退出:指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。

无论当前方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行。而虚拟机栈中的“返回地址”就是用来帮助当前方法恢复它的上层方法执行状态。

一般来说,方法正常退出时,调用者的 PC 计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。

3.本地方法栈

本地方法栈和上面介绍的虚拟栈基本相同,只不过是针对本地(native)方法。在开发中如果涉及 JNI 可能接触本地方法栈多一些,在有些虚拟机的实现中已经将两个合二为一了(比如HotSpot)。

4. 方法区

方法区(Method Area)也是 JVM 规范里规定的一块运行时数据区。方法区主要是存储已经被 JVM 加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。该区域同堆一样,也是被各个线程共享的内存区域。

注意:关于方法区,很多开发者会将其跟“永久区”混淆。所以我在这里对这两个概念进行一下对比:

方法区是 JVM 规范中规定的一块区域,但是并不是实际实现,切忌将规范跟实现混为一谈,不同的 JVM 厂商可以有不同版本的“方法区”的实现。

HotSpot 在 JDK 1.7 以前使用“永久区”(或者叫 Perm 区)来实现方法区,在 JDK 1.8 之后“永久区”就已经被移除了,取而代之的是一个叫作“元空间(metaspace)”的实现方式。

总结一下就是:

方法区是规范层面的东西,规定了这一个区域要存放哪些数据。

永久区或者是 metaspace 是对方法区的不同实现,是实现层面的东西。

5.堆

Java 堆(Heap)是 JVM 所管理的内存中最大的一块,该区域唯一目的就是存放对象实例,几乎所有对象的实例都在堆里面分配,因此它也是 Java 垃圾收集器(GC)管理的主要区域,有时候也叫作“GC 堆”(关于堆的 GC 回收机制将会在后续课时中做详细介绍)。同时它也是所有线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全问题。

按照对象存储时间的不同,堆中的内存可以划分为新生代(Young)和老年代(Old),其中新生代又被划分为 Eden 和 Survivor 区。具体如下图所示:

堆的内存划分:

d97aed8c4837b5d657044da390821f87.png

Java堆的内存划分如图所示,分别为年轻代、Old Memory(老年代)、Perm(永久代)。其中在Jdk1.8中,永久代被移除,使用MetaSpace代替。

1、新生代:

(1)使用复制清除算法(Copinng算法),原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。

(2)分为Eden、Survivor From、Survivor To,比例默认为8:1:1

(3)内存不足时发生Minor GC2

2、老年代:

(1)采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象。

3、Perm:用来存储类的元数据,也就是方法区。

(1)Perm的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代进场内存不够用,或者发生内存泄漏。

(2)MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

4、堆内存的划分在JVM里面的示意图:

b9e1540734c930453348e800245efbb9.png

总结来说,JVM 的运行时内存结构中一共有两个“栈”和一个“堆”,分别是:Java 虚拟机栈和本地方法栈,以及“GC堆”和方法区。除此之外还有一个程序计数器,但是我们开发者几乎不会用到这一部分,所以并不是重点学习内容。 JVM 内存中只有堆和方法区是线程共享的数据区域,其它区域都是线程私有的。并且程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

Java内存模型:

1、 Java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯。多个线程之间是不能通过直接传递数据交互的,它们之间交互只能通过共享变量实现。

2、 主要目的是定义程序中各个变量的访问规则。

3、 Java内存模型规定所有变量都存储在主内存中,每个线程还有自己的工作内存。

(1) 线程的工作内存中保存了被该线程使用到的变量的拷贝(从主内存中拷贝过来),线程对变量的所有操作都必须在工作内存中执行,而不能直接访问主内存中的变量。

(2) 不同线程之间无法直接访问对方工作内存的变量,线程间变量值的传递都要通过主内存来完成。

(3) 主内存主要对应Java堆中实例数据部分。工作内存对应于虚拟机栈中部分区域。

27a5f9d15ffabf538bd356bf5b43b67a.png

4、Java线程之间的通信由内存模型JMM(Java Memory Model)控制。

(1)JMM决定一个线程对变量的写入何时对另一个线程可见。

(2)线程之间共享变量存储在主内存中

(3)每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本。

(4)JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证。

5、可见性、有序性:

(1)当一个共享变量在多个本地内存中有副本时,如果一个本地内存修改了该变量的副本,其他变量应该能够看到修改后的值,此为可见性。

(2)保证线程的有序执行,这个为有序性。(保证线程安全)

6、内存间交互操作:

(1)lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

(3)read(读取):作用于主内存变量,把主内存的一个变量读取到工作内存中。

(4)load(载入):作用于工作内存,把read操作读取到工作内存的变量载入到工作内存的变量副本中

(5)use(使用):作用于工作内存的变量,把工作内存中的变量值传递给一个执行引擎。

(6)assign(赋值):作用于工作内存的变量。把执行引擎接收到的值赋值给工作内存的变量。

(7)store(存储):把工作内存的变量的值传递给主内存

(8)write(写入):把store操作的值入到主内存的变量中

6.1、注意:

(1)不允许read、load、store、write操作之一单独出现

(2)不允许一个线程丢弃assgin操作

(3)不允许一个线程不经过assgin操作,就把工作内存中的值同步到主内存中

(4)一个新的变量只能在主内存中生成

(5)一个变量同一时刻只允许一条线程对其进行lock操作。但lock操作可以被同一条线程执行多次,只有执行相同次数的unlock操作,变量才会解锁

(6)如果对一个变量进行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者assgin操作初始化变量的值。

(7)如果一个变量没有被锁定,不允许对其执行unlock操作,也不允许unlock一个被其他线程锁定的变量

(8)对一个变量执行unlock操作之前,需要将该变量同步回主内存中。

参考:https://mp.weixin.qq.com/s/oBKu-WG9-UdRRPBg_WWLDQ

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1855

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值