第2章 Java 内存区域与内存溢出
2.2 运行时数据区域
...运行时数据区域包括哪些?
程序计数器 Java 虚拟机栈 本地方法栈 Java 堆 方法区 运行时常量池 直接内存2.2.1 程序计数器(线程私有)
程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。
由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。
如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是 Native 方法,这个计数器的值为空。程序计数器是唯一一个没有规定任何 OutOfMemoryError 的区域。
2.2.2 Java 虚拟机栈(线程私有)
Java 虚拟机栈(Java Virtual Machine Stacks)是线程私有的,生命周期与线程相同。
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧(Stack Frame),存储
局部变量表 操作栈 动态链接 方法出口每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的:
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
这个区域有两种异常情况:
2.2.3 本地方法栈(线程私有)
与虚拟机所发挥的作用非常相似,不同的是:
在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它,甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError和OutOfMemoryError异常
2.2.4 Java 堆(线程共享)
Java 堆(Java Heap)是 Java 虚拟机中内存最大的一块。Java 堆在虚拟机启动时创建,被所有线程共享。
作用:存放对象实例。
垃圾收集器主要管理的就是 Java 堆。
Java 堆在物理上可以不连续,只要逻辑上连续即可。
主流的虚拟机都是按照可扩展实现的(通过 -Xmx和 -Xms控制)
如果在堆中没有内存完成实例分配,并且对也无法再扩展时,将会抛出OutOfMemoryError异常
2.2.5 方法区(线程共享)
方法区(Method Area)被所有线程共享,用于存储:
和 Java 堆一样,不需要连续的内存,可以选择固定的大小,更可以选择不实现垃圾收集。
它有一个别名叫 “非堆” ,HotSpot 在 JDK1.7 之前是用永久代(Permanent Generation)来实现的方法区,从 JDK1.8 开始使用元空间(MetaSpace)来实现。
这区域的内存回收主要是针对常量池的回收和对类型的卸载,但是回收效果并不好
当方法区中无法满足内存分配需求时,将会抛出OutOfMemoryError异常
方法区、永久代和元空间的对比:
此处为语雀内容卡片,点击链接查看:https://www.yuque.com/senknight/note/kr933u
2.2.6 运行时常量池
参见:
此处为语雀内容卡片,点击链接查看:https://www.yuque.com/senknight/note/ba3416e9-985a-4773-a0ad-53fad7292b0a
2.2.7 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。
在 JKK 1.4 中新加入了 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用 Native 函数库直接分配对外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显着提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
显然,本机直接内存的分配不会收到 java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际额你存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个区域综合大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。
2.3 HotSpot 虚拟机对象探秘
2.3.1 对象的创建
2.3.2 对象的内存布局
此处为语雀内容卡片,点击链接查看:https://www.yuque.com/senknight/note/iyr8ca
2.3.3 对象的访问定位
两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针访问方式的最大好处就是速度快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
就Sun HotSpot而言,使用的是第二种方式,但是从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也时分常见。
2.4 OutOfMemoryError
JVM--参数调优 - 暗夜飞羽睿 - 博客园
通过设置参数 -XX:+HeapDumpOnOutMemoryError 可以让虚拟机在出现内存溢出时Dump出当前的内存堆转储快照,以便时候进行分析
参数名称
含义
默认值
说明
-Xms
初始堆大小
物理内存的1/64(<1GB)
默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制
-Xmx
最大堆内存
物理内存的1/4(<1GB)
默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
-Xss
每个线程的堆栈大小
1M
JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右 一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)
-Xmn
年轻代大小
注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小. 增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
-XX:NewSize
设置年轻代大小
-XX:MaxNewSize
年轻代最大值
-XX:PermSize
永久代初始值
物理内存的1/64
Jdk1.8开始移除永久代,由-XX:MetaSpaceSize(元空间)代替
-XX:MaxPermSize
永久代最大值
物理内存的1/4
Jdk1.8开始移除永久代,由-XX:MaxMetaSpaceSize(元空间)代替