本人推荐,如果要看虚拟机的相关内容,并且英语基础不错,可以直接看https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.6.4
1.运行时内存数据区域
根据Java虚拟机规范的规定,Java虚拟机所管理的内存包括以下几个运行时数据区域:
程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区.
2.虚拟机管理的内存区域介绍
2.1 程序计数器(Program Counter Register,PC寄存器):程序计数器是线程私有的,也就是说每个线程中都有一个程序计数器,在虚拟机的概念模型中,字节码解析器工作时就是通过改变程序计数器的值去获取下一条要执行的字节码指令.
如果执行的是Java方法,这个计数器记录的是当前线程正在执行的字节码指令的地址;如果是Native方法(本地方法),这个计数器的值是Undefined.此内存区域是唯一一个在Java虚拟机中规范中没有规定任何OutOfMemoryError情况的区域.
2.2 Java虚拟机栈(Java Virtual Machine Stacks):Java虚拟机栈是线程私有,它随线程的建立而建立(生命周期与线程相同).
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时,都会在栈中建立一个对应的栈帧.当方法调用正常结束或者异常终止,栈帧被弹出.
在Java虚拟机栈上,会出现两类异常:
如果线程请求的栈的深度超过最大栈深度(max_stacks),会抛出StackOverflowError.
如果虚拟机栈可以动态扩展,当扩展无法申请到足够的内存空间时,会抛出OutOfMemoryError异常.
栈帧(Stack Frame):栈帧是支持虚拟机中方法调用和方法执行的数据结构,它是虚拟机运行时数据区域Java虚拟机栈的栈元素.
栈帧中存储的有:局部变量表,操作数栈(也叫操作栈),方法返回值(return Value),当前方法所属类的运行时常量池的引用和一些额外的附加信息.栈帧中需要多大的局部变量表,需要多深操作数栈(max_stacks),在编译器就已经确定,并且写入方法表的Code属性中,当前方法-->当前栈帧.方法执行引擎运行的所有字节码指令只针对当前栈帧.
2.2.1 局部变量表
2.2.1.1 局部变量表是方法中一组变量值的存储空间,它里面存放的是方法的参数和方法内部的局部变量.在Java代码编译为Class文件时,就在方法的Code属max_locals数据项中确定了执行该方法所需要分配的局部变量表的最大"容量"(编译期分配完成).局部变量中的容量以Slot为单位,虚拟机允许Slot的长度随着操作系统,虚拟机,处理器而不同,但是要求只要在外观上保证在64位虚拟机和32位虚拟机上看起来一样就行(使用对齐补白).一个Slot可以存放一个32位以内的数据类型(boolean,byte,char,short,int,float,reference,returnAddress)8种.对于64位的数据类型(long,double),虚拟机会用高位对齐的方式为其分配两个连续的Slot空间.由于局部变量表建立在线程的堆栈上,是线程私有数据,无论读写两个连续的Slot是否为原子操作,都不会有数据安全问题.因为虚拟机规范中明确规定了,如果遇到无论以任何方式访问64位数据类型的两个Slot中的其中一个,都要在类加载的验证阶段抛出异常.
2.2.1.2 局部变量表的使用:在方法执行时,虚拟机使用局部变量表完成参数值到参数变量表的值传递过程.
虚拟机采用"索引定位"的方式使用局部变量表,索引值的范围从0开始到局部变量表最大的Slot数量.
如果执行的是实例方法,那局部变量表的第0个Slot中存放的是指向方法所属对象实例的引用(可以用this访问到当前对象),其余参数分配按照参数顺序排列,占用从1开始,方法参数分配完成后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot.另外,Slot是可以重用的,如果当前程序计数器中记录的字节码指令地址已经超出某个变量的作用域,那这个变量对应的Slot可以交给其他变量使用.
2.2.2 操作数栈
2.2.2.1 操作数栈也称为操作栈(后入先出),操作数栈的最大"深度"(栈说的就是深度,局部变量表说的是容量)在编译器编译阶段就已经分配完成,并且写入方法的Code属性max_stacks,在方法执行的任何时候,都不会超过操作栈的最大深度.其中,32位的数据类型所占操作栈容量为1,64位数据类型所占栈容量为2.编译器在编译程序代码的时候,必须保证操作数栈中元素的数据类型与字节码指令的序列"严格"匹配,在类校验阶段的数据流分析中还要验证这一点.
2.2.2.2 操作数栈执行过程:当一个方法开始执行的时候,这个方法的操作数栈是空的.在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容(入栈和出栈).例如,int类型的加法运算(int a=98;int b=2;int c=a+b),先使用iload_0和
iload_1指令将两个int型局部变量(98和2)推送至操作数栈中,然后执行iadd加法指令(将操作数栈顶两个int操作数出栈,相加计算结果,然后并把计算结果入操作数栈栈顶),最后使用istore_2指令,将操作数栈顶的int型数值出栈,并存入局部变量表的第2个Slot中.
2.2.2.3 在Java虚拟机的概念模型中,两个栈帧是相互独立的.但是在实际情况中,有些虚拟机的实现中可以令两个栈帧出现重叠,实现数据共用(让下面栈帧的操作数栈与上面的局部变量表重叠在一起,无需进行额外的参数复制传递).另外,Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",这里的栈就是操作数栈.
2.2.3 栈帧信息
2.2.3.1 栈帧信息:在实际开发中,我们把动态连接,方法返回值,附加信息全部归为一类,叫栈帧信息.
2.2.3.1.1 动态连接:每一个栈帧中都有一个指向运行时常量池的引用,该引用指向栈帧所对应的正在执行的方法所属类的运行时常量池.(这句话在zzm的jvm深入理解里面翻译是错误的,原英文是The reference points to the constant pool for the class of the method being executed for that frame.)
持有这个引用就是为了支持在方法调用过程中实现动态连接.动态连接是相对于"静态分析"来说的.在类加载阶段和第一次使用符号引用的时候,把符号引用转换为直接引用的转化就叫做"静态解析".
动态连接是在程序执行的时候,将符号引用转化为直接引用,这叫动态连接.
2.2.3.1.2 方法返回值:方法调用返回有两种方式,一种是正常调用完成(Normal Method Invocation Completion),一种是异常调用完成(Abrupt Method Invocation Completion,英文原意是突然,异常阻断).
对于正常调用返回,如果有返回值,会将方法返回值返回到方法调用者(方法的返回值和返回类型由对应指令确定,如返回int类型,对应指令是ireturn).在正常调用返回时,当前栈帧需要帮忙恢复调用者的栈帧的局部变量表和操作数栈,把返回值压入
调用者栈帧的操作数栈中,并调整调用者的程序计数器的值指向方法调用完成后的下一条指令地址.
如果是异常调用完成(如虚拟机内部出现异常,或者方法调用中使用athrow指令显式抛出异常),如果异常未被当前方法捕获,方法就会退出,不会向调用者返回任何值.
2.2.3.1.3 附加信息:虚拟机规范中允许在具体的虚拟机实现中,可以增加一些规范里没有描述的信息到栈帧中,如调试相关的信息.这些信息叫附加信息.
2.3 本地方法栈
在Java虚拟机中,没有规定本地方法栈的具体实现(也可以不实现).Java虚拟机一般使用C-linkage模型实现Java本地调用(JNI),因此我们把本地方法栈也叫"Cstacks".本地方法栈也是线程私有的,同线程的生命周期相同.
它服务于本地方法(Native Method),本地方法(Native Method)可以反过来调用虚拟机中的Java方法,在这种情况下,线程会离开本地方法栈,在Java虚拟机栈上开辟一个新的栈帧.
2.4 非堆内存
非堆内存包含永久代(Permanent Generation)和代码缓存区(Code Cache).
2.4.1永久代(Permanent Generation)中包含两部分,一部分是方法区(Method Area),一部分是字符串常量(interned strings,被拘禁,被固定的字符串--字符串常量).
2.4.1.1 方法区(Method Area):方法区随着虚拟机的启动而创建(The method area is created on virtual machine start-up),它存储了每个类的信息,比如,
①类加载器的引用(Classloader Reference)
②运行时常量池(字符串常量,数值型常量,类引用,成员变量引用,实例方法引用等)
③成员变量数据(成员变量名,类型,修饰符,属性表):包含实例变量和类变量(static,静态变量)
④成员方法数据(方法名,返回值类型,参数类型(按顺序),修饰符,属性表)
⑤方法代码:经过编译器编译成字节码指令后,存放在方法属性表集合(attributes)中一个名为Code的属性中,Code中包含(字节码,操作数栈大小,局部变量大小,局部变量表,异常表[开始点,结束点,异常处理代码的程序计数器(PC)偏移量,被捕获的异常类对应的常量池下标]).
所有线程共享同一个方法区,因此访问方法区数据的和动态链接的过程必须线程安全的。如果两个线程试图访问一个还未加载的类的字段或方法,必须只加载一次,而且两个线程必须等它加载完毕才能继续执行。
2.4.1.2 驻留字符串(字符串常量):在JDK1.7的HotSpot中,已经把字符串常量部分移除了方法区.
2.4.2 代码缓存区(Code Cache)用于编译和存储那些被 JIT编译器编译成原生机器码的方法。Java字节码是解释执行的,但它没有在JVM的主机CPU上执行机器码那么快.为了提升性能,Hotspot VM会找到执行频繁的热点代码,并把它们编译成本地代码,保存在非堆内存的代码缓存区中.
2.5 堆内存
堆被用来在运行时分配类实例、数组.数据和对象不能在Java虚拟机栈上分配,因为栈帧被设计为创建以后无法调整大小,栈帧只存储指向堆中对象或数组的引用.
对象总是堆上分配,只能由垃圾回收器回收.堆是垃圾收集器管理的重点区域.
为了支持垃圾回收机制,堆被分为了下面三个区域:新生代(Young Generation,经常被分为 Eden 和 Survivor[From Survivor空间和To Survivor空间]),老年代(Old Generation),永久代(Permanent Generation).