《深入理解Java虚拟机》二:Java内存区域与内存溢出异常
2018-11-22 本文已影响4人
亦猿非猿
断断续续把这本书的重点章节看完了,然而,重新回想一下,感觉又还回去了,既然这样,那就再刷一篇,用输出,归纳去倒逼自己理解!
这一章,主要讲解Java虚拟机内存的各个区域,以及对对象的创建,布局和访问进行大概讲解。
大纲
image运行时数据区域
image程序计时器(Program Counter Register)
- 定义:是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。用于线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计时器,为线程私有。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。
- 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈
- 定义:描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame[1]),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 方法调用—>在虚拟机栈中入栈,执行完成—>出栈。
- 栈主要是储存局部变量表,存放各种基本数据类型,对象引用,returnAddress类型
- 线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展且扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈
- 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
- 会抛出StackOverflowError异常和OutOfMemoryError异常
Java堆
- 存放几乎所有的对象实例,是垃圾收集器管理的主要区域,很多时候也被称做“GC堆”,垃圾收集器基本都采用分代收集算法
- 会抛出OutOfMemoryError异常
- 线程共享
方法区
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 会抛出OutOfMemoryError异常
- 线程共享
Class文件常量池
- 方法区的一部分
- 存放类的版本,字段,方法,接口等描述信息
运行时的常量池
- 方法区的一部分
- 用于存放编译期生成的各种字面量和符号引用
- 具备动态性(运行期间加入新的常量)
直接内存
- 非Java虚拟机规范中定义的内存区域,但是也被频繁使用到
- 会抛出OutOfMemoryError异常
线程私有
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。如果每个线程中都有一个独立的内存区域,各条线程之间互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
对象的创建,布局和访问
Java对象的创建
- 遇到new指令,检查指令的参数是否能在常量池中定义到一个类的符号引用
- 检查这个符号代表的类是否已被加载,解析和初始化(没有则执行类加载过程)
- 虚拟机为新生对象分配内存,分配方式:
- 指针碰撞:Java堆中内存是绝对规整的
- 空闲列表:Java堆中的内存并不是规整的
- 解决并发情况下不是线程安全的问题:
- 对分配内存空间的动作进行同步处理
- 把内存分配的动作按照线程划分在不同的空间之中进行,称为本地线程分配缓冲
- 对分配的内存控件进行初始化,对对象进行必要设置
- 执行<init>方法
对象的内存布局
对象在内存中存储的布局可以分为3块区域:
-
对象头(Header),包含内容如下:
- 存储对象自身的运行时数据
- 对象指向它的类元数据的指针
- 数组长度(如果为数组)
- 实例数据 (Instance Data):对象真正存储的有效信息
- 对齐填充(Padding):不是必要存在,仅仅器占位符的作用
对象的访问定位
目前主流的访问方式有使用句柄和直接指针两种,对主要虚拟机Sun HotSpot而言,它是使用直接指针进行对象访问。
使用句柄
Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据的具体地址信息。
优点:在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针
reference中存储的直接就是对象地址。
优点:速度更快