JAVA内存区域与内存溢出异常(参考书籍:深入理解Java虚拟机 周志明 著 机械工业出版社)

概述

Java与C/C程序不同,Java开发人员不需要考虑每一个被new的对象的delete/free问题,这些问题交给Java虚拟机来管理,因此,一旦出现了内存的溢出和泄露,如果不了解虚拟机的工作机制就需要无法排查错误。

1.运行时的数据区域

数据区可以分为四大块:方法区、堆、栈(包括虚拟机栈和本地方法栈)、程序计数器。下面一一介绍:

图片来源:博客园   作者: fengbs 《JAVA运行时数据区域》  

https://www.cnblogs.com/fengbs/p/7029013.html

程序计数器:
        1. 线程私有
        2. 它是一块较小的内存空间,可以看做行号指示器,也就是说它作用是取下一条执行的字节码指令(如分支循环跳转异常处理线程恢复等等)。每一条线程对应一个独立的程序计数器,因此各个线程计数器之间互不影响独立存储,因此它是“线程私有的”。
        3. 执行Java方法:计数器记录正在执行的虚拟机字节码指令的地址
            执行Native方法:计数器值为Undefined
        4. 因此,它是唯一一个没有规定任何OutOfMemoryError的区域

Java虚拟机栈:
        1. 线程私有

       2. 他描述的是Java方法执行的内存模型,每一个方法执行的时候会创建一个栈帧(Stack frame),它存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直到执行,就有一个栈帧在虚拟机中入栈和出栈。
       3. 局部变量表:存储编译期间可知的各种基本数据类型(boolean、byte、char、long、double)和对象引用(reference类型,对象的引用指针)类型。个人认为也就是存储JAVA类代码“上面”的数据声明,暂时不知道对不对,日后再说。其中double和long占用两个局部变量空间(Slot),其余的数据类型只占用一个。这个表需要多大内存呢?这在编译期间决定。因此一个方法进入时,分配多大的局部变量空间是确定的。
       4. 这个区域有两种异常:如果线程请求的栈深度大于虚拟机允许的深度,抛出StackOverflowError;如果虚拟机支持动态扩展,但是扩展也无法满足请求的内存的时候,会抛出OutOfMemoryError异常

本地方法栈:

        与虚拟机栈作用相似,区别在于它为虚拟机使用到的native方法服务,而不是Java方法。
        他会抛出StackOverflowError和OutOfMemoryError。

Java堆

        这是JVM所管理的内存中最大的一块,被所有线程共享,虚拟机启动时创建。
        作用:存放对象实力,所有的对象实例在这里分配内存。
        Java堆可以位于物理上不连续的内存,但是需要逻辑上连续。Java堆可以细分为:新生代和老年代,再细致一点可以分为Eden空间、From Survivor空间、To Survivor空间等。但是他还可以划分出某些多个线程私有的分配缓冲区。这是垃圾收集器管理的主要区域,因此JAVA堆又被叫做GC堆(Garbage Collected Heap),具体在下一章详细介绍

方法区

        它也是所有线程共享的内存区域,虽然JVM规范把它描述为堆的逻辑部分,但是它的名字叫做Non-heap,与Java堆区分。
        作用:存放被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码。
        和Java堆一样,可以可以位于物理上不连续的内存,但是需要逻辑上连续。这部分的垃圾回收针对常量池回收和类型的卸载,条件相当苛刻,但这是必要的。当它的内存分配需求没有得到满足的时候会抛出OutOfMemoryError异常。

运行时常量池(Runtime Constant Pool)
        这是方法区的一部分,因此也会抛出OutOfMemoryError.
        Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息便是常量池(Constant Pool table),它用来存放编译器生成的各种字面量和符号的引用,它们在类被加载后进入方法区的运行时常量池中存放
        JVM规范对Class文件的每一部分的格式都有严格的规定,但是唯独对运行时常量池没有做任何细节的要求,不同的虚拟机可以按照自己的实际需求来实现这个内存区域。
        它具备动态性,常量不仅可以在编译期间通过方法区进入运行时常量池,运行期间也可以把新的常量放进池中,很常见的一个便是String类的intern()方法。

直接内存(Direct Memory)
        注意!!这不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,他也会抛出OutOfMemoryError。
        它的内存分配不会受到Java堆的大小限制,但是他会受到本机总内存包括Ram和Swap区或者分页大小的限制。服务器管理员在分配虚拟机参数的时候容易忽略直接内存,是的各个内存区域总和大于无力内存限制,从而导致动态扩展的时候出现OutOfMemoryError。


HotSpot虚拟机与对象

        现在我们大致知道了JVM中内存大概有哪些东西以及分别是干什么的。下面以常用虚拟机HotSpot和常用内存区域Java堆为例,看看如何在Java堆中分配、布局、访问一个对象

