在上一篇文章中,咱们花了较大的篇幅去介绍了JVM的运行时数据区,而且重点介绍了栈区的结构及做用,相关内容请猛戳!在本文中,咱们将主要介绍对象的建立过程及在堆中的分配方式。java
相关连接(注:文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8,我的技术博客www.17coding.info)
一、 你必须了解的java内存管理机制-运行时数据区
二、 你必须了解的java内存管理机制-内存分配
三、 你必须了解的java内存管理机制-垃圾标记
四、 你必须了解的java内存管理机制-垃圾回收算法
在上文咱们提过一些问题,你的对象是怎么new出来的?new出来又放在哪里?怎么引用的? 老规矩,咱们仍是经过字节码来了解一下。安全
public static void main (String[] args){
People p = new People(); }
这样的代码你们一点也不会陌生,咱们都知道使用new关键字能够建立一个对象,对应的字节码以下
咦!一看字节码才知道,咱们的一行new的代码,对应的字节码原来要作这么多操做!咱们逐一来分析一下。多线程
JVM遇到new指令时,先检查指令参数(上面字节码中的#2)是否能在常量池中定位到一个类的符号引用(上面最终定位到常量池中的com/test/entity/People):
1)、若是能定位到,检查这个符号引用表明的类是否已被加载、解析和初始化过;
2)、若是不能定位到,或没有检查到,就先执行相应的类加载过程;
具体类的加载、解析、初始化的过程你们能够去查找JVM类加载机制相关资料,这里就不展开啦!咱们须要知道的是这一步保证了在方法区中,存在要建立实例对象的类对象!jvm
我们到了适婚年龄,也就该找个对象了吧!你看上了一个姑娘,长得楚楚动人,就跑去跟他妈说:“我要一个对象,把你女儿嫁给我吧!”。她妈妈却是十分爽快:“好啊,我女儿总得有个地方住吧,小伙子你有房吗?”。这时候场面一度十分尴尬,内心嘀咕着“要是国家能分配房子就行了!”。这在当前社会显然不现实,毕竟我们还没进入共产主义社会!然而在JVM王国里,对象住的“房子”倒是“国家”统一分配的。国家集中圈了一大块“地”,谁家要娶“媳妇”,就给他家分配一块“地”,“媳妇”胖点呢,地就大一点,“媳妇”瘦一点呢,“地”就小一点。在这里,你一我的能够同时拥有多个对象,在这里,多我的能够拥有同一个对象。因此这里的老百姓安居乐业、这里一片祥和……固然,因为这块“地”大小有限,而你又同时拥有不少对象,还有其余人也要娶对象,因此那些不用了的对象的“地”国家就会进行统一征收(固然这里不会给补贴,毕竟是免费分配的~)以继续分给其余人用。
上面扯了这么多,相信你已经知道“你”就表明着一个线程,“国家”指的是JVM,“国家”圈的一块“地”就是堆空间,你娶的“对象”就是实例对象,“国家”分配地的动做就是内存分配,而国家征收的动做就是垃圾回收。
因为要找对象的人太多了,因此分配的操做也很频繁,那么摆在“国家”的问题就来了:怎么合理分配?怎么最大限度的提升空间利用率?怎么提升分配效率?不用了的空间怎么回收?怎么知道哪些空间不用了?上面不少问题都须要结合后面的垃圾回收相关的内容来讨论,这里只讨论分配内存的方式。
一个对象须要占用多大的内存?这个问题其实在类加载完成后就已经肯定啦!JVM能够经过普通java对象的类元信息肯定对象大小。为对象分配内存至关与把一块肯定大小的内存从java堆中划分出来。那么问题来了,这么大的一块堆空间摆在JVM的面前,JVM该划哪一块空间来分配内存呢?随机找一块空间分配算了?or紧挨着以前分配的空间后面进行分配?这里须要说到的是两种分配方式:
1)、 指针碰撞
若是Java堆是绝对规整的:一边是用过的内存,一边是空闲的内存,中间一个指针做为边界指示器,分配内存只需向空闲那边移动指针,这种分配方式称为"指针碰撞"(Bump the Pointer)。这里有个条件就是“绝对规整”,相似下图,左边全是被绿过了的,右边则全是等着被绿的。新分配对象时候就是多绿了一块,边界指示器向后移动!
性能
2)、 空闲列表
若是Java堆不是规整的:用过的和空闲的内存相互交错。须要维护一个列表,记录哪些内存可用。分配内存时查表找到一个足够大的内存,并更新列表,这种分配方式称为"空闲列表"(Free List)。相似下图,好好的一块内存被绿得乱七八糟,用上面指针碰撞的方式是碰不动了!因此就用一个小本本记着哪里有多大的空闲空间能够绿!固然下图的地址编号是虚拟的,空闲列表的样子也是我意淫出来的,表达的意思你懂就行!
this
咱们能看到,致使这两种方式的差别主要取决于java堆是否规整,而java堆是否规整又是由jvm采用的垃圾收集器是否带有压缩功能决定的。使用Serial、ParNew等带Compact过程的收集器时,JVM采用指针碰撞方式分配内存。而使用CMS这种基于标记-清除(Mark-Sweep)算法的收集器时,采用空闲列表方式。(下篇文章会具体介绍不一样的垃圾收集器)
不论是指针碰撞仍是空闲列表,都会存在同一个问题,那就是在多线程的场景下的线程安全问题。多个线程同时在new的时候把对象分配到同一块内存了咋办,不得干起来么!因而jvm采用了两种方案来解决:
1)、 同步处理:JVM采用CAS(Compare and Swap)机制加上失败重试的方式,保证更新操做的原子性。CAS机制是一种轻量级锁机制,后续在聊多线程的时候再讲!
2)、 本地线程分配缓冲区:把分配的内存按照不一样的线程划分在不一样的空间进行,每一个线程在java堆区预先分配一小块内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer)。哪一个线程须要分配就从哪一个线程的TLAB上分配,只有在TLAB用完须要分配新的TLAB的时候才须要作同步处理(经过上一点中的CAS机制)。spa
内存分配完后,就须要初始化实例对象了,虚拟机须要将分配到的内存空间中的数据类型都初始化为零值(不包括对象头,若是是使用TLAB,初始化0值的操做提早至分配TLAB时)。接下来虚拟机要对对象进行必要的设置,例如这个对象是哪一个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都存放在对象的对象头中。作完以上之后,从虚拟机视角来看,一个新的对象已经产生了!线程
JVM完成对象内存的分配及对象初始化以后,会返回对象的地址,而且压入操做数的栈顶,供后续操做!指针
dup命令没猜错的话是duplicate的简写。在讨论dup命令前,咱们先看一个简单的例子
public static void main (String[] args){
int a; int b = a = 88; }
咱们看看对应的字节码
public static void main(java.lang.String[]);
Code:
0: bipush 88
2: dup 3: istore_1 4: istore_2 5: return
因为88这个值在一条语句中须要重复赋给两个变量,因此使用dup指令对栈顶的值进行了复制,且压入栈顶。咱们在new对象的时候,new指令后面都会紧跟dup指令!而后是invokespecial和astore指令,相信聪明的你应该想到invokespecial和astore指令都会须要从栈顶弹出值来执行!在执行完dup指令后,操做数栈栈顶就有两个指向该对象实例内存的reference数据,若是<init>方法有参数,还须要把参数加载到操做栈。
invokespecial指令调用对象实例方法<init>,经过符号引用#3定位到的是People对象的实例方法<init>。这时候操做数栈栈顶值(指向对象实例的内存reference)会被弹出(若是<init>方法有参数,参数也会出栈)。执行<init>方法会在java虚拟机栈中建立<init>方法的栈帧(相关栈和栈帧的介绍看上一篇文章),而且把出栈的数据放入栈帧的局部变量表中。变量表中指向对象实例的内存reference就是咱们常常用到的this,表示对该对象实例进行操做!执行完该指令后,一个完整的对象就建立完成啦!
astore依然须要弹出栈顶值,而后存储到编号为1的变量中供后续使用。至此一个完整的对象已经建立且返回对象内存引用给本地变量存储了。
咱们上面已经把对象建立的问题解决了,同时咱们也都知道,引用类型的变量存储的是**对象的引用**!那这个引用类型数据怎么定位到堆中的对象呢?目前主流的对象访问方式有两种:
JVM在堆区划分一块内存做为句柄池,引用类型变量中存储就是对象的句柄地址。对象句柄包含两个地址(以下图):
一、在堆中分配的对象实例数据的地址。
二、这个对象类型数据地址。
引用类型变量中存储就是在堆中分配的对象实例数据的地址。
句柄池的方式会在句柄池中存放类型对象的相关信息,而直接访问的方式会把类型对象的信息放入实例对象的对象头中(咱们知道对象头包含“指向对象类型数据的指针”,其实这并非必须的,咱们经常使用的HotSpot虚拟机采用的是直接指针的方式,因此对象头中会包含“指向对象类型数据的指针”,若是某类虚拟机采用的是句柄的方式访问对象,那可能就不须要在头部存储这个指针了)。这两种方式都互有优缺点: 1)、 句柄方式访问对象时,多一次指针定位的时间开销。可是对象移动时(垃圾回收时常见的动做),栈上的变量的引用不须要修改,只需改变句柄中实例数据指针。 2)、 直接指针对象相对句柄方式访问节省了一次指针定位的时间开销,性能更好。若是对象访问很是频繁,提高会更明显!可是在对象移动时,栈上的变量的引用也须要变化。