一.初识运行时数据区
运行时数据区就是Java程序运行时管理内存的区域。

运行时数据区可以分为以下五个模块
线程不可共享:程序计数器,Java虚拟机栈,本地方法栈
线程共享:方法区 堆区
二.程序计数器
程序计数器也叫PC寄存器,每个线程会通过程序计数器确定要执行的字节码指令的地址。
当加载器加载字节码文件到内存中之后,字节码文件的偏移量会被该写完为内存地址,这样,每一个字节码指令都有了一给内存地址。
当执行完字节码指令后,就会去程序计数器中找到下一个要执行的字节码指令然后执行。
在cpu中,不同线程是来回切换执行的。在线程的来回切换中,JVM虚拟机就是通过程序技术器来找到下一个需要执行的字节码指令的。
那么程序计数器存在内存溢出问题吗?
内存溢出指的是JVM存储数据时,存储数据需要的内存高于该区域虚拟机所能提供的内存上线。
不存在,因为其只存一个字节码指令。
三.Java虚拟机栈
Java虚拟机栈采用栈的数据结构来管理方法调用时的基本信息。
当一个方法开始执行时,将一个栈帧压入栈内存中,当方法结束时,将栈帧从栈内存中取出。
Java虚拟机栈在线程开始时创建,在线程结束时回收,每个线程都有一个独立的Java虚拟栈。
当抛出异常时,Java虚拟站会将所有的栈帧弹出,此时程序停止,并打印方法的全限定名和位置。
Java虚拟机栈包括 局部变量表,操作数栈,帧数据。
1.局部变量表
字节码文件中的局部变量表存放方法中的方法参数,方法中的局部变量,实例方法会存储this关键字(类对象的引用).
虚拟机栈中的局部变量表以数组的方式存储变量,long类型占两个槽位,int类型占一个槽位。、
并且,局部变量表中的槽位时可以复用的
2.操作数栈
操作数栈是虚拟机在执行字节码指令时开辟出来的临时存放数据的一块栈内存。
操作数栈有两个属性:操作数栈的最大深度,操作数栈的最大槽数
3.帧数据
<1>动态链接
动态连接保存了编号到运行时常量池的内存地址的映射关系。
当一个类引用其他类的的属性或者方法时,就需要动态连接完成这种映射。因为在连接的过程中,完成了符号引用到直接引用的解析。
<2>方法出口
当栈帧被弹出时,就应该指向上一个栈帧的下一个字节码指令。但是如何获取字节码指令呢?
我们就通过栈帧存储方法出口的地址,即上一个栈帧的下一个字节码指令,被弹出时,存入程序计数器中。
<3>异常表
包含异常捕获的生效范围和异常发生后跳转到的字节码指令的位置。

4.栈内存溢出
当栈帧的数量过多,占用的内存超过虚拟机所能提供的栈内存的最大值时就会出现占内存溢出。
(StackOverflowError)
如果我们不指定栈内存的大小,JVM虚拟机会设置一个默认大小的栈,默认值栈取决于操作系统和计算机的体系结构。
要修改Java虚拟机栈的大小,可以使用虚拟机参数 -Xss 。
-
语法:-Xss栈大小
-
单位:字节(默认,必须是 1024 的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
例如:
-Xss1048576
-Xss1024K
-Xss1m
-Xss1g
注意,hotspot等虚拟机对栈内存的最大值和最小值有要求,不在要求范围内就会报错。
局部变量过多,操作数栈深度过大会影响栈帧的内存,进而影响虚拟栈所能容纳的栈帧的多少。
四.本地方法栈
本地方法栈存的时native修饰的方法的栈帧。在hotspot中,本地方法栈与Java虚拟机栈公用一个栈空间。
五.堆内存
1.used/total/max
userd表示虚拟机已经使用的堆内存
total表示虚拟机已经为提供的堆内存
max表示虚拟机能提供的最大堆内存
当虚拟机已经提供的堆内存不足时,total就会申请扩张。当然,total的最大值不能超过max
2.什么时候会出现堆内存溢出
并不是used=total=max的时候就会出现堆内存溢出。当total<max的时候,堆内存溢出就会出现
3.如何设置堆内存
堆内存的默认值,max默认为系统的1/4,totoal默认为1/61,一般我们自己设置。
要修改堆的大小,可以使用虚拟机参数 --Xmx(max最大值)和-Xms (初始的total)。
语法:-Xmx值 -Xms值
单位:字节(默认,必须是 1024 的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
限制:Xmx必须大于 2 MB,Xms必须大于1MB
-Xms6291456
-Xms6144k
-Xms6m
-Xmx83886080
-Xmx81920k
-Xmx80m
一般我们让max和total相等,就可以避免total申请扩张和total收缩的时间,提高效率。
4.堆内存与栈内存
堆内存中储存着对象。栈内存里拥有局部变量表。当创建对象的时候,我们将对象的地址赋值给局部变量并储存到局部变量表中,这样方法就可以通过对象的地址找到我们所需要用的对象。
我们知道,栈内存是线程独立的,那么我们如何实现多线程之间的对象共享呢?
我们不难想到堆内存中储存着静态变量,我们只要将对象的地址赋值给静态变量,就可以实现多线程之间的对象共享。
五.方法区
1.初识方法区
方法区主要包含三部分内容:
类的元信息,运行时常量池,字符串常量池。
方法区通过instanceklass对象储存类的基本信息,方法,字段,常量池,还有其他信息,比如实现多态的虚方法表。
在源码被编译成字节码文件时,字节码中的静态常量池用来储存字节码指令编号,在字节码加载到内存中,连接阶段将编号解析为内存地址,用运行时常量池来存储这些内存地址。
JDK7及之前,方法区存在于堆区的永久代空间中,堆的大小由虚拟机参数-XX:MaxPermSize=值来控制。
JDK八开始,方法区存在于操作系统维护的直接内存中,这样方法区的可用内存大大提升,只要不达到操作系统的上限,就可以一直分配,可以使用-XX:MaxMetaspaceSize=值将元空间最大大小进行限制。
当类的字节码文件占用内存超过所设置的方法区的内存上限,就会出现方法区的内存溢出。
2.字符串常量池
JDK6及之前,字符串常量池位于方法区中。
JDK7及之后,字符串常量池位于堆区中。
JDK8之后,方法区从永久代空间转移到元空间中。
字符串常量池有两种加载方法:
一是编译阶段直接加载字符串常量到常量池中,二是通过intern()方法。
JDK6及之前,静态变量存在方法区中。
JDK7及之后吗,静态变量存在堆区中。
介绍以下intern方法:
JDK6及之前,intern方法将第一次见到的字符串存到堆区中,并将引用存到变量中。
JDK7及之后,intern方法将第一次见到的字符串引用存到堆区中。