内存的结构与划分
我们上一章已经讲了JVM的概念引入,那么这一章我想从其内存结构和划分上进行拓展。
什么是JVM内存结构呢?
JVM 内存结构:Java 虚拟机定义了若干种程序运行期间会使用的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁,另一些则与线程一一对应,随着线程的开始而创建,随着线程的结束而销毁
内存划分
-
首先,JVM内存区域大致划分为以下几个部分:
1.Class文件
2.类装载子系统
3.运行时数据区
4.执行引擎 -
在本章只讲解运行时数据区,下面是运行时数据区结构图:
-
线程共享(线程公有):
在运行时数据区中,方法区和堆都是属于线程私有的,
也就是说这块区域可以进行“循环利用”,并且要为其进行垃圾回收,在虚拟机启动时创建。 -
线程私有:
指线程本身独自具备的,虚拟机栈,本地方法栈和程序计数器都属于线程私有,
其伴随着线程产生和消亡,也就是说“一次性”,所以说,不用对其进行垃圾回收。
1.方法区
- 方法区是系统分配的一块逻辑区域,是线程共享的,用来存储类的描述信息,如:已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。方法区的大小决定着方法区能包含着多少个类,如果说系统类太多而导致方法区中因内存不够而导致溢出,虚拟机会抛出内存溢出等信息。
- 方法区在实际内存空间中可以不是连续的,对于方法区的容量,你可以是固定的,也可以随着程序的执行动态扩展,并且在不需要过多空间时自动收缩(可以选择在这个区域中是否实现垃圾收集与压缩)。
- 方法区中还包含着一个运行时常量池。
1.1存放内容
- 类的全限定名(类的完整路径名)
- 类的直接超类全限定名(若为Object,则其没有超类)
- 类的类型(类或接口)
- 类的访问修饰符(public,abstract,final等)
- 类的直接接口全限定名的有序列表
- 常量池(字段,方法信息,静态变量,常量,类型引用(class)等)
1.2特点
-
是线程安全的,由于所有线程共享方法区,所以在方法区里的线程必须被设计成线程安全的。
例如:如果有两个线程同时去访问方法区中的同一个类,但是此时类还没有被装载进JVM中,那么只允许一个线程装载,而另外一个线程需要等待。 -
方法区的大小可以不是固定的,可以动态调整;内存空间不一定连续;可以在堆中自由分配。
-
可以被垃圾回收,如:某个类不在使用时,将这个类卸载并且回收
1.3JDK8与之前版本区别
在jdk1.8 中方法区被称为元数据区 (元空间)
-
-XX:MetaspaceSize(
-XX:PermSize)初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那在不超过MaxMetaspaceSize时,适当提高该值。 -
-XX:MaxMetaspaceSize(
-XX:MaxPermSize)最大空间,默认是没有限制的
2.堆
- 堆,是用来解决数据存储问题的,它是被所有线程共享的一块内存区域,在随着虚拟机启动的时候创建。这个区域是用来存放对象实例的并且几乎所有对象实例都在堆中分配内存。
- 堆,是Java垃圾收集器管理的主要区域,通过GC实现自动内存管理,从而实现垃圾自动销毁。
2.1堆内存结构
- 如图所示:
1.GC回收角度:堆内存中分别有新生代(伊甸区,新生代0,新生代1),老年代。
2.内存分配角度:可以划分出多个线程私有的多个缓冲区。
2.2特点
- 堆是虚拟机中内存最大的一个区域,大概占了四分之三
- 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。
2.3GC垃圾回收器
- 在之前逛csdn看到篇讲的很精细的博客,链接转载如下:
https://blog.csdn.net/weixin_44588495/article/details/104097669
3.虚拟机栈
- 虚拟机栈是每个线程自己都具备的,属于线程私有。当线程执行方法的时候,虚拟机栈都会创建一个栈帧,从执行到完成对应着入栈到出栈的过程。
- 每个栈帧中都存储着:
1.局部变量表
2.操作数栈
3.动态链接
4.返回地址
5.附加信息
1.1局部变量表
-
概念:局部变量表用来存放方法参数和局部变量,是一组变量值的储存空间。
-
容量大小:在编译期间就可以完成分配,而且在运行期间不会改变其大小,在Class文件中的方法表中Code属性中max_locals就指明了所需局部变量表的最大容量。
-
变量槽:是局部变量表中的最小单位,在JVM中没有指明一个Slot所需占用的内存空间。
-
存放数据类型:
1.八种基本数据类型2.reference-对象引用
(对象在Java 堆中存放的起始地址的索引和该数据所属数 据类型在方法区的类型信息)3.returnAddress-字节码指令地址
-
空间分配:除了64位的long,double占两个局部变量空间,其余只占一个;为了节省栈空间,Slot可以重用。
1.2操作数栈
- 概念:操作数栈也称“操作栈”,是JVM中用于计算的临时数据存储区。
- 区别:与局部变量表一样是一个以字长为单位的数组;与局部变量表不同的是它不是通过索引来访问的,而是通过压栈和出栈来访问。
- 例子:通过代码,然后javap -c反编译出字节码
public static void main(String [] args){
int a=3;
int b=4;
int c=a+b;
}
public static void main(java.lang.String[]){
Code:
0: iconst_2 //将int型(2)推送至栈顶
1: istore_1 //将栈顶int型数值存入第二个本地变量
2: iconst_4
3: istore_2
4: iload_1 //将第二个int型本地变量推送至栈顶
5: iload_2
6: iadd //将栈顶两个int型数值相加的结果压入栈顶
7: istore_3 //将栈顶int类型存入第四个本地变量
8: return //void
}
1.3动态链接
-
概念:指Class文件中常量持有大量的符号引用。
字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。 -
区别:
1.静态链接:符号的引用在类加载期间或者第一次使用时就转化为直接引用(指向数据地址)。2.动态链接:在运行期间转化为直接引用。
-
常量池:每个Class都有属于自己的常量池,用来存放接口符号等,每个被JVM加载的Class都有内部常量池(运行时常量)。
1.4返回地址
-
概念:紧邻函数调用语句的下一条语句的地址
(函数调用结束后程序要继续执行, 所以先把这个地址压入堆栈, 等函数调用结束以后, 把这个地址从堆栈里面弹出来, 接着执行) -
方法执行后退出情况:
1.正常完成出口:当执行遇到返回指令,会将返回值传递到上层调用者。2.异常完成出口:当执行遇到异常,会导致方法体没有得到处理而退出,没有返回值。
-
正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。
-
异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。
-
方法返回可能的操作:
1.恢复上层方法的局部变量表和操作数栈
2.把返回值压入调用者调用栈帧的操作栈
3.调整PC程序计数器的值以指向方法调用后面的一条指令
4.本地方法栈
-
该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。
-
本地方法是非java语言实现,也就是说Native方法就是一个java调用非java代码的接口
5.程序计数器
- 若线程执行的是java方法,那么这个计数器记录的就是虚拟机字节码指令的地址
- 若线程执行的是Native方法,那么计数值为空
- jVM报错分为ERROR和EXCEPTION 两大类,而程序计数器这一内存区域是JVM中唯一一个在规范中没有任何规定OutOfMemoryException情况的区域。例如操作数栈讲到的Code中程序计数器可以记录当前程序执行到哪一行。
6.总结
在本次内存划分的章节中,有许多知识点需要扩展,总结的只是其中表面层次,想要有更扎实的计算机知识功底,应该多阅览大神们的学习资料,不断总结归纳,才会引起质变!
本章思维导图: