Java内存区域与内存溢出非常(自动内存管理)

打印 上一主题 下一主题

主题 1019|帖子 1019|积分 3057

        序言:Java与C++之间有一堵由内存动态分配和垃圾网络技能所围成的高墙,墙外面的人想进去,墙里面的人却想出来。
1.1概述

        对于从事C、C++程序开发的开发人员来说,在内存管理范畴,他们既是拥有最高权力的“皇帝”,又是从事最底子工作的劳动人民——既拥有每一个对象的“全部权”,又担负着每一个对象生命从开始到终结的维护责任。对于Java程序员来说,在假造机自动内存管理机制的帮助下,不再需要为每一个new操纵去写配对的delete/free代码,不容易出现内存走漏和内存溢出问题,看起来由假造机管理内存一切都很美好。不过,也正是因为Java程序员把控制内存的权力交给了Java假造机,一旦出现内存走漏和溢出方面的问题,如果不相识假造机是怎样使用内存的,那排查错误、修正问题将会成为一项非常艰难的工作。
1.2运行时数据区域

       Java假造机在实行Java程序的过程中会把它所管理的内存划分为若干个差别的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着假造机进程的启动而一直存在,有些区域则是依靠用户线程的启动和结束而创建和销毁。根据《Java假造机规范》的规定,Java假造机所管理的内存将会包括以下几个运行时数据区域,如图所示。
1.2.1程序计数器


       程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所实行的字节码的行号指示器。在Java假造机的概念模型里,字节码表明器工作时就是通过改变这个计数器的值来选取下一条需要实行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、非常处理、线程规复等底子功能都需要依靠这个计数器来完成。由于Java假造机的多线程是通过线程轮番切换、分配处理器实行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会实行一条线程中的指令。因此,为了线程切换后能规复到正确的实行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。如果线程正在实行的是一个Java方法,这个计数器记录的是正在实行的假造机字节码指令的地址;如果正在实行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java假造机规范》中没有规定任何OutOfMemoryError情况的区域。 
1.2.2 Java假造机栈

        与程序计数器一样,Java假造机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。假造机栈描述的是Java方法实行的线程内存模型:每个方法被实行的时候,Java假造机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操纵数栈、动态连接、方法出口等信息。每一个方法被调用直至实行完毕的过程,就对应着一个栈帧在假造机栈中从入栈到出栈的过程。经常有人把Java内存区域笼统地划分为堆内存(Heap)和栈内存(Stack),这种划分方式直接继续自传统的C、C++程序的内存结构结构,在Java语言里就显得有些粗糙了,实际的内存区域划分要比这更复杂。不过这种划分方式的盛行也间接阐明了程序员最关注的、与对象内存分配关系最密切的区域是“堆”和“栈”两块。其中,“堆”在稍后专门报告,而“栈”通常就是指这里讲的假造机栈,大概更多的情况下只是指假造机栈中局部变量表部门。
        局部变量表存放了编译期可知的各种Java假造机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄大概其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,别的的数据类型只占用一个。局部变量表所需的内存空间在编译期间完身分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。请读者注意,这里说的“大小”是指变量槽的数目,假造机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,大概更多)来实现一个变量槽,这是完全由具体的假造机实现自行决定的事情。
        在《Java假造机规范》中,对这个内存区域规定了两类非常状态:如果线程请求的栈深度大于假造机所允许的深度,将抛出StackOverflowError非常;如果Java假造机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError非常。
1.2.3本地方法栈

        本地方法栈(Native Method Stacks)与假造机栈所发挥的作用是非常相似的,其区别只是假造机栈为假造机实行Java方法(也就是字节码)服务,而本地方法栈则是为假造机使用到的本地(Native)方法服务。《Java假造机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的假造机可以根据需要自由实现它,甚至有的Java假造机(譬如Hot-Spot假造机)直接就把本地方法栈和假造机栈合二为一。与假造机栈一样,本地方法栈也会在栈深度溢出大概栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError非常。
1.2.4Java堆

        对于Java应用程序来说,Java堆(Java Heap)是假造机所管理的内存中最大的一块。Java堆是被全部线程共享的一块内存区域,在假造机启动时创建。此内存区域的唯一目的就是存放对象实例,Java天下里“几乎”全部的对象实例都在这里分配内存。在《Java假造机规范》中对Java堆的描述是:“全部的对象实例以及数组都应当在堆上分配”,而这里笔者写的“几乎”是指从实现角度来看,随着Java语言的发展,现在已经能看到些许迹象表嫡后可能出现值类型的支持,即使只考虑现在,由于即时编译技能的进步,尤其是逃逸分析技能的日渐强盛,栈上分配、标量更换优化本领已经导致一些玄妙的变革寂静发生,所以说Java对象实例都分配在堆上也徐徐变得不是那么绝对了。
        Java堆是垃圾网络器管理的内存区域,因此一些资料中它也被称作“GC堆”(Garbage Collected Heap,幸好国内没翻译成“垃圾堆”)。从接纳内存的角度看,由于现代垃圾网络器大部门都是基于分代网络理论设计的,所以Java堆中经常会出现“新生代”“老年代”“永久代”“Eden空间”“From Survivor空间”“To Survivor空间”等名词,这些概念在本书后续章节中还会反复登场亮相,在这里笔者想先阐明的是这些区域划分仅仅是一部门垃圾网络器的共同特性大概说设计风格而已,而非某个Java假造机具体实现的固有内存结构,更不是《Java假造机规范》里对Java堆的进一步细致划分。不少资料上经常写着雷同于“Java假造机的堆内存分为新生代、老年代、永久代、Eden、Survivor……”这样的内容。在十年之前(以G1网络器的出现为分界),作为业界绝对主流的HotSpot假造机,它内部的垃圾网络器全部都基于“经典分代”[插图]来设计,需要新生代、老年代网络器搭配才气工作,在这种配景下,上述说法还算是不会产生太大歧义。但是到了本日,垃圾网络器技能与十年前已不可同日而语,HotSpot里面也出现了不接纳分代设计的新垃圾网络器,再按照上面的提法就有许多需要商讨的地方了。
        如果从分配内存的角度看,全部线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的服从。不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地接纳内存,大概更快地分配内存。在本章中,我们仅仅针对内存区域的作用进行讨论,Java堆中的上述各个区域的分配、接纳等细节将会是下一篇的主题。
        根据《Java假造机规范》的规定,Java堆可以处于物理上不一连的内存空间中,但在逻辑上它应该被视为一连的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都一连存放。但对于大对象(典范的如数组对象),多数假造机实现出于实现简朴、存储高效的考虑,很可能会要求一连的内存空间。Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java假造机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,而且堆也无法再扩展时,Java假造机将会抛出OutOfMemoryError非常。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

圆咕噜咕噜

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表