伸手摘星,即使一无所获,亦不致满手污泥。
请关注公众号:星河之码
说到Jvm 内存模型有时候会把它跟Java内存模型搞混,甚至认为它们就是一个东西,其实不是的,它们其实两个不同的东西。所以我们在理解Jvm内存模型模型之前先来区分一下这两个的概念。
1.1 Java内存模型
Java内存模型即JMM(Java Memory Model),它并不是真实存在的,而是Java中抽象出来的一个概念用于多线程场景,JMM描述了一组规则或规范,定义了一个线程对共享变量的写入时对另一个线程是可见的。
在我之前的文章《并发理论》中就介绍了再Java的多线程编程中,是采用的共享内存的方式来进行线程与线程之间的通信。而在这个过程中会有可见性、原子性、顺序性等问题,而JMM就为解决这些问题而建立的模型。
通过这张图我们可以得出一个结论:
在JMM中,把多个线程间通信的共享内存称之为主内存,而在并发编程中每个线程都维护一个自己的本地内存,其中保存的数据是主内存中拷贝来的数据副本。而JMM主要是控制本地内存和主内存之间的数据交互。
想要更加深入了解Java内存模型,可以看看我之前的几篇文章
- 《谈谈Java中的锁》
- 《一篇ThreadLocal走天下》
1.2 Jvm内存模型
Jvm内存模型是Java虚拟机在运行时为Java进程对内存进行的逻辑划分,将其分为几个部分,
- 方法区
- 堆内存
- 虚拟机栈
- 本地方法栈
- 程序计数器
这些区域分别在不同数据结构对申请到的内存进行不同的使用。
既然jvm内存模型是对内存的划分,那就先从内存分划分开始入手,我们知道Java中有个一个OOM的异常,是由于虚拟机提供了一些参数设置了虚拟机的大小
当运行的内存超过设置的大小的时候就会抛出OOM异常。因此我们将内存可以分为本地内存和虚拟机内存
-
虚拟机内存:Java虚拟机在执行时将管理的内存分配成不同的区域,这些区域被称为虚拟机内存。
受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报OOM
-
本地内存:虚拟机没有直接管理但是却使用的物理内存,这些被利用却不在虚拟机内存数据区的内存,称为本地内存。
本地内存只受物理内存容量的限制,如果内存的占用超出物理内存的大小,也会报OOM
通过上面的描述,似乎Java程序的运行不仅仅是只需要jvm内存,还需要本地物理内存,两者相互配合,从而运行Java程序,那么本地内存和虚拟机内存分别存储的是什么东西呢?这里我们先提前看一张图理解一下,这张图在下面会频繁用到。
这张图就非常清晰的向我们本地内存和虚拟机内存分别存储那些东西。具体的内容接着下面分析。
在了解Jvm内存模型之前,先来简单了解一下JVM 的架构组成,下图是一张JVM的架构图
从图中可以看到,JVM的组成一共分为5类,分别是:
- 类装载器子系统
- 运行时数据区
- 执行引擎
- 本地方法接口
- 垃圾收集模块
在之前的文章《Java从编译到执行发生了什么》中介绍了一个Java文件冲被装载到执行的过程,其实就是在【类装载器子系统】与【执行引擎 】中的执行过程,本文介绍的JVM内存模型主要就是【运行时数据区】的功能。接下来就来看看JVM内存模型中各个区域的运行原理。
前面我们说Jvm内存模型将虚拟机管理的内存划分了【方法区】【堆内存】【虚拟机栈】【本地方法栈】【程序计数器】五个部分,
首先看一张图来感受一下这五个部分所在的位置。如下:
上面这张图很清晰的表达了五个部分的位置,并且将其以线程为维度区分:
-
线程私有的:程序计数器,虚拟机栈,本地方法栈
-
线程共享的:堆、方法区、本地内存
注意:上面提到的本地内存(没有被虚拟机管理的本地内存)也是线程共享的
4.1 存放类方法区
我们知道在Java要运行,必须先将.Java文件加载到内存中,加载过程可以参考之前的文章《Java从编译到执行发生了什么》,有详细描述。我们看一个例子,比如现在有一个user.java文件,它被变成成user.class后要加载到内存中,那么它被加载到哪里去了呢?
其实我们通过上面那张图就可以知道
我们的类信息被加载后放在了方法区里面,也就是说class后要加载到内存中方法区,在图中可以看到,方法区并没有在JVM中,而是在本地内存中。
这是因为在JDK1.8改版了,在JDK1.8之前的版本中,方法区是放在JVM的内存区域的。
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
在JDK1.8中,方法区改名字了,称之为元空间(metaspace),并将其移出了jvm的内存区域,放到了本地内存中,存储的信息基本没有变化。
元空间的变化,我们理解永久代后再回过来看这个,这个在后面的文章会讲,这里就不做展开。
-
运行时常量池
运行时常量池原本是方法区的一部分。
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆中开辟了一块区域存放运行时常量池。
4.2 程序计数器
程序计数器占用的内存空间很小,主要用来记录执行代码指令的。程序计数器
通俗的说就是:程序计数器记录当前线程所执行的字节码指令的行号,
字节码解释器通过改变计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等功能都需要依赖程序计数器来完成。
我们知道Java多线程执行的时候是通过线程轮流切换并分配CPU的时间片的方式实现的,为了线程切换后能恢复到正确的位置继续执行,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,因此程序计数器的内存区域为【线程私有】的内存。
如果对多线程CPU时间片不了解,可以参考看看我之前的文章《并发理论》。
总结一下程序计数器的作用,主要有两个:
-
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。如:顺序执行、选择、循环、异常处理。
-
在多线程的情况下,程序计数器用于记录当前线程执行的位置,保证当线程被切换回来的时候能够知道自己上次运行到哪儿了。
注意:程序计数器是唯一不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
4.3 Java虚拟机栈
我们在写代码的时候,会定义方法,方法里面可能会有一些方法内的局部变量。比如
以上这个main方法在运行的时候,会有一个线程来执行它,这个线程就会有一个程序计数器记录这个方法被执行到那一步。
在上述案例的main方法中有个count变量,这个变量很显然是main方法的局部变量,那么这个count变量是放在哪里的呢?Java虚拟机栈这个时候就出来了。
Java虚拟机栈是JVM为保存每个方法的局部变量而划分的内存区域,它是线程私有的,生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。先先来看个例子
比如一个线程在执行funA()时,就会将funA(),就会往当前线程的Java虚拟机栈中放一个栈帧,当执行到funB()时,又会往Java虚拟机栈放一个栈帧。如下
从上图可以看到,funA栈帧与funB栈帧的出栈入栈的时机,先入后出
- funA方法被封装成栈帧入栈
- funB方法被封装成栈帧入栈
- funB方法出栈
- funA方法出栈
Java虚拟机栈中存放的是由一个个栈帧,【线程在执行一个方法时,便会向栈中放入一个栈帧】,每个栈帧中都拥有【局部变量表】、【操作数栈】、【动态链接】、【方法出口】等信息。
Java 虚拟机栈会出现两种异常:
-
StackOverFlowError
若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
-
OutOfMemoryError
若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
4.4 本地方法栈
看过Java的一些底层API的(IO相关、socket相关等)就会知道,底层已经不是走的Java代码了,而是通过Native 方法调用操作系统的一些方法。调用这些方法也需要一个方法栈,这就是本地方法栈。它也是线程私有的。
本地方法栈和Java虚拟机栈基本类似,只不过Java虚拟机栈是为Java代码(字节码)使用的,而本地方法栈则是虚拟机使用到的 Native 方法时使用的。
-
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
-
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
4.5 堆内存
通过上述的描述,我们知道每个线程都会有一个自己的【程序计数器】【Java虚拟机栈】【本地方法栈】,它们的生命周期与当前线程一样。
前面我们说了,线程在执行一个方法的时候,会将这个方法的栈帧压入自己的虚拟机栈中。而在Java中,调用方法是通过对象去调的。比如
以上代码是一个很简答的示例,funA中有一个User对象,通过User对象调用了它的queryUserById方法。此时当前线程的虚拟机栈中就会有funA与queryUserById两个方法的栈帧。但是有个问题,new出来的User对象在哪里呢?
此时就需要使用到JVM中另外一块内存区域了【Java堆内存】,堆内存是JVM中所管理的最大的一块内存区域,并且是所有线程共享的一块内存区域,堆内存区域的目的就是存放对象实例,在1.7的时候将【字符串常量池】【静态变量】都放进了堆中,这在上面那种图中也有体现。
看上述案例,在funA方法中创建了User对象,它们的引用关系大致可以用下图来体现
通过这张图我们可以看到,当创建一个User对象的时候,User对象实际上会被放到堆内存中,而方法funA中有一个局部变量user指向了这个对象在堆内存中的地址。
因为堆内存中存放的是对象实例,因此一般我们会把堆内存设置的比较大,主要就是设置以下两个参数
- -Xmx:表示堆区的起始内存
- -Xms:表示堆区的最大内存
对象虽然放在堆内存中,但是它也不是随意乱放,我们都知道JVM是有GC的,为了高效的GC,堆内存是做了更细致的划分的,也就是常说的新生代,老年代,永久代,他们分别对应不同的GC,这部分我会在后续的文章中单独描述,这里就不展开了,反正首先知道对象实例是存放在堆中就可以了。
本地内存有时候又会被称之为直接内存,还是来看看这张图
从图中可以看到本地内存并不是JVM运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分的内存也会被频繁地使用。而且也可能导致OutOfMemoryError异常出现。
本地内存是所有线程都共享的一部分内存,一般用于直接使用Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。
这样做可以避免了在 Java 堆和 Native 堆之间来回复制数据,从而提高性能。
本地内存的分配不会收到 Java 堆的限制,但是会受到本机物理内存大小以及处理器寻址空间的限制。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ri-ji/36896.html