-
1.1 学习目的
-
- 不同于C/C++,Java将内存管理的工作交给虚拟机自动完成,由于将控制内存的权利交给了虚拟机,因此只有了解虚拟机的内存管理机制,才能在发生内存泄漏/内存溢出时即时排查并解决问题;
-
1.2 运行时数据区
- 虚拟机在执行Java程序时,会将所管理的内存划分为不同的区域,不同的区域具有不同的功能以及创建和销毁时间
-
(1)程序计数器
- 相当于“解释器解释字节码时的行号指示器”,解释器通过改变计数器的值来选取下一条指令;
- 线程私有,Java虚拟机的多线程依靠线程轮换,分配处理器执行时间来完成,为了保障线程之间跳转时程序正常执行,需要每个线程独立拥有 程序计数器;
- 是一块较小的内存区域,是唯一在Java虚拟机规范中没有规定OutOfMemoryError情况的区域;
-
(2)Java虚拟机栈
- 线程私有,生命周期与线程相同
- 描述Java方法执行的线程内存模型:每个方法执行时会同步创建一个栈帧,栈帧存放局部变量表、操作数栈、动态连接,方法出口;方法的调用和执行完成对应栈帧出栈和入栈;
- 局部变量表中存放了编译期可知的基本数据类型,对象引用,returnAddress类型,存放单位是局部变量槽;
- 运行期间槽的数量不变,不同虚拟机变量槽的大小不一定一样;
- 两类异常:(1)线程请求的超出虚拟机最大栈深度:StackOverflowError;(2)栈空间申请失败:OOMError
-
(3)本地方法栈
- 功能与虚拟机栈类似,虚拟机栈执行Java方法,本地方法栈执行本地方法;
- 两类异常:同Java虚拟机栈;
-
(4)Java堆
- 线程共享,虚拟机启动时创建;--<优化>-->划分出多个线程私有的分配缓冲区(TLAB)以提升内存管理的效率
- 存放实例对象,几乎所有对象实例都在堆上分配内存;
- 垃圾收集器管理的内存区域,大多收集器基于分代收集理论设计,但基于分代理论的区域划分只是一种设计风格,并不是标准;
- 堆可以被实现为固定大小or扩展,通过参数-Xmx和-Xms设定;
- OOMError:当实例对象无法分配内存且空间无法扩展时抛出OOMError;
-
(5)方法区
- 线程共享;
- 存放虚拟机加载后的类型信息、常量、静态变量,即时编译后的代码缓存等;
- 以前HotSpot虚拟机用永久代实现方法区,但后来用本地内存的元空间(放在一个与堆不相连的本地内存区域-JDK8)取代了永久代;
- 当方法区无法满足新的内存分配需求时,抛出OOMError;
-
(6)运行时常量池
- 方法区的一部分,JDK7中放在堆开辟的一块区域中;
- 存放的是类加载后Class文件常量池表中的字面量和符号引用,具有动态性,不止存放编译期间已经产生的常量,运行期间也可以将新产生的常量放入常量池
- Class文件的格式有严格规定,但jvm运行时常量池的具体实现可以由厂商自行决定
- 受方法区内存的限制,当常量池无法再申请到内存时抛出OOMError
-
(7)直接内存
- 不是运行时数据区的一部分
- 不受Java堆大小的限制,但受本机总内存的限制,如果设置参数时忽略了这一部分内存需求会导致抛出OOMError
- 虚拟机在执行Java程序时,会将所管理的内存划分为不同的区域,不同的区域具有不同的功能以及创建和销毁时间
-
1.3 HotSpot虚拟机对象探秘
- 讨论内存细节需要把范围固定到具体的虚拟机和具体的内存区域才有意义,本节讨论以HotSpot虚拟机和Java堆为例
-
(1)对象的创建
- Step1: 检查类加载情况(常量池中类的符号引用,类是否被加载、解析和初始化),如果检查不通过,则需要先进行相应类的加载;
- Step2:虚拟机为新生对象分配内存空间
- 划出可用空间:指针碰撞or空闲列表
- 保障线程安全:同步处理orTLAB
- Step3:必要设置,内存空间初始化为零值,对象头设置
- Step4:执行构造函数,即Class文件中的<init>()方法
-
(2)对象的内存布局
- 对象在堆中的存储布局包含对象头、实例数据和对齐填充三个部分;
- ➀对象头:包含两类信息
- 第一类是Mark Word,用于存储对象自身的运行时数据,为了能够在极小的空间内存储尽量多的信息,被设计为动态定义的数据结构,能够根据对象的状态复用存储空间;
- 第二类是类型指针,指向对象对应类型的元数据,用于确认该对象是哪个类的实例,(使用句柄的话不需要保留类型指针)如果对象是java数据,需要记录数组长度;
- ➁实例数据,父类和子类的字段存储顺序受参数设置和在源码中定义的顺序影响;
- ➂对齐填充,没有特别含义,也不是必然存在,以8字节为单位,没有对齐则需要填充;
-
(3)对象的访问定位
- 方式一:使用句柄,Java堆分为句柄池和实例池两个部分,栈中的reference存储对象的句柄地址,句柄中包含两个指针,其中到对象实例指针指向实例池的对象实例数据,到对象类型指针指向方法区的对象类型数据
- 好处是栈中的reference存储稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,reference本身不需要被修改(对象移动在垃圾收集时非常普遍)
- 方式二:直接指针,栈中的reference存储堆中对象地址,对象中包含到对象类型的指针和实例数据,到对象类型的指针指向方法区的对象类型数据
- 速度更快,节省了一次指针定位的开销
- 方式一:使用句柄,Java堆分为句柄池和实例池两个部分,栈中的reference存储对象的句柄地址,句柄中包含两个指针,其中到对象实例指针指向实例池的对象实例数据,到对象类型指针指向方法区的对象类型数据
-
2.4 实战:OutOfMemoryError异常
-
(1)Java堆溢出(-Xmx-Xms)
- 异常排查:首先Dump出当前的内存转储快照,通过内存映像分析工具对堆转储快照进行分析,确认是内存溢出(导致OOM的对象是必要的)还是内存泄漏(反之)
- 异常处理:如果是内存泄漏,通过工具进一步找到GC Roots引用链找到泄漏的具体代码位置;如果是内存溢出,检查堆参数设置,对比机器内存,检查代码是否存在不合理(是否存在某些对象生命周期过长,持有时间过长等)
-
(2)栈溢出(-Xss)
- 异常1: StackOverflowError --> 线程的栈深度超出,新的栈帧内存无法分配,出现时可以先定位到具体线程再相应解决,一般来讲栈深度是够用的
- 异常2: OutOfMemoryError --> 扩展栈容量是无法申请到足够的内存(支持扩展) or 创建线程申请内存时就无法获得足够内存(不支持扩展),出现时考虑减少线程数量/更换64微虚拟机/减少最大堆/减少栈容量(可以通过“减少内存”的方式换取更多线程)
-
(3)方法区和运行时常量池溢出
- JDK7及以后,原本存放在永久代的运行时常量池被移至Java堆之中;
- JDK6中intern()方法会把首次出现的字符串实例复制到永久代的字符串常量池中存储,返回永久代中这个字符串实例的引用;JDK7中intern()方法由于字符串常量池移动至堆中,不需要拷贝字符串实例到永久代,而是在常量池中记录其首次出现的实例引用
-
(4)本机直接内存溢出
- 由直接内存溢出导致的OOMError明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果发现内存溢出后产生的Dimp文件很小并且使用了直接内存,就需要考虑一下这方面的问题
-
深入学习JVM- (1)理解Java内存区域与内存溢出异常
于 2024-08-28 20:22:24 首次发布