JVM 第2章 Java内存区域与内存溢出异常

可以参考下,这个写的简练
https://blog.csdn.net/seu_calvin/article/details/51404589

1 概述

对于java程序员来说,在虚拟机自动内存管理机制的帮助下,不需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出的问题。不过,一旦出现内存泄漏和内存溢出问题,如果不了解虚拟机是怎么使用内存的,那么排查错误会很困难。

2 运行时数据区域

java虚拟机在执行java程序的过程中会把内存划分若干个不同的数据区域。有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁。java虚拟机所管理的内存包括以下几个运行时数据区域:…

2.1程序计数器

1、功能:是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的执行。为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间的计数器互补影响,独立存储,称这类内存区域为“线程私有”的内存。

2、存放内容:如果线程执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值为空。

3、占用内存空间大小:在内存空间中占用较小的区域。

4、是否线程私有:是。

5、异常:此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2.2 java虚拟机栈

1、定义:虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部表量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

2、存放内容:栈帧用于存储局部表量表、操作数栈、动态链接、方法出口等信息。

3、占用内存空间大小:在内存空间中占用较小的区域。

4、是否线程私有:是。它的生命周期和线程相同。

5、异常:此区域包括两种异常情况:a、如果线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError异常。b、如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemeoryError异常。

6、经常有人把java内存分为堆内存和栈内存,这种分法比较粗糙,java内存区域的划分远比这复杂。这种划分方法的流行智能说明大多数程序员最关注的的、与对象内存分配关系最密切的内存区域是这两块。这里的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中局部变量表部分。

7、局部变量表存放了编译器可知的各种基本数据类型(boolean, byte, char, short, int, float, long, double)、对象引用类型(不等同于对象本身,可能是一个指向对象起始地址的引用指针)和returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(slot),其余类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小。

2.3 本地方法栈

1、功能:本地方法栈和虚拟机栈发挥的作用类似。二者之间的区别:虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用的Native方法服务。

2、异常:同java虚拟机栈。

2.4 java堆

1、定义:java堆是java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的内存区域,在虚拟机开启的时候创建。

2、功能:唯一作用就是存放对象实例(所有的实例对象和数组)。java虚拟机规范中的描述是:所有的对象实例和数组都要在对上分配,但随着JIT编译器的发展,所有对象的分配都在堆上也没有那么绝对了。

3、也称“GC堆”:java堆是垃圾收集器管理的主要区域。

4、分类:现在的收集器基本都采用分代收集算法,所以java堆可被分为新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度,线程共享的java堆可能划分出多个线程私有的分配缓冲区。无论如何划分,存储的依然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快的分配内存。

5、java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。在实现时,既可以实现成固定大小的,也可以是可扩展的,当前的主流虚拟机都是可扩展的。

6、异常:如果在堆中没有内存完成实例分配时,并且堆也无法在扩展时,会抛出OutOfMemoryError异常。

2.5 方法区

1、功能:方法区和java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

2、虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但它却有一个别名叫做“Non-Heap(非堆)”,目的是和java堆区分开来。

3、“永久代”:很多人把方法区称为“永久代”,但本质上二者不等价。这样称呼的原因是:HotSpot虚拟机设计团队把GC分代收集扩展至方法区,用永久代实现了方法区,以便垃圾收集器可以像管理java堆一样管理方法区。

4、方法区是否在内存上是连续的:不需要。大小可固定也可可扩展。还可以选择不实现垃圾收集。但并非数据进入了方法区就“永久”存在了。这区域主要针对常量池的回收和对类型的卸载,回收成绩令人满意,尤其是对类型的卸载。

5、内存回收目标:常量池的回收、对类型的卸载。当然,在JDK1.7后,字符串常量池已经从方法区移出。

2.6 运行时常量池

1、定义:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池,用于存放编译期生成的字面量、符号引用和翻译出来的直接引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

2、运行时常量池相对于class文件常量池的另外一个特征是具备动态性。Java常量产生的方式:
1)编译器产生(并非预置入Class文件中常量池的内存才能进入方法区运行时常量池)
2)运行期间产生(String类的intern()方法体现了这种特性)

3、异常:运行时常量池是方法区的一部分,也会受到方法区内存的限制,常量池无法申请到内存时会抛出OutOfMemoryError异常。

2.7 直接内存

1、直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但这部分内存也会被频繁使用。JDK1.4加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过存储在java堆中的DirectBuffer对象作为这块内存的引用进行操作。因为避免了在Java堆和Native堆中来回复制数据,所以可以显著提高性能。

2、直接内存不受Java堆大小的限制,但是会受到本机总内存大小以及处理器寻址空间的限制。

3、异常:服务器管理员在配置虚拟机参数时,根据实际内存设置-Xmx等参数信息,经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

3 HotSpot虚拟机对象

以常用的虚拟机HotSpot和常用的内区域Java堆为例,深入探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。

3.1 对象的创建

虚拟机遇到一条new指令时,先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。若没有,则先执行相应的类加载过程(第7章介绍)。

在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的认为就是把一块确定大小的内存从Java堆中划分出来。为对象划分内存的方式有2中:
1)若Java堆中的内存时绝对规整的,用过的内存放一边,空闲的内存放另一边,中间有一个指针作为分界点指示器,那么分配内存就是把指针向空闲空间那边挪动对象大小的距离,这种分配方式称为“指针碰撞”。
2)若Java堆中的内存不规整,已用过的内存和空间内存相互交错,虚拟机需要维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式成为“空闲列表”。
Java堆是否规整由垃圾收集器是否带有压缩整理功能决定。使用Serial, ParNew等带有Compact过程的收集器时,系统采用指针碰撞分配内存,而使用CMS这种基于Mark-Sweep算法的收集器时,采用空闲列表。

