把JVM揉碎了,碾成末的去理解(二)

JAVA内存区域与内存溢出异常

未曾开发过C和C++以及其他语言,听大神们将,C和C++都需要自己对内存进行回收,否则会遇到内存泄露和溢出的情况。据说java是综合了两者的优点,不需要自己处理内存回收的动作,这些都是JVM帮我们出处理,在编码层基本解除不到内存回收,但是在开发和生产过程中经常用到有内存溢出(OutOfMemoryError)的情况,来一探究竟。
JVM运行时的数据区有几个?哪些区域会发生内存溢出呢?哪些是跟随虚拟机的生命周期有关呢?哪些跟线程的生命周期有关?

这几个问号是面试的时候可能会问到的,你如果能回答的非常完美,则会给你增加不少分。

程序计数器

程序计数器(Program Counter Register )也称PC寄存器, 是一块较小的内存空间,当前程序执行字节码的行号指示器(分支、循环、跳转、异常处理、线程恢复)。支持多线程同时执行,每一条虚拟机程序都有自己的PC 寄存器,各条线程之间计数器互不影响,独立存储,是属于"线程私有"的内存,在任意时刻,一条java虚拟机线程只会执行一个方法的代码,这个被线程执行的方法称该线程的当前方法(Current Method)。 如果这个方法不是 native 的,那 PC 寄存器就保存 Java 虚拟机正在执行的字节码指令的地址,如果该方法是 native 的,那 PC 寄存器的值是 undefined。 PC 寄存器的容量至少应当能保存一个 returnAddress(指向一条字节指令的地址) 类型的数据或者一个与平台相关的本地指针的值。此区域是唯一一个在虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

java 虚拟机栈

和程序计数器一样,虚拟机栈的内存空间也是私有的,生命周期和线程生命周期相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法创建的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。栈就是用于存储局部变量与一些过程结果的地方。 它在方法调用和返回中也扮演了很重要的角色即是入栈到出栈的过程, 除了栈帧的出栈和入栈之外, Java 虚拟机栈不会再受其他因素的影响,所以栈帧可以在堆中分配, Java 虚拟机栈所使用的内存不需要保证是连续的。
Java 虚拟机规范允许 Java 虚拟机栈被实现成固定大小的或者是根据计算动态扩展和收缩的。如果采用固定大小的 Java 虚拟机栈设计,那每一条线程的 Java 虚拟机栈容量应当在线程创建的时候独立地选定。 Java 虚拟机实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段,对于可以动态扩展和收缩 Java 虚拟机栈来说,则应当提供调节其最大、最小容量的手段。
局部变量表存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等于对象本身,可能是因为一个指向对象其实地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
其中64位长度的long和 double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

Java 虚拟机栈可能发生如下异常情况:

  • 如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量时,Java 虚拟机将会抛出一个 StackOverflowError 异常。

  • 如果 Java 虚拟机栈可以动态扩展(当前大部分的java虚拟机都可动态扩展,只不过Java虚拟机栈规范中也允许固定长度的虚拟机栈),并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java虚拟机将会抛出一个OutOfMemoryError 异常。

本地方法栈

本地方法栈(Native Method Stack)和java虚拟机栈发挥作用非常类似,区别在于java虚拟机栈是针对的执行Java方法(也就是字节码)服务,本地方法栈则是为虚拟机使用的Native方法服务。**在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此虚拟机可以自由实现它。**甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。如果虚拟机不支持Native方法,并且自己也不依赖传统栈的话,可以无需支持本地方法栈,如果支持本地方法栈,那么这个栈一般会在线程创建的时候按线程分配。
java 虚拟机规范允许本地方法栈被实现成固定大小的或者是根据计算动态扩展和收缩的。如果采用固定大小的本地方法栈,那一条线程本地方法栈容量应该在栈创建的时候独立地选定。一般情况下,Java虚拟机实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段,对于长度可动态变化的本地方法栈来说,则应提供调节其最大、最小容量的手段。
本地方法栈可能发生如下异常情况

  • 如果线程请求分配的栈容量超过本地方法栈允许的最大容量时,java虚拟机将会抛出一个StackOverflowError异常
  • 如果本地方法栈可以动态扩展, 并且扩展的动作已经尝试过,但是目前无法申请到足够内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应本地方法栈,那Java虚拟机将会抛出一个OutofMemoryError异常。

java堆

java堆(Java Heap)是java虚拟机所管理的内存中最大的一块。 堆(Heap)是可供各条线程共享的运行时内存区域,在虚拟机启动时创建。此内存唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展,与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作“GC堆”(Garbage Collected Head)。从内存回收的角度来看,由于现在垃圾收集器基本都采用分代收集算法,所以java堆中还可以细分为:新生代和老年代;再细致一点有Eden空间、From Survivor空间、To survivor 空间等。从内存分配角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer , TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
根据java虚拟机规范的规定,Java堆可以处于物理不连续的内存空间中,只要逻辑上是连续的即可,就像我们磁盘空间一样,在实现时,既可以实现成固定大小的,也可以是扩展的,不过当前主流的虚拟机都是按照可以扩展实现的(通过 -Xmx和-Xms控制)如果在堆中没有内存完成实例分配,并堆也无法再扩展时,就会抛出OutOfMemoryError异常

方法区

方法区是各个线程共享区域,它用于存储已被虚拟机加载的类信息、常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。虽然java虚拟机把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
方法在虚拟机启动的时候被创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集。
Java7虚拟机规范也不限定实现方法区的内存位置和编译代码的管理策略。方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。 方法区在实际内存空间中可以是不连续的。
Java虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态扩展和收缩方法来说,则应当提供调节其最大、最小容量的手段。

  • 如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个OutOfMemoryError 异常。

运行时常量池

运行时常量池(Runtime Constant Pool )是方法区的一部分。Class文件中除了有类版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池是每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池扮演了类似传统语言符号(symbol Table)的角色,不过它存储数据范围比通常意义上的符号表更为广泛。
每一个运行时常量池都分配在Java虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。
Java虚拟机对Class文件每一部分(自然包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行**,但对于运行常量池,Java虚拟机规范没有做任何细节的要求**,不同的提供商实现的虚拟机可以按照自己的需求来实现这个内存区域,不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译器才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以能将新的常量放入池中,这种特性被开发人员利用的较多的便是String类和intern()方法
创建类和接口的运行时常量池时,可能会发生如下异常情况:

  • 当创建类和接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那Java 虚拟机将会抛出一个OutofMemoryError异常

直接内存

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

在这里插入图片描述
在这里插入图片描述

此文章是为了加深理解作为笔记学习
抄袭:
《Java虚拟机规范(Java SE 7)》周志明、吴璞渊、冶秀刚 译
《深入理解Java虚拟机》第二版 周志明老师

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值