JVM内存模型

1.JVM简介

JVM是Java语言实现跨平台技术的核心所在,我们通常称它为Java运行所需要的虚拟机。当我们通过javac命令将Java代码编译成.class文件之后就可以将他们放到JVM中进行运行。之所以能在不同的操作系统上跑相同的Java代码取决于jdk对不同操作系统的实现,当然这里最大的功臣就是JVM了。所以我们所熟悉的java代码实际上运行的流程是这样的。

2.JVM的内存模型以及主要区块介绍

根据上面的简单介绍我们大概明白了Java能运行于跨平台的核心基础在哪里,所以今天我们着重的来剖析一下JVM的工作原理,他是如何运行Java代码,并且在不同的操作系统上来实现相同的功能的。

首先我们来查看一下JVM的内存结构。

以jdk1.8版本之后的内存模型为例子,这里与早期版本的区别就是将【永久代】变成了【元数据】,由于JVM过去将常量池,static变量等信息放置在永久代中管理,导致永久代一旦内存达到上限就会出现OOM的问题,所以1.8之后将永久代舍弃改为元空间这也是为了促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。

接下来我们先宏观的分析上图的内存模型,我们需要再了解一下堆和栈的管理机制。JVM的内存分为线程共享内存和线程私有内存。

根据上图我们分析内存中的存储位置也是分公有和私有的,因为Java是一门多线程执行的语言,支持在同一时刻开启多个线程同时执行代码,这样再每个线程中就存在线程内部的参数和函数,由于多线程在同时执行的时候本质上并非两条链路一起工作,而是通过时间片的申请进行往复操作,这样我们就需要知道每一个线程运行到了什么位置,所以程序计数器主要的作用就是用来记录每个线程代码执行位置的地址方便线程切换的时候可以继续之前的进度进行代码执行。而线程共享的内存空间主要的作用是用来记录不同线程中共有的变量信息.

2.1 虚拟机栈(JVM Stack)

虚拟机栈是Java线程运行的容器,每一条Java线程都会对应一个虚拟机栈,每一条线程上的方法就相当于栈中的一个栈帧,栈帧中包括Java代码运行的详细信息。除了Native方法以外的所有方法都是放到JVM的栈中进行管理和执行的。

当开启一条线程的时候(包括主线程),线程执行其内部代码的时候JVM栈就会开始工作,程序会将当前运行时的函数逐层压入栈中,栈顶的栈帧就是当前程序执行到的函数位置,由于栈的容量是有限的所以当线程中的函数执行过多的时候会触发StackOverflow(栈溢出)异常,这个异常主要体现在不正确的递归操作中。执行完成的栈帧会从栈顶移除,直到将栈拿空,我们的程序就执行完毕进入线程销毁阶段。这里我们暂时只考虑非NativeMethod调用的场景。具体流程参考下图。

了解完毕JVM栈的执行流程之后我们对栈帧中的重要内容进行逐一的介绍。

2.1.1 局部变量表

局部变量表用来存储栈帧函数在运行时所需要的参数和函数内部的局部变量,这里涉及到上面介绍的线程私有和线程共享两种内存的应用。由于每一个函数在运行的时候都会有相应的这些参数,当函数内部的局部变量为基本类型的时候他所处的位置就是局部变量表,如果函数内部的变量存储的是一个对象的引用,那么局部变量表中只记录该对象的引用地址,对象会放在JVM的堆区中,通过引用地址进行访问,这里就涉及到了访问线程共享区域。

2.1.2 执行栈(操作数栈)

在JVM虚拟机栈的栈帧创建之后同时会为栈帧创建一个操作数栈,他的作用是用来将我们每个函数内部的代码执行的一个空间,因为每个函数内部会包含他本身的流程结构,这个流程可以很短也可以很长,这些代码就会按照入栈出栈的顺序进行函数的整体执行。这里最简单的方式我们可以通过javap命令来执行一个代码片段。

public class Test {
  public static void main(String[] args) {
    int a = 1;
    int b = 2;
    int c = 3;
    int sum = a+b+c;
    System.out.println(sum);
   }
}

然后我们进入文件的命令行部分使用javac命令将它编译成.class文件

然后运行javap -c命令对代码反汇编一下查看

