Java虚拟机在执行Java程序的过程中,会把它所管理的内存划分为若干个不同的数据区域。
程序计数器(Program Counter Register)是一块较小的内存区域,字节码解释器工作时,就是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等都是依托程序计数器完成的。
也就是说处理器在时间片切换时,为了线程切换后能恢复到(找到)正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间互不影响,独立存储,线程私有的内存
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,生命周期与线程相同;
虚拟机栈描述的是Java方法执行的线程内存模型
:每个方法在执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)
用于存储局部变量表,操作数帧,动态链接,方法出口等信息。
一个方法的调用到执行结束,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
可知
的各种Java虚拟机基本类型(boolean、byte、char、short、int、float、long、double)、对象的引用(reference类型,指的是对象地址的引用或句柄或其他于此对象相关的地址)、returnAddress(指向了一条字节码指令的地址)。存储空间以局部变量槽(Slot)
表示,64位的长度(long,double)占用2个变量槽,其余都是1个。在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部面量表的大小(大小指的是变量槽的数量)
《Java虚拟机规范》中这个内存区域规定了两类异常状况:
栈深度就是指栈帧的数量
HotSpot虚拟机栈容量不支持动态扩展,所以只要线程申请到栈空间就不会出现OOM,如果申请失败则会出现OOM异常
本地方法栈(Native Method Stack):本地方法栈服务的对象是JVM执行的native方法
,而虚拟机栈服务的是JVM执行的java方法。
Java堆(Java Heap)是虚拟机所管理内存中最大的一块。Java 堆是被所有线程共享的一块内存区域
,在虚拟机启动时创建
,用于存放对象实例。Java堆是垃圾回收器管理的内存区域
也称为GC堆。
Java堆可以处于物理上不连续的内存区域,但在逻辑上应该被视为连续的,但对于大对象(典型的如数组对象),多数虚拟机实现处于简单实现,存储高效的考虑,很可能会要求连续的内存空间
Java虚拟机的堆内存分为新生代、老年代、永久代、Eden、Survivor,在当前的HotSpot中上述提法就有很多需要商榷的地方。Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存
Java堆既可以被实现成固定大小,也可以是可扩展的,当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 设定),如果Java堆中没有内存完成实例分配,并且堆也无法再扩展,Java虚拟机将会抛出OutOfMemoryError异常
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域
,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据
JDK 8 HotSpot完全放弃了永久代的概念,改用本地内存中实现的元空间(Meta-space)来代替
《Java虚拟机规范》对方法区的约束是非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定大小或可扩展外,甚至可以选择不实现垃圾收集
。相对而言,垃圾收集行为在这个区域的确比较少出现,,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标最主要是针对常量池的回收和对类型的卸载
,但是回收效果比较难令人满意,尤其是类型的卸载。
方法区无法满足新的内存分配需求时将会抛出OutOfMemoryError异常
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table)
用于存放编译期生成的各种字面量与符号引用
,在类加载后存放到方法区运行常量池中
,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中
字面量:等号右边的八种基本类型的值、字符串值、声明为final的常量的值
。
//a为常量,10为字面量
final int a = 10;
// b 为变量,hello world!为字面量
string b = "hello world!";
符号引用:可以是任意类型的字面量。只要能无歧义的定位到目标。在编译期间由于暂时不知道类的直接引用,因此先使用符号引用代替。最终还是会转换为直接引用访问目标。符号引用就是某个变量,在编译的时候,无法确定其内存地址。
String str = "Hello World!"
// str在编译的时候就会编译为符号引用。
System.out.println(str);
直接引用:程序运行时可以定位到引用的东西(类, 对象, 变量或者方法等)的地址
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
intren方法:通俗的讲,是将字符串放入常量池中。
/**
* 表达式右边是纯字符串常量,则存放在常量池中
* 表达式右边存在字符串引用,则存放在堆中
*/
public class test {
public static void main(String[] args) {
String s1="aaa";
String s2="bbb";
String s3="aaabbb";
String s4=s1+s2;
String s5="aaa"+"bbb";
String s6=new String("aaabbb");
// false
System.out.println(s3==s4);
// true
System.out.println(s3==s4.intern());
// true
System.out.println(s3==s5);
// false
System.out.println(s3==s6);
// true
System.out.println(s3==s6.intern());
}
}
说明:s1,s2,s3,s5均存放在常量池中,s4,s6存放在堆中。
当常量池无法再申请到内存时会抛出OutOfMemoryError异常
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域,但是这部分内存也被频繁的使用,也可能导致OutOfMemoryError异常
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、及SWAP区或者分页文件)的大小及处理器寻址空间的限制
。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理 上的和操作系统 级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
参考《深入理解Java虚拟机》-周志明