基础知识
JVM是什么
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行 ,是Java跨平台的原因。
JVM内存结构
JVM的内存结构分为五大区域,分别是堆、方法区、虚拟机栈、本地方法栈、程序计数器。
属于线程共有的是堆和方法区(还有直接内存),其中JDK1.8以后移除了方法区,移动到了本地内存里,成为元空间(MetaSpace)。
关系如下图:
程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,用于记录当前虚拟机正在执行的线程指示地址。
程序计数器的作用有两个:
(1)字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理等。(字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。)
(2)在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来时能够知道该线程上次运行到哪了。(为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。)
简单来说就是读取指令和记录位置。
(程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡)
虚拟机栈
虚拟机栈由一个个栈帧组成,每个栈帧又包括:局部变量表、操作数栈、动态链接、方法出口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈;每一个函数调用结束后,都会有一个栈帧被弹出虚拟机栈。
局部变量表:用来存放方法参数和方法内的局部变量。这些数据类型包括各种基本数据类型、对象参考和returnAddress类型。
操作数栈:主要用于存储计算过程的中间结果,同时作为计算过程中变量的临时储存空间。
动态链接:每个栈帧都包含一个指向运行时常量池中一个该栈所属方法的符号引用,在方法调用过程中,会进行动态链接,将这个符号引用转化为直接引用。
本地方法栈
本地方法栈与虚拟机栈是类似的,只是虚拟机栈执行的java方法,本地方法栈执行的是native本地方法,也使用栈帧的入栈弹栈来操作方法。
堆
虚拟机中线程共享的内存区域,在虚拟机启动时分配内存创建。此区域的唯一目的是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。同时,堆也是垃圾收集器管理的主要区域,因此也称为GC堆。堆可以继续细分为新生代(Young Generation),老年代(Old Generation),永久代(Permanent Generation);其中永久代是JDK7及JDK7以前版本的内容,到了JDK8以后PermGen 已被 Metaspace(元空间) 取代,元空间使用的是直接内存。
方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
方法区也被称为永久代。
方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
元空间
元空间的本质与永久代类似,都是对JVM规范中的方法区的实现,但是元空间与永久代的最大区别在与元空间并不在虚拟机中,而是使用直接内存。更换为元空间的原因是:永久代内存受限于JVM可用内存,而元空间使用的是直接内存,收本机可用内存影响,虽然元空间仍然有可能溢出,但是比永久代溢出的概率小很多。
整体关系梳理
上面这样形容这些区域太过于抽象和虚无,所以使用一些图片和实例来描述他们之间的关系更为直观易懂。
堆:类的所有对象都放在堆中,也就是我们通过new方法所创建的对象都存放在堆中,同时栈会创建堆中类对象的(内存地址),而栈可以通过这个内存地址来调用该类对象。
栈:存放快速执行的任务,基本数据类型的数据,上述的对对象地址的引用。每一个线程都有一个栈区,栈区非常的小,大小只有1M,但是栈区的存取速度快,所以一般用来执行任务。
方法区:用来存放所有的不变的元素,比如Class类、static变量(静态变量)、静态方法、常量、成员方法。也因为它存放的都是永远唯一的元素,所以方法区被所有线程共享,其实本质上方法区也是堆。
下面我们举一个例子:
代码如下:
public class Student{
public String name;
public Student(String name){
this.name = name;
}
public void study(){
System.out.println("studying!");
}
public int getId(int x){
x = 10; //修改值
return x; // 返回值 把x的临时值 返回出来
}
public static void main(String[] args){
Student std = new Student("johnn"); //实例化对象 需要有一个class模版 这里的模版为Student类 在堆里开辟空间
int a =1 ;
a = std.getId(a); //调用方法 方法储存在堆里的对象空间里 同时在栈中执行(开辟临时方法执行空间)
std.study();
}
}