JVM内存模型和对象的创建过程

一、运行时数据区域
    JVM在执行java程序的过程中会把他所管理的内存划分为若干个不同的数据区域。这些区域有各自的创建和销毁的时间,这些区域可分为线程私有和共有两类,线程私有的有:程序计数器java虚拟机栈本地方法栈线程公有的有:方法区。如图所示:
JVM内存模型和对象的创建过程
1、程序计数器(线程私有)
也就是字节码的行号指示器模拟的是CPU的程序计数器),指示当前进程所要执行的字节码(而不是下一条),这个指令将交给字节码解释器去完成解释功能。
因为要保证线程切换后能恢复到正确的执行位置,每条线程都需要有个独立的程序技术器,各条程序计数器之前互不影响,独立存储。所以他是“线程私有的”。如果一个线程正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法,这个计数器值为null

2、java虚拟机栈(线程私有)
描述的是java方法执行的内存模型栈中存储的是一个一个的栈帧每个方法在执行的同时都会创建一个栈帧),而一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧:存储局部变量表(方法中的局部变量)、操作数栈、动态链接、方法出口等信息。
JVM内存模型和对象的创建过程
1、局部变量表
用来存储方法参数和方法内部定义的局部变量。【包括:8种基本数据类型、对象引用(eference)和returnAddress()】。局部变量表的容量是以变量槽为最小单位的。其中long和double会占用两个局部变量空间,其余的只占1个空间局部变量表所需要的内存空间在编译期已经完成分配。局部变量表中的变量使用之前必须赋予初始值,不想类变量有两次赋初始值的过程(一次在准备阶段,赋予系统初始值;一次在初始化阶段,赋予程序员定义的值)

2、操作数栈
操作数栈的每一个元素可以是任意Java数据类型,32位数据类型所占的栈容量为1字宽,64位数据类型所占的栈容量为2字宽。对于32位虚拟机来说,一个”字宽“占4个字节,对于64位虚拟机来说,一个”字宽“占8个字节。
当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值
另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下栈帧的部分操作数栈与上面栈帧的部*部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了,重叠过程如下图:

3、动态链接
每个栈帧都包含一个指向运行时常量池(方法区中的一部分空间,用来存储各种字面量和符号的引用)中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

4、方法返回地址
一个方法在执行过程中有两种返回方式,一种是正常返回return,另一种是遇到异常返回。无论哪种退出方式,在方法退出之前,都需要返回到方法被调用的位置上。一般来说,方法正常返回,调用者的程序计数器的值可以作为返回地址,栈帧可能保存这个值。如果异常退出,返回地址需要通过异常处理器来确定,栈帧是不会保存着部分信息的。

5、附加信息
虚拟机规范中允许具体的虚拟机增加一些规范中没有的信息到栈帧中,这部分信息取决于具体的虚拟机。

3、本地方法栈
本地方法栈和虚拟机栈所发挥的作用非常类似,他们之间的区别不过是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务但是要注意一下,虚拟机规范中没有对本地方法区中的方法作强制规定,虚拟机可以*实现,即可以不是字节码。但是也可以是字节码,这样虚拟机栈和本地方法区就可以合二为一,事实上,OpenJDKSunJDK所自带的HotSpot虚拟机就直接将虚拟机栈和本地方法区合二为一。

4、java堆(线程共享)
存放对下对象的实例,在java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配
java堆(GC堆)是垃圾回收器管理的主要区域。为了更好的回收内存或者更快的分配内存,现在收集器采用分代收集算法,将java堆进行划分,分为新生代和老年代(空间比:1:2)。新生代又划分的有: Eden空间、FromSurvivor空间、To Survivor空间(空间比:8:1:1)等。进一步的划分只是为了更好的回收内存,或者更快的分配内存。

5、方法区(线程共享)
存储已被虚拟机加载的类的信息。比如:类中定义的常量、静态变量、即时编译器编译后的代码等数据。也称为有“永久代”。GC也会回收它的内存空间

运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号的引用这部分将在类加载后进入方法区的运行时常量池中存放
java虚拟机对Class文件每一个部分(包括常量池)的格式都有严格的规定,每一个字节用于存储那种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行。但对于运行时常量池,java虚拟机并没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中
运行时常量池相对于Class文件常量池的另一个重要的特征是具备动态性java语言并不要求常量一定只有编译期才能产生,运行期间也可以将新的常量放入池中,这种特性被开发人员用的比较多的是String类的intern()方法。
如果常量池无法在申请到内存时会抛出OutOfMemoryError异常。
常量池:常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。
字符串常量池:是常量池中的一部分,存储编译期类中产生的字符串类型数据。
运行时常量池:方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。
字面量:文本字符串、声明为final的常量值等;
符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符

直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但是这部分区域被频繁使用。在JDK1.4版本中加入了NIO(New input/output)类,引入了基于通道(Channel)与缓冲区(Buffer)的I/O方式,也就是说通过这种方式,不会在运行时数据区域分配内存,这样就避免了在运行时数据区域来回复制数据,直接调用外部内存。

