第2章-Java内存区域和内存溢出异常
第二章·Java内存区域和内存溢出异常
1.JDK8中的JVM内存布局图
1.1程序计数器
简介:
程序计数器是一块比较小的内存空间,可以看作是当前线程所执行字节码指令的行号指示器。
字节码解释器在工作时通过改变这个计数器的数值,完成字节码指令的跳转,从而完成分支、循环、跳转等操作。
多线程与线程私有:
- JVM中多线程的实现,是通过线程之间轮流切换并分配处理器执行时间来完成的,所以在视觉上,感觉是同步的。实际上,在特定时刻,只有一个线程在执行。
- 因此,为了保证线程恢复之后,能够从上次的断点位置,继续执行下去。每个线程都私有程序计数器,各条线程之间互不干扰。线程私有
Java方法和Native方法:
- Java方法:
- 其程序计数器记录的是当前正在执行的虚拟机字节码指令的地址。
- Native方法
- 例如C方法 系统库方法
- 计数器为空值(undefined)
注意
程序计数器是JVM中唯一一个没有OutOfMemoryError的区域
1.2 Java虚拟机栈
线程私有
简介:
Java虚拟机栈描述的是Java方法执行的内存模型。每个方法在执行的时候,都会创建一个栈帧,栈帧中存储着局部变量表、操作数栈、动态链接、方法出口等信息。实际上,java程序方法的调用到执行完成,实质上都是栈帧在java虚拟机栈中的入栈出栈过程。
局部变量表:
- 一组变量存储空间,用于存放方法参数和方法内部定义的局部变量 只保存和方法有关的变量
- 基本数据类型变量(boolean byte char short int float long double)
- double和long是64长度,会占2个局部变量空间(Slot),其他的类型均占1个Slot。
- 对象引用 (并不是真正的对象,大多是指向对象实际存储位置的引用指针)
- 返回地址
- 基本数据类型变量(boolean byte char short int float long double)
编译期敏感:
局部变量表所需的内存空间,在编译期间就已经完成了分配。在方法运行期间不会再改变局部变量表的大小。
两个异常:
- *Error
- 当线程请求的栈深度大于java虚拟机栈所允许的最大栈深度
- OutOfMemoryError
- 虚拟机栈在扩展时,无法申请到足够的内存
1.3 本地方法栈
线程私有
简介:
与Java虚拟机栈类似,只不过Java虚拟机栈处理的是java方法,但是本地方法栈处理的Native方法。
两种异常:
- *Error
- 当线程请求的栈深度大于java虚拟机栈所允许的最大栈深度
- OutOfMemoryError
- 虚拟机栈在扩展时,无法申请到足够的内存
1.4 Java堆
线程共享
简介:
java堆是JVM中内存区域最大的一块,在虚拟机启动的时候创建,java堆的唯一目的就是存放对象实例。所有的对象实例以及数组都必须在堆上分配。
垃圾回收的主要区域
区域划分:
-
内存回收角度:
- 新生代
- Eden空间
- From Survivor空间
- To Survivor空间
- 老年代
- 新生代
-
内存分配角度:
- 划分出多个线程私有的分配缓冲区(TLAB)
内存空间和异常:
- java堆在物理内存上不要求必须是连续的内存空间,只要逻辑上是连续的即可。
- 如果堆中没有内存空间进行分配了,并且对也无法进行扩展,则抛出OOM异常。
1.5 方法区
线程共享
简介:
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 方法区是java虚拟机给出的一种规范,并没有规定如何实现,所以在不同的虚拟机上实现肯定是不同的。
- 方法区和java堆一样,都不需要连续的物理内存,可以动态扩展或者选择固定的大小。除此之外,方法区还可以选择不进行垃圾回收。
- 这并不代表,此处没有垃圾回收,只是方法区的垃圾回收较java对而言相对较少。
- 这个区域主要回收的目标是针对运行时常量池回收和类型的卸载
方法区和永久代:
方法区是规范,是逻辑。永久代在jdk1.7之前是方法区的主要实现方式。
JDK8中,元空间(元数据区)替代了永久代?
-
符串存在永久代中,容易出现性能问题和内存溢出
-
类及方法信息难定大小,永久代大小指定不好控制
-
永久代为GC带来复杂度,降低回收效率
-
将HotSpot和JRockit合并
永久代和元空间的联系和区别?
异常:
OOM
运行时常量池
- 是方法区的一部分,用来存储class文件在编译期间生成的各种字面量和符号引用。
- 并不要求常量一定只有在编译期间才能进入常量池,运行期间也可以将新的常量放入常量池中。
异常:
OOM
1.6 直接内存
简介:
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。
我的理解就是java堆之外的本机内存。
异常:
OOM
1.7 HotSpot虚拟机在java堆中对象分配、布局、访问的全过程?
对象的创建:
- 是否完成类加载
- 当new指令执行时,JVM首先将会检查这个指令的参数是否能在常量池中找到对应类的符号引用,从而检查这个符号引用代表的类是否已完成了类加载工作(加载,连接、初始化),如果没有则需要进行类加载。
- 为对象分配内存
- 如果完成了类加载工作,jvm将会为新生对象分配内存。对象的内存大小在类加载完成后就确定了,java的对象存储在heap中,实际上也就是从heap上划定固定大小的空间,分配给对象。
- 指针碰撞
- java堆中的内存是绝对规整的,空闲区域一边,已使用区域在另一边,中间使用指针分界。
- 分配内存,仅用指针在空闲区域移动新生对象大小距离即可
- 空闲列表
- java堆中的内存是非常不规整的,空闲和占用区域相互交错。
- 使用列表记录内存的单元的使用情况,从表中挑选内存较大的区域分配给新生对象,并更新表中的内存单元的使用状态。
- 指针碰撞
- 多线程创建对象时,划分内存指针的同步问题?
- 对内存分配空间的动作进行同步处理。
- 把内存分配动作根据线程划分在不同的空间之中
- 每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)
- 哪个线程要分配内存就在哪个线程的TLAB上分配,TLAB用完需要分配新的TLAB时,才需要同步锁定
- 如果完成了类加载工作,jvm将会为新生对象分配内存。对象的内存大小在类加载完成后就确定了,java的对象存储在heap中,实际上也就是从heap上划定固定大小的空间,分配给对象。
- 对象内存空间初始化
- 虚拟机需要将分配到的内存空间都初始化为0值
- 解释了 为什么对象的实例字段不需要赋初始值,也能编译通过。这是因为,程序访问到的是默认的初始零值。
- 虚拟机需要将分配到的内存空间都初始化为0值
- 虚拟机设置对象信息
- 虚拟机对对象进行必要的设置,如对象是哪个类的实例、如何找到类的元数据信息、等等。这些信息都存放在对象头中。
- init()方法执行
- 执行init()方法,根据程序员的个性化设置,初始化对象。对象产生。
对象的内存布局:
对象的内存布局主要包括:对象头、实例数据、对齐填充。
- 对象头
- 存储对象自身的运行时数据
- 哈希码、GC分代年龄、等等
- 如果是java数组对象,则对象头还需要保存数组长度信息
- 类型指针
- 对象指向它的类元信息的指针,虚拟机通过这个指针来确定对象是哪个类的实例。
- 存储对象自身的运行时数据
- 实例数据
- 是对象真正存储的有效信息,也是程序代码中各种类型的字段内容。
- 无论是从父类继承的,还是子类特有的,都要进行存储。
- 存储顺序:
- 默认分配顺序:longs/doubles、ints、shorts/chars、bytes/booleans、oops (Ordinary Object Pointers),同宽分配在一起,长度由长到短(oops除外)
- 默认分配顺序下,父类字段分配在子类字段前面
- 对齐填充
- 不是必要存在的,占位符的作用。
对象的访问:
JVM通过java虚拟机栈中的局部变量表所保存的引用数据(对象引用)来访问Java堆上保存的实例对象。具体的访问方式:
-
句柄
-
在java堆上划分一块内存作为句柄池,此时java栈中保存引用数据就是当前对象的句柄地址,包含了其对象实例数据的指针,也包好对应的类元信息的指针
-
优点:
- 稳定 因为reeference保存的是句柄指针,句柄内部如何改变不影响reference的值
-
-
直接指针
- 在java堆中的对象实例数据中放置一个到方法区对象类型数据的指针,直接访问即可。
- 优点:
- 速度快 因为少了一次指针定位操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tkPvVDkI-1599119626263)(https://github.com/TangBean/understanding-the-jvm/raw/master/
1.8 异常总结
除了程序计数器,JVM中其他运行时区域都有可能发生OutOfMemoryError。
OOM异常
java堆异常:
- 标志:java.lang.OutOfMemoryError: Java heap space
- 出现原因:
- 当不断实例化对象,导致对象数量达到最大堆的容量限制后就会产生
- 解决:
- 首先通过内存印象分析工具对Dump出来的堆转储快照进行分析,确定是内存泄漏还是内存溢出
- -XX:HeapDumpOnOutOfMemoryError 虚拟机参数,dump出堆转储快照。
- 内存泄漏:
- 查看泄漏对象到GCRoots的引用链,定位泄漏位置
- 内存溢出
- 如果内存中的对象都活着,则调大虚拟机的堆参数 -Xms
- 首先通过内存印象分析工具对Dump出来的堆转储快照进行分析,确定是内存泄漏还是内存溢出
java虚拟机栈和本地方法栈溢出:
- 单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,内存无法继续分配时,都会抛出*Error
- 多线程下,java虚拟机栈和本地方法栈可能会出现OOM
- 因为本机内存固定的情况下,jvm程序计数器基本不占据内存,主要就是方法区、堆、本地方法栈、java虚拟机栈占据内存。当栈帧容量较大,多线程的操作就容易耗尽剩余的内存。
- 解决:
- 如果是建立多线程导致的内存溢出(OOM),可以采取
- 减少线程数
- 更换64位虚拟机
- 减少最大堆内存大小
- 减少栈容量大小
- 如果是建立多线程导致的内存溢出(OOM),可以采取
方法区和运行时常量溢出:
- 因为方法区主要存储的是类信息,如果存在动态生成大量类的应用时,方法区存储的压力将大大上升,最终溢出。
本机直接溢出
本地方法栈、java虚拟机栈占据内存。当栈帧容量较大,多线程的操作就容易耗尽剩余的内存。
- 解决:
- 如果是建立多线程导致的内存溢出(OOM),可以采取
- 减少线程数
- 更换64位虚拟机
- 减少最大堆内存大小
- 减少栈容量大小
- 如果是建立多线程导致的内存溢出(OOM),可以采取
方法区和运行时常量溢出:
- 因为方法区主要存储的是类信息,如果存在动态生成大量类的应用时,方法区存储的压力将大大上升,最终溢出。