jvm内存分区模型(一)

本文详细介绍了JVM内存分区,包括元空间的引入及其原因,以及JVM运行时数据区的各个部分如方法区、堆、栈、程序计数器和本地方法栈。分析了线程角度下的JVM分区,并讨论了栈溢出的原因和并发安全问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


最近一直有在学习java的jvm模型,由繁入简,网上的一些博客写的点都比较片面,今天简单系统的总结一下!

jvm分区

在这里插入图片描述
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。

Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。

Execution engine(执行引擎):执行classes中的指令。

Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。

Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

作用首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

元空间

jdk1.8后移除了永久代,并引进了元空间的概念。元空间独立于jvm,由直接内存进行管理

为什么jdk1.8要把方法区从JVM里移到直接内存?

原因一:因为直接内存,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先复制到直接内存,再利用本地IO处理。

从数据流的角度,非直接内存是下面这样的作用链:本地IO --> 直接内存 --> 非直接内存 --> 直接内存 --> 本地IO
而直接内存是:本地IO --> 直接内存 --> 本地IO

原因二:整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError。

可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。
-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

jvm运行时数据区(jvm内存)Alt

如上图,jvm在运行时,会把其所管理的内存分为以上5个模块的,其中永久区已经在jdk 1.8中被元空间所替代。下面咱们分点描述。
1.方法区(永久区) java7实现:
方法区和堆类似,是各个线程共享的内存区域,它用于存储类信息,常量,静态变量,即时编译器编译后的代码(项目发布jsp等会被解析成java代码,大量内容会进进出出,就有可能出现方法区溢出的异常)等数据。分析下Java虚拟机规范,一个存储对象数据(堆),一个存储静态信息(方法区)。

-XX:PermSize;

  • XX:MaxPermSize;
  • -XX:MetaspaceSize;
  • XX:MaxMetaspaceSize
    -

永久代和堆是相互隔离的,但它们使用的物理内存是连续的
永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。

2.堆 (线程共享)
对于大多数应用来说,堆是Java虚拟机管理的内存的最大一块,这块区域随着虚拟机的启动而创建。我们创建的对象和数组就是存放在堆里面。它是一块共享的区域,操作共享区域的成员就有了锁和同步(线程的安全问题)。堆是垃圾回收器管理(GC)的主要区域。新生代、老生代、永久代的概念就是在堆里堆内存可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
3.栈(线程私有)栈是线程私有的,它的生命周期与线程相同。栈描述的是Java方法执行时的内存模型,每个方法执行时都会创建一个栈帧(Stack Frame)用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每个线程在执行一个方法时,都意味着有一个栈帧在当前线程对应的栈帧中入栈和出栈。
4.程序计数器
程序当前执行的指令的地址线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。  由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。不会发生内存溢出
5.本地方法栈
jvm是Java执行的平台,但是程序也会调用一些本地操作系统的方法(在windows就是windows中的本地方法,linux则是linux的本地方法),例如一些IO操作等,这些方法就在本地方法栈中。

从线程角度分析jvm分区

在这里插入图片描述
如上图,线程私有部分是 程序计数器,本地方法栈,以及虚拟机栈,而线程共享部分则是堆和方法区(或者元空间)

程序计数器:记录当前指令的字节码地址,为了之后线程切换时可找到执行的位置

一种先入后出的数据结构,可以理解为水杯喝水,先倒进去的水后倒出。当调用一个多重内嵌方法时,如A内调B方法,B内调C方法,则A、B、C依次入栈,C必定最先执行完最先出栈,之后依次B、A出栈。

每当启动一个新线程的时候,java虚拟机都会为它分配一个java栈。java以栈帧为单位保存线程的运行状态。虚拟机栈的大小影响可创建线程的多少。
虚拟机只会对java栈执行两种操作:以栈帧为单位的压栈或者出栈。类中每一个方法代表一个栈帧

当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部
在这里插入图片描述

一个栈帧中包含以下四个部分(在此简单概述,在之后的文章中再做探讨):
1、局部变量表
局部变量表(Local Variable Table) 是一组变量值存储空间,用于存放方法参数、函数和方法内部定义的局部变量。(打开clasess文件,查找Local Variable Table局部变量表,我们会发现0的位置就是this对象)
局部变量表的容量以变量槽(Variable Slot)为最小单位,虚拟机中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性的说到每个Slot都应该能存放一个8种基本数据类型的其中一个。
且变量槽是可以复用的。即定义一个局部变量A,A用到230行时没再使用了。此时在230行之后又定义了一个局部变量B,那么B是可以复用变量A的槽位的。
2、操作数栈
与局部变量表一样,均以字长为单位的数组。不过局部变量表用的是索引,操作数栈是弹栈/压栈来访问。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中
3、动态链接
符号引用和直接引用在运行时进行解析和链接的过程(文章末尾介绍一下符号引用)。即Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接
4、方法返回地址

除此之外,咱们来简单讨论下经常容易发生的栈溢出
一)、是否有递归调用
二)、是否有大量循环或死循环
三)、全局变量是否过多
四)、 数组、List、map数据是否过大
五)使用DDMS工具进行查找大概出现栈溢出的位置

栈溢出的根本原因还是执行的虚拟机栈深度大于虚拟机栈允许的最大深度。

关于符号引用和 直接引用:
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可,在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。而在解析时就是把这个符号引用转化成为真正的地址的阶段。
直接引用 :直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。如果有了直接引用,那么直接引用的目标一定被加载到了内存中。

直接引用可以是:

1:直接指向目标的指针。(个人理解为:指向对象,类变量和类方法的指针)

2:相对偏移量。 (指向实例的变量,方法的指针)

3:一个间接定位到对象的句柄。

处理并发安全问题

对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
在这里插入图片描述
博客新人,有问题还望大家指出,下篇文章再来将堆。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值