对于java程序员来说,虚拟机自动内存管理机制帮助javer们管理内存,不需要再delete/free,不容易出现内存泄漏和溢出问题,也正因为如此,一旦出现了泄露和溢出,如果不了解虚拟机是如何使用内存的,那么排查将非常艰难.
一. 我们先来了解一下java内存的划分.
由图可以看出,java内存划分分为两种类型,一种是所有线程共享的数据区,另一种是线程隔离的数据区.
程序计数器:
- 是内存中较小的一块空间,可以看作是当前线程所执行的字节码的行号指示器.
- 每个线程都有一个线程指示器,用于多线程切换时记录当前执行的行号.
- 当线程执行java方法时,计数器记录字节码的地址,如果正在执行Native方法,这个计数器值为空(Underfined)
- 此内存是唯一一个没有规定OutOfMemoryError情况的区域.
- 线程私有
java虚拟机栈:
- 每个方法在执行的时候都会创建一个栈帧,存储局部变量表,操作数栈,动态链接,方法出口等信息.一个方法从调用到执行完成的过程,就是一个栈帧在虚拟机栈中入栈到出栈的过程.
- 局部变量表存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用和returnAddress类型.其中64位的long和double会占用2个局部变量空间,其余的数据只占用一个.
- 局部变量表所需的内存空间在编译器完成分配,当进入一个方法时,所需要的局部变量空间是完全确定的.在方法运行期间不会改变局部变量表的大小
- 本区域两种异常情况:线程请求栈深超过所允许的深度,抛出StackOverflowError异常;如果本区域可以动态扩展,并且无法申请到足够的内存,会抛出OutOfMemoryError异常.
- 线程私有
本地方法栈:
- 与虚拟机栈发挥的作用非常相似,不同的是本地方法栈为虚拟机使用到的Native方法服务.
- 虚拟机规范对本区域的语言,方式,数据结构没有强制规定,可以自由实现.
- Sun HotSpot虚拟机将本区域与虚拟机栈合二为一.
- 抛出异常同虚拟机栈
- 线程私有
java堆:
- java虚拟机管理的内存中最大的一块,也是GC工作的主要区域.
- 此内存存放对象实例(对象,数组),规范中规定所有对象实例都在这里分配内存.
- 栈上分配和标量替换是的规范不再绝对.
- java堆可以处于物理上不连续的内存空间中,只要逻辑连续即可.
- 线程共享
方法区:
- 用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据.
- 可以实现垃圾回收,但回收成果很小,hotSpot将方法区列为永久代,进行垃圾回收.
- java虚拟机规范将方法区描述为堆的一个逻辑部分,但却有另一个名字Non-Head(非堆)
- 无法满足内存分配的需求时,将抛出OutOfMemoryError异常.
- 线程共享
运行时常量池:
- 是方法区的一部分.
- 运行时常量池存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行期常量池存放.
- 运行时常量池具备动态性,并不要求只有编译器才能产生,运行期间可以将新的常量放入池中.
- 作为方法区的一部分,自然受到方法去内存限制,无法再申请内存时抛出OutOfMemoryError.
直接内存:
- 不是虚拟机运行时数据区的一部分,也不是虚拟机规范定义的内存区域,但被频繁使用,也会导致OutOfMemoryError出现.
- 直接内存不受java堆大小的限制.会受到本机总内存大小及处理器寻址空间的限制.
二. 下面来讲解各内存是如何创建,布局和访问的.首先讲解堆中内存的分配问题.
对象的创建:
- 虚拟机遇到new指令时,检查到常量池中能否定位一个类的符号引用,并检查这个符号引用代表的类是否已加载,解析和初始化过,如果没有,那必须先执行类的加载过程.
- 在类加载检查通过后,为新生对象分配内存.对象所需的内存大小已经完全确定,分配内存分为:指针碰撞和空闲列表两种方式.区别时java堆中内存空间是否规整决定方式.
- 划分内存空间需要注意并发问题.
- 划分空间后,内存空间初始化为零值(不包括对象头)
- 接下来,虚拟机对对象进行必要的整理,将这些信息存放到对象头中.已虚拟机角度看,对象创建已经完成.
- java程序上,开始执行初始化方法.对象创建完成.
对象的内存布局:
- 对象头(Header):对象头包含两部分信息,一部分:存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志等,这部分数据在31和64虚拟机中占32bit和64bit,官方称为"Mark Word";另一部分是:类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例.
- 实例数据(Instance Data):是对象真正存储的有效信息,也是程序代码中定义的各种类型的字段内容.
- 对齐填充(Padding):这部分并不是必然存在,也么有特别的含义,仅仅是占位符的作用.对象的大小必须是8的整数倍,这部分用来对齐.
对象的访问定位:
对象的访问通过栈上reference数据来操作堆上的具体对象,java虚拟机只规定了对象的引用,没有规定该如何去定位,这部分又虚拟机实现而定.主流方式有二
- 句柄访问:Java堆中会划分出一块内存来作为句柄池,reference中存储对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息.
- 直接指针访问:java堆对象的布局中就必须放置访问类型数据的相关信息,而reference中存储的直接就是地址.
两种方式各有优势.句柄访问好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不修改.
使用指针访问的好处是速度更快,节省了一次指针定位的时间开销.虚拟机Sun HotSpot使用指针访问.
本节内容到此结束,内容为书本知识的总结,切勿认为是抄袭.
致词,感谢.