2.1 自动内存管理机制--Java内存区域与内存溢出异常

时间:2022-05-17 03:16:29

自动内存管理机制

第二章、Java内存区域与内存溢出异常

【虚拟机中内存如何划分,以及哪部分区域、什么样代码和操作会导致内存溢出、各区域内存溢出的原因】


一、运行时数据区域

  Java虚拟机所管理的内存包括以下几个运行时数据区域【虚拟机内存模型】:

2.1 自动内存管理机制--Java内存区域与内存溢出异常

1.程序计数器:

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

  如果线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数器的值为

  程序计数器这个内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError(内存溢出)情况的区域。

2.虚拟机栈

  Java虚拟机栈也是线程私有的,其生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成对应着一个栈桢在虚拟机中入栈到出栈的过程。

  有人会把虚拟机内存分为堆内存和栈内存,这种比较粗糙。其中的栈内存讲的就是虚拟机栈或者说是虚拟机栈中的局部变量表。局部变量表存放了编译器的各种基本数据类型、对象引用、returnAddress类型(指向字节码指令地的址)。其中64位的long和double类型数据占用两个局部变量空间(Slot),其余类型只占用一个。

  Java虚拟机规范中,①如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常;②如果虚拟机栈可以动态拓展,如果拓展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

3.本地方法栈

  本地方法站和虚拟机栈作用相似,虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法站则为虚拟机使用到的Native方法服务。和虚拟机栈一样,本地方法站也会抛出*Error和OutOfMemoryError异常。

4.Java堆

  Java堆是Java虚拟机管理的内存中最大的一块,是被所有线程共享的一块内存区域。在虚拟机启动时创建,唯一目的就是存放对象实例Java堆是垃圾收集器管理的主要区域,又称为GC堆,可以细分为新生代和老年代,可再细致划分为Eden空间、From Survivor空间、To Servivor空间。

  Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存可以完成对象实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

5.方法区

  方法区也是线程共享的一块内存区域,用于存储已被虚拟机加载类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机规范将方法去描述为堆的一个逻辑部分,并且对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可拓展外,还可以选择不实现垃圾收集。

  当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常

6.运行时常量池

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

  Java虚拟机对Class文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

  运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

  既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

7.直接内存

  直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。


二、HotSpot虚拟机对象

【内存中的数据如何创建、如何布局以及如何访问】

1.Java对象的创建

  第一步:【类加载检查】虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、初始化过,如果没有,那必须先执行相应的类加载过程【第七章】。

  第二步:【内存分配】对象所需内存的大小在类加载完成后便可以完全确定。

    内存分配方法:①指针碰撞:Java堆中内存是绝对规整的,所有用过的内存放在一边,没用过的内存放在另一边,分配内存仅仅是把指针向空闲空间那边移动一段与对象内存空间大小相等的距离。

           ②空闲列表:Java堆中内存不是规整的,虚拟机维护一个列表,记录内存使用情况。在分配的时候从列表找到一块足够大的空间划分给对象实例。

    解决并发创建对象问题:①对分配内存空间操作进行同步处理——CAS配上失败重试保证更新操作的原子性。②将内存分配动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存(本地线程分配缓冲)。

  第三步:【内存空间初始化】虚拟机需要将分配到的内存空间初始化为零值。这一步保证了对象的实例字段可以不赋值就可以直接使用。

  第四步:【必要设置】根据对象的对象头中的信息,虚拟机对对象进行必要的设置,例如该对象是哪个类的实例、如何找到类的元数据(数据的描述)信息、对象的哈希码、GC分代年龄等信息。

  此时,对于虚拟机来说Java对象的创建已经完成了,但是从Java程序来说,对象的创建才刚刚开始——init方法还未执行,所有字段还为0。把对象按照程序员的意愿进行初始化,这样一个对象才算完全产生出来。

2. 对象的内存布局

  对象在内存中存储的布局分为三个部分:对象头、实例数据、对齐填充

  第一部分【对象头】:对象头包括两部分信息:①用于存储对象自身的运行时数据,例如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。②另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是那个类的实例。

  第二部分【实例数据】:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的还是在子类中定义的,都需要记录下来,存储顺序收到虚拟机分配策略参数影响。

  第三部分【对齐填充】:对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的1倍或者2倍,当对象实例数据部分没有对齐时,通过对齐填充来补全。

3. 对象的访问定位

  上面讲了对象的创建和内存分配,这节将怎么使用对象,我们的Java程序需要通过虚拟机栈上的reference数据来操作堆上的具体对象。目前有两种访问方式:使用句柄和直接指针

  第一张【使用句柄】:Java堆中会划分一块内存作为句柄池,类似一个中间表,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据类型数据各自的具体地址信息。2.1 自动内存管理机制--Java内存区域与内存溢出异常

  第二种【直接指针】:Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference存储的就是对象地址。使用直接指针最大的好处就是速度快,节省了一次指针定位的时间开销。

2.1 自动内存管理机制--Java内存区域与内存溢出异常


三、内存溢出异常OutOfMemoryError(OOM)

  内存泄漏(Memory Leak):指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎没有太大影响,但是内存泄漏堆积的后果就是内存泄露。

  内存溢出(Memory Overflow):指程序申请内存时没有足够的内存供申请者使用。或者说,给了一块int类型的数据空间,但是要存储long类型的数据=》内存不够用。

1.Java堆溢出

  堆的最小值 :-Xms;堆的最大值:-Xmx;如果将堆的最小值和堆的最大值设置为一样的,可以防止堆自动扩展。

2.虚拟机栈和本地方法栈溢出

  栈容量设置:-Xss;

  如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error异常;

  如果虚拟机在拓展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常;

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

  方法去存放Class相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

  通过-XX:PermSize和-XX:MaxPermSize限制方法区大小。

4.本机直接内存溢出

  DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。如果发现OOM之后Dump文件很小,并且程序中直接或间接使用了NIO,那就可以考虑一下检查一下是不是直接内存溢出。

  【直接内存VS堆内存】

  ①直接内存申请空间耗费更高的性能,当频繁申请到一定量时更为明显。

  ②直接内存读写的性能优于普通堆内存,在多次读写的情况下差异明显。