这一章要学会的:
-
虚拟机中的内存时如何划分的(程序计数器 Java虚拟机栈 本地方法栈 Java堆 方法区)
-
Java堆中如果不断创建新的实例则会产生OutOfMemoryError异常
-
虚拟机栈和本地方法栈溢出是由于为对象不断分配栈空间,导致栈空间不够用
-
方法区和运行时常量池溢出是由于不断调用本地方法导致出现OutOfMemoryError异常
2.1 概述
1 程序员内存控制的权利交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将成为异常艰难的工作。接下来我们开始了解虚拟机的内存区域
2.2运行时数据区域
一 程序计数器
1 程序计数器是一块较小的内存空间,可以将其看作是当前线程所执行的字节码的行号指示器
2 每条线程都需要有一个独立的程序计数器,各条线程之前计数器互不影响,独立存储,称这类内存区域为“线程私有”的内存
3 如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址
如果正在执行的事Native方法,这个计数器值为空
4 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
二 Java虚拟机栈
1 线程私有的
2 每个方法在执行的同时会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息
3 通常所说的“栈”即为Java虚拟机栈,也可以说是虚拟机栈中局部变量表部分(存放了编译期可知的各种基本数据类型,对象引用类型和returnAddress类型)
4 ps:局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小
5 该区域的两种异常情况:
-
StackOverflowError异常:如果线程请求的栈深度大于虚拟机所允许的深度
-
OutOfMemoryError异常:如果虚拟机栈可以动态扩展,这时如果申请不到足够的内存
三 本地方法栈
1 作用与虚拟机栈所发挥的作用相似
区别在于:虚拟机栈为虚拟机执行Java方法服务;而本地方法栈则为虚拟机使用到的本地方法服务(服务对象不同)
2 和虚拟机栈一样,也会抛出上述两个异常
四 Java堆
1 是Java虚拟机所管理的内存中最大的一块,被所有线程共享,在虚拟机启动时创建
2 此内存区域的唯一目的是存放对象的实例,目前所有对象都分配在堆上逐渐变得不那么绝对了
3 Java 堆是垃圾收集器管理的主要区域。从垃圾回收的角度来看Java堆可以细分为:新生代和老年代,划分的目的是为了更好地回收内存,分配内存
4 Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续就可以
五 方法区
1 是各个线程共享的内存区域(与Java堆一样)
2 它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据
3 方法区又称为永久代
4 方法区可以选择不实现垃圾收集,这一区域的内存回收目标主要是针对常量池的回收和对类型的卸载
六 运行时常量池
1 运行时常量池属于方法区的一部分,当常量池无法再申请到内存时会抛出OutOfMemoryError异常
2 Class文件中除了有类的版本,字段,方法,接口等,还有一项信息是常量池(用于存放编译器生成的各种字面量和符号引用),这部分内容将在类加载后进入方法区的运行时常量池存放
3 Java语言并不要求常量只有编译期才能产生。并非预置入Class文件中的常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中
2.3 HotSpot虚拟机
一 对象的创建
1 当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号代表的类是否已被加载,解析,初始化
2 虚拟机为新生对象分配内存,对象所需的内存大小在类加载完成后就可以完全确定下来
3 指针碰撞:假设Java堆中内存时绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
空闲列表:虚拟机必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
选择哪种内存分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
4 保证创建对象时线程的安全性:
-
一种是对分配内存空间的动作进行同步处理
-
把内存分配的动作按照线程划分在不同的空间之中进行
5 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零,接下来虚拟机要对对象进行必要的设置,最后执行<init>方法
二 对象的内存布局
1 在HotSpot虚拟机中,对象在内存中存储的布局分为三块区域:对象头,实例数据,对齐填充
2 其中对象头包括两部分信息:一部分用于存储对象自身的运行时数据(Mark Word),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。在对象头中必须要有一块用于记录数组长度的数据
3 实例数据部分是对象真正存储的有效信息,这部分会受到虚拟机分配策略参数和字段在Java源码中定义顺序的 影响
4 对齐方式不是必然存在的,只是起到占位符的作用
三 对象的访问定位
1 通过栈上的reference数据来操作堆上的具体对象,对象的访问方法取决于虚拟机实现而定。目前有使用句柄和直接指针
2 使用句柄访问:Java堆会划分出一块内存来作为句柄池,栈上的reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
3 使用直接指针访问:reference中存储的直接就是对象地址
4 两种访问方式的比较:使用句柄来访问最大的好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针(在堆中),而reference本身不需要修改
使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销
2.4 实战:OutOfMemoryError异常
对Debug Configuration的设置
一 Java堆溢出
1 只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量可达最大堆的容量限制后就会产生内存溢出异常
例如:
二 虚拟机栈和本地方法栈溢出
1 在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,在Java虚拟机栈规范中描述了两种异常:
-
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
-
如果虚拟机在扩展栈时没有申请到足够的内存空间,将抛出OutOfMemoryError异常
2 在单个线程下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常
三 方法区和运行时常量池溢出
首次出现原则
四 本机直接内存溢出