您的位置:首页 > 编程语言 > Java开发

部分笔记—Java内存区域与内存溢出异常

2017-01-02 17:43 232 查看

Java与c++内存管理对比

C或C++

在内存管理区域,即拥有每一个对象的“所有权”,又担负者每一个对象生命开始到终结的责任

Java

在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题,由虚拟机管理内存。

运行时数据区域

Java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个数据区域。

【(方法区、堆)(所有线程共享区域),(虚拟机栈、本地方法栈、程序计数器)(线程隔离数据区)】

程序计数器

可当做当前线程所执行的字节码的行号指示器。在虚拟机概念模型中,字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖该计数器。

为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,称该内存区域为“线程私有”的内存。

若线程正在执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址。如果是Native方法,计数器为空(Undefine)

Java虚拟机栈

线程私有,生命周期与线程相同。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表操作数栈动态链接方法出口等信息。每个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表,存放了编译期可知的所有基本数据类型、对象引用和returnAddress类型(指向一条字节码指令地址)

在Java虚拟机规范中,对该区域规定了两种异常状况:

* 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;

* 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。

本地方法栈

作用与虚拟机栈类似,之间的区别: 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。本地方法栈也会抛出StackOverflowError异常和OutOfMemoryError异常。

Java堆

是Java虚拟机所管理的内存中的最大一块

所有线程共享

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存

Java堆为垃圾收集器管理的主要区域,又称GC堆。Java堆可以处于物理上不连续的内存空间中,只要逻辑连续即可。即可实现成固定大小的,也可以是可扩展的,主流虚拟机都是可扩展的(用过-Xms与-Xmx控制)。如果堆中没有内存完成实例分配,并且堆无法扩展时,将抛出OutOfMemoryError异常。

方法区

与Java堆一样,是各个线程共享区域,存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。别名Non-Heap

Java虚拟机规范对方法区限制很宽松,除了和Java堆一样,不需要连续的内存和可选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但非数据进入方法区就像“永久代”的名字一样可以永久存在了,这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

Java虚拟机规范规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池

属方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内存将在类加载后进入方法区的运行时常量池中存放。

Java对Class文件的每个部分(包括常量池)的格式有明确规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,Java虚拟规范则没做任何细节要求。

运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生。运行期间也可以把新的常量放在运行时常量池中。

跟方法区一样会抛出OOM.

对象的创建流程

1、虚拟机遇到一个new指令,首先去检查指令参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否被加载、解析和初始化过。

2、在类加载检查过后,接下来虚拟机为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定,为对象分配控件的任务等同于把一块确定大小的内存从Java堆中划分出来。(“指针碰撞”,“空闲列表”,TLAB(Thread Local Allocation Buffer))

3、内存分配完后,虚拟机需要将分配到的内存空间都初始化为0值(不包括对象头)

4、接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。

5、上面工作完成后,从虚拟机的视角来看,一个新的对象已经产生,但从Java程序来看,对象创建才刚刚开始,
<init>
方法还没有执行,所有字段都还为零。执行完new指令后还会接着执行方法,把对象按照程序员的意愿初始化,这样一个真正可用的对象才算完全产生出来。

对象的内存布局

在HotSpot虚拟机中,对象在内存中的布局可以分为3部分:对象头、实例数据和对齐填充

对象头

对象头包含两个部分信息:

一部分为存储对象自身的运行时的数据,如哈希码、GC分代年龄、锁状态标识、线程特有的锁、偏向线程的ID、偏向时间戳。该数据长度在32位和64为虚拟机中分别为32bit和64bit。

另外一部分为类型指针,即对象指向它的类元数据的指针,虚拟机通过该指针来确定这个对象是哪个类的实例。若对象是个数据,则对象头还有一部分为数组长度。

实例数据

是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。

对齐填充

仅仅起占位符的作用。因为HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8的整数倍。也就是对象的大小是8的整数倍。而对象头正好是8的整数倍,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补齐。

对象的访问定位

为了使用对象,Java程序需要从栈上的reference数据来操作堆上的具体对象。而主流的访问方式有两种,使用句柄访问和直接指针访问。





通过句柄访问最大的好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄数据中的实例数据指针,而reference本身不需要修改。

使用直接指针访问最大的好处就是速度快,节省了一次指针定位的时间开销。

对于Hotspot VM而言,虚拟机使用的是直接指针访问。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: