《深入理解Java虚拟机》第一二章读书笔记
使用的书籍为《深入理解Java虚拟机》第三册,该书记录的Java版本为Java7,和目前主流的Java8还有一些区别,在阅读结束该书之后还需要学习最新的Java虚拟机技术。
记录开始于2022-02-22 记录自己阅读《深入理解Java虚拟机》的提纲。
第一章 了解Java
图个乐,介绍了历史,动手编译Java版本。
第二章 自动内存管理机制
2.1 概述
学习目标:了解Java虚拟机内存的各个区域,了解这些区域的作用,服务对象,以及其中可能产生的问题。
2.2 运行时数据区域
Java程序将管理的内存划分为很多个不同的区域,这个区域的合集,就叫做 运行时数据区域。
- 所有线程共享
- 方法区 Method Area Java8之后改为元空间
- 堆 Heap
- 各线程隔离
- 虚拟机栈 VM Stack
- 本地方法栈 Native Method Stack
- 程序计数器 Program Couter Register
2.2.1 程序计数器
概念: 当前线程所执行的字节码的行号指示器,工作时通过改变计数器的值来选取下一条需要执行的字节码指令。保证多线程轮流切换后能恢复到正确的执行位置。
注意:执行Natvie方法,这个计数器的值为空Undefined。
程序计数器市内存区域唯一一个没有OOM(OutOfMemoryError)情况的区域。
2.2.2 虚拟机栈
概念:在每个方法执行的时候都会同时船舰一个栈帧(Stack Frame) 用于储存局部变量表、操作栈、动态链接、方法入口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
问题:
-
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出**SOF(StackOverflowError)**异常
-
如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时,会抛出OEM异常。
2.2.3 本地方法栈
概念:本地方法栈和虚拟机栈发挥的作用相似。虚拟机栈执行的时Java方法服务,而本地方法栈则视为虚拟机使用到的Native方法服务。HotSpot虚拟机直接将两个合并。
问题:
- 本地方法栈区域一样会抛出SOF和OOM。
2.2.4 Java堆
概念:Java堆是被所有线程共享的内存区域,在虚拟机启动的时候创建,目的是为了存放对象实例。
注意:堆是垃圾收集器管理的主要区域,因此很多时候也被称作GC堆。Java堆还可以细分为新生代和老年代。再细致的又Eden空间 From Survivor空间,To Survivor空间。
问题:
- 如果再堆中没有内存完成实例分配,并且堆也无法再扩展的时候,将会抛出OOM异常。
2.2.5 方法区
概念:方法区和Java堆一样,是各个线程共享的内存区域,用于储存已被虚拟机加载的类信息,常量、静态变量、即时编译器编译后的代码等数据。垃圾收集行为在这个区域是比较少出现的。
问题:
- 当方法区无法满足内存分配需求时,将抛出OOM异常。
2.2.6 运行时常量池(Runtime Constant Pool)
概念: 运行时常量池时方法区的一部分。常量池用于存放编译时期生成的各种字面量和符号引用,在类加载之后存放到方法区的运行时常量池中。
注意:Java虚拟机规范没有堆常量池的格式做任何细节上的要求,不同的提供商实现的虚拟机都可以按照自己的需要来实现这个内存区域。一般是保存Class文件中描述的符号引用和翻译出来的直接引用。
学习:String类的intern()方法 ,在运行期间也可能将新的常量放入池中。
问题:
- 当常量池无法再申请到内存的时候会抛出OOM异常。
2.2.7 直接内存(Direct Memory)
概念:直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁的使用而且也可能导致OOM异常。在JDK1.4中加入了NIO(New Input/Output类),引入了一种基于通道与缓冲区(Buffer)的 I/O方式,它可以使用Native函数库直接分配堆外内存,任何通过一个存储在Java堆里面的DIrectByteBuffer对象,作为这块内存的引用进行操作。这样在一些场景中能显出提高性能,因为避免了在Java堆和Native堆中来回复制数据。
问题:动态扩展时出现OOM异常。
2.3 对象访问
目标: 了解Java时如何访问对象的,涉及Java栈,堆、方法区这三个重要的内存区域之间的关联关系。
Object obj = new Object();
- Object obj会存入Java栈的本地变量表中,作为一个reference类型数据出现。
- **new Object()**会反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存。
- 在堆中可以找到对象的类型数据(对象类型,父类,实现的接口,方法等)
在使用的时候,在reference类型中,可以指向对象的引用,以及访问到Java堆中对象的具体位置。因为Java虚拟机规范中没有规定应该使用那种方法去定位,因此,有两种主流的方法:使用句柄和直接指针
句柄:
- 概念:Java堆会分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址。句柄中包含了对象实例数据和各类型数据各自的具体地址。
- 优点:在对象被移动(垃圾回收时移动对象非常普遍)时只会改变聚丙种的实例数据指针,而reference本身不需要被修改。
直接指针
- 概念:reference中直接存储的就是对象地址。
- 优点:速度更快,节省了一次指针定位的时间和开销。
- 由于对象访问十分频繁,所有HotSpot使用直接指针。
2.4 实战:OOM异常
除了程序计数器之外,虚拟机内存的其他几个区域都有可能会出现OOM异常。
目标:使用实战来看效果和如何处理
2.4.1 Java堆溢出
不断往List中添加新对象,可以创造出OOM异常。显示效果如下。
解决方法:
- 要先分清楚是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)可以使用内存映像分析工具。
- 如果是内存泄漏,可以通过工具查看泄漏对象到GC Roots的引用链。查看内存泄漏的原因,定位内存泄漏的具体代码段,定位泄漏代码位置。
- 如果不存在泄漏,就检查虚拟机的堆参数(-Xmx与-Xms),与计算机物理内存对比看看是否还可以调大,从代码上检查是否存在 某些对象生命周期过长,持有状态时间过长。
2.4.2 虚拟机栈和本地方法栈溢出
-Xoss参数:设置本地方法栈大小 -Xss 设置栈大小
但是在Hotspot虚拟机中不区分虚拟机栈和本地方法栈,因此只需要使用 -Xss来设置。
栈中有两种异常:
- 线程请求的栈深度大于虚拟机允许的最大深度:SOF异常
- 虚拟机栈在扩展时无法申请到最够的内存空间:OOM异常
虽然有两种定义,但是在单线程下,只会出现SOF异常,因为无法判断时由于内存空间不够无法扩展栈深度还是本身栈深度不足。
在多线程下,每个栈分配的内存越大,越容易发生OOM异常,因为一个进程的总内存是固定的。
2.4.3 运行时常量池溢出
常量池通常只在方法区内,使用 -XX:PermSize 和 -XX:MaxPermSize 限制方法区的大小。
向常量池内添加内容最简单的做法就是使用String.intern()这个Native方法。该方法的作用就是:如果池中已经包含此String对象的字符串,则返回代表池中这个字符串的String对象;否则就将此String对象包含的字符串添加到常量池中,并且返回此对象的引用。
2.4.4 方法区溢出(常见)
方法区用于存放Class的相关信息:类名,访问修饰符,常量池,字段描述,方法描述等。
当前很多主流框架:Spring中对类进行增强时,都会用到这种,增强的类越多,就需要越大的方法区来保证动态生成的Class可以载入内存。
解决思路: 在经常动态生成大量Class的应用中,需要特别注意类的回收状况。除了GCLib字节码增强之外吗,常见的还有大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用。
2.4.5 本机直接内存溢出
没使用过Unsafe类,看不懂,抄写一波。
DirectMemory容量可以通过 -XX:MaxDirectMemorySize 指定,如果不指定则默认与Java堆的最大值(-Xmx指定)一样。
2.5 小结
本章按照Java虚拟机的内存区域划分,可能出现的问题,以及出现时的现象,描述了运行时数据区域的概念。同时演示了,虽然Java自带了垃圾回收机制,但是依然很容易在程序中造成内存溢出异常,更加需要我们学习出现的原因和解决的方法。