对象的创建
        虚拟机遇到一个new关键字的时候,首先检查这个指令的参数,也就是类名能否在常量池中定位到这个类的符号引用,然后检查这个类是否被加载解析和初始化过,如果有就先执行相应类的加载过程。后面将会详细探讨
        紧接着,虚拟机为新生对象分配内存(一个对象需要多大内存,在这个类被加载完成后就确定了),分配内存的过程相当于把一块固定大小的内存从Java heap中划分出来。这个过程,如果Java堆内存是规整的(一边是用过的内存,一边是空闲的),那么很简单,只需要把那个分界点指针向空闲区挪动这个类那么大的大小的距离,这个过程叫做指针碰撞(Bump the pointer)。但是如果堆内存不规整,那就复杂了,那样的话,虚拟机就要多干一件事——记录一个表,记录哪些内存是可以用的,分配的时候查查这个表,看看哪里的内存可以拿,这个分配方式叫做“空闲列表(Free List)”。
        用哪种方式取决于堆内存是否规整,堆内存是否规整取决于采用的垃圾收集器是否带有压缩整理的功能。如果收集器是Serial、ParNew等带有Compact过程的收集器,采用的是指针碰撞法,而使用CMS这种基于Mark-Sweep算法的收集器,常采用空闲列表。
        以上介绍了划分内存的过程,还有一个需要考虑的问题是,指针碰撞是线程安全的吗?在并发状态下,指针正在给A分配空间,如果B也要求分配内存呢?这个问题有两种解决方法。1. 对分配空间的动作进行同步处理(虚拟机采用CAS和失败重试的方法保证操作的原子性)。2. 把内存分配的动作按照线程划分在不同的空间中,即每个线程在Java堆中预先分配一小块内存,叫做本地线程分配缓冲(Thread Local Allocation Buffer)。哪个线程要分配,就在谁的TLAB上分配。
        接下来,虚拟机吧分配到的内存初始化为0(不包括对象头),如果使用了TALB分配,这个过程在TLAB分配的时候进行。然后虚拟机对对象进行设置,如它是谁的实例?对象的哈希码?对象的垃圾分代等,这些信息都在对象头中。
        好了,虚拟机里面已经有了你new的对象了,但是这个对象所有字段都是0,所以Java程序还需要执行init方法,把这个对象按照编程者的想法进行初始化,这样一个对象就诞生了!!

对象的布局
        对象在内存中有三块:对象头(Header),实例数据(Instance Data),对齐填充(Padding)。下面一一介绍。
        对象头:第一部分放自身运行时数据,哈希码、垃圾回收分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,另外一部分是存放一个类型指针,指向它的类元数据,这样他就知道了这个对象是哪个类产生的实例。如果这个东西是个数组,他还要有一块区域存储数组长度的记录,因为普通对象可以通过第二部分的类型指针知道这个对象的大小,但是数组不可以。
        实例数据部分:包括从父类继承过来的数据以及子类自己定义的数据。他们的存出顺序受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中被定义的顺序的影响。以HotSpot虚拟机为例,它的默认分配策略为longs/doubles,ints,shorts/chars,bytes、booleans,oops(Ordinary Object Pointers),其实就是把相同宽度的字段放在一起,并且父类变量在前,子类在后。
        对齐填充部分:这不一定存在。它没什么用,仅仅拿来作为占位符,所以才叫它对齐填充部分。因为HotSpot虚拟机要求对象的长度必须是8字节的整数倍,而对象头刚好是八字节的倍数,因此如果第二部分的示例数据部分不是8字节整数倍,就拿第三部分的Padding来填充对齐。
对象的访问定位
        我们应该还记得,之前的介绍的栈,具体地说是Java虚拟机栈中的本地变量表中有一个reference,它是用来存储对象的引用的,在JAVA虚拟机规范中只规定看了这是一个指向对象的引用,但是没有定义这个引用如何找到并且访问这个对象的具体位置,所以这两步不同的虚拟机有不同的实现方式。主流的话,有两种方式来实现:
        句柄访问:Java堆会留出一块内存来作为句柄池,顾名思义,这里存放所有实例对象的句柄指针,两个,一个到对象实例数据(实例池)的指针,还有一个到对象类型数据(方法区)的指针

        直接指针访问:这个就简单了,它直接指向位于方法区的对象类型数据

        使用句柄访问的优势:reference存储的是稳定的句柄地址,对象被移动的时候,只会改变其中的实例数据指针,reference本身不变。
        使用直接指针访问的优势:速度快,少了一次指针定位的时间开销。积少成多,速度就是快!
        就HotSpot而言,它是用直接指针访问的。





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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值