JVM内存模型及对象的创建过程
一、运行时JVM内存模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jKgQ960a-1585809832703)(https://img2018.cnblogs.com/blog/1626845/201904/1626845-20190424211439503-1707501469.png “jvm运行时内存模型”)]
1.程序计数器(线程私有)
- 一块较小的内存空间,是当前线程所执行的字节码的行号指示器。
- 每个线程都有一个独立的程序计数器,各个线程之间的程序计数器互不影响,所以程序计数器也被称为“线程私有”的内存。
作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。比如:顺序执行,选择,循环,异常处理等。
- 在多线程情况下,程序计数器用于记录当前线程执行的位置,当线程被切换时,能够知道当前线程上次执行的位置。
注意:程序计数器是唯一不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
2.Java虚拟机栈(线程私有)
- Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是Java方法执行的内存模型。
- Java虚拟机栈是由一个个栈帧组成,线程在执行一个方法时,便会向栈中放入一个栈帧,每个栈帧中都拥有局部变量表、操作数栈、动态链接、方法出口信息。
栈帧:
用于存储局部变量表,操作数栈,动态链接,方法出口等。栈帧随着方法的创建而创建,随着方法的结束而销毁。
局部变量表:
存放编译期可知的各种基本数据类型,引用类型,回调类型。局部变量表的内存在编译期就完成分配,当非局部变量表进入一个方法时,在栈帧中的内存是固定的,方法运行期间不会改变局部变量表的大小。
Java虚拟机栈会抛出两种异常:
- *Error:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出*Error异常。
- OutOfMemoryError:若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
3.本地方法栈(线程私有)
- 和Java虚拟机栈作用类似,区别是:Java虚拟机栈为java方法服务,而本地方法栈为Native方法服务。
4.堆(线程共享)
- 是jvm虚拟机中内存最大的一块区域,是所有线程共享的一块区域,在虚拟机启动时创建。
- 几乎所有对象和数组的实例都保存在Java堆中,也是垃圾收集器进行垃圾收集最重要的区域,因此也被称为“GC堆”。
由于现在垃圾器基本都采用分代垃圾收集算法,所以java堆还可以细分为:新生代和老年代。
其种新生代又分为:Eden区、From Survivor、To Survivor区。进一步划分是为了更好的回收内存,或快速分配内存。
“分代回收”是因为:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。
- 新生代(Young Generation):大多数对象在新生代被创建,其种很多对象生命周期很短,每次垃圾回收后只有少量对象能够存活,所以用“复制算法”,只需要少量复制成本就可以完成回收。
新生代内又分三个区:一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。
-
老年代(Old Generation):在新生代进过N次垃圾回收后仍然存活的对象,就会被放入老年代,该区域对象存活率高。老年代的垃圾回收通常使用“标记-清除”或“标记-整理”算法。
-
永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。
注意:在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
5.方法区(线程共享)
- 线程共享的内存区域
- 用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译期编译后的代码等数据。
运行时常量池:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LUxm1R56-1585809832707)(https://img2018.cnblogs.com/blog/1626845/201904/1626845-20190424214122702-646796832.png “运行时常量池”)]
-
运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
-
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
-
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
二、对象创建的过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l7eITXBL-1585809832708)(https://img2018.cnblogs.com/blog/1626845/201904/1626845-20190424215459617-548074721.png “对象创建的过程”)]
1. 类加载检查
虚拟机遇到一条 new 指令时,检查这个 new 的参数是否能在常量池中定位一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析或初始化过,如果没有,执行相应的类加载过程。
2.分配内存
在类加载检查通过后,接下来虚拟机将会为新生的对象分配内存。对象所需要的内存大小在类加载完成后便可完全确定,为对象分配空间等同于把一块确定大小的内存从java堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
(1)指针碰撞法
假设Java堆中内存是完整的,已分配的内存和空闲内存分别在不同的一侧,通过一个指针作为分界点,需要分配内存时,仅仅需要把指针往空闲的一端移动与对象大小相等的距离。使用的GC收集器:Serial、ParNew,适用堆内存规整(即没有内存碎片)的情况下。
(2)空闲列表法
事实上,Java堆的内存并不是完整的,已分配的内存和空闲内存相互交错,JVM通过维护一个空闲列表,记录可用的内存块信息,当分配操作发生时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。使用的GC收集器:CMS,适用堆内存不规整的情况下。
Java 堆内存是否规整,取决于 GC 收集器的算法是”标记-清除”,还是”标记-整理”(也称作”标记-压缩”),值得注意的是,复制算法内存也是规整的。在使用Serial、ParNew等待整理过程的收集器时,采用的是指针碰撞,在使用CMS这种mark-sweep算法的收集器时,使用的是空闲列表。
内存分配并发问题:
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,例如正在给A对象分配内存,但是指针还没修改,这时候对象B可能使用原来的指针来分配内存的情况。作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
- CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块内存。JVM 在给线程中的对象分配内存时,首先在各个线程的TLAB 分配,当对象大于TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。虚拟机是否启用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
3.初始零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。如果使用TLAB,这一工作过程也可以提前到TLAB分配时进行。
4.设置对象头
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希吗,对象的GC分代年龄等信息,这些信息存放在对象的对象头中。根据虚拟机当前的运行状态的不同,对象头会有不同的设置方式。
5.执行init方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。