【JVM】JVM 内存模型


引言

本篇文章主要讲述在程序运行时,JVM 虚拟机会分配出的各个数据区域,并文章中会讲述一些关于 JVM 调优的知识,做到理论与实践相结合。
另外还需要补充的是,如果本篇文章中有讲的不对的地方,可以直接私我或在下方评论,感谢!


JVM 运行时数据区域

首先我要先放一张程序运行时 JVM 虚拟机数据区域所分布的图片:
在这里插入图片描述
由图所示,程序在运行时 JVM 虚拟机数据区会分为两个大类,分别是:所有线程所共享的数据区域 与 各个线程私有的独立区域。
所有线程所共享的数据区域又分为:堆 与 方法区;
各个线程私有的独立区域又分为:虚拟机栈、本地方法栈 与 程序计数器;

下面会对各个数据区域进行比较详细的讲解。


线程共享区域


一、堆(Heap)

堆是虚拟机所管理中最大的数据区域,它随着虚拟机启动时所创建,也是 GC(垃圾回收器)所管理的区域,JAVA 中基本上所有的实例都会在这里分配内存的(关于分配内存的处理可以去看这一篇文章 从JVM虚拟机角度去看一个对象的创建过程),工作或学习中我们会常说 new 一个对象,这个 new 的对象的实例其实就存在堆中。
从 GC 回收内存的角度看,现在的垃圾回收都是基于分代收集设计的,我们常说的年轻代、老年代、Eden空间、Survior空间其实都是在形容 GC 垃圾堆回收时所划分的空间区域,至于为什么会划分出这样的区域,划分出这样的区域有什么好处,暂不在本篇文章中讲述,我会在后续的 GC 算法中详细的讲述。
在这里插入图片描述
堆的内存大小既可以被实现成固定大小的,也可以是扩展的,项目正式打包启动运行的时候我们一般都是去配置一下堆的最大内存(-Max)与最小内存(-Mas)这两个参数,来控制堆内存的大小。设置堆的最大与最小值也是我们常用且常见的 JVM 调优手段。


二、方法区(Method Area)

方法区它是用于存储已经被虚拟机加载后的类信息、常量、静态变量、即时编译器编译后的代码缓存、运行时常量池等数据。在 JDK1.8 之前(不包括 1.8),我们更习惯称方法区为 永久代非堆,大家不要把方法区与堆中的永久代混肴了,他们两者并不是等价的,HotSpot 虚拟机设计团队使用永久代来实现方法区罢了,这样使得垃圾收集器能够像管理堆一样管理方法区这部分内存。在 JDK1.7 的时候,HotSpot 团队就将 方法区 中的常量和静态变量移到了堆中,而在 JDK1.8 之后,HotSpot 开发团队废除了永久代这个概念,改用了在本地内存中实现的 元空间,并将永久代中剩余的内容(其实也就剩下类信息)全部移到了 元空间 中。
讲到这里大家应该就明白了,在 JDK1.8 以后 方法区 就被放到了本地的内存中,脱离了堆,那我们就不能使用 -Xmx 和 -Xms 来设置与约束 元空间 了,而使用 -XX:MetaspaceSize(初始值)和 -XX:MaxMetaspaceSize(最大值) 来进行设置。
下面看一下元空间的图:
在这里插入图片描述
运行时常量池它是当 Class 文件被加载到内存后, Java 虚拟机会 将Class文件常量池里的内容转移到运行时常量池里(运行时常量池也是每个类都有一个),而与它相互搭配的是堆中的字符串常量池

我们知道,在Java中有两种创建字符串对象的方式:
第一种是采用字面值的方式赋值;
第二种是采用 new 关键字新建一个字符串对象;
这两种方式在性能和内存占用方面存在着差别。

String str1 = "123";
String str2 = "123";
System.out.println(str1 == str2);// true

