概述
Java的虚拟机使Java的成为了一种跨平台的语言,Java的不直接与操作系统接触,而是通过虚拟机这个中间桥梁,通过JVM与底层接触。不同的系统有不同的JVM,但是所有的这些JVM都完美的支持Java语法,这就使得write once,run everywhere成为可能。
除此之外,JVM的内存管理机制使得不需要再为每一个新的操作去删除/免费代码,由机器代替程序员这样就不容易出现内存泄露和内存溢出的问题了,但是一旦出现了这种问题如果不了解JVM是怎样使用内存的,那么排查错误将会非常困难。
JVM运行时数据区域
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区
有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户进程的启动和结束而创建和销毁。
1. 程序计数器
解释:【指向当前线程所执行的字节码的行号】,其实就是一小块内存,记录着当前程序运行到哪了字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令(字节码指令是什么?)。分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完成。
由于Java的多线程是通过线程轮流切换完成的,一个线程没有执行完时就需要一个东西记录它执行到哪了,下次抢占到了CPU资源时再从这开始,这个东西就是程序计数器,正是因为这样,所以它也是“线程私有”的内存。
如果一个线程执行一个主要方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个本地方法,这个计数器的值则为空,此内存区域是唯一一个在Java的虚拟机规范中没有规定任何OutOfMemoryError异常情况的区域。
2. Java虚拟机栈
与程序计数器一样,Java的虚拟机栈也是线程私有的,虚拟机栈描述的是Java的方法执行的内存模型,方法每个执行在同时的创建³³都会一个栈桢用于存储局部变量表,操作数栈,动态链接,方法出口等信息,下图为栈桢结构图:
通常所说的JVM里的堆和栈里这个栈就是Java的虚拟机栈,或者说是Java的虚拟机栈中的局部变量表部分。
局部变量表存放了编译期可知的各种基本数据类型,对象引用(仅限局部变量的,不包含成员变量的)。其中每个局部变量空间(Slot)有32位,所以long和double类型的数据会占用两个局部变量空间,其他类型包括对象引用占用一个。对象引用调用的是存在堆中的对象,这个引用可以是对象的起始地址或者是指向对象的句柄。局部变量表所需的内存在编译期就已经确定了也就是进入这个方法时就已经确定了,运行期间不会更改。
操作数栈则存储方法内一些进行了运算操作后的结果。
动态链接,在方法内调用接口,通过字面量链接到具体的实现类,实现Java的动态特性。
方法出口(返回地址),return或者发生Exception等。
如果方法methodOne方法调用了methodTwo,那么methodOne就会先入栈创建一个栈桢,接着methodTwo再入栈成为栈顶(假设没有其他的方法执行),methodTwo执行完先出栈,接着methodOne执行完出栈。
在使用递归的情况下,如果线程请求的栈的深度超过虚拟机所允许栈的深度就会抛出StackOverflowError;但是大部分虚拟机栈的深度都可以动态扩展,HotSpot中使用XSS可以设置栈的深度,如果扩展时无法请求到足够的内存就会抛出OutOfMemoryError。
3. 本地方法栈
本地方法栈和虚拟机栈相似,区别就是虚拟机为虚拟机栈执行Java服务(字节码服务),而本地方法栈为虚拟机使用到的Native方法服务。本地方法栈中使用的语言,使用方式,数据结构没有强制要求。
4. Java堆
堆是JVM里最大的一块内存区域,被所有线程共享,在虚拟机启动时创建,此区域的目的就是存放对象实例和数组,几乎所有的对象实例都在这分配(随着JIT的发展已经不是那么绝对了)的.java堆是垃圾收集管理的主要区域,由于现在收集器基本都采用分代收集方法,所以Java的堆中还可以分为新生代,老年代,永久代.1.8之后取消了永久代;其中新生代又划分为Eden空间,From Survivor空间,To Survivor空间。无论怎么划分都是为了更好的回收,分配,利用内存。下图为1.8后的内存模型
根据的Java虚拟机规范,Java的堆可以处于物理不连续的空间中,只要逻辑连续即可。在实现时,既可以实现成固定大小的也可以是可扩展的(通过-Xmx和-Xms控制),如果堆中没有足够的内存完成实例分配,并且堆也无法得到扩展时,将会抛出的OutOfMemoryError异常。
5. 方法区
方法区也是一个线程共享的区域,存储已被虚拟机加载的类信息,常量(final),静态变量(static),JIT(即时编译器)编译后的代码等数据。
Java虚拟机规范把方法区描述为堆的一个逻辑部分,其实堆和方法区可以看成数据部分;虚拟机栈和程序计数器可以看成指令部分;方法区存储一些不会变更的数据,之前热点上使用GC分代收集管理方法区,所以方法区也被称为永久代(本质上两者不等价),但是现在已经使用Native Memory来代替永久代了。
虚拟机对方法区规范非常宽松,除了和Java的堆一样不需要连续的内存和可以选择固定大小意外,还可以选择不实现垃圾回收。垃圾回收行为在这个区域比较少见但还是有必要的,主要是针对常量池回收和类型的卸载。
5.1运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,运行时常量池相对于类常量池另外一个特性就是具备动态性,运行期间可能将新的常量放入池中。