目录
(一)程序计数器(Program Counter Register)
(二)Java虚拟机栈(Java Virtual Machine Stacks)
一、Java虚拟机运行时数据区
![Java虚拟机运行时数据区](https://img-blog.csdnimg.cn/20191031151144330.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zODIwNzcyMg==,size_16,color_FFFFFF,t_70)
(一)程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它可以看成是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
(二)Java虚拟机栈(Java Virtual Machine Stacks)
Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。虚拟机栈中局部变量表存放了编译期可知的各种基本类型以及对象引用,其中64位长度的long和double类型的数据会占用2个局部变量空间。局部变量表所需的空间在编译期间完成分配,且方法运行期间不会改变局部变量表的大小。
(三)本地方法栈(Native Method Stack)
本地方法栈为了虚拟机使用到的Native方法服务。对与本地方法栈虚拟机规范并无强制规定,各虚拟机可自由实现它。HotSpot VM直接就把本地方法栈和虚拟机栈合二为一。
(四)Java堆(Java Heap)
Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存放对象实例。Java虚拟机规范中的描述是:所有对象实例以及数组都要在堆上分配,但是随着JIT(JUST-IN-TIME)编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将使之发生一些微妙的变化,所有对象都被分配在堆上也不那么绝对了。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为"GC堆(Garbage Collected Heap)"。
(五)方法区(Methed Area)
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆)。HotSpot最初实现方法区的方式为将其定义为Java堆的永久代做统一管理,在JDK7已将原本放在永久代的字符串常量池移除了。运行时常量池(Runtime Constant Pool)是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还包含常量池,用于存放编译期生成的各种字面量和符号的引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行期间也可将新的常量放入常量池中,这种特性被使用较多的是String.intern()方法。
二、HotSpot虚拟机对象
(一)对象创建
2.1.1new指令
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号的引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需空间大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
2.1.2对象内存分配
内存分配方式又按照Java堆是否绝对规整(Java堆是否绝对规整在于垃圾收集器是否带有压缩整理功能)分为:
- 指针碰撞:Java堆绝对规整,所有用过的内存都放在一边,空闲的内存都放在另一边,中间放着一个指针作为分界点的指示器,那分配内存仅仅是将指针向空闲部分移动一段相应距离即可完成。
- 空闲列表:Java堆不是绝对规整,虚拟机就必须维护一个列表,记录那些内存是可用的,分配内存就是在列表中划出足够大小的空间,并更新列表。
对象创建在虚拟机中是非常频繁的行为,即使仅仅修改指针也不能保证不会被别的线程重复分配。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。线程分配内存的动作都在TLAB上操作,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
2.1.3.对象初始化
内存分配完之后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)如果使用TLAB,这一过程要提前至TLAB分配时进行。 虚拟机要对对象进行必要的设置,例如对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息,存储在对象头(Object Header)之中 。从虚拟机视角来看,一个新的对象已经产生,但是从Java程序的视角来看,对象的创建才刚刚开始,执行new执令之后会接着执行<init>方法,按照程序员的意愿来对对象进行初始化。
(二)对象在内存中存储的布局
2.2.1.对象头(Header)
对象头包含两个部分,第一部分用于存储自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位虚拟机中分别位32bit和64bit,官方称之为“Mark Word”。对象头需要存储的数据已经超过了32位、64位所能记录的极限,为了虚拟机空间效率,对象头被设置为不定格式的存储结构,以复用自身的存储空间。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
2.2.2.实例数据(Instance Data)
实例数据是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义的顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略上看,相同长度的字段总是被分配到一起。如果CompactFields=true(默认为true),那么子类之中较短的变量可能会插入到父类变量的间隙中。
2.2.3.对齐填充(Padding)
非必要,HotSpot VM要求对象大小以及对象头必须是8字节的整数倍,因此当对象实例数据部分未满足条件时,使用对齐填充来补全。
2.2.4.对象的访问定位
- 使用句柄
- 直接指针(Sun HotSpot使用)
![](https://img-blog.csdnimg.cn/20200102194838876.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zODIwNzcyMg==,size_16,color_FFFFFF,t_70)
![](https://img-blog.csdnimg.cn/20200102194849415.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zODIwNzcyMg==,size_16,color_FFFFFF,t_70)
三、OutOfMemoryError异常
(一)Java堆溢出
-Xms20m 设置堆的初始值,默认情况是机器物理内存的1/64
-Xmx20m 设置堆的最大值,默认是机器物理内存的1/4(-Xms等于-Xmx时,即可避免自动扩展)
Java堆用于存储对象,只要不断的创建对象,并且避免被GC,那么在对象数量到达最大堆的容量限制后就会产生OutOfMemoryError异常。
(二)虚拟机栈和本地方法栈溢出
对于HostSpot来说,虽然-Xoss(设置本地方法栈大小)参数存在,由于HostSpot将本地方法栈放入虚拟机栈中实现的,所以-Xoss是无效的,栈容量只由-Xss参数来设定。
在单个线程下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。通过不断的创建线程,加大为每个线程分配的内存,可导致OutOfMemoryError异常。
(三)方法区和运行时常量池溢出
-XX:PermSize=10M
-XX:MaxPermSize=10M(JDK8移除了持久代,所以PermSize被弃用了,多了一个元数据区,设置元数据区的参数为MetaSpaceSize)
只要不停的向字符串常量池中放入字符串就可导致OutOfMemoryError异常。
JDK6中String.intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用。JDK7以后String.intern()方法不会再复制实例,只是在常量池中记录首次出现的实例引用。
(四)直接内存溢出
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在JDK4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在Java堆和Native堆中来回复制数据。
直接内存大小可由-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值一样。由Direct Memory导致的内存溢出,一个明显的特征是在Head Dump文件中不会看见明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑一下是否是由于Direct Memory导致的。