欢迎访问我的个人博客
参考书籍:《深入理解JAVA虚拟机》
对于Java程序来说,在虚拟机自动内存管理机制的帮助下,不容易出现内存泄漏和内存溢出的问题。对于程序员来说,不需要向C/C++程序员一样书写delete/free的代码。但是正因为将内存管理的权利交给了虚拟机,JAVA程序一旦出现内存泄漏和内存溢出的问题,如果程序员不了解Java是如何使用内存的,那么问题的排查将会是一件异常苦难的工作。
Java虚拟机在执行Java程序时,会将Java虚拟机管理的内存划分为若干个不同的数据区域。这些数据区域有着不同的用途。Java虚拟机所管理的内存包括以下几个区域。
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行字节码的行号指示器,在Java虚拟机的概念模型中,字节码解释器工作时通过改变程序计数器的值选取下一条需要执行的字节码指令。
由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式进行实现。因此为了线程切换后,程序计数器能够指到正确的位置,每个线程都有一个独立的程序计数器(线程的工作内存中),各线程独立存储,互不影响。
Java虚拟机栈与程序计数器一样,也是线程私有的,生命周期和线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存储了编译器已知的各种Java虚拟机基本数据类型,对象引用(可能指向Java堆中的句柄池,可能直接指向Java堆中的对象)。
本地方法栈与虚拟机栈发挥的作用类似,其区别是Java虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用的到的本地方法(Native)服务。
堆是虚拟机所管理的内存中最大的一块,Java堆是一块被线程共享的区域,在虚拟机启动时创建。此区域唯一的作用就是存放对象实例,Java中几乎所有的对象实例都在这里分配内存。。
Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称为“GC”堆。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆会经常出现新生代,老生代等名词。这里的区域划分仅仅只是一种垃圾收集器的设计风格,不代表这是具体实现的固有内存分区。
从分配内存的角度来看,线程共享的Java堆中可以划分出多个线程私有的分配缓冲区,以提升对象分配时的效率。但无论从什么角度,无论如何划分,都无法改变Java堆中存储内容的共性,无论哪个区域,存储的都只能是对象的实例,将Java堆的戏份只是为了更好的回收内存/更快地分配内存。
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量,即时编译器编译后的代码缓存等数据。
运行时常量池是方法区的一部分。除了有类的版本、字段、方法、接口等描述信息之外,还有一项是常量池表,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Java是一门面向对象的编程语言,在程序运行过程中不断有对象被创建出来。在语言层面上,创建对象的动作仅仅是一个new关键字而已,而在虚拟机中的创建是一个什么样的过程呢?
类加载检查:当Java虚拟机遇到了一个字节码new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号的引用,并且检查这个符号引用锁代表的类是否已经被加载、解析、初始化过。如果没有,那必须先执行相应的类加载过程。
内存分配:在类加载检查通过后,虚拟机会向新生对象分配内存。为对象分配内存空间的任务实际上等同于把一块确定大小的内存块从Java堆中划分出来。
总的来说,分配方式是由Java堆空间是否规整来决定,而Java对是否规整是又由所采用的垃圾收集器是否带有空间压缩整理的能力决定。
线程安全:由于Java是支持多线程的语言,且new对象是一个频繁的行为,那么在并发情况下new对象是不安全的,可能会出现正在给对象A分配内存,指针还没有修改,对象B又同时使用了原来的指针来分配内存的情况。
内存空间初始化:内存分配完成后虚拟机必须将分配到的内存空间(不包括对象头)都初始化为零值,这不操作保证了实例对象字段在Java代码中不用赋初值就可以直接使用,使程序能访问到这些字段的数据类型所对应的零值。
对象必要设置:接下来Java虚拟机需要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希值(实际上对象的哈希值会延后到真正调用hashCode()方法时进行计算)、对象的GC分代年龄等信息。这些信息存放在对象头中。
构造函数:上述工作完成后,从虚拟机角度来看,一个新的对象产生了。但是从Java角度来说,对象的创建才刚刚开始—构造函数,这是所有的字段都还是零值,对象需要的资源,状态信息还没有构造好。一般来说new指令之后会执行init()方法,这时一个真正可用的对象才构造出来。
在HotSpot虚拟机内,对象在堆内存中的存储布局可以划分为三个部分:对象头,实例数据,对齐填充。
对象头
虚拟机对象的对象头部包括两类信息,第一类用于存储对象自身运行时数据,如:哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
实例数据
实例数据是在对象真正存储的有效信息,即在我们程序代码中所定义的各种类型字段的内容。
对齐填充
不是必然存在的也没有特殊的含义,仅仅起到占位符的作用。
由于虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,对象头的已经被精心设计为8字节的整数倍,但是实例数据部分可能没有8字节的整数倍,所以需要对齐填充来进行补全。
创建对象后要适用对象,Java程序会通过Java虚拟机栈上的reference数据来操作堆上的具体对象。但是对象的访问方式是由虚拟机实现而定的,主流的实现方式有一下两种:
这两种访问方式各有优势:
在《深入理解Java虚拟机》一书中主要描述的HotSpot虚拟机而言,主要是使用第二种方式进行访问。
对象访问示例图:
句柄访问:
直接访问: