首页 > 编程知识 正文

java虚拟机内存模型,jvm结构模型

时间:2023-05-04 20:54:00 阅读:23701 作者:3153

从今天开始,我将和你一起讨论Java虚拟机(JVM )的性能调节。 JVM是面试中的高频问题,通常请说明JVM的内存模型。 你做过JVM的性能调整吗?

为什么JVM在Java中这么重要? 首先,必须指导您运行Java APP应用程序。 必须首先安装JDK或JRE软件包。 这是因为Java APP应用程序在编译后成为字节码,并在字节码中JRE的核心组件JVM上运行。

JVM不仅负责Java字节码的分析(JIT compiler )和执行(Runtime ),还内置了自动内存分配管理机构。 该机制的金额大大降低了手动分配回收机制可能带来的内存泄漏和内存溢出风险,使Java开发人员不必再关注每个对象的内存分配和回收,而是可以集中精力处理业务本身。

从了解内存模型入手启动JVM自动内存分配管理机制的好处很多,但实际上是一把双刃剑。 该机制在提高Java开发效率的同时,容易使Java开发人员过于依赖自动化而削弱内存管理能力,容易出现JVM堆内存异常、垃圾回收(GC )方式不完善、GC次数过多等问题

因此,要进行JVM级别的优化,必须深入了解JVM的内存分配和回收原理,以便在出现问题时通过日志分析快速发现问题。 即使系统遇到性能瓶颈,也可以通过分析JVM优化来优化系统的性能。 这也是整个模块4的重点内容,今天我们从JVM内存模型中学习,为后续学习奠定了坚实的基础。

JVM内存模型的具体设计首先通过JVM内存模型图熟悉其具体设计。 在Java中,JVM内存模型主要分为堆、程序计数器、方法区域、虚拟机堆栈和本地方法堆栈

JVM的五个分区具体是怎么实现的呢? 我们逐一分析。

1 .堆(Heap )堆时JVM内存中的最大内存空间。 此内存由所有线程共享,大多数对象和数组都分配给堆内存。 堆分为新生代和老年代,新生代更为小心翼翼的香菇(伊甸园)和Survivor幸存者)区,最后Survivor由From Survivor和To Survivor组成。

在Java6中,它永久位于非堆内存区域中; 到Java7发行版时,永久贵带的静态变量和运行时常量池已合并到堆中; 到了Java8,永久世代被元空间取代了。 结构如下图所示。

2 .程序计数器程序计数器是一个小的内存空间,主要用于存储每个线程执行的字节码的地址,如分支、循环、跳转、异常、线程恢复等

由于Java是多线程语言,因此如果执行的线程数超过CPU数,则CPU资源会基于轮询时间片而在线程之间发生争用。 如果线程的时间片耗尽,或者线程的CPU资源由于其他原因被提前占用,则已终止的线程需要另一个程序计数器来记录下一个要执行的指令。

3 .方法区域(Method Area )很多开发人员都习惯将方法区域称为“永久世代”,但其实两者并不等效。

HotSpot虚拟机使用永久层代实现方法空间,但其他虚拟机例如在Oracle jrockit和IBM j9中不存在永久层代。 因此,方法区域是JVM规范的一部分,在HotSpot虚拟机中,设计者可以说是使用永久层代来实现JVM规范的方法区域。

方法主要用于存储有关虚拟机加载的类的信息,如类信息、运行时常量池和字符串常量池。 类信息还包括类的版本、字段、方法、接口和父类等信息。

JVM在运行某个类时,必须经过加载、连接和初始化,连接包括验证、准备和分析三个阶段。 加载类时,JVM首先加载class文件。 class文件类型除了描述信息(如类的版本、字段、方法和接口)之外,还包括常量池(Constant Pool Table ),用于存储编译时生成的各种文字和符号引用。

文字为字符串(String a=“b”)、基本类型常量(final限定变量)、符号引用为类和方法的完全限定名(例如类String,其完全限定名为Java/lang/String )、字段引用

当类加载到内存种子中时,JVM会将class文件常量池种子的内容存储在运行时常量池中。 在分析阶段,JVM用直接引用(对象的索引值)替换符号引用。

例如,如果类中的字符串常量位于class文件中,则将其存储在class文件常量池中; JVM加载类后,JVM将此字符串常量放在运行时常量池中,并在分析阶段指定字段字符串的索引值。 运行时常量池是全局共享的,多个类共享一个运行时常量池。 class文件中的常量池在多个运行时常量池中只有一个相同的字符串。

方法区域与堆区域类似,也是共享内存区域,因此方法区域由线程共享。 如果两个线程都尝试访问方法区域中的同一类信息,但该类尚未加载到JVM中,则此时只有一个线程被允许加载,另一个线程必须等待。

HotSpot虚拟机、Java

7 版本中已经将永久代的静态变量和运行时常量池转移了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地内存。之前永久代的类的元数据存储在了元空间,永久代的静态变量(class static variables)以及运行时常量池(runtime constant pool)则跟 Java7 一样,转移到了堆中。

那你可能又有疑问了,Java8为什么使用元空间替代永久代,这样做有什么好处呢?

官方给出的解释是:

移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。
永久代内存经常不够用或发生内存移除,爆出异常 java.lang.OutOfMemoryError:PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。

4. 虚拟机栈(VM stack)

Java 奴你寄栈式线程私有的内存空间,它和 Java 线程一起创建。当创建一个线程时,会在虚拟机栈中申请一个线程栈,用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息,并参与方法的调用和返回。每一个方法的调用都伴随着栈帧的入栈操作,方法的返回则是栈帧的出战操作。

5. 本地方法栈(Native Method Stack)

本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实现的。

JVM 的运行原理

看到这里,相信你对 JVM 内存模型已经有了重复呢的了解了。接下来,我们通过观察一个案例来了解下代码和对象时如何分配存储的,Java 代码又是如何在 JVM 中运行的。

在这里插入代码片

当我们通过 Java 运行以上代码时,JVM 的整个处理过程如下:

JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给 JVM,接下来 JVM 就进行内部分配。JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。class 文件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值(这部分我在第 21 讲还会详细介绍)。
完成上一个步骤侯,将会进行最后一个初始化阶段。在这个阶段中,JVM 首先会执行构造器 <clinit> 方法,编译器会在 .java 文件被编译成 .class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 <clinit>() 方法。
执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 student 对象,对象引用 student 就存放在栈中。
此时再次创建一个 JVMCase 对象,调用 sayHello() 非静态方法,sayHello() 方法属于对象 JVMCase,此时 sayHello() 方法入栈,并通过栈中的 student 引用调用堆中的 Student 对象;之后,调用静态方法 print(),print() 静态方法属于 JVMCase 类,是从静态方法中获取,之后放入到栈中,也是通过 student 应用调用堆中的 student 对象。

了解完实际代码在 JVM 中分配的内存空间以及运行原理,相信你会更清楚内存模型中各个区域的职责分工。 总结

这讲我们主要深入学习了最基础的内存模型设计,了解其各个分区的作用及实现原理。

如今,JVM 在很大程度减轻了 Java 开发人员投入到对象生命周期的管理精力。在使用对象的时候,JVM 会自动分配内存给对象,在不使用的时候,垃圾回收期会自动回收对象,释放占用的内存。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。