一个类的实例从new开始的过程
对象的内存布局、对象的访问定位
对象的几个部分的作用:
1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;
3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;
4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;
5.对齐字是为了减少堆内存的碎片空间(不一定准确)。
1、 Java对象创建过程:
(1)虚拟机遇到一条new指令时,首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经加载、连接和初始化。如果没有,就执行该类的加载过程。
(2)为该对象分配内存。
A、假设Java堆是规整的,所有用过的内存放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器。那分配内存只是把指针向空闲空间那边挪动与对象大小相等的距离,这种分配称为“指针碰撞”
B、假设Java堆不是规整的,用过的内存和空闲的内存相互交错,那就没办法进行“指针碰撞”。虚拟机通过维护一个列表,记录哪些内存块是可用的,在分配的时候找出一块足够大的空间分配给对象实例,并更新表上的记录。这种分配方式称为“空闲列表“。
C、使用哪种分配方式由Java堆是否规整决定。Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
D、分配对象保证线程安全的做法:虚拟机使用CAS失败重试的方式保证更新操作的原子性。(实际上还有另外一种方案:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,TLAB。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才进行同步锁定。虚拟机是否使用TLAB,由-XX:+/-UseTLAB参数决定)
(3)虚拟机为分配的内存空间初始化为零值(默认值)
(4)虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到对象的元数据信息、对象的Hash码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。
(5) 执行<init>方法,把对象按照程序员的意愿进行初始化。
2、 对象的定位访问的方式(通过引用如何去定位到堆上的具体对象的位置):
(1)句柄:使用句柄的方式,Java堆中将会划分出一块内存作为作为句柄池,引用中存储的就是对象的句柄的地址。而句柄中包含了对象实例数据和对象类型数据的地址。
(2)直接指针:使用直接指针的方式,引用中存储的就是对象的地址。Java堆对象的布局必须必须考虑如何去访问对象类型数据。
(3)两种方式各有优点:
A、使用句柄访问的好处是引用中存放的是稳定的句柄地址,当对象被移动(比如说垃圾回收时移动对象),只会改变句柄中实例数据指针,而引用本身不会被修改。
B、使用直接指针,节省了一次指针定位的时间开销。
说法1
jvmP44
1,首先到常量池中找类的带路径全名,然后检查对应的字节码是否已被加载,连接(验证、准备、解析)初始化,如果没有先执行类加载过程(class.forname())。
2,类加载过程完成后,虚拟机会为对象分配内存。分配内存有两种方式,根据使用的垃圾收集器的不同使用不同的分配机制。
(1)指针碰撞,当虚拟机使用复制算法或标记整理算法实现的垃圾收集器时,内存区域都是规整的,这时候使用指针碰撞分配内存,用过的内存放在一边,空闲的内存在另一边,中间用一个指针作为分界点,当需要为新对象分配内存时只需把指针向空闲的一边移动一段与对象大小相等的距离。
(2)空闲列表,当虚拟机使用标记清除算法实现的垃圾收集器时,内存都是碎片化的,那虚拟机就要记录哪块内存是可用的,当需要分配内存时,找一块足够大的内存空间给对象实例,并更新记录。
3,设置对象头信息,如所属类,元数据信息,哈希码,gc分代年龄,等等。
4,调用对象的init()方法,根据传入的属性值给对象属性赋值。
5,在线程栈中新建对象引用,并指向堆中刚刚新建的对象实例。
说法2
在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。在实例化一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。在类初始化过程中或初始化完毕后,根据具体情况才会去对类进行实例化。
1:寻找类定义
jvm会在自己的一个名叫“方法区”的内存块中,寻找名叫“MyObject”的Class对象(注意class也是一个对象,该对象记录了所有类的定义),如果有,则按照Class对象的定义,生成一个MyObject对象。
2:加载类定义
如果“方法区”中没有名为“MyObject”的Class对象,jvm会用当前类的类加载器(classloader)从当前的classpath路径寻找名为"MyObject.class"的文件,如果找到,则将文件进行分析,转换为Class对象存放在“方法区”中,否则抛出“ClassNotFoundException”。对于jdk的class,jvm启动时,会用启动类加载器加载,对于用户的class,则会用应用程序类加载器实时加载,所谓实时加载,指的是遇到的时候再加载,而不是预先一次性加载。关于类加载器,有三级,jvm严格的限制了每一级的加载权限,加载模式为“双亲委托模式”,加载任何类,都先由父加载器加载。
3:给对象分配内存空间
找到MyObject的类定义后,jvm在内存“堆”中,开辟一个空间,该空间按照MyObject类定义开辟,并将该空间中的各个内存段设置默认值,对应的就是对象的属性初始化默认值。
4:对象的初始化顺序
对象的初始化都先从父类开始,顺序如下:
给父类静态变量默认值 ;
对父类静态变量赋值 ;
执行父类静态块 ;----父类的类构造器<clinit>()
给当前类静态变量默认值 ;
对当前类静态变量赋值 ;
执行当前类静态块; ----子类的类构造器<clinit>()
给父类变量默认值 ;
对父类变量赋值 ;----父类的成员变量和实例代码块
执行父类构造函数; ----父类的构造函数
给当前类变量默认值 ;
对当前类变量赋值 ;----子类的成员变量和实例代码块
执行当前类构造函数;----子类的构造函数
5:对象构造完成
注:当new一个String的时候,只是生成一个String对象,而没有生成Object对象,Object的类定义在“方法区”这块内存中,当new String的时候,jvm会检查String的父类,找出父类的定义,并找出哪些是String可以拥有的,然后按照筛选出来的父类定义和String本身的类定义,在堆中分配一个内存块(就是俗称的生成了一个对象),而没有专门为String的父类Object分配空间。
说法3
对于用new 创建一个对象,我们需要弄清楚它的过程:
引用和创建一个对象的格式是:
类名 变量名;
变量名=new 类名(参数列表);
比如 Vehicle veh1=new Vehicle();
这个语句具体的执行过程是:
1.右边的“new vheicle"是以vehicle类为模板,在堆空间里创建一个vehicle类对象(也简称vehicle对象)。
2.末尾的()意味着,在对象创建后,立即调用vehicle类的构造函数,对刚生成的对象进行初始化。构造函数肯定是有的,如果没有创建,java会补上一个默认的构造函数。
3.左边的'Vehicle veh1'创建了一个vehicle类引用变量
4.“=”操作符使对象引用指向刚创建的Vehicle对象。
将上面的语句分为两个步骤:
Vechicle veh1;
veh1=new Vechicle;
这样写,就比较清楚了,有两个实体:一是对象引用变量,一是对象本身。在堆空间里创建的实体,与在栈空间里创建的实体不同。尽管它们也是确确实实存在的实体,但是似乎很难准确的“抓”住它。我们仔细研究一下第二