参考文献:深入学习java虚拟机
概览
java虚拟机在执行java程序的过程中会把它所管理的内存划分成若干个不同的数据区域。这些区域各有用途,以及创建和销毁的时间。有的区域随着虚拟机的进程的启动而存在,有的则依赖用户线程的启动和结束而建立和销毁。
- 运行时数据区域
- 线程共享区:方法区、堆
- 线程隔离区:虚拟机栈、本地方法栈、程序计数器
java内存区域详解
程序计数器:
是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。
java多线程就是通过对线程的轮流切换并分配处理器的执行时间的方式来实现的,在任意时刻,一个处理器都只会执行某一个线程中的指令。所以每条线程为了切换后能恢复到正确的执行位置,每条线程都会有一个独立的程序计数器,各条线程之间互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
JAVA虚拟机栈:
也是线程私有的,生命周期和线程一样。是描述java方法执行的内存模型。在每个方法执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。局部变量表包括了编译器可知的各种基本数据类型、对象引用和returnAddress类型(指向了一个字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个。局部变量表所占用的内存在编译器就完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
本地方法栈:与java虚拟机栈发挥的作用十分相似,区别是java虚拟机栈是为虚拟机执行java方法的,本地方法栈是为虚拟机执行native方法的。在本地方法栈中使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(如sun hotspot)直接把本地方法栈和虚拟机栈合二为一。
JAVA堆:
是java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。所有的对象实例和数组都要在堆上分配。java堆是垃圾收集器管理的主要区域,因此很多时候也被称作GC堆。
从内存回收的角度来看,由于现在收集器基本都采用分代手机算法,所以java堆还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。
从内存分配的角度来看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区。不论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步的划分目的是为了更好地回收内存和更快地分配内存。java堆可以处于物理上的不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区:
方法区与java堆一样。是各个线程所共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然java虚拟机规范把方法去描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与java堆区分开来。对于习惯在hotspot虚拟机上开发的开发者来说,更愿意把方法区叫做永久代。本质上并不一样,仅仅是因为hotspot设计团队把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样hotspot的垃圾收集器可以像管理java堆一样管理这部分内存,能够省去专门为方法去编写内存管理代码的工作。但是这样更容易出现内存溢出的问题。永久代有上限。而其他不存在永久代的虚拟机只要没有触碰到进程可用内存上限,就不会出现问题。在jdk7中,已经将放在永久代的字符常量池移出。
运行时常量池是方法去的一部分,用于存放编译器生成的各种字面量和符号引用。运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性。
在jvm层次如何理解对象的创建和内在?
对象的创建,当虚拟接接收到new指令,会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查该引用是否已经被加载、解析和初始化过。如果没有必须经历该过程。当类加载检查通过后,对象创建所需的堆内存空间在类加载期间就已经完全确定,接下来在堆中以指针碰撞、空闲表格等方式来划下对象在堆中的存储块。然后将虚拟机会将对象分配到的内存空间都初始化为零。最后将对象进行必要的设置,比如该对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄。
对象的内存布局
- 对象头:分为二个部分,第一个部分存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。第二个部分是类型指针,即对象指向它的类元数据的指针,java数据对象的话,在头中还有一块记录数组长度的数据。
- 实例数据:这部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
- 对齐填充:不是必然存在的,也没特别的含义,仅仅是占位符的作用。由于hotspot的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,对象的大小必须是8字节倍数,而对象头的大小刚好是8字节的倍数(一倍或者二倍),因此,当对象实例数据部分没有对齐时,可以通过对齐填充进行补全。