Java8虚拟机规范
《深入理解Java虚拟机_JVM高级特性与最佳实践》(第二版)
链接:https://pan.baidu.com/s/1RQ1Rtsq3tfb2ZesbXy8FCg 密码:4tjn
文章目录
1 什么是Java运行数据区
官方文档给出的定义如下:
The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.
虚拟机的组成部分如上图所示,众所周知,Java的源码文件通过javac命令编译为class文件,然后通过类加载器将其加载进虚拟机进行执行,运行时数据区就是用于存储运行时的数据(class文件信息、常量、变量等信息)的内存。
2 Java 8中的运行时数据区
在《Java虚拟机规范(Java SE 8版)》中对运行时数据区的定义有如下6部分:
- PC(program count) Register 程序计数器
- Java Virtual Machine Stacks Java虚拟机栈
- Heap 堆
- Method Area 方法区
- Run-Time Constant Pool 运行时常量池 是方法区的一部分
- Native Method Stacks 本地方法栈
在该规范对运行时常量池的描述有如下的内容
Each run-time constant pool is allocated from the Java Virtual Machine’s method area.
从这句话我们可以发现,运行时常量池是方法区的一部分,因此通常对运行时数据区的分类有程序计数器、Java虚拟机栈、堆、方法区和本地方法栈5部分,如上文图片所示。
接下来我们详细的了解下这几部分的作用及存储的内容
2.1 程序计数器
每个线程有属于自己的程序计数器,我们知道,Java是支持多线程的且多线程的执行是通过线程切换的方式来进行线程的调度的,该区域的作用是用于存储当前线程的执行位置,保证当切换至该线程时能按照中断的位置继续执行。该区域存储的内容如下
- 执行的为Java程序时,存储的是当前当前执行的代码的地址
- 执行的为本地方法时为’undefined’
这块区域没有规定任何OutOfMemoryError
2.2 Java 虚拟机栈
Java虚拟机栈也是线程私有的,随着线程的创建而创建,用来存储栈帧,一个方法的执行过程就是一个栈帧的入栈和出栈过程。
栈帧是用于支持虚拟机进行方法调用和执行、用来存储数据和部分过程结果的数据结构,也用来处理动态链接、方法返回值和异常,随着方法的调用而创建。在每个栈帧中有自己的局部变量表、操作数栈及当前方法所属类的常量池引用。局部变量表的大小及操作数栈的深度在编译器便可确定并设置到方法相关的code属性中,因此一个栈帧需要分配多少内存取决于具体的虚拟机实现,不会受到程序运行期变量数据的影响。
- 局部表量表:一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
- 操作数栈:是一个先入后出的栈,在方法的执行过程中,各种字节码指令往操作数栈中写入和提取内容,即入栈和出栈操作。最大深度在编译的时候确定写入Code属性的max_stacks数据项中
- 动态链接:每个栈帧中都包含一个指向当前方法所在类的运行时常量池的引用,以便对当前方法的代码实现动态链接。在class文件里面方法的调用和成员变量的访问是通过符号引用来表示的,这些符号引用一部分会在类加载阶段或者第一次使用的时候转换为直接引用,这种转换称为静态解析。另一部分在运行期间转换为直接引用,称为动态链接。
- 方法返回地址:方法的退出方式有正常完成退出和异常完成退出两种,在方法退出后需要返回到方法被调用的位置,方法返回地址就是用来帮助恢复上层方法的执行状态。正常退出时可以使用调用者的PC计数器的值作为返回值,异常退出时通过异常处理器表来确定。
Java虚拟机规范允许Java虚拟机栈被实现为固定大小或者可动态扩展和收缩,如果采用固定大小,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。
异常
- 线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 如果虚拟机栈无法申请到足够的内存,将抛出OutOfMemoryError异常
2.3 本地方法栈
本地方法栈同Java虚拟机栈所发挥的作用类似,唯一不同的是本地方法栈是为本地方法的执行服务。
2.4 堆
Java堆是所有线程共享的一个运行时数据区,是用于存储对象实例和数组的区域。该数据区随着虚拟机的启动而创建、虚拟机的关闭而销毁。
这块区域是垃圾收集器管理的主要区域。现在收集器基本都采用分代收集算法,因此这块区域可以进行进一步的细分为新生代和老年代两部分,新生代又分为一个Eden区和两个Survivor区,如下图所示:
规定的异常:
- 当无法申请到足够的内存时会抛出OutOfMemoryError异常
2.4.1 常用的虚拟机参数
-
-Xms设置堆的初始大小 -
-Xmx设置堆的最大值 -
-Xmn设置新生代的大小 -
-NewRatio设置新生代和老年代的比例 -
-SurvivorRatio设置Survivor区和Eden区的比例
2.5 方法区
方法区也是各个线程共享的内存区域,该区域用于存储已被虚拟机加载的类的信息、常量、静态变量、即时编译后的代码等数据。
在Java7及之前版本的方法区是通过永久代来实现的,在Java8使用元空间(MetaSpace)实现。
规定的异常:
- 当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常
2.5.1 虚拟机参数
-
-XX:MetaspaceSize=size设置元空间大小 -
-XX:MaxMetaspaceSize=size设置元空间的最大值 - 在Java7之前可以使用
-XX:PermSize=size和-XX:MaxPermSize=size设置永久代的大小和最大值
3 内存分配策略
在了解了Java的运行时数据区后,我们知道对象是从堆内存上进行分配内存的,堆内存也被分成了好多个区域,那对象具体是在堆的哪块区域上进行分配呢?
接下来,我们了解一下最普通的几条内存分配规则
3.1 对象优先在Eden分配
多数情况下,对象在新生代的Eden区进行分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。当Minor GC之后,若还没有足够的内存进行分配对象,会通过分配担保机制提前转移到老年代中。
Minor GC 是发生在新生代的垃圾收集,通常使用的是复制算法,将Eden和一个Survivor区存活的对象复制到另一块Survivor区
Major GC 是发生在老年代的垃圾收集,通常会伴随着一次Minor GC。Major GC的速度会比Minor GC慢
3.2 大对象直接进入老年代
为了避免大对象在Eden和两个Survivor区因为垃圾收集而存在较多的内存复制,可以将大对象直接在老年代进行分配,可通过-XX:PretenureSizeThreshold=size参数来设置直接进入老年代的阈值,该参数只对Serial和ParNew两款收集器有效
3.3 长期存活的对象将进入老年代
在Java虚拟机中会为每一个对象定义一个年龄计数器,每经过一次Minor GC,该值会增加1,当年龄增加到一定值时(默认为15)后,该对象会晋升到老年代中。可以通过-XX:MaxTenuringThreshold=age参数来设置该年龄的阈值
3.4 动态对象年龄判定
为了更好的适应不同程序的内存状况,Java虚拟机不是永远地要求对象的年龄达到了阈值才能晋升老年代,如果Survivor空间中相同年龄的对象大小大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
3.5 空间分配担保
在发生Minor GC时,Java虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果不满足,会检查HandlePromotionFailure设置是否允许担保失败,如果允许再检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC。否则会进行一次Full GC