二、对象的创建过程(new)
①首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用并且检查这个符号引用代表的类是否已被加载、解析和初始化过。
②如果没有,那必须先执行相应的类加载过程(加载 链接 初始化
其中初始化包括:验证、准备、解析
③加载完成后,接下来为新生对象在堆中分配内存(对象所需内存的大小在类加载完成后便可完全确定)。具体的分配方式有两种:一种是指针碰撞,一种是空闲链表
指针碰撞:java堆内存绝对规整,指针的一边是使用过的,另一边是未使用
空闲列表:堆内存不规整,需要维持一个列表记录哪块内存可用。
注意:
选择哪种分配方式有java堆是否规整决定,而java堆是否规整又由所采用的垃圾回收器是否带有压缩整理功能。比如向Serial、PerNew带有整理过程的收集器,采用指针碰撞算法。而向CMS基于内存整理算法的收集器时,采用空闲列表。
④内存分配完成后,给分配的内存空间都初始为零值。
⑤然后设置对象的对象头信息。例如:对象的GC年龄,对象的哈希码等
⑥执行完new指令后,接着执行<init>方法,按照程序员的设计进行初始化(有字节码中是否跟随invokespecial指令所决定),这样一个真正可用的对象才算完全产生出来)
JVM内存模型和对象的创建过程

过程:
类加载的过程包括了者五个阶段:加载、验证、准备、解析、初始化。这几个阶段指的是按顺序开始,并不是按顺序执行完成,通常在一个阶段执行的过程中会调用或**另一个阶段。

一个类从加载到卸载,他的生命周期包括以下过程:
JVM内存模型和对象的创建过程
在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性
1、加载:
①通过一个类的权限定名称获取这个类的二进制字节流
②将这个二进制字节流代表的静态存储结构转化为方法区的运行时数据结构
在java堆中创建这个类的Class对象,作为方法区这些数据的访问入口

2、链接:
a>验证
确保Class文件中的字节流符合虚拟机要求
文件格式验证:验证字节流是否符合Class文件格式的规范
元数据验证:对类的元数据信息进行语义校验,保证描述的信息符合java规范
字节码验证:确定程序的语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正常执行。
b>准备
类的静态变量分配存储空间(这些内存都将在方法区进行分配
注意:仅包括类变量(static),而不包括实例变量;初始值是数据类型默认的零值,boolean的零值是:false
c>解析
将常量池内的符号引用替换为直接引用的过程。
符号引用:可以是任意的字面量,只要能无歧义的定位到目标即可
直接引用:直接指向目标的指针、相对偏移量或者句柄(与虚拟机的内存布局有关)
3、初始化
类加载的最后一步,初始化阶段才真正开始执行我们的java代码,

注意:
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(RTTI)(也称为动态绑定或晚期绑定)

注意:划分空间时的并发问题?
比如在正给对象A分配内存,指针还没来的急修改,对象B又同时使用了原来的指针来分配内存。有两种解决办法:
1、采用CAS配上失败重试的方法保证更新操作的原子性
2、将内存分配动作按照线程划分在不同的空间之中进行。每个线程在java堆中预先分配一小块内存【本地线程分配缓存(TLAB)】。哪个线程要分配内存,就在哪个线程的线程分配缓冲中分配,只有缓存用完了并分配新的缓存时,才需要同步锁定。设置虚拟机是否使用本地线程分配缓存(TLAB),可以通过-XX:+/-UseTLAB参数来设定。

对象被成功分配内存。我们知道通过一个对象,我们可以通过getClass()方法获取类,默认比较两个对象实际比较的是对象内存的哈希值,这又是怎么实现的呢?其实在分配完内存后,虚拟机会对对象进行必要的设置,对象的类,对象的哈希码等信息都存放在对象的对象头中,所以分配的内存大小绝不止属性的总和。


三. 堆中的对象

1、堆中对象的内存布局
对象在堆中的布局分为三个区域:对象头实例数据对齐填充

①对象头
HotSpot虚拟机中对象头包含 两部分信息。
第一部分,用于存储对象自身的运行时数据。例如:hashcode、gc分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据官方称它为“Mark Word”。Mark Word被设计成一个非固定的数据结构(节省空间),会根据对象的状态复用自己的存储空间。
另外一部分是类型指针,即对象指向它的类元数据的指针虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
JVM内存模型和对象的创建过程

②实例数据
对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。
从分配策略中可以看出,相同宽度的字段总是被分配到一起,如:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),在满足这个前提条件的情况下在父类中定义的变量会出现在子类之前。

③对齐填充
它仅仅起着占位符的作用。HotSpot VM的自动内存管理系统要求对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

2、对象的访问定位
java程序需要通过栈上的reference数据来操作堆上的具体对象,那么虚拟机栈变量表中的reference引用如何在堆中找到对象实例呢?这里有两种方法:句柄访问直接访问。HotSpot使用的就是直接访问方法。

①句柄访问:
Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址句柄中包含了对象实例数据与类型数据各自的具体地址信息。
JVM内存模型和对象的创建过程
优点:
reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。

②直接指针访问
JVM内存模型和对象的创建过程
优点:
使用直接指针访问方式的最大好处就是速度更快。


四、类加载器
类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
大致可以分为3种:启动类加载器、扩展类加载器、应用程序类加载器
JVM内存模型和对象的创建过程
启动类加载器:
这个类将器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。

扩展类加载器:
它负责加载<JAVA_HOME>\lib\ext目录中的jar包,开发者可以直接使用扩展类加载器。

应用程序类加载器:
由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载

优点:
保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中。
先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方
法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载

破坏双亲委派模型
双亲委派模型并不是一个强制性的约束模型,但也有例外,到目前为止,双亲委派模型主要出现过3较大规模的“被破坏”情况。
第一次“被破坏”其实发生在双亲委派模型出现之前,由于双亲委派模型在JDK 1.2之后才被引入,而类加载器和抽象类java.lang.ClassLoader则在JDK 1.0时代就已经存在。为了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass()。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。