java内存区域和内存溢出

1.运行时数据区

jvm在执行java程序时会把它所管理的内存划分为若干个不同的数据区域.这些区域有各自的用途,以及和创建和销毁时间,有的区域随着虚拟机进程的启动而一直存在,有的区域则是依赖用户线程的启动和结束而创建和销毁.

1.1 程序计数器

是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器.
由于java虚拟机的多线程是通过线程轮换,分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(一个内核)都只会执行一条线程中的指令.因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,称这类区域为"线程私有".

1.2 java虚拟机栈

与程序计数器一样,java虚拟机栈也是线程私有的,生命周期与线程相同.虚拟机栈描述的是java方法执行的线程内存模型:每个方法被执行的时候,java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息.每一个方法在被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程.
局部变量表存放了编译期可知的各种jvm基本数据类型,对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址).
这个内存区域有两类异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果java虚拟机栈容量可以动态扩展,当展扩展时无法申请到足够的内存会抛出OOM.

1.3 本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用的本地方法服务.
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OOM.

1.4 java堆

java堆是虚拟机所管理内存中最大的一块.java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建.此内存区域的唯一目的就是存放对象实例.
在十年之前(以G1收集器的出现为分界),作为业界绝对主流的HotSpot虚拟机,
它内部的垃圾收集器全部都基于"经典分代"来设计,需要新生代,老年代收集器搭配才能工作,到了今天,垃圾收集技术与十年前已不可同日而语,HotSpot里面也出现了不采用分代设计的新垃圾收集器.
java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放.
当前主流的java虚拟机都是按照可扩展来实现的,如果在java堆中没有内存完成实例分配,并且堆也无法再扩展时,jvm会抛出OOM.

1.5 方法区

方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据.
永久代,当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得HotSpot的垃圾收集器能够像管理java堆一样管理这部分内存,但这种设计导致了java应用更容易遇到内存溢出的问题(永久代内存有上限,即使不设置也有默认大小),到了JDK8,完全废弃了永久代的概念,改用在本地内存中实现的元空间来代替.
如果方法区无法满足新的内存分配需求时,将抛出OOM.

1.6 运行时常量池

运行时常量池是方法区的一部分.String类的intern()方法联系到常量池的使用.
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OOM.

1.7 直接内存

直接内存并不是虚拟机运行时数据区的一部分,
在JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数直接分配堆外内存,然后通过存储在java堆里面的DirectByteBuffer对象,作为这块内存的引用进行操作.这样能在一些场景中显著提高性能,因为避免了在java堆和Native堆中来回复制数据.
在配置虚拟机参数时,若忽略掉直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OOM.

2.对象的创建

java创建对象通常(例外:复制,反序列化)仅仅是一个new关键字而已,而虚拟机中对象(不包括数组和Class对象等)的创建过程:

  1. 当java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化过,如果没有,如果没有那就必须要执行相应的类加载过程,类加载检查通过后,接下来虚拟机为新生对象分配内存
  2. 将分配到的内存空间全都初始化为零,
  3. 接下来jvm还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息.根据虚拟机当前运行状态的不同,如是否启用偏向锁等.
  4. 上面的工作完成之后,熊虚拟机的视角来看,一个新的对象产生了,但是java程序的角度看来,对象创建才刚刚开始–构造函数.

2.1 对象的内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头,实例数据和对齐填充.

对象头部分包含两类信息,
第一部分是用于存储对象自身的运行时数据,如哈希吗,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等
第二部分是类型指针(确定该对象是那个类的实例),此外,如果对象是一个java数组,那在对象头中还必须有一块用于记录数组长度的数据.

实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在之类中定义的字段都必须记录起来.

对齐填充,仅仅起到占位符的作用,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说任何对象的大小都必须是8的整数倍.对象头已经被设置为8字节的倍数.

2.2 对象的访问定位

我们的java程序会通过栈上的reference数据来操作堆上的具体对象.reference类型只是一个指向对象的引用,引用主要通过句柄和直接指针的方式进行访问,

  • 若使用句柄访问的话,java堆将可能划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,如下图所示.
  • 若使用直接指针访问的话,java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就少一次间接访问的开销.

在这里插入图片描述
这两种对象访问方式各有优势,使用句柄访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时,只会改变句柄中的实例数据指针,而reference本身不需要被修改.
使用直接指针来访问的最大好处就是速度快,它节省了一次指针定位的时间开销(因为对象访问在java中非常频繁).

3.OOM异常

除了程序计数器,虚拟机的其他几个运行时区域都有可能发生OOM

3.1 java堆溢出

不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清楚这些对象,创建对象所需的内存触及最大堆的容量限制后就会产生内存溢出异常.
出现OOM后可通过内存堆转储快照进行分析,分清楚到底是出现了内存泄漏还是内存溢出
若是内存泄漏,在使用Eclipse时可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径,与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以准确定位到这些对象创建的位置,进而找出产生内存泄露的代码的具体位置.
若不是内存泄漏,那就看jvm堆参数是否还有向上调整的空间.再从代码上检查是否存在某些对象生命周期过长,持有状态时间过长,存储结构设计不合理等情况,尽量减少程序运行期的内存消耗.

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

无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配时,HotSpot虚拟机抛出的都是StackOverflowError

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

String:intern()是一个本地方法,如果字符串常量池中已经包含一个等于此String对象的字符串,则返回这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此对象的引用.
在JDK6之前的虚拟机,常量池被分配在永久代中,从JDK7起,常量池被分配在了java堆中
方法区的主要职责是用于存放类型的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等.若要对方法区内存做溢出测试,就可以在运行时产生大量的类去填满方法区,直到溢出,具体可借助CGLib直接操作字节码运行时生成大量的类,当前的很多主流框架,如Spring,Hibernate对类进行增强时,都会使用到CGLib这类字节码技术.
在这里插入图片描述

3.4 本机直接内存溢出

由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用是NIO),那就可以考虑重点检查一下直接内存方面的原因了.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值