Java 虚拟机读书笔记——走近 Java

Java 虚拟机读书笔记——走近 Java

第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),存储

局部变量表 操作栈 动态链接 方法出口


每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。


局部变量表存放了编译期可知的:


基本数据类型(boolean、byte、char、short、int、float、long、double) 对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置) returnAddress类型(指向了一条字节码指令的地址)。


其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。


局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。


这个区域有两种异常情况:


StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度 OutOfMemoryError:虚拟机栈扩展到无法申请足够的内存时


2.2.3 本地方法栈(线程私有)


与虚拟机所发挥的作用非常相似,不同的是:


虚拟机栈为虚拟机执行 Java 方法(字节码)服务。 本地方法栈(Native Method Stacks)为虚拟机使用到的 Native 方法服务。


在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它,甚至有的虚拟机(譬如 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 对象的创建


检查类是否已被加载:常量池中检查类是否已被加载、解析和初始化过,若没有则先加载类 分配内存:新生代,“指针碰撞”和”空闲列表“,CAS保证更新操作的原子性 初始化为零值:分配到的内存空间都初始化为零值(不包括对象头) 设值对象头:哪个类的实例、如何才能类的元数据信息、对象的哈希码、对象的GC分代年龄等 初始化方法:执行初始化方法设置属性值


2.3.2 对象的内存布局


对象头 运行时数据(Mark word):哈希码、GC分代年龄、锁状态、线程持有的锁、偏向线程ID、偏向时间戳等 类型指针:对象指向它的类元数据的指针,虚拟机通过这指针来确定这个对象是哪个实例 实例数据:对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容 对齐填充:不是必然存在,HotSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象大小必然是8字节的整数倍,而对象头部分正好是8字节的倍数,所以,当实例数据部分没有对齐时,用对齐填充来补全

此处为语雀内容卡片,点击链接查看:https://www.yuque.com/senknight/note/iyr8ca

2.3.3 对象的访问定位


句柄:Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息 ...


直接指针:Java堆对象的布局汇总就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址 ...


两种对象访问方式各有优势,使用句柄来访问的最大好处就是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(元空间)代替


虚拟机栈内存溢出总结


在HotSpot中,虚拟机栈和本地方法栈不做区分 StackOverflowError在正常情况能承受1000-2000深度,一般在单线程下此异常优先抛出 OutOfMemoryError多线程情况下更易出现,可通过减少堆内存或减少栈容量来避免此异常 由于每个线程都有一个栈, -Xss控制的是一个线程栈的容量 运行时数据区容量为:栈+堆+寄存器,当总容量和堆内存确定的情况下,每个栈容量越小,能容纳的栈数量也就也多,能执行的线程数越多,这个就解释了第2条。