zhangyunpeng@zhangyunpengdeMacBook-Pro test % javap -c Test
警告: 文件 ./Test.class 不包含类 Test
Compiled from "Test.java"
public class com.leozhang.test.Test {
  public com.leozhang.test.Test();
   Code:
    0: aload_0
    1: invokespecial #1          // Method java/lang/Object."<init>":()V
    4: return
​public static void main(java.lang.String[]);
   Code:
    0: iconst_1 #将常量1压入到操作数栈
    1: istore_1 #将常量1从操作数栈放到局部变量表的第1个位置
    2: iconst_2 #将常量2压入到操作数栈
    3: istore_2 #将常量从操作数栈2放到局部变量表的第2个位置
    4: iconst_3 #将常量3压入到操作数栈
    5: istore_3 #将常量从操作数栈3放到局部变量表的第3个位置
    6: iload_1 #加载局部变量第一个位置放到操作数栈顶
    7: iload_2 #加载局部变量第二个位置放到操作数栈顶
    8: iadd #将两个数据相加并放到操作数栈的顶端,即完成a+b=3
    9: iload_3 #将局部变量表第三个位置放到操作数栈顶
   10: iadd #将当前操作数栈的两个变量相加放到操作数栈顶,即 (a+b)的结果+c=6
   11: istore    4 #没有需要放到栈中操作的内容,将操作数栈顶的6放到局部变量表的第四个位置
   13: getstatic   #2  #获取System对象的静态变量out         // Field java/lang/System.out:Ljava/io/PrintStream;
   16: iload     4 #将局部变量表第四个位置放到操作数栈顶
   18: invokevirtual #3  #执行System.out的Print函数输出栈顶内容        // Method java/io/PrintStream.println:(I)V
   21: return #方法结束
}

根据上面的实际代码执行我们可以从汇编的角度来分析在执行简单的变量相加在内存中实际执行的过程,这些动作都是在同一个栈帧中的操作数栈中完成的。

2.1.3 动态链接

动态链接其实在上面我们已经使用过了,他的作用就是在执行栈内操作的时候如果遇到常量池中的变量,我们通过动态链接通过内存地址访问常量池中的变量,上面的代码中用到了System.out对象这个对象就是System对象中的out他是一个静态对象,所以这个对象并不会在程序运行的时候保存在栈中,而是在程序初始化的时候在元空间中保留,来保证接下来其他程序在使用的时候不需要重复的创建该对象,所以我们在上面介绍中看到的getstatic的作用就是将常量池中的数据拿到栈中运行。

2.1.4 方法返回地址

就是在方法执行结束之后,要返回下一条要执行代码位置的值,也就是程序计数器的值。程序分为正常返回和异常返回两个方式,当程序正常返回时返回的地址就可以作为程序计数器的值用来记录当前代码执行到的位置,当程序异常返回的时候栈中不会记录该方法返回的数据,但是会将异常信息记录。

2.2 本地方法栈

本地方法栈是用来执行Native方法的一个栈,根据名称可以得知JVM栈主要是用来执行Java中的函数和流程的,本地方法栈是用来执行Java本身不能执行的功能,这里就需要借助操作系统的api了。所以本地方法栈是在Java代码运行过程中如果需要调用C++等语言的能力来提高运行效率这样就从JVM栈跳出到本地方法栈。

2.3 内存划分

2.3.1 直接内存区

在jdk1.8之前JVM的内存模型中的常量池,static变量等信息是放置在“方法区”的,也叫做永久代。他归JVM的内存管理。

方法区:

  1. 在 《Java虚拟机规范》中只是规定了有方法区这么个概念跟它的作用。HotSpot在JDK8之前 搞了个永久代把这个概念实现了。用来主要存储类信息、常量池、静态变量、JIT编译后的代码等数据。
  2. PermGen(永久代)中类的元数据信息在每次FullGC的时候可能会被收集,但成绩很难令人满意。而且为PermGen分配多大的空间因为存储上述多种数据很难确定大小。因此官方在JDK8剔除移除永久代。

官方解释移除永久代:

  1. This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
  2. 即:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。

根据上面的信息我们得知现在的jdk中将这些过去放置在JVM永久代的数据转移到了一个新的区域,这个区域叫做元空间,他在内存管理中属于堆外内存,通过Java的NIO技术实现在JVM外直接通过操作系统开辟内存空间来保存数据,这个空间的数据容量没有限制,取决于操作系统的物理内存大小,但是仍然可以通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize来指定元空间的大小,直接内存区中除了保存元空间的数据以外还会保存JIT即时编译产生的code cache。由于直接内存区是通过Native方式来开辟的堆外内存,所以他的读写性能是没有JVM内存速度快的。他和元空间的关系并不是全等的,他们属于互相依存的关系。

2.3.2 线程共享

2.3.2.1 元空间

