Java内存区域与内存溢出异常

第一部分:Java内存模型

一、Java内存模型



二、程序计数器

1、这是一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。

在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。

2、程序计数器是线程私有内存

Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间互不影响独立存储,这个是线程私有内存。

3、存储的是什么?

  • 如果线程正在执行的是一个Java方法,这个计数器存储的是正在执行的虚拟机字节码指令的地址;
  • 如果线程正在执行的是一个Native方法,这个计数器值为空,Nndefined。

4、是否会出现OOM异常?

此内存区域是Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域

三、Java虚拟机栈 Java virtual Machine Stack

也是线程私有内存,生命周期与线程相同。

1、Java虚拟机栈描述的是什么?

描述的是Java 方法执行的内存模型:每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等。

每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

1.1局部变量表存放了什么?

  • 存放了编译期可知的各种基本数据类型(boolean,byte,char,short,long,int,float,double)
  • 对象引用(reference 类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象句柄或其他与此对象相关的位置)
  • returnAddress类型(指向了一条字节码指令地址)

1.2分配的内存大小和分配时间

64位长度的long和double类型数据会占用2个局部变量空间(slot),其余数据类型只占用一个。

局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小

1.3定义的异常

  • StackOverFlowError异常:如果线程请求的栈深度大于虚拟机所允许的深度,抛出这个异常
  • OutOfMemoryError异常:如果虚拟机栈可以动态扩展(当前大部分虚拟机都可以动态扩展,虚拟机规范也允许固定长度的虚拟机栈),如果扩展时无法申请足够的内存,抛出这个异常

四、本地方法栈

这个内存区域与Java虚拟机栈发挥的作用相同,他们之间的区别是:

虚拟机栈为Java方法(也就是字节码)服务,本地方法栈为虚拟机使用到的Native方法服务,但是抛出的异常一样。

Sun hotspot虚拟机直接把本地方法栈和虚拟机栈合二为一。

五、Java堆

这是Java虚拟机所管理的内存中最大的一块。这个区域是所有线程共享的内存区域。在虚拟机启动时创建。

1、Java堆是做什么的?

存放对象的实例。几乎所有的对象都在这里分配内存,Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。但是实际上,随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术使得这一定论不是那么绝对

2、GC堆

  • Java对是垃圾收集器管理的主要区域,因此也被称为GC堆。
  • 从垃圾回收的角度来看,Java堆可以细分为新生代和老年代。
  • 从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
  • 无论如何划分,与存放内容无关,无论那个区域,存放的都是对象的实例,而划分的目的是:为了更好的回收内存,或者更快的分配内存

3、异常

Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的就可以。当前主流的虚拟机都是按照可扩展来实现的,通过-Xmx和-Xms控制。如果在堆中没有内存完成实例分配,并且堆也无法扩展时,抛出OOM异常.

六、方法区

这是线程共享的内存区域。

1、存放的是什么?

存放已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据

2、永久代

方法区被描述为堆的逻辑部分,但是别名是Non-heap,目的是为了和Java堆区分。
对于hotspot虚拟机,方法区又被称为永久代。两者并不等价,仅仅是因为Hotspot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区

这样hotspot的垃圾回收器可以像管理Java堆一样来管理这部分内存。

3、异常

Java虚拟机对方法区的规范很宽松,除了和Java一样不需要连续的内存和课一选择固定大小或者可扩展外,还可以选择不实现垃圾回收

当方法区无法满足内存分配需求时,抛出OOM异常

七、运行时常量池 Runtime Constant Pool

是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息之外,还有一向信息是常量池 (constant pool table),用于存放编译期生成的各种字面值常量和符号引用。

八、直接内存

不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域,但是被频繁的使用,也可能抛出OOM异常

第二部分:Hotspot 虚拟机对象探秘

一、对象的创建

对象创建分为三个阶段

阶段一:类加载检查

虚拟机遇到一条new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过,如果没有,那么执行响应的类加载过程。

阶段二:分配内存

对象所需的内存大小在类加载完成后便可确定,为对象分配内存等同于在Java堆上把一块确定大小的内存从Java堆中划分出来。

两种划分方法(分配方法)

1、指针碰撞(Bump the point)----Java堆中的内存是绝对规整的

Java堆中的内存是绝对完整的,那么所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那么,所分配的内存就是

把这个指针向空闲内存方向移动一个对象大小的距离即可

2、空闲列表(Free List)----Java堆中的内存不是规整的

Java堆中的内存不是规整的,也就是说,使用过的内存和没有使用过的内存相互交错,那么就没办法使用指针碰撞了,虚拟机就必须维护一个列表,记录那些块是可以使用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

选择那种分配方式是由Java堆是否规整决定的,Java堆是否规整又是由所采用的垃圾回收器是否带有压缩整理功能决定的。

所有使用Serial,ParNew等带有Compact过程的收集器采用的是指针碰撞

使用CMS这种基于Mark-Sweep算法的收集器,采用的是空闲列表

内存分配完成后,虚拟机将所有分配的内存空间初始化为零值(不包括对象头)

阶段三:必要设置

虚拟机对对象进行必要的设置,设置这个对象是那个类的实例,如何才能找到类的元数据信息,对象的哈希吗,对象的GC分代年龄等信息,这些信息存放在对象的对象头之中。

从虚拟机的视角,一个新的对象已经产生,但从Java程序的视角,对象的创建才刚刚开始,<init>方法还没有执行,所有的字段还都是零。

所以,一般执行new 指令之后,还需要执行<init>方法,把对象按照程序员的意愿进行初始化,这样,一个真正的对象才创建完成。

二、对象的内存布局 

Java对象在内存中的布局可以分为三块区域:对象头(Header),实例数据(Instance data),对象填充(Padding).

1、对象头(Header)

Hotspot虚拟机的对象头包括两部分信息:第一部分用于存储对象自身的运行时数据,入哈希碼,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等.这部分数据长度在32位和54位虚拟机上分别是32bit和64bit,官方称为Mark Word。

第二部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例,并不是所有的虚拟机实现都必须在对象数据上保留类型的指针。如果对象是一个数组,那么对象头还必须有一块用于记录数组长度的数据。

2、实例数据(Instance data)

这是真正有效的信息。,也就是程序代码中所定义的各种类型的字段内容,无论是继承自父类或者子类中定义的,都需要记录下来。

这部分的存储顺序会收到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。

3、对象填充(Padding).

这部分并不是必然存在,也没有特殊的含义,仅仅起着占位符的作用。

三、对象的访问定位

建立对象就是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中至规定了一个指向对象的

引用,并没有规定这个引用应该用何种方式去定位,访问堆中的对象的具体位置。所以对象的访问也是取决于虚拟机的实现。目前主流的访问方式有使用句柄和直接指针两种。

1、使用句柄

Java堆划分出一块内存来作为句柄池,reference中储存的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。


2、直接指针

Java堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,而reference中储存的直接就是对象的地址。


两种方式各有千秋,使用句柄的方式的好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收时,对象移动是很普遍的现象)时值会改变句柄中实例的指针,而reference本身不需要修改。

但是直接指针的方式的最大的好处就是速度更快,节省了一次指针定位的时间开销。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值