十二.吊打面试官系列-JVM优化-深入JVM内存模型

JVM内存模型

1.JVM的组成

整个JVM组成由 :运行时数据区 , 类加载子系统 , 执行引擎 , 本地方法库 几部分组成
在这里插入图片描述
上面是Java7的内存模型,Java8以后做了一些调整,把方法区变成了元空间,元空间不在JVM中,而使用直接内存(计算机内存)

  1. 运行时数据区

    见名知意,运行时数据区是Java虚拟机在执行Java程序时,用于存储和管理运行时数据的内存区域,运行时数据区由: 方法区,虚拟机栈,本地方法栈,程序计数器,堆 五部分组成,其中堆和方法区是线程共享的,其他区域是线程隔离的,也就是线程私有,比如:程序计数器在每个线程执行的时候都有一个私有的程序计数器

  2. 类加载子系统

    类加载器子系统的主要作用是负责从文件系统或网络中加载.class文件,并将其加载到JVM中。加载后的类信息存放在方法区(或JDK 8及以后版本的元空间)中,具体的执行则交给执行引擎去操作

  3. 执行引擎

    JVM(Java Virtual Machine)执行引擎是JVM的核心组件之一,它负责将编译后的字节码(Bytecode)解释成可执行的机器指令。而JIT(Just-In-Time)编译器则是执行引擎中的一个重要部分,用于提高Java程序的执行效率。

    JVM执行引擎的主要任务是将字节码转换为特定平台上的本地机器指令,以便在JVM上执行。它主要包括解释器(Interpreter)和JIT编译器两部分。

    解释器是JVM执行引擎的基本组件之一,它采用逐行解释的方式执行字节码。当JVM启动时,解释器会根据预定义的规范对字节码进行解释,将每条字节码指令“翻译”为对应平台的本地机器指令并执行。然而,由于解释执行的方式效率较低,因此JVM还引入了JIT编译器来优化性能。

  4. JIT编译器

    JIT编译器是JVM执行引擎中的另一个重要组件,它可以在程序运行时将频繁执行的字节码编译为本地机器码,从而提高执行效率。JIT编译器通过动态编译技术,将热点代码(即频繁执行的代码)优化为本地机器码,并存储在代码缓存区中。在后续的执行中,JVM可以直接从代码缓存区中加载机器码并执行,而无需再次解释字节码。这种方式可以显著提高Java程序的执行速度

  5. 本地方法库接口

    本地方法库接口(Native Method Interface,简称JNI),它允许Java代码与其他语言写的代码进行交互。JNI是Java虚拟机(JVM)提供的一组函数和协议,用于Java应用程序与本地应用程序(如C、C++编写的程序)进行交互。本地方法库就是C,C++编写的一些代码库,比如: xxx.dll文件,在Java中很多功能底层就是C++来实现的。

下面我们针对这些区域详细来说明

2.程序计数器

程序计数器是线程私有的,虽然名字叫计数器,但主要用途还是用来确定指令的执行顺序,比如循环,分支,跳转,异常捕获等。它记录了当前线程执行的字节码指令的地址。JVM对于多线程的实现是通过轮流切换线程实现的,所以为了保证每个线程都能按正确顺序执行,将程序计数器作为线程私有.程序计数器是唯一一个JVM没有规定任何OOM的区块.(out of memory)

程序计数器是一块非常小的内存空间,可以看做是当前线程执行字节码的行号指示器,每个线程都有一个独立的程序计数器,因此程序计数器是线程私有的一块空间,此外,程序计数器是Java虚拟机规定的唯一不会发生内存溢出的区域

3.方法区(元空间)

方法区主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据。在jdk1.7及其之前,方法区是堆的一个“逻辑部分”(一片连续的堆空间),但为了与堆做区分,方法区还有个名字叫“非堆”,也有人用“永久代”(HotSpot对方法区的实现方法)来表示方法区。

