文章目录
- 内存模型的基本概念
- 案例
- 程序计数器
- 栈
- `Java`虚拟机栈
- 局部变量表
- 栈帧中局部变量表的实际状态
- 栈帧中存放的数据有哪些
- 操作数栈
- 帧数据
- 本地方法栈
- 堆
- 堆空间是如何进行管理的?
- 方法区
- 静态变量存储
- 直接内存
- 直接内存的作用
内存模型的基本概念
在前面的学习中,我们知道了字节码文件(.class
)会通过类加载器加载到JVM
虚拟机中,接下来JVM
虚拟机就会执行其中的字节码指令.我们把JVM
虚拟机被分配的内存叫做运行时数据区域
而内存模型就是指运行时数据区域中被划分的不同区域.
JDK6
版本:
字符串常量池存放在方法区中,方法区存放在堆中;
JDK1.7
版本:
-
方法区脱离堆,单独占用一部分内存
-
字符串常量池依旧存储在堆中
JDK1.8
版本:
-
方法区发生移动,从
JVM
虚拟机内存中,移动到本地内存中
Java
虚拟机(JVM
)的内存模型是Java
程序运行时内存管理的基础。它定义了Java
程序如何在内存中分配、使用和回收资源。
案例
class Person{
int id;
String name;
public Person(int id,String name){
this.id=id;
this.name=name;
}
}
public class JvmTest {
public void func1(int a){
int b=10;
Person p=new Person(1,"张三");
a=11;
}
public static void main(String[] args) {
int a=10;
new JvmTest().func1(a);
System.out.println(a);
}
}
程序计数器
- 控制程序解释执行指令的顺序.在代码执行过程中,程序计数器会记录下一行字节码指令的地址.执行完当前指令之后,虚拟机的执行引擎根据程序计数器执行下一行指令.程序计数器可以控制程序指令的进行,实现分治,跳转或者异常等等逻辑
- 保证在多线程的情况下线程之间的切换.
JVM
虚拟机需要通过程序计数器记录CPU
切换前解释执行到哪一行指令并继续解释运行
程序计数器会不会发生内存溢出?
内存溢出:程序在使用某一块内存区域时,存放的数据需要占用的内存大小超过了虚拟机能够提供的内存上限
程序计数器不会发生内存溢出的情况.原因:每个程序计数器只会存储一个固定长度的内存地址,也就是字节码指令的内存地址.
栈
Java
虚拟机栈
保存在java
中实现的方法
采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧来保存.
Java
虚拟机栈随着线程的创建而创建,随着线程的结束而销毁
在每一个栈帧中,存放的内容有:局部变量表,操作数栈,帧数据
局部变量表
在方法执行过程中存放所有的局部变量,编译成字节码文件时,就可以确定局部变量表的内容
栈帧中局部变量表的实际状态
栈帧中的局部变量表是一个数组,数组中的每一个位置称之为槽,long
和double
类型占用两个槽,其他类型占用一个槽.
栈帧中存放的数据有哪些
- 实例方法中的序号为
0
的位置存放的是this
,指的是当前调用方法的实例对象,运行时会在内存中存放实例对象的地址 - 方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致
局部变量表保存的内容有:
- 实例方法的
this
对象- 方法的参数
- 方法体声明的局部变量
为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用
操作数栈
操作数栈是栈帧中虚拟机执行指令过程中用来存放临时数据的一块区域
- 操作数栈是栈帧中虚拟机在执行指令的过程中用来存放中间数据的一块区域.他是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值.
比如:在字节码指令执行的过程中,会产生一些临时数据,这些临时数据会先存放到操作数栈中,然后再通过下一条指令放入到局部变量表中
- 在编译期就可以确定操作数栈的最大深度,从而执行时正确的分配内存大小
帧数据
(这个并不是由虚拟机设定标准)
帧数据主要包含动态链接,方法出口,异常表引用
-
动态链接:当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址.动态链接就保存了编号到运行时常量池的内存地址的映射关系.
-
方法出口:方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址.所以在当前栈帧中,需要存储此方法出口的地址.
-
异常表:存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置.
Java
虚拟机栈是否会出现栈内存溢出?
Java
虚拟机栈如果栈帧过多,占用内存超过栈内存可以分配的最大容量,就会出现栈溢出(虚拟机为每一个线程分配的栈的大小是有限的)Java
虚拟机栈内存溢出时会出现StackOverflowError
的错误
本地方法栈
保存的是在java
中实现的使用native
修饰的,实际是由C++
编写的本地方法
Java
虚拟机栈存储了Java
方法调用时的栈帧,而本地方法栈存储的是native
本地方法的栈帧.- 在
HotSpot
虚拟机中,Java
虚拟机栈和本地方法栈实现上使用了同一个栈空间.本地方法栈在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来.
堆
一般Java
程序中堆内存是空间最大的一块内存区域.创建出来的对象都存在于堆上
Java
堆是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都存放在这里,是垃圾收集器管理的主要区域。
栈上的局部变量表中,可以存放堆上对象的引用.静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间的共享.
堆内存是否会出现溢出?
堆内存大小是有上限的,当对象一直向堆中放入对象达到上限之后,就会抛出OutOfMemory
错误
堆空间是如何进行管理的?
堆空间有三个需要关注的值:uesd
,total
,max
used
指的是当前已经使用的堆内存;total
是java
虚拟机已经分配可用堆内存;max
是java
虚拟机可以分配的最大堆的内存
随着堆中对象增多,当total
可以使用的内存即将不足的时候,虚拟机会继续分配内存给堆,扩展total
的大小
- 如果堆内存不足,
java
虚拟机就会不断地分配内存,total
值会变大.- 是不是当
used=total=max
,就会导致内存溢出?
答案:不一定
在实际应用中一般都需要设置total
和 max
的值
- 要修改堆的大小,可以使用虚拟机参数
-Xmx
(max
最大值)和-Xms
(初始的total
)- 语法:
-Xmx
值,-Xms
值 - 单位:字节(默认,必须是
1024
倍数),K
或者k
(KB
),m
或者M
(MB
),g
或者G
(GB
) - 限制:
Xmx
必须大于2MB
,Xms
必须大于1MB
- 语法:
Java
服务端程序开发的时候,建议将-Xmx
和-Xms
设置为相同的值(total
=max
),这样在程序启动之后可使用的总内存就是最大内存,而无需向Java
虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况
方法区
方法区是存放每个类基础信息的位置,线程共享,主要包含三部分内容:
- 类的元信息:每个类的基本信息
一般称之为InstanceKlass
对象.在类加载阶段完成
- 运行时常量池
- 常量池中存放的是字节码中的常量池内容
- 字节码文件中通过编号查表的方式找到常量,这种常量池称之为静态常量池
- 当常量池加载到内存中之后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称之为运行时常量池.
- 字符串常量池:保存了字符串常量
字符串常量池存储在代码中定义的常量字符串内容.比如:"123"
,这个123
就会被放入字符串常量池
这里我们再来举个例子加深印象:
举例1:
public class StringTest {
public static void main(String[] args) {
String a="1";
String b="2";
String c="12";
String d=a+b;
System.out.println(c==d);
}
}
这里的运行结果就代表了我们的d
是存放在字符创常量池中还是堆内存中.
运行结果false
,原因如下:
举例2:
public class StringTest {
public static void main(String[] args) {
String a="1";
String b="2";
String c="12";
String d="1"+"2";
System.out.println(c==d);
}
}
运行结果是:true
,为什么?
我们来观察一下此时的字节码指令状态:
所以,此处d
存放在常量池中的原因是:这里并没有使用StringBuilder
对象来进行字符串的相加,而是直接使用的ldc
字节码指令进行的,没有使用对象,所以就不需要存放在堆中
静态变量存储
- 在
JDK7
之前的版本中,静态变量是存放在方法区中的,也就是永久代 - 在
JDK7
及其之后的版本中,静态变量时存放在堆中的Class
对象中,脱离了永久代
直接内存
首先我们要确定的是,直接内存并不属于Java
运行时的内存区域.
在JDK1.4
中引入了NIO
机制,使用了直接内存,主要为了解决以下两个问题:
Java
堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用(可能会出现卡顿的现象)IO
操作比如读文件,需要先把文件读入直接内存中,再把数据复制到Java
堆中.
现在放入直接放入到直接内存中即可,同时在Java
堆上维护直接内存的引用,减少了数据复制的开销.写文件也是这种思路.
直接内存的作用
- 提高读写文件的性能
- 避免垃圾回收机制影响对象的创建和使用
- 在`JDK1.78即之后存放方法区