采用字面值的这种方式创建一个字符串时,JVM 会首先去字符串常量池中查找是否存在 “123” 这个对象,如果存在,则不在字符串常量池中创建任何对象,直接将池中 “123” 这个对象的地址返回,赋给字符串常量;
如果不存在,则在字符串常量池中创建 “123” 这个对象,然后将字符串常量池中新创建的 “123” 这个对象的引用地址返回给字符串常量,这样字符串常量会指向池中 “123” 这个字符串对象;

String str1 = new String("123");
String str2 = new String("123");
System.out.println(str1 == str2);// false

采用 new 关键字新建一个字符串对象时,JVM 首先在字符串常量池中查找有没有 “123” 这个字符串对象,如果有,则不在字符串常量池中再去创建 “123” 这个对象,直接在堆中创建一个 “123” 字符串对象,然后将堆中的这个 “123” 对象的地址返回赋给引用 str1,这样,str1 就指向了堆中新创建的这个字符串对象;如果没有,则先在字符串常量池中创建一个 “123” 字符串对象,然后再在堆中创建一个 “123” 字符串对象,然后将堆中新创建的这个 “123” 字符串对象的地址返回赋给 str1 引用,这样,str1 指向了堆中创建的这个 “123” 字符串对象。

从这里就能看出,第一种是直接使用字符串常量池(字符串常量池虽然也是堆中,但是它是一个独立的区域)中的字面量,有的话直接返回引用,没有则创建并返回,而第二种则是以创建对象(这里的创建对象是放在堆中的)的方式,并将对象的引用返回。

现在用比较切合实际工作的理解来讲,元空间中到底放置了什么,其实就放了类的一些信息,如类中的方法、字段、类、包的描述信息和运行时常量池。



线程私有区域


一、程序计数器(Program Counter Register)

程序计数器是一块较小的空间,它是当前线程所执行的字节码的行号指示器。
在这里要先讲一下字节码解释器,字节码解释器它的工作是通过改变计数器的值来选取下一条要执行的字节码指令,就相当于你程序的下一步要走哪里。而程序计数器的工作是,当前线程所执行到哪一行字节码了,两者相辅相成。


二、JAVA 虚拟机栈(VM Stack)

虚拟机栈描述的是 JAVA 中每个方法被执行的时候,虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从被调用到执行完成的过程,就对应着一个战阵从虚拟机栈中从入栈再到出栈的过程。
局部变量表中存放了方法中的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象的引用(reference 类型,它并不是对象数据的本身)和 returnAddress 类型(方法返回地址)。
下面我用实际代码的方式来说明一下虚拟机栈存储的信息:

private int size = 0;
private void createStudent(int number) {
	int nowNumber = ++number;
	Student stu = new Student();
}

在这里我想让各位同学能直观的感受到堆与虚拟机栈所管辖的区域,所以创建了一个 size 成员变量和一个方法。
成员变量 size 是存放在堆中,而方法中的 number、nowNumber 和 stu 是存放在虚拟机栈中的,但是 stu 并不是存放的数据本身,而是存放了一个地址,虚拟机通过这个地址去堆中找到这个对象的实例。


三、本地方法栈(Native Method Stack)

本地方法栈其实和虚拟机栈发挥的作用是非常相似的,两者的区别在于,虚拟机栈是为虚拟机执行 JAVA 方法服务,而本地方法栈则是为虚拟机栈使用到的本地方法服务。
这里我用图和文字解释一下:
在这里插入图片描述
在这里呢,JAVA 的 A 栈帧(方法)调用了 B 本地方法,而 B 本地方法中又调用了 C 本地方法,C 本地方法调用了 JAVA 的 D 栈帧(方法)。
本地方法其实是调用的 native 方法,native 对应的是本地的 C 或 C++ 方法,常见的 hashCode 方法就是一个 native 方法。
java.lang.Object.hashCode 源码:

public native int hashCode();


本篇文章主要来源于《深入理解JAVA虚拟机》


End


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值