深入理解Java内存区域与内存溢出异常

深入理解Java内存区域与内存溢出异常

前言
  1. Java开发中,我们不需要像在进行C/C++开发那样,需要自己去写delete/free来释放申请的内存,在Java中内存是交给虚拟机管理,凡事都是两面的,因为我们把内存控制权交给了Java虚拟机,一旦出现了内存泄漏和溢出问题,如果我们不了解虚拟机时怎么使用的内存,那么排查问题将会非常困难。
运行时的数据区域

Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。
这里写图片描述

Java堆
  1. Java堆是被所有的线程共享的内存区域,在虚拟机启动的时候创建,此内存区域唯一的目的是存放对象实例,几乎所有的对象都在这里分配内存。Java虚拟机规范中描述的是所有的对象实例以及数组都在堆上分配。
  2. Java堆是垃圾管理器主要管理的区域,
  3. 根据Java虚拟机规范中规定,Java堆可以处在物理上不连续的内存区域,只要逻辑上是连续的就可以。就像我们的磁盘一样,在实现的时候既可以实现成固定大小的,也可以是可扩展的。如果在堆中没有内存用于实例分配,且堆也没有办法扩展时,将会抛出OutOfMemoryError异常。
Java中的方法区
  1. 方法区与堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆)。目的是把它与Java堆分开来。

  2. java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。

  3. 相对而言,垃圾收集行为在这个区域是比较少出现的。但非数据进入了方法区就如同永久代这个名字一样,永久的存在了,这个区域的内存回收目标是正对常量池的回收和堆类型的卸载。

程序计数器
  1. 程序计数器是一块较小的内存区域,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方法去实现),字节码解释器工作时就是通过改变这个计数器的值来选去下一条执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都依赖这个计数器来完成。
Java虚拟机栈
  1. 与程序计数器一样,Java虚拟机栈(Java virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈道出栈的过程。

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

本地方法栈
  1. 本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用非常相似,它们之间的区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务,在虚拟机规范中对本地方法栈中方法使用的语言,使用方法与数据结构没有强制规定,因此具体的虚拟机可以自由的实现它。甚至有的虚拟机(比如Sun HotSpot虚拟机)直接把本地方法栈和虚拟机合二为一。与虚拟机一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryErrory异常。
运行时常量池
  1. 运行常量池是方法区的一部分,Class文件中除了有类型的版本,字段,方法,接口等描叙信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

  2. Java虚拟机对Class文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可,装载,和执行,但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需求来实现这个内存区域,不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

直接内存
  1. 直接内存(Direct Memory)并不是虚拟机运行时数据的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常出现。
  2. 在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存区域的引用进行操作。这样能在一些场景中显著的提高性能,因为避免了在Java堆和Native堆之间来回复制数据。
  3. 本机直接内存不会受Java堆大小的限制,但是,既然是内存,那么还是会受到本机总的内存(包块RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理人员在配置虚拟机参数时,会根据实际内存设置-Xmx等信息参数信息,但经常忽略直接内存,使的各个内存区域总和大于物理内存限制从而导致动态扩展时出现OutOfMemoryError异常。
对象的创建
  1. Java是面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建出来,在语言层面,创建对象通常仅仅就是一个new关键字而已,而在虚拟机中,对象(仅仅是普通Java对象,不包括数组和class对象)的创建过程又是怎么样呢?
  2. 虚拟机遇到一条new指令的时候,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析,和初始化,如果没有,那必须先执行相应的类加载过程。
  3. 在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完毕过后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java 堆中划分出来,假设Java 堆中的内存是绝对规整的,所有用哟的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲的空间那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞。
  4. 如果Java堆中的内存并不是规整的,已使用的内存和空闲内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个队表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值