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

11 篇文章 0 订阅

Java虚拟机在执行java程序时会把它所管理的内存会分为若干个不同的数据区域,不同的区域在内存不足时会抛出不同的异常。

1 运行时数据区域的划分

1.1 程序计数器

  • 程序计数器(Program Counter Register)是一块比较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器;PCR为线程私有内存,程序计数器是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域。

1.2 JVM栈

  • 虚拟机栈(Java Virtual Machine Stacks)描述的是Java方法执行的内存模型:每个方法在在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法接口等信息。
  • 每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈出栈的过程。
  • Java虚拟机栈也是线程私有,它的生命周期与线程相同。
  • Java内存区常分为堆内存(Heap)和栈内存(Stack)。
  • OOM情况:
    线程请求的栈深度>虚拟机所运行的最大深度;
    虚拟机动态扩展时无法申请到足够的内存

1.3本地方法栈

  • 与虚拟机栈作用很相似,区别是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则是为虚拟机用到的Native方法服务。和虚拟机栈一样可能抛出StackOverflowError和OutOfMemoryError异常。

1.4Java堆

  • Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。主要存放对象实例。
  • Java堆是垃圾收集器管理的主要区域,其可细分为新生代和老年代。如果在堆中没有内存完成实例分配,并且也无法再扩展时,会抛出OutOfMemoryError异常。
  • 所有的对象实例以及数组都应当在堆上分配。
  • Java可以在物理上处于不连续的内存空间,但在逻辑上他应该被视为连续的。
  • 如果从内存分配的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区,以提升对象分配时的效率。

1.5 方法区

  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 也称为永久代(Permanent Generation)但随着Java8的到来,已放弃永久代改为采用Native Memory来实现方法区的规划。
    此区域回收目标主要是针对常量池的回收和对类型的卸载。

1.6 运行时常亮池

  • 是方法区的一部分
  • 用于存放编译期生成的各种字面量与符号复用,这部分内容将在类加载后存放到方法区的运行时常亮池中。

1.7 直接内存

  • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。
  • 能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
    直接内存的分配不会受到Java堆大小的限制,但会收到本机总内存(RAM以及SWAP/分页文件)大小以及处理器寻址空间的限制。
  • 设置Xmx等参数信息时注意不能忽略直接内存,不然会引起OOM。

2 HotSpot虚拟机对象探秘

2.1 对象的创建

  • 1.检查: 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过,如果没有,先执行相应的类加载过程,接下来为新生对象分配内存
  • 2.分配内存: 为对象分配空间的任务等同于把一块确定大小的内存从堆中划分出来。(两种方式由java堆是否规整决定《–由采用的垃圾收集器是否带有压缩功能决定)
    • 指针碰撞(堆中内存绝对规整)
    • 不规整,空闲和使用的内存相互交错,JVM必须维护一个列表,记录哪些内存块是可用的,分配时候找到一块足够大的分配给对象实例,这种分配方式被称为‘空闲列表’。
  • 并发创建对象(非线程安全)
    • 方案1:对分配内存空间的动作进行同步处理–实际上JVM采用CAS配上失败重试的方式保证更新操作的原子性。
    • 方案2:把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配心得TLAB时,才需要同步锁定。
    • 是否使用TLAB可以通过-XX:+/-UseTLAB参数来设定。
  • 3.初始化: 内存分配完成后,需要将分配到的内存空间都初始化为零(不包括对象头:存放对象的各种信息),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。用以保证对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
  • 4.设置参数: Java虚拟机还要对对象进行必要的设置,
  • 5.构造函数: 从java程序视角看,对象创建才刚刚开始–构造函数。按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全构造出来。

2.2 对象的内存布局

对象在内存中存储的布局可以分为三个区域:对象头(Header),实例数据(Instance Data) 和对齐填充(Padding)
  • Header:
    • 第一类信息:存储对象自身的运行时数据(Mark Word):hashCode,Gc age,线程持有的锁,偏向线程ID,偏向事件戳等。
    • 第二类信息:类型指针(对象指向它的类元数据的指针,通过其确定是哪个类的实例)。
  • 实例数据:
    • 存储代码中所定义的各种类型的字段内容,HotSpot虚拟机默认的分配策略为longs/doubles,ints,shorts/chars,bytes/booleans,oops(ordinary Object Pointers)(相同宽度的字段被分在一起),在这个前提下,父类中定义的变量会出现在子类之前。如果CompactFields参数值为true,那么子类之中较窄的变量也可能会插入父类变量的间隙中。
  • Padding:
    • 不是必然存在的,仅仅是占位符 HotSpotVm的自动内存管理系统要求对象的起始地址是8字节的整数倍,就是对象的大小必须是8字节的整数倍,而header是8的整数倍,因此当对象实例没有对齐,就通过对齐填充来补全。

2.3 对象的访问定位

#### java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在JVM规范中只规定了一个指向对象的引用,并未定义这个引用如何定位和访问具体位置,所以对象访问方式由具体虚拟机实现而定。主要有句柄和直接指针两种方式

  • 句柄访问
    • 堆中划分一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息
    • 优势:在对象被移动(GC中移动对象很常见)时只会改变句柄的实例数据指针,reference本身不需要修改。
  • 直接指针
    • 堆对象的布局中必须考虑如何放置访问类型数据的相关信息,reference中存储的直接是对象地址。
    • 优势:速度快,节省了一次指针定位的时间开销,对象的访问在java中非常频繁,因此可以减少执行成本(Sun HotSpot虚拟机主要使用)。

2.4 实战:OutOfMemoryError

2.4.1 Java溢出

  • 在对象数量到达最大堆得容量限制后就会产生内存溢出异常:java.lang.OutOfMemoryError:Java heap space。
  • 解决方法:一般的手段先通过内存映像分析工具(Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析,目的是确认内存中的对象是否是必要的(先分清楚是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow))。
    • 内存泄漏(堆内存无法释放造成系统内存的浪费):查看泄漏对象到GC Roots的引用链。就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收他们的,就可以准备定位泄漏代码的位置。
    • 如果不存在泄漏,即所有对象都必须存活,检查虚拟机的堆参数(-Xmx与-Xms)与物理内存对比是否需要上调。
    • 从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

2.4.2 虚拟机栈和本地方法溢出

  • HotSpot不分区虚拟机栈和本地方法栈,因此栈容量只由-Xss参数设定
  • StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的最大深度(主)(阅读错误堆栈)
  • OutOfMemoryError:如果虚拟机在拓展栈时无法申请到足够的内存空间。操作系统分配给每个进程的内存是有限制的,虚拟机提供参数来控制Java堆和方法区的内存的最大值。
    • 剩余的内存为=2GB(操作系统限制)-Xmx(堆最大容量)-MaxPermSize(最大方法区容量)
    • 每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽
    • 如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量来换取更多的线程
    • 无论是由于栈内存太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机跑出的都是StackOverError。

2.4.3 方法区和运行时常量池溢出

  • String.intern()是一个Native方法,它的作用是:如果字符串常量池已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
  • JDK1.6 intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用
  • JDK1.7 intern()方法不会再复制实例,只是在常量池中记录首次出现的实例引用
  • 方法区用于存放Class的相关信息:类名,访问修饰符,常量池,字段描述,方法描述。
  • 方法区溢出(大量JSP或者动态产生JSP文件的应用,基于OSGI的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类))也是一种常见的内存溢出异常,一个类要被GC回收掉,判断条件比较苛刻。在经常动态生成大量Class的应用中,需要注意类的回收

2.4.4 本机内存直接溢出

DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,默认与-Xmx一样
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值