一. Java内存区域
Java虚拟机在运行程序时会把其自动管理的内存划分为以上几个区域,每个区域都有其用途以及创建销毁的时机,其中蓝色部分代表的是所有线程共享的数据区域,而绿色部分代表的是每个线程的私有数据区域。
1.程序计数器
程序计数器是一块较小的内存空间,是当前线程执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选区下一条需要执行的字节码指令,jvm在任何一个确定的时刻,一个处理器只会处理一条线程中的指令,因此,每个线程只会有一个独立的程序计数器,各个线程之间的计数器互不影响,这类内存区域为“线程私有”的内存。
如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。
2.java虚拟机栈
与程序计数器一样,java虚拟机栈也是线程私有的,生命周期与线程相同,总数与线程关联,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。每个方法从调用直结束就对于一个栈桢在虚拟机栈中的入栈和出栈过程。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。
其中“栈”指的就是虚拟机栈或者说是虚拟机栈中的局部变量表部分。表中存放的编译器可知的各种基本数据类型(8中基本类型),long和double占用2个局部变量空间,其余为1个。其中变量所需的内存空间在编译期间完成分配。
3.本地方法栈
本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。
本地方法栈也是线程私有的。
4.java堆
java堆是jvm中内存最大的一块,java堆是所有线程共享的一块内存区域,在虚拟机启动时创建. 此空间唯一目的就是存放对象实例。堆区也是Java GC机制所管理的主要内存区域,一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。
5.方法区
方法区也为线程共享的内存区域。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实上,方法区并不是堆(Non-Heap);
很多人喜欢将方法区称之为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。
java虚拟机规范对这个区域限制非常宽松,除了和java堆一样不需要连续的内存和可选固定大小或者可扩展外,还可以选择不实现垃圾收集,这个区域的回收目标主要为常量池的回收和对类型的卸载,在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。
5.1 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。
二.Java对象访问过程
Java中的对象访问过程会涉及到栈、堆、方法区这三个内存区域
下面以一段代码进行说明:
首先对上句代码进行一个简单的说明:
1)定义一个Object类型的变量obj,这个obj就是一个引用类型的变量,保存在java的栈中
2)new Object(),将会在java的堆内存中形成一块存储Object类型所有实例数据值的结构化内存,根据具体对象类型以及虚拟机实现对象内存局部表的不同,这块内存的长度是不固定的。同时Java堆中还包含查找此对象信息的地址信息。
java中对象的访问流程如下:
1)定义的对象名称保存在java栈的本地变量表
2)通过本地变量表中的栈内存地址可以找到堆内存。
3)利用堆内存存的对象进行本地方法的调用
通过reference类型如何访问Java堆中的对象?主流的访问方式有两种:
使用句柄访问方式
Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄地址中包含了对象实例数据和类型数据各自的具体地址。
使用直接指针访问方式:
Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象堆地址。
这里对图中提到的对象实例数据和对象类型数据做一个解释:
Java对象类型数据:指class类信息,定义了一个类的元数据、它包含的成员等
Java对象实例数据:基于某个类new出来的一个或多个具体实例
优势对比:
句柄访问方式:最大的好处就是:reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收时移动对象是非常普遍的行为)时只要修改句柄中的实例数据指针,而reference本身不需要被修改。
直接指针访问方式:最大好处就是速度快,它节省了一次指针定位的时间开销。
---------详细信息请参考《深入理解Java虚拟机 JVM高级特性与最佳实践》
本篇文章只为本人阅读后个人理解摘要,如有错误,请见谅!!!