JVM内存模型基础

news/2024/10/31 1:29:36/

大家好,我是易安!

我们知道运行一个Java应用程序,我们必须要先安装JDK或者JRE包。这是因为Java应用在编译后会变成字节码,然后通过字节码运行在JVM中,而JVM是JRE的核心组成部分。

JVM不仅承担了Java字节码的分析(JIT compiler)和执行(Runtime),同时也内置了自动内存分配管理机制。这个机制可以大大降低手动分配回收机制可能带来的内存泄露和内存溢出风险,使Java开发人员不需要关注每个对象的内存分配以及回收,从而更专注于业务本身。但是懂得JVM内存分配以及调优才算是真正入门了Java这门语言,今天我们先看下JVM的内存模型。

JVM内存模型设计

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

alt

JVM的5个分区具体是怎么实现的呢?

1. 堆(Heap)

堆是JVM内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由From Survivor和To Survivor组成。

在Java6版本中,永久代在非堆内存区;到了Java7版本,永久代的静态变量和运行时常量池被合并到了堆中;而到了Java8,永久代被元空间取代了。 结构如下图所示:

alt

2. 程序计数器(Program Counter Register)

程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。

由于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虚拟机、Java7版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在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中运行的。

public class JVMCase {

 // 常量
 public final static String MAN_SEX_TYPE = "man";

 // 静态变量
 public static String WOMAN_SEX_TYPE = "woman";

 public static void main(String[] args) {

  Student stu = new Student();
  stu.setName("nick");
  stu.setSexType(MAN_SEX_TYPE);
  stu.setAge(20);

  JVMCase jvmcase = new JVMCase();

  // 调用静态方法
  print(stu);
  // 调用非静态方法
  jvmcase.sayHello(stu);
 }

 // 常规静态方法
 public static void print(Student stu) {
  System.out.println("name: " + stu.getName() + "; sex:" + stu.getSexType() + "; age:" + stu.getAge());
 }

 // 非静态方法
 public void sayHello(Student stu) {
  System.out.println(stu.getName() + "say: hello");
 }
}

class Student{
 String name;
 String sexType;
 int age;

 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }

 public String getSexType() {
  return sexType;
 }
 public void setSexType(String sexType) {
  this.sexType = sexType;
 }
 public int getAge() {
  return age;
 }
 public void setAge(int age) {
  this.age = age;
 }
}

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

1.JVM向操作系统申请内存,JVM第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给JVM,接下来JVM就进行内部分配。

2.JVM获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。

3.class文件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值

alt

4.完成上一个步骤后,将会进行最后一个初始化阶段。在这个阶段中,JVM首先会执行构造器 方法,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 () 方法。

alt

5.执行方法。启动main线程,执行main方法,开始执行第一行代码。此时堆内存中会创建一个student对象,对象引用student就存放在栈中。

alt

6.此时再次创建一个JVMCase对象,调用sayHello非静态方法,sayHello方法属于对象JVMCase,此时sayHello方法入栈,并通过栈中的student引用调用堆中的Student对象;之后,调用静态方法print,print静态方法属于JVMCase类,是从静态方法中获取,之后放入到栈中,也是通过student引用调用堆中的student对象。

alt

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

总结

今天我们主要深入学习了最基础的内存模型设计,了解其各个分区的作用及实现原理,内容篇基础,相信你很快就能掌握。

- END -

本文由 mdnice 多平台发布


http://www.ppmy.cn/news/62973.html

相关文章

什么是机房UPS?一文带您了解!

什么是机房UPS? 机房UPS是指专门为机房等大型计算机设施设计的不间断电源。它通常由高性能的电池组、充电器、逆变器、静态开关、监控系统和管理软件等组成,能够为计算机设备提供稳定、可靠的电源保障。 机房UPS的作用 机房UPS的作用主要是两方面: 1…

Unity 向量

向量的基本概念 向量:既有大小又有方向的量叫做向量。 零向量:各分量都是0的向量。 归一化向量:方向不变,将向量的长度变为1。 单位向量:长度为1的向量(归一化后的向量)。 向量的模:只有长度没有方向。…

Amper Music:AI创意音乐工具

【产品介绍】 Amper Music 是一家位于美国纽约的人工智能音乐技术公司,成立于2014年。 Amper Music是一个AI创意音乐工具,能让任何人为自己的内容制作原创音乐。无论你需要为视频、播客或互动内容配乐,Amper Music都能提供一个简单而强大的解…

阿里云Alibaba Cloud Linux镜像操作系统性能兼容如何?

阿里云服务器操作系统Alibaba Cloud Linux镜像怎么样?可以代替CentOS吗?Alibaba Cloud Linux兼容性如何?有人维护吗?漏洞可以修复吗?Alibaba Cloud Linux完全兼容CentOS,并由阿里云官方免费提供长期维护。 …

IP报文结构

文章目录 IP报文结构分片 IP报文结构 4位版本号(version): 指定IP协议的版本, 对于IPv4来说, 就是4. 4位头部长度(header length): 类似于TCP4位首部长度,通常填的是0101(十进制5) 16位总长度(total length): IP数据报整体占多少个字节.这用…

2分钟搞懂人工智能、机器学习和深度学习

不少高校的小伙伴找我聊入门人工智能该怎么起步,如何快速入门,多长时间能成长为中高级工程师(聊下来感觉大多数学生党就是焦虑,毕业即失业,尤其现在就业环境这么差),但聊到最后,很多…

NtfsStreamsEditor下载使用

NtfsStreamsEditor 是一款免费的 NTFS 流编辑器,它可以帮助用户查看和编辑 NTFS 文件系统中的备用数据流。备用数据流是一种特殊的 NTFS 文件数据结构,允许用户在单个文件中存储多个数据流。 使用 NtfsStreamsEditor,用户可以浏览、创建、编…

每天一道算法练习题--Day21 第一章 --算法专题 --- ----------位运算

我这里总结了几道位运算的题目分享给大家,分别是 136 和 137, 260 和 645, 总共加起来四道题。 四道题全部都是位运算的套路,如果你想练习位运算的话,不要错过哦~~ 前菜 开始之前我们先了解下…