对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也不是线程安全的,可能出现正在给A对象分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决方案2种:
1)对分配内存空间的动作进行同步处理–虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
2)每个线程在Java堆中预先分配小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。线程在TLAB上分配内存,只有TLAB用完并分配新的TLAB时,才同步锁定。虚拟机是否使用TLAB,可通过-XX:+UseTLAB参数设定。

内存分配后,虚拟机将分配到的内存空间初始化为零值(不包括对象头),若使用TLAB,这一过程可以提前至TLAB分配时进行。该操作保证了对象的实例字段在Java代码中不赋值就能直接使用,程序能访问到这些字段的数据类型所对应的零值。

虚拟机要对对象进行对象头信息的设置,包括:该对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄。

从虚拟机角度来看,一个新对象到这时就产生了,但从Java程序角度看,对象创建才刚开始–方法还没执行,所有字段还为零。所以,执行new指令后会接着执行方法,把对象按照程序员的意愿初始化。

3.2 对象的内存布局

对象内存中存储的布局可以分为3块区域:对象头、实例数据和对齐填充。
在这里插入图片描述
类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。查找对象的元数据信息不一定通过对象本身,所以不是所有的虚拟机实现都必须在对象数据上保留类型指针。另外,如果对象是一个Java数组,那么对象头中还需要记录数组的长度,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。

实例数据:是对象真正存储的有效信息,也是程序代码中定义的各种类型的字段内容。无论是父类继承下来的,还是子类中定义的,都要记录下来。存储顺序受虚拟机分配策略参数和字段在Java源码中的定义顺序的影响。HotSpot虚拟机默认的分配策略是longs/doubles, ints, shorts/chars, bytes/booleans, oops(Ordinary Object Pointers普通对象指针),相同宽度的字段总是被分配到一起。在满足这个条件的情况下,父类中定义的变量会出现在子类的前面。若CompactFields参数为true,那么子类之中较窄的变量可以插入到父类变量的空隙之中。

对齐填充:并不是必然存在的,仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。对象头部分正好是8字节的倍数,因此对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3.3 对象的访问定位

java程序需要通过栈上的reference数据来操作java堆上的具体对象。通过引用定位、访问堆中对象的具体位置的主流的方式有2中:使用句柄、直接指针。

1)使用句柄:java堆中会有一块内存区域专门作为句柄池,reference中存储的就是对象的句柄地址。
在这里插入图片描述
好处:reference中存储的是句柄地址,在对象被移动(垃圾收集时移动对象)时,只会改变句柄中的实例数据指针,而reference本身不用改。

2)直接指针:reference存储的直接就是对象地址。
在这里插入图片描述
好处:访问对象速度快,因为它节省了一次指针定位的时间开销。HopSpot使用的访问定位方式是用直接指针访问对象。

4 实战:OutOfMemoryError异常

4.1 Java堆溢出

java堆用于存储对象实例,保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么当对象数列到达最大堆的容量限制后就会出现内存溢出(OOM)异常,异常堆栈会进一步提示"java heap space"。

将堆的最大值和最小值设置成一样即可避免堆自动扩展。

解决方法:确认内存中的对象是否是必要的,即到底是出现了内存泄漏还是内存溢出。
1)如果是内存泄漏,可用工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们,从而较准确定位出泄漏代码的位置。
2)如果不存在内存泄漏,即内存中的对象确实必须活着,那就检查虚拟机的堆参数(-Xmx, -Xms),与物理内存对比看是否可以调大,从代码上检查是否存在某些对象的声明周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

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

由于HotSpot不区分虚拟机栈和本地方法栈,所以本地方法栈-Xoss参数存在,但无效,栈容量只由-Xss参数设定。2种异常:
1)如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError异常。
2)虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。
2种异常存在重叠的地方:当栈空间无法分配时,到底是内存太小,还是已使用的栈空间太大。

单线程下,无论由于栈帧太大还是虚拟机栈容量太小,都抛出StackOverflowError异常。
多线程下,通过不断新建线程可以出现OutOfMemoryError异常。每个线程的栈分配的内存越大,越容易产生内存溢出。

操作系统分配给每个进程的内存都是有限制的,32位限制为2GB,虚拟机提供了控制java堆和方法区的内存大小的参数。2GB-最大堆容量(Xmx)-最大方法区容量(MaxPermSize),程序计数器占内存很小可忽略,如果虚拟机进程本身耗费内存不算其中,剩余的内存就被虚拟机栈和本地方法栈瓜分了。每个线程的栈分配的内存越大,可建立的线程数量就越少,越容易产生内存溢出。

StackOverflowError异常一般容易解决。如果出现内存溢出异常时,可通过减少最大堆和减少栈容量来换取更多的线程。java线程是映射到操作系统内核线程上的。

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

运行时常量池是方法区的一部分。String.intern()是一个Native方法,作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将次String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK1.6及之前的版本中,常量池分配在永久代内,可通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而限制常量池的容量。
运行时常量池溢出,在OutOfMememoryError后面跟着提示信息“PermGen space”,说明运行时常量池是方法区(HotSpot虚拟机中的永久代)的一部分。

方法区用于存放class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。同一个类文件,被不同的加载器加载也会视为不同的类。p58

4.4 本机直接内存溢出

DirectMemory容量可通过-XX:MaxDirectMemorySize指定,若不指定,则默认与java堆的最大值(-Xms)一样。p59

猜你喜欢

转载自blog.csdn.net/csdnlijingran/article/details/85084497