JVM运行时区域划分
名称 | 线程私有 | 作用 | 异常和发生条件 | 重现异常 | 避免异常配置 |
---|---|---|---|---|---|
程序计算器(Program Counter Register) | 是 | 线程所执行字节码的行号指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖于这个计数器来完成。如果是native方法则这个数值则为空。 | 唯一在《Java虚拟机规范》中,没有规定任何OutOfMemoryError情况的区域 。 | 无 | 无 |
Java虚拟机栈(VM Stack) | 是 | 每个方法被执行是,Java虚拟机都会同步创建一个栈桢(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息,并压入栈内,一个方法调用到执行完毕,就对应着一个栈帧的入栈和出栈。 | 1. 如果栈深度超过了虚拟机设置的深度则抛出StackOverflowError 2.如果扩展时无法申请到足够的内存时抛出OutOfMemoryError |
1.缩小栈大小,然后递归调用方法,触发StackOverflowError 2.通过不停的创建线程,可以触发OutOfMemoryError |
-Xss:调大栈的大小 |
本地方法栈(Native Method Stack) | 是 | 与Java虚拟机栈(VM Stack类似) ,但为Native方法服务 | 与Java虚拟机栈类似 | 不做分析 | 不做分析 |
Java堆(Heap) | 否 | 所有的对象实例以及数组都应当在堆上分配,垃圾回收器主要管理的区域,因此也被成为GC堆(Garbage Collected Heap)。==但随着逃逸分析技术日渐强大,栈上分配、标量替换优化手段,并非所有对象都在堆上分配。== | 无法扩展时,抛出OutOfMemeoryError | 设置一个足够小的堆,创建大量对象,并被一个根对象强引用 | -Xms:初始化堆大小 -Xmx:最大堆内存大小 |
方法区(Method Area) | 否 | 用于存储已被虚拟机家在的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据,并未规定要发生垃圾回收行为,具体是否回收依赖于垃圾回收器。在JDK8之前,HotSpot实现方法区的方式为“永久代(Permannet Generation)”,JDK8之后,则采用“元空间(Metaspace)的方式实现,并将存储空间从JVM转移到了直接内存中。JDK7开始,将字符串和静态变量转移到了堆中。原因:原永久代存储在JVM中,需要开发人员去估算永久带使用大小。这是一个很变态的行为。常常引起OutOfMemory:PermGen异常。并且将字符串常量池和静态变量放入堆中,也可以方便垃圾回收。 | 空间不足产生 OutOfMemoryError | JDK7之前可以通过缩小永久代大小,并创建大量类或字符串来实现。JDK7之后,设置Metaspace空间大小,并创建大量的类 | -XX:PermSize,设置永久代初始化大小。 -XX:MaxPermSize,设置永久代最大值,JDK7之前管用。JDK8之后使用直接内存。 -XX:MaxMetaspaceSize:设置元空间的大小,默认是-1不限制。 -XX:MetaspaceSize:设置初始化空间单位是字节 |
两个特殊的区域:
- 运行时常量池:特殊在于它是方法区的一部分,但具有特殊的作用。Class文件有一项信息,是常量池表(Constant Pool Table)用于存放编译期生成的各种字面量与符号引用,类加载后,这部分信息将会被存放到方法区的运行时常量池中。
- 直接内存:直接内存与主机本身的总内存(包括了物理内存和SWAP分区或者分页文件)大小直接有关,随着NIO的引入,还有MetaSpace实现方法区,这部分区域也被经常用到。如果忽视了直接内存,将堆设置太大了,那么直接内存在动态扩展的时候也会出现OutOfMemoryError异常。容量大小通过-XX:MaxDirectMemorySize参数制定,如果不指定,则默认与Java堆最大值一致。通过Unsafe::allocateMemory()进行申请,DirectByteBuffer,对导致直接内存溢出。
对象创建过程

堆上分配空间的方式
一个对象占用的内存空间大小,在类加载之后就已经确定,为对象分配空间的过程,就是把一块固定大小的内存从堆中划分出去。分配方式重点有两种。
- 指针碰撞(Bump The Pointer):被使用的内存被存放在一边,未被使用的内存放在另一边,中间有个指针作为分界线的指示器。但已被使用的内存和空闲内存相互交错在一起时,就不能使用了(也就是在垃圾回收过后,必须要要整理内存,将未使用的放一起,已使用的放一起)。简单高效
- 空闲列表(Free List):JVM需要维护一个列表,记录了哪些内存是空闲的。对象分配内存时,就从列表中找到一块空闲的,划分给对象,并更新列表的记录。即使不整理堆内存也能高效利用。
具体使用哪一种方式,由Java堆是否规整决定。Java堆是否规整则与其采用的垃圾回收器是否带有压缩整理(Compact)能力有关。因此,当时用Serial,ParNew等带压缩整理过程的垃圾收集器,就会采用指针碰撞。对于CMS基于标记清楚算法的收集器,理论上就只能采用空闲列表的方式。
内存分配是一个频繁的操作,并且堆是有线程共享的,所以存在并发安全问题。解决这个问题也有两种方式。
- 对内存分配动作进行同步处理:采用CAS+失败重试,保证更新操作的原子性。
- 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):每个线程在Java堆中预先分配一部分内存,然后线程自身分配的这一部分使用完毕之后,在通过同步的方式,给线程重新分配一部分。
-XX:+/-UseTLAB
参数进行设定。
分配完毕内存,会讲对象的初始化为零值,如果使用TLAB
机制,则会提前到,内存分配的时候。
对象的内存布局
之后,需要对对象进行必要的设置。例如:对象是哪个类的实例,对象的哈希吗,对象的GC分代年龄等。
对象内存布局主要分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID。在32位虚拟机和64位虚拟机中,分别对应32比特和64比特。在对象未被同步的情况下,25个比特重来存储对象哈希码,4个比特存储对象分代年龄,2个比特存储锁标志位,1个比特固定为0。==注意,在标志位不同的情况下,对象头存储的内容是不同的。== ,对象头的另一部分是类型指针,对象指向它的类型元数据的指针。如果是一个数组对象,那么对象头中还必须有一块用于记录数组长度的数据。
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 重量级锁定 |
空、不需要记录信息 | 11 | GC标志 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
- 实例数据部分(Instance Data):用来存储对象的有效数据。无论是我们定义的字段,还是从父类中继承下来的字段,都必须要有记录。存储顺序会受到JVM分配策略参数(-XX:FieldsAllocaationStyle)和字段定义的顺序影响。如果HotSpot虚拟机+XX:CompactFields参数值为true时(默认true),则子类字段可以插入在父类空袭中。
- 对齐填充:HotSpot虚拟机的自动内存管理要求对象的起始地址必须是8字节的整数倍。如果不够则需要填充。
对象的访问定位
Java程序会通过栈上的reference类型来找到堆上的具体对象。《Java虚拟机规范》里规定了,reference类型它是一个指向对象的引用,但并没有规定通过什么方式去定位、访问到对象的具体位置,所以访问方式是根据具体虚拟机实现而定的。目前主流的有两种方式。
- 使用句柄访问的方式:Java堆中会划分出一块内存作为句柄池,reference中,存储的是对象的句柄地址,而句柄中包括了对象实例数据与类型数据各自的具体地址。需要两次寻址,一次找到句柄的地址,再通过句柄中对象的实例数据指针,找到堆中对象的地址。句柄的好处是,在对象被怎么移动(有些垃圾收集器会整理内存移动对象),只需要改变句柄中的实例数据指针。
-
使用直接指针访问的方式:reference直接只想堆中对象的地址,如果访问对象本身的话,直接就可以找到。好处是,减少了一次指针定位的时间开销。就HotSpot而言,主要是使用第二种方式的(Shenandoah收集器的话也会有一次额外的转发)。
Reference2HeapObject.png
Java中的引用
自JDK1.2版本后,引用类型有被划分为了四种,分别是
- 强引用(Strongly Reference):指程序中普遍存在的引用赋值,蕾丝
Object obj = new Object()
,无论什么情况,被强引用的对象,绝对不会被回收。 - 软引用(Soft Reference):描述一些有用但是并不重要的对象,只被软引用的对象,在即将发生内存溢出前,会被列进回收范围中进行二次回收。如果回收后,仍然内存不足则抛出内存溢出异常。软饮用通过
SoftReference
进行实现。 - 弱引用(Weak Reference):用来描述那些非必须的对象,引用强度比软引用还要弱。被弱引用的对象,只能活到下次垃圾回收前。通过
WeakReference
实现。 - 虚引用(Plantom Reference):又被成为“幽灵引用”或者“幻影引用”,它的存在不会对垃圾回收产生任何的影响,存在的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。通过,
PlantomReference
实现,通常与ReferenceQueue
一起使用,被虚引用的对象在被回收后,会被放入到ReferenceQueue
中。
网友评论