目录
1. 运行时数据区域
1.1.程序计数器
-
作用
记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。 -
意义
JVM的多线程是通过线程轮流切换并分配处理器来实现的,对于我们来说的并行事实上一个处理器也只会执行一条线程中的指令。所以,为了保证各线程指令的安全顺利执行,每条线程都有独立的私有的程序计数器。 -
存储内容
当线程中执行的是一个Java方法时,程序计数器中记录的是正在执行的线程的虚拟机字节码指令的地址。
当线程中执行的是一个本地方法时,程序计数器中的值为空。 -
可能出现异常
此内存区域是唯一一个在JVM上不会发生内存溢出异常(OutOfMemoryError)的区域。 -
疑问
Q:执行本地方法,为什么程序器中的值为空?我们知道,程序计数器用来存放字节码指令地址;通过这个地址,虚拟机就能知道执行到哪里,以及怎么往下执行,可调用native方法,值就变成空了,那么机器不就直接崩溃了吗?
A:参考C++理解是:当线程中调用native方法的时候,则重新启动一个新的线程,那么新的线程的计数器为空则不会影响当前线程的计数器,相互独立。
Q:如果是新启动的一个线程,那么不会因为线程异步问题,无法控制执行顺序吗?
A:当前线程应当会被阻塞,直到另外一个线程执行结束。例如:通过死循环来控制阻塞(当然死循环效率太低,这里只是一个例子) |
1.2.虚拟机栈
-
什么是虚拟机栈
虚拟机栈是一个后入先出(LIFO)栈
线程私有,每个线程都有一个虚拟机栈,生命周期与线程相同
虚拟机栈的栈元素是栈帧,一个方法开始被调用,这个方法的栈帧入虚拟机栈,这个方法执行完毕返回时,其栈帧出虚拟机栈,因此,虚拟机栈中栈帧的入栈顺序就是方法调用顺序
-
可能出现的异常
线程请求的栈深度大于虚拟机允许的深度时(栈帧过多),抛出 StackOverFlow 异常。
大部分虚拟机都可以动态扩展栈深度
如果栈扩展时,无法申请到足够的内存,则抛出 OutOfMemoryError 异常(虚拟机栈过多)
-
疑问
Q:什么是句柄
句柄就是引用。在java中我们在实例化完对象后,在对其进行操作时,用来去操作对象的就叫做句柄。他代表了当前对象的唯一一个标识,并不能代表当前对象的内存地址。例如:
Tree t1 = new Tree();
-
上边例子中,t1就属于当前新建对象的句柄,它指向新建对象的实例,我们通过他去操作对象。
Q:虚拟机默认的栈深度是?
如果使用虚拟机默认参数,栈深度在大多数情况下达到 1000 ~ 2000 完全没有问题。
Q:如何动态扩展栈深度,含义是,单个栈的内存大小增加,还是整体栈内存增加?
1.3.栈帧
一个线程中,当线程调用某个方法时,JVM会相应的创建一个栈帧(Stack Frame),放入虚拟机栈中,用来表示某个方法的调用。
栈帧是用来存储数据,和存储部分过程结果的数据结构,同时也用来处理动态链接(Dynamic linking)、方法返回值、异常分派(Dispatch Exception)。
线程对(某个对象的)方法的调用,就对应着一个栈帧的入栈、出栈(虚拟机栈)的过程。线程在运行过程中,只有一个栈帧是处于活跃状态,称为当前活动栈帧,当前活动栈帧,始终处于虚拟机栈的栈顶,当前活动栈帧对应的方法,也是当前线程正在执行的方法。
-
局部变量表
局部变量表存储的是一个方法中定义的局部变量。局部变量表的数量在编译期就已经确定了,存储到了方法的Code属性中。
-
操作数栈
JVM底层字节码指令集是基于栈类型的,所有的操作码都是对操作数栈上的数据进行的操作。对每一个方法调用,JVM都会创建一个操作数栈,供计算使用。栈的深度,在编译期就已经确定了,其栈深度存储在方法的Code属性中。
-
动态链接
没一个栈帧内部都存储着一个指向运行时常量池的引用,来支持当前方法的代码实现动态链接
动态链接指向的是,当前执行方法的指令信息,即class文件中方法表中的Code属性信息。
-
方法出口(返回地址)
- 若方法正常返回,当前栈帧承担着恢复调用者状态的责任。
其状态包括:调用者的局部变量表、操作数栈、被正确增加过来表示执行了该方法调用指令的程序计数器等。使得调用者的代码,能在被调用的方法返回、并且返回值被推入调用者的栈帧的操作数栈后,能够继续正确的执行。
- 若方法执行过程中,某些指令导致了JVM抛出异常,且方法内部并没有对异常进行捕获处理,方法异常调用完成后,一定不会有方法返回值返回给他的调用者。
1.4.本地方法栈
本地方法栈,为虚拟机使用到的 native 方法服务。在虚拟机规范中对本地方法栈中方法是用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方发展和虚拟机栈合二为一。
1.5.Java 堆
-
什么是 java 堆
Java 堆,即Java Heap 是所有线程共享的。
-
java 堆的分区
可细分为:新生代、老年代,再细致还可以分为:Eden 空间、From Suvivor空间、ToSuvivor空间。
-
jvm 参数
-Xmx、-Xms 控制堆内存的最大值和最小值
1.6.方法区
从 JDK1.8 开始,已经移除方法区
- JDK1.8 后,原方法区中的内存如何存储
-
什么是方法区
方法区是各线程共享的内存区域。
存储被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。
对于 HotSpot 虚拟机,很多人把方法区成为"永久代"
-
jvm 参数
-XX:MaxPermSize 控制方法区的最大值
-
可能出现的异常
当方法区无法满足内存分配的需求时,会抛出 OutofMemoryError 异常。
1.7.运行时常量池
-
什么是运行时常量池
方法区的一部分,用于存放编译器生成的各种字面量、符号引用,在类加载后进入运行时常量池中存放
1.8.直接内存
直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。
2.虚拟机中的对象
2.1.对象创建过程
-
虚拟机遇到一条 new 指令
-
检查 new 指令参数在常量池中是否能够定位到一个类的符号引用
符号引用?
-
检查这个符号引用代表的类,是否已经被加载、解析、初始化过,若没有,则必须先执行响应的类加载过程
-
类加载检查通过后,虚拟机为新生对象分配内存。
一个类所需要的内存大小,在类加载通过后就可以完全确定
-
内存分配完成后,需要将分配到的内存空间初始化为零值(不包括对象头)
例如类中的基础类型属性,设置为其对应的初始值,引用类型属性,设置为 null
-
对对象做必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC分代年龄等信息,这些信息,存放到对象的对象头(Object Header)中。
对象的分代年龄,保存在哪里?
-
以上是虚拟机对对象所进行的操作,下面是执行方法,进行初始化
方法,是否就是执行构造方法?
2.2.对象内存布局
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(Object Header)、实例数据(Instance Data)、对齐填充(Padding)
2.2.1.对象头
对象头记录对象的信息,包括哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向时间戳,类型指针
HotSpot虚拟机的对象头包括两部分信息。
-
第一部分:Mark Word
用于存储对象自身的运行时数据如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。
对象需要存储的运行时数据很多,其实已经超出了32位、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的32bit空间中的25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0,而在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容见下表。
存储内容 标志位 状态 对象哈希码、对象分代年龄 01 未锁定 指向锁记录的指针 00 轻量级锁定 指向重量级锁的指针 10 膨胀(重量级锁定) 空,不需要记录信息 11 GC 标记 偏向线程 ID、偏向时间戳、对象分代年龄 01 可偏向 -
第二部分:类型指针
即对象指向它的类元数据的指针。虚拟机根据这个指针来判断这个对象是哪个类的实例。
并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。
另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
2.2.2. 32位虚拟机Mark Word
| |
2.2.3. 64位虚拟机Mark Word
| |
2.2.4. Mark Word各部分含义
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向管程Monitor的指针。
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起,表达的锁状态含义如下:
2.2.2.实例数据
对象真正存储的有效信息,即程序代码中定义的各种类型的字段内容
实例数据存储的是什么内容?是否是 Class 加载后的信息?
2.2.3.对齐填充
HotSpot JVM 要求对象的大小是 8 字节的整数倍,如果数据没有对齐,则需要对齐填充来补全
补全的策略是什么
2.3.对象访问定位
-
句柄方式
优势
reference 中存储的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,refrence 不需要更改
-
直接指针方式
优势
速度快,节省了一次指针定位的时间开销
目前 HotSpot 虚拟机是使用直接指针的方式实现的
3.虚拟机栈与其他内存的关系
注意:-Xss 设置的是单个线程的栈大小。
-
进程内存
操作系统分配给每个进程的内存,是有限制的,如 32 位 windows限制为 2GB
进程内存减去 Xmx (若 java8 以下的虚拟机还需要减去 MaxPermSize),程序计数器消耗内存很小可以忽略
剩下的内存由虚拟机栈和本地方法栈瓜分。
所以每个线程分配到的栈容量越大,可以建立的线程数量就越小。
参考资料