JVM内存
JVM内存分为两个区域,一个是以线程为单位,每个线程都有各自的(包括:虚拟机栈、本地方法栈、程序计数器),另一个是以jvm为单位,它是唯一的,相当于全局的(包括:方法区、堆)。
程序计数器
它记录JVM中执行的字节码指令的行号(即指令所在的地址),也就是JVM在解释执行我们编译后的字节码文件时边执行边做的标记。JVM的多线程是通过CPU时间片轮转算法来实现的,所以当切换执行到某个线程时,需要知道之前这个线程执行到哪里了,此时程序计数器就可以为其提供上次执行的地址。
1.每个线程有各自的程序计数器
2.执行java方法时,记录当前执行的字节码指令在JVM中的地址。
3.执行本地方法时,程序计数器的值为空(Undefined)
本地方法指的是,java通过JNI直接调用本地C/C++库中的方法函数,这些方法不是由java实现的(并不在jvm中),所以无法为其做标记
4.程序计数器占用内存极小,可忽略不计,这里不存在内存溢出。
虚拟机栈
java虚拟机栈是方法调用和执行的空间,每个方法会封装成一个栈帧压入占中。
虚拟机栈是后进先出的,栈顶为当前栈,也就是当前正在执行的方法。
当执行方法A时,将其封装成栈帧入栈,当执行到A中的另一个方法B时,将B封装成栈帧入栈,栈顶也就变成了方法B的栈帧。
每一个方法从调用到执行完毕的过程,对应着 “该方法封装成的栈帧” 的入栈和出栈。
虚拟机栈存在一个最大深度(也就是栈的容量),超过了会造成StackOverflowError。
虚拟机栈可以被设置为固定大小(-Xss),也可以动态扩展或收缩,如果虚拟机栈动态扩展时无法申请到足够的内存,会造成OutOfMemaryError
Java 虚拟机栈(Java Virtual Machine Stacks)是线程私有的,它的生命周期与线程相同
一个不包含其他方法的方法内,还能剩下什么呢?
只有最单纯的 变量赋值、数据计算、返回值
虚拟机栈的栈结构,其实就是为了让方法变成最单纯的方法,入栈出栈,执行的就是 ”变量赋值、数据计算、返回值“ 操作。
栈帧内包含:局部变量区,操作数栈,动态链接,方法的放回地址
局部变量区:用来存放方法参数和方法内部变量的,它的单位为“变量槽”,在使用时是按索引(0~局部变量表最大容量)来查找的,其中第0位是当前方法所在实例的引用,也就是我们常用的 this 关键字。变量槽存放32位以内的数据,如果>32,则用两个变量槽来存储。变量槽是可重用的,如果方法内超出了变量作用域,那么这个变量槽就可以交给其他变量使用。
class ...{
public get(){
//代码块
{
String s = "222";
}
String a = "4444"; // s的变量超出作用域,变量槽不在为其保留,可交给其他变量使用该变量槽
}
}
操作数栈
为了更好的说明它,先了解两点
1.Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
2.执行引擎运行的所有字节码指令只会针对当前栈帧进行操作
也就是说在JVM对字节码文件进行解释执行过程中,当一个方法(这里假设它是最单纯的方法)开始被执行时,会创建一个新的栈帧,这个栈帧一开始是空的,随着JVM字节码指令的解释执行,会向操作数栈中存放临时变量和结果,它只是一个存储区域。即,操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时存储空间
假设以下是一个最单纯的方法
public int calc(){
int a=100;
int b=200;
int c=300;
return (a+b)*c;
}
在方法执行过程中,根据字节码指令,往操作数栈中写入数据或提取数据,即入栈/出栈。
比如有返回值的话,会将该值压入操作数栈中,然后等待其他方法获取。
操作数栈也是后进先出的。
i++ 和 ++i 的区别
1.i++:从局部变量表中取出,压入操作数栈,在局部变量表中对i+1,从操作数栈顶取出数据,所以取出的是+1之前的。
2.++i:在局部变量表中+1,然后取出,压入操作数栈,从操作数栈顶取出数据,所以取出的是+1之后的。
动态链接
每一个栈帧都包含着一个指向运行时常量池中该栈帧所属方法的引用,这个引用指向存放方法代码的地址。在java源文件编译为字节码文件时,所有变量和方法引用都作为符号引用保存在运行时常量池中,也就是说这个符号引用指向的内容是相应的变量或方法引用,方法引用指向的才是具体的代码(指令)存放地址。动态链接的作用就是将这些符号引用转换为调用方法的直接引用,也就是为了找方法代码的存放地址。
GC垃圾回收是不会涉及到虚拟机栈的。
方法出口:
1.即将栈帧出栈,如果有返回值,会将返回值压入调用者的操作数栈。
2.返回异常,异常信息抛给能够处理的栈帧。
3.void返回,PC计数器指向方法调用后的下一条指令。
本地方法栈
指Native方法服务(本地C/C++库),相当于操作系统的api。
执行本地方法时,因其使用的是Native方法,不再受JVM约束。
本地方法可以通过 JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和 JVM 相同的能力和权限。
如果大量使用本地方法,会使程序丧失跨平台性。
JAVA堆
JAVA堆是所有线程共享的内存区域,该区域作用就是存放对象实例
堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”
GC垃圾收集器采用分代收集算法,所以JAVA堆中也分为 新生代、老年代、永久代。
新生代中有划分为 Eden 空间、From Survivor 空间、To Survivor 空间等。
JAVA堆可以选择固定大小或者扩展。
OutOfMemoryError 异常时刻:如果在堆中没有用来完成实例分配的足够内存,并且堆也无法再扩展时,将会抛出该异常。
方法区
它是所有线程共享的一块内存区域,存储包括:类信息(元数据)、常量、静态变量、JIT即时编译的代码、运行时常量池。
方法区独立于堆,其大小可设置固定或伸缩。
方法区只是个逻辑概念,永久带和元数据区是具体设计与实现。
方法区的大小决定了JVM可保存多少个类,如果超了会导致OOM(内存溢出)
JDK8中,以元数据区代替了方法区,且元空间不再虚拟机设置的内存中,而是使用本地内存
元数据区可用 -XX:MetaspaceSize、-XX:MaxMetaspaceSize 来设置大小和上限。默认大小为21MB
当加载的类大于限制时,执行full gc卸载掉没用的类,如果释放后的空间还是超过限制,视超出的大小来重置MetaspaceSize(不超过MaxMetaspaceSize)。所以MetaspaceSize要适当的设置大一些,不然会频繁的执行gc操作。
如何解决OOM(内存溢出)
先判断是内存泄漏还是普通的内存溢出
内存泄漏是指一个不再被程序使用的对象或变量还在内存中占有存储空间。
如果是内存泄漏,我们要通过工具查看泄漏对象与GC Root对象的链关系进行解决。
如果是普通的内存溢出,我们要检查堆大小的参数配置及方法区(元数据区)大小的参数配置。