jdk1.7已经开始准备“去永久代”的规划,jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中,(常量池除字符串常量池还有class常量池等),这里只是把字符串常量池移到堆内存中;在jdk1.8中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。元空间的大小仅受本地内存限制
在这里插入图片描述
JVM在后续版本中去掉永久代(PermGen)的原因主要有以下几点:

  • 避免内存溢出(OOM)问题:永久代存储的是类信息、常量、静态常量等信息,因此存储空间不容易界定,容易发生内存溢出问题(如常见的java.lang.OutOfMemoryError: PermGen space异常)。在Java 8中,采用了元空间(Metaspace)代替永久代,元空间使用的是直接系统内存,可以动态地扩展和收缩,从而避免了永久代空间不足导致的内存溢出问题。
  • 提高垃圾回收效率:相对于新生代回收效率(通常为70~95%),永久代的垃圾收集效率较低。这主要是因为永久区的回收机制相对新生代来说更为复杂,要求条件更多。去掉永久代后,采用元空间可以更好地管理类的元数据,提高垃圾回收的效率。
  • 统一内存管理:去掉永久代也是为了更好地融合HotSpot JVM与其他JVM(如JRockit VM),因为JRockit VM并没有永久代的概念。这样做可以使得JVM的内存管理更加统一和标准化。
  • 避免内存泄漏:永久代中的某些数据(如类加载器)可能在不再需要时仍然被引用,导致内存泄漏。采用元空间后,可以更好地管理这些数据,避免内存泄漏问题。

需要注意的是,虽然永久代已经被去掉,但方法区(Method Area)的概念仍然存在。方法区主要用于存储类的信息、常量池、方法数据、方法代码等。在Java 8及以后的版本中,方法区的实现由元空间来完成。Java8内存模型如下
在这里插入图片描述

4.虚拟机栈

Java虚拟机栈也是线程私有的,每个方法执行都会创建一个栈帧,局部变量就存放在栈帧中,还有一些其他的动态链接之类的.

虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应Java代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个Java方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程
在这里插入图片描述

  • 栈帧(方法执行形成栈帧):栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,线程私有。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程,栈帧随着方法调用而创建,随着方法结束而销毁

  • 局部变量表(储存方法参数和局部变量):局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。

  • 操作数栈(用于计算的临时数据存储区):操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO),当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

  • 动态链接(用来转化方法的内存地址直接引用的):在一个class文件中,一个方法要调用其他方法, 需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。

  • 返回地址:方法的返回出口,一般就是调用该方法的上一个方法的地址。

下面是一个类中的方法执行的详细流程:以Hello.class 为例

  1. 首先当使用到Hello的时候比如:new Hello(),类加载子系统会把Hello.class 加载到JVM中,存储到元空间,并且会把 new Hello 对象实例存储到堆中
  2. 程序是通过执行引擎去执行的,它通过驱动线程来执行Hello,线程会分得一个私有的虚拟机栈,每个方法执行都会形成栈帧进行压栈。
  3. 随着方法的调用结束,后被调用的方法就先出栈,最先被调用的方法最后出栈,随着方法全部执行完成,栈帧释放,线程结束。
  4. 如果方法中有对象的引用,那么方法结束后局部变量也被释放,那堆中的对象实例就没有引用关系将被识别为垃圾,等待垃圾回收器回收
    在这里插入图片描述

5.本地方法栈

本地方法栈与虚拟机栈的区别是,虚拟机栈执行的是Java方法,本地方法栈执行的是本地方法(Native Method),其他基本上一致,在HotSpot中直接把本地方法栈和虚拟机栈合二为一,这里暂时不做过多叙述。

6.堆内存

堆和方法区一样(确切来说JVM规范中方法区就是堆的一个逻辑分区),就是一个所有线程共享的,存放对象的区域,也是GC的主要区域.其中的分区分为新生代(YoungGeneration),老年代(OldGeneration).新生代中又可以细分为一个Eden,两个Survivor区(From,To).Eden中存放的是通过new 或者newInstance方法创建出来的对象

绝大多数都是很短命的.正常情况下经历一次gc之后,存活的对象会转入到其中一个Survivor区,然后再经历默认15次的gc,就转入到老年代.这是常规状态下,在Survivor区已经满了的情况下,JVM会依据担保机制将一些对象直接放入老年代。`
在这里插入图片描述
文章到此结束,码字很辛苦,如果对你有所帮助请给个好评。下一章:深入分析class字节码文件结构

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨家巨子@俏如来

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值