一、JAVA内存结构和JAVA内存模型JMM的区别
面试过很多人,这两个概念都分不清楚。JAVA内存结构一般是指JVM运行代码时会将自己管理的内存分成几个运行时数据区,这些运行时数据区包括方法区、虚拟机栈、本地方法栈、堆和程序计数器。而Java内存模型只是一种规范,抽象的概念,不是具体存在的。java内存模型规定以下几点:
1、所有的变量存储在主内存中。
2、每一个线程都有自己的工作内存,且对变量的操作都是在自己的工作内存中进行的。
3、不同的线程之间无法直接访问彼此工作内存中的变量,要想访问只能通过主存来传递(线程通信)。
JAVA内存模型JMM规定将所有的变量(不包括局部变量)都存放在公共内存中,当线程使用变量时会把主存的变量复制到自己的工作空间(私有内存),线程对变量的读写操作,都是在自己的工作空间内完成的。操作完成之后,再把自己私有内存的变量刷新到主内存中。
二、JAVA运行时数据区
方法区
方法区是一块各个线程共享的区域,主要存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。根据《Java虚拟机规范》,方法区逻辑上属于堆的一部分,为了和堆区分又叫做非堆(Non-Heap)。
方法区只是JVM的一个规范,所有虚拟机必须要遵守。但是并没有严格要求如何实现。而在JDK7之前,Hotspot实现的方式是永久代,而在jdk7的时候,已经把原本放在永久代的字符串常量池、静态变量等移到堆中,而到了JDK8完全废弃了永久代的概念,而改为使用与JRockit、J9一样使用元空间来实现方法区。而元空间使用的是本地内存实现的,也就是说它不再受限于Jvm分配内存(jdk7之前使用-XX:MaxPermSize=16m 设置永久代的大小)大小的限制,理论上物理机还有内存就可以分配,这样从一定程度上避免了OutOfMemoryError(OMM)异常。
Java虚拟机栈
Java虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈可以理解成我们平时的一个Java方法,每个方法被执行的时候,JAVA虚拟机都会创建一个栈帧用来存放局部变量表、操作数栈、动态链接、方法出口(方法返回)等信息。
1、局部变量表
局部变量表存放了编译期可知的Java虚拟机的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference)和returnAddress类型。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量标的大小(这里的大小是指变量槽的数量,而一个变量槽的大小是由虚拟机自行决定的,比如一个槽占用32bit或者64bit或者更多)。
2、操作数栈
后进先出LIFO,最大深度由编译期确定。栈帧刚建立使,操作数栈为空,执行方法操作时,操作数栈用于存放JVM从局部变量表复制的常量或者变量,提供提取,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。操作数栈可以存放一个jvm中定义的任意数据类型的值。在任意时刻,操作数栈都一个固定的栈深度,基本类型除了64位长度的long、double占用两个深度,其它占用一个深度。
3、动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
4、方法返回地址
当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令(lreturn、freturn、dreturn以及areturn)或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
本地方法栈
本地方法栈与虚拟机栈所发挥的作用是基本一致的,区别在于虚拟机栈执行的是虚拟机中的JAVA方法,而本地方法栈执行的是Native方法。
JAVA堆
JAVA堆是虚拟机所管理内存的最大一块区域,是线程共享的一块区域。而几乎所有的对象实例都是在堆中分配内存。而且堆也是垃圾回收主要回收的区域,堆又被分成“新生代”、“老年代”、“永久代”、“Eden区”、“From Survivor”和“To survivor”。
默认情况下JVM默认的最大内存-Xmx为物理机内存的1/4,比如我的计算机是24G的那么最大内存大概就是6G,而初始化内存是物理机内存的1/64,也就是384M。你可能说你不信,那么看代码。
public class Test {
public static void main(String[] args){
long maxMemory = Runtime.getRuntime().maxMemory() ;//Java 虚拟机试图使用的最大内存量。
long totalMemory = Runtime.getRuntime().totalMemory() ;//Java 虚拟机中的内存总量,初始化内存。
System.out.println("最大内存Xmx为:" + maxMemory + "(字节)、" + (maxMemory / (double)1024 / 1024) + "MB"); //6120MB
System.out.println("初始化内存Xms为: "+ totalMemory + "(字节)、" + (totalMemory / (double)1024 / 1024) + "MB"); //384MB
while (true){
}
}
}
输出的最大内存为6120MB大概就是6G,而刚运行的内存为384MB,也可以理解为初始化内存Xms。
还可以通过JAVA VisulVM可以看到 402,653,216B大概也是384MB。
JVM中堆的空间是可以配置的,使用-Xmx和-Xms可以配置最大内存和初始化内存,但是一般我们会考虑将初始化内存与最大内存设置成一样,避免在初始化内存扩容时发生对象的移动。当然当Java堆中没有足够的空间来存放对象时就会发生OOM异常。
程序计数器
程序计数器是一块较小的内存空间,几乎可以忽略不计,是运行速度最快的存储区域。它可以理解为当前线程执行的字节码行号。在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址。如果是在执行native方法,则是未指定值(undefined)。它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下–条需要执行的字节码指令。它是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域。