元空间的作用在上面已经做了一些介绍了,他主要存储的内容就是代码执行前的类加载信息,系统的常量池,系统的运行时常量池,系统的static变量等内容,这些内容都是在JVM运行时一旦创建就可能会一直使用的数据,所以他们会持续占用空间并且少量的进行释放,过去被放在方法区也就是永久代,但是这样会导致随着代码运行时间的推移,内存慢慢变大导致OOM。所以之后的新的内存模型中元空间的数据被安排在了堆外内存也就是上面说的直接内存区中,这样我们元空间的内容并不直接交个JVM进行管理他只收操作系统内存的限制,这样他可以使用更加大的内存空间来减少OOM的场景。但是如果元空间的数据存储超过了系统内存的剩余空间也会触发OOM的。

2.3.2.2 堆

堆是被线程共享的一块内存区,他主要用于存储对象内部的数据,由于对象内部的数据分散度高或连续性高并且占用内存普遍比较大,所以不是和在栈中频繁操作。这样的对象就会被放在堆区保存。堆区与元空间的区别是堆区存储的大多是活跃对象。这里的活跃对象指的是在程序运行中创建的对象,这些对象可能创建出来不久就不需要用了,还有一些可能随着程序运行创建完成之后会持续很久才失去作用,那么这些对象不光需要存储,还需要垃圾回收器GC进行清理,否则当频繁创建的对象增多时有限的堆空间没有足够的位置来存放又会出现OOM的错误。

针对堆的结构和功能特性我们将堆内存分为老生代和新生代两种

【老生代】:用来存储Java代码运行过程中的大对象,长时间使用的对象,以及通过担保机制创建到此的对象,他们的特点是使用时间长不需要频繁的进行垃圾回收操作。所以老生代也可以理解为持续使用的对象所存放的内存空间。当老生代的存储空间占用达到阈值的时候会触发一次MajorGC也叫做(FullGC)进行垃圾回收,但是老生代上的垃圾回收性能会很低并且有阻塞效果。所以老生代的数据垃圾回收的频率比较低。

【新生代】:用来存储Java代码运行过程中频繁创建的对象和新创建的对象,比如我们在代码中new一个对象的时候他会优先选择放在新生代中。除非新生代的内存空间不足或者对象占用的空间超过了阈值,会将对象直接放在老生代中。新生代中又分为Eden区,Survivor From区和Survivor To区。他们的关系是新生代内存空间 = Eden(80%)+Survivor From(10%)+Survivor To(10%)的关系,新生代的活动对象主要存储在Survivor From和Eden区,Survivor To区总是为空。他们的关系是8:1:1。新生代的特点是存在大量朝生夕死的对象,他们可能创建之后立即就不用了,所以新生代是垃圾回收最频繁的内存区域。具体的垃圾回收方式我们在后续的垃圾回收章节中进行介绍。他所使用的垃圾回收算法是MinorGC(也就是常说的标记复制法)

总结起来JVM的堆区内存总占用=新生代+老生代

通常新生代:老生代=1:2,但是这个属于默认状态,我们在开发过程中是可以使用JVM参数进行指定新生代和老生代所占用的内存空间的。根据不同的应用场景可能参数和配比完全不同。

2.3.3 线程私有

线程私有的内存空间主要指的是虚拟机栈在运行过程中使用的内存空间,他们主要应用栈存储,在运行时直接加载到局部变量表中,并且随着栈帧出栈就会被一起销毁掉。

3.其他内存部分前瞻

3.1 JIT介绍

当JIT编译启用时(默认是启用的),JVM读入.class文件解释后,将其发给JIT编译器。JIT编译器将字节码编译成本机机器代码。

通常javac将程序源代码编译,转换成java字节码,JVM通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。很显然,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢。为了提高执行速度,引入了JIT技术。

在运行时JIT会把翻译过的机器码保存起来,已备下次使用,因此从理论上来说,采用该JIT技术可以,可以接近以前纯编译技术。

3.2 类加载器介绍

Java的类加载器的作用就是在运行时加载类到虚拟机中,首先不管对于什么样的java应用肯定是由很多class组成实现的,不同的功能所在的class是不一样的(你说我把所有的功能放在一个class里面,那你很棒棒哦),比如当我们的入口函数被调用的时候,入口函数使用了其他class(静态代码块、实例)的功能,这时类加载器就会按需加载将需要的类加载进内存中,类加载器不是一次性全部class加载到内存中,而是按需加载。类加载器加载class的原则是:委托、可见性和单一性。

转载某大佬文章,感觉写的非常清楚!!

原文链接JVM 从入门到放弃1:内存模型

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值