个人博客 翔仔年轻有力量:【JVM】Java内存区域
运行时数据区的划分
- 划分规范:
- 对于HotSpot虚拟机,在JDK8 以前
- HotSopt虚拟机在JDK 8 以后
注:"方法区"这一概念定义在Java虚拟机规范中,其具体实现在不同虚拟机中各不相同。
在HotSpot虚拟机中,JDK8以前,永久代作为方法区的具体实现,划分为堆的一部分,同用堆内存的GC回收机制
在JDK8以后,元空间替代了永久代,其内存位于本地内存中,不易产生OOM异常
内存各区域介绍
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
-
它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
-
在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
-
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined)。
-
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
-
字节码解释器工作时就是通过改变这个计数器的值来选取下一跳需要执行的字节码指令
-
它是唯一一个在java虚拟机规范中没有规定任何OOM情况的区域
-
Java虚拟机栈
描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。 每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口信息,每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出战的过程。
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应这个一次次的java方法调用。它是线程私有的,生命周期和线程是一致的,作用包括:主管java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)即局部变量表、部分结果,并参与方法的调用和返回。
虚拟机栈会出现以下两种异常状况:1. 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。 2. 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。
注:java虚拟机规范允许Java栈的大小是动态的或者是固定不变的,HotSpot不支持扩展
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
Java堆既可以被实现成固定大小的,也可以是可扩展的,主流Java虚拟机均为可扩展。当堆中没有内存完成实例分配(在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发错误):OOM Error
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
常用参数:
- 元数据区大小可以使用参数一XX:MetaspaceSize和一XX :MaxMetaspaceSize指定
- -XX:MetaspaceSize: 设置初始的元空间大小。对于一个64位的服务器端JVM来说, 其默认的一XX :MetaspaceSize值为21MB.这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)
运行时常量是相对于常量来说的,它具备一个重要特征是:动态性。当然,值相同的动态常量与我们通常说的常量只是来源不同,但是都是储存在池内同一块内存区域。Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。
Hotspot方法区随jdk版本具体演化:
jdk1.6及之前:有永久代(permanent generation) ,静态变量存放在 永久代上
jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
jdk1.8及之后: 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆
永久代为什么被元空间替换
整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize
控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程
-
对象的创建
- 类加载检查:检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程
- 分配内存:虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
-
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
- CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
-
初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
-
对象基础设置: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
-
执行init()方法,程序定义初始化对象: 执行
<init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 -
对象的内存布局
- 在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。
- Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。
- 实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
- 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。
-
对象的访问定位(创建后如何访问)
-
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种
-
使用句柄方式:Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。好处在于reference中存放的是稳定的句柄地址,当对象被移动是只会改变句柄中的实例数据指针,reference本身不修改
-
使用直接指针方式:reference中存储的直接就是对象地址。必须考虑如何放置访问类型数的相关信息。优点在于速度更快,节省了一次指针定位的时间开销。Hotspot主要使用直接指针方式进行访问
-
参考
- 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第三版)》
- Github@Snailclimb/JavaGuide:java内存区域
- 尚硅谷@宋红康JVM教程