第2章 Java内存区域与内存溢出异常


1. 虚拟机运行时数据区域

1.1 Java虚拟机运行时数据区图
注:图中浅蓝色部分为线程共享区, 图中的白色部分为线程私有部分 

1.2 线程共享
线程共享部分的所有区的生命周期和所属虚拟机的生命周期是相同的。
1.2.1 方法区
方法区用于存储已经被虚拟机加载的 类信息,常量,静态变量,即时编译器编译后的代码等数据。在JDK1.7之前,方法区的数据是不会进行垃圾回收的;在JDK1.7之后,方法区也会进行垃圾回收,主要的回收目标是针对常量池的回收和对类型的卸载。只是相对于堆中的垃圾回收,方法区的垃圾回收行为比较少, 因为在这个区域的回收条件很苛刻。
1.2.1.1 运行时常量池
运行时常量区是方法区的一部分,类Class文件加载到方法区时会将Class文件中描述的常量池内容加载到运行时常量区池存放, 并且运行时常量池具备动态性,运行期间产生的新的常量也会放置在此池中。字符串常量区就在其中。
1.2.2 堆区

堆区用于存放对象实例和数组,几乎所有的对象实例都在这里分配。Java堆还可以再细分为:新生代和老年代, 而新生代又可以再分为:Eden空间,From Survivor空间,To survivor空间。 为什么要这么分呢? 这是因为现在的垃圾收集器基本都是采用分代收集算法。堆区这么分就是因为分代收集算法的要求所致。堆区是垃圾收集器管理的主要区域。

Java堆可能划分出多个线程私有的分配缓冲区, 叫做本地线程分配缓冲(TLAB),其目的是为了解决多线程时对象分配空间时内存分配冲突的问题, 通过划分TLAB使得分配内存的效率更高,虚拟机是否使用TLAB可以通过参数来进行设置。
1.3线程私有
线程私有部分的所有区的生命周期和所属线程的生命周期是相同的。
1.3.1 虚拟机栈
虚拟机栈用于存储所属线程中Java方法执行时局部变量表, 操作数栈,动态链接, 方法出口等信息,每一个方法从调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。每个方法的栈帧的大小并不相同。
局部变量表中存放了编译器可知的基本数据类型(其中64位的double和long类型数据会占用两个局部变量空间), 对象引用和返回地址。方法运行期间局部变量表的大小是不会改变的。
1.3.2 本地方法栈
本地方法栈用来为使用到的Native方法服务。这个栈也会和虚拟机栈一样可能会抛出StackOverFlowError和OutOfMemoryError异常。
1.3.3程序计数器
程序计数器是用来存储当前线程所执行的字节码的行号指示器。字节码解释器工作时需要依赖程序计数器来实现取下一个字节码指令, 分支, 循环, 跳转, 异常处理,线程回复。
注意:如果正在执行的是Native方法,这个程序计数器的值为空(Undefined ).
1.4 直接内存
直接内存并不是虚拟机运行时数据区的一部分, 但是这部分内存也会被频繁的使用。直接内存不会受到Java堆内存大小的限制, 而是会受到本机总内存的限制。
NIO就使用了直接内存来进行操作。NIO引入了一种基于通道和缓冲区的I/O方式,使用Native库函数分配直接内存,然后通过在Java堆中的一个对象DirectByteBuffer对这部分内存进行操作。这种方式可以显著提高性能。

2. 对象

2.1 对象的内存布局
对象在内存中的布局分为3块区域:对象头, 实例数据,对齐填充
1. 对象头
对象头又可分为两部分:
第一部分被称作“mark word”, 里面存储了该对象的HashCode, GC分代年龄, 锁状态标志,线程持有的锁, 偏向线程ID,偏向时间戳等。对象头的大小在32位虚拟机中为32位, 在64位的虚拟机中为64位。 
第二部分为类型指针,指向对象类的元数据, 虚拟机通过这个指针来确定这个对象是哪个类的实例。如果该对象是一个Java数组,那在对象头中还会有一块用于记录数组长度的空间。
2. 实例数据
这部分是对象真正存储的有效信息, 即在程序代码中所定义的各种类型的字段的内容。从父类继承下来的和本类中定义的都记录。
字段的存储顺序会受到虚拟机配置参数的影响。在默认的情况下,相同宽度的字段总是被分配在一起: longs和doubles, ints, shorts和chars, bytes和booleans,oops(普通对象指针,即对象变量)。 在这个前提下,父类中定义的变量会出现在本类的之前。
3. 对齐填充
这部分并没有什么实际的意义, 仅仅是起着占位符的作用。存在这部分的原因是由于虚拟机的自动内存管理系统要求对象的起止地址必须是8字节的整数倍,因此当对象的实例数据部分没有对齐时, 就需要通过填充占位符来对齐。
2.2 对象的访问定位
在程序中我们使用reference数据(对象变量)来操作堆上的对象实例, Java虚拟机使用直接指针访问的方式, 这种方式的图示见下图:
                  
                                    直接指针访问方式                                                                                    句柄访问方式
使用直接指针的方式的最大的好处就是速度快, 这对于Java中需要频繁访问对象来说是一个很大的优势。 
相对于句柄的访问方式,句柄访问方式不仅要维护一个句柄池, 而且在访问对象时需要进行两次指针定位。 如果频繁操作的话效率明显不如直接指针访问方式。但是句柄访问方式也有直接指针访问不具备的优点:那就是reference中存储的句柄是稳定的,在对象被移动(多数由于垃圾回收时移动)时只会改变句柄中的实例数据指针,并不会修改reference。  
2.3 对象的创建过程
1. 当虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并检查这个符号引用代表的类是否已经被加载,解析和初始化了;如果没有,那么必须先执行相应类的加载过程。
2. 类加载检查通过后。创建所需内存的大小便可以确定, 但是在堆中进行分配内存的时候,垃圾回收在使用不同的回收算法时,会造成不同的内存分配行为:
在使用Serial, ParNew等带compact(压缩)过程的垃圾收集器时, 因为经过压缩后Java堆中的内存是规整的,所以内存分配的过程就是简单的将指针挪动与对象大小相等的距离。这种方式叫做指针碰撞。
在使用像CMS这种基于标记-清理算法的垃圾收集器时, 因为Java堆内存不是规整的(有很多碎片), 所以虚拟机需要维护一个内存碎片列表,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。这种方式叫做"空闲列表"。
由于创建对象是一个非常频繁的行为,在并发的时候分配内存的操作就不是线程安全的。这个问题的解决有两个方法:
1. 对分配空间的动作进行同步处理---虚拟机采用CAS配上失败重试的方式保证内存更新操作的原子性
2. 每个线程在Java堆中预先分配一小块儿内存,称为本地线程分配缓冲(TLAB),哪个线程分配内存,就在哪个线程的TLAB上分配。(有木有觉得很眼熟。。)
3. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为0, 这一步操作保证了对象的实例字段在Java代码中可以不赋初值就可以直接使用。(这就是为什么如果不给字段赋初值,字段的默认值为0, null, false的原因)。
4. 接下来虚拟机要对对象进行必要的设置,设置对象头。并且执行<init>方法(个人认为这个<init>方法应该指的是字段赋予的初始值,代码块,构造函数等用于初始化的操作)来按照程序员的意愿进行初始化, 这样一个真正可用的对象才算完全产生出来。

3. 一些散乱的知识

1. windows平台的虚拟机中, Java的线程是映射到操作系统的内核线程上的。

博客中的图片大部分来自网络, 侵通删!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值