摘要
本节主要讲解java中对象是如何创建?如何布局?如何访问?
内容
Java是一门面向对象的程序语言,Java程序运行过程中无时无刻都有对象被创建,语言层面上,常见对象,创建一个对象是一个new关键字而已,在虚拟机中,对象又是怎样创建的呢?
思维导图
1、对象创建
举例来说;我们通过以下代码创建一个对象A;
public class A {
private int a;
public static void main(String[] args) {
A a = new A();
}
}
他对应的底层jvm虚拟机中又是一个怎样的过程呢?
对象的创建主要包括六个步骤:
类的初始化对于引用变量初始化为null,对于基本类型变量就具体赋值;init方法执行:主要包括静态方法、静态代码块等。
- 对象检查:虚拟机遇到new指令时,首先去检查这个指令参数是否在常量池中定位到一个类的符号引用;并检查这个符号引用代表的类是否已经被加载、解析、初始化。
- 类加载:虚拟机进行符号引用的类加载、解析、初始化。
- 分配内存:类检查通过后,虚拟机为新对象分配内存;对象分配的 内存在虚拟机类加载完成之后就是可以确定的;为对象分配空间的任务 等同于把一块确定大小的内存从java堆中部划分出来。
内存分配有两种方式:
a、指针碰撞、b、空闲列表
指针碰撞:
假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一 边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示 器,那所分配内存就仅仅是把那 个指针向空闲空间方向挪动一段与对象 大小相等的距离。
空闲列表
如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相 互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维 护一个列表,记录上哪些内存块是可用的,在分 配的时候从列表中找 到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配 方式称 为“空闲列表”(Free List)。
采取哪种内存分配方式?
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采 用 的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。因 此,当使用Serial、ParNew等带压缩 整理过程的收集器时,系统采用 的分配算法是指针碰撞,既简单又高效;而当使用CMS这种基于清除 (Sweep)算法的收集器时,理论上[1]就只能采用较为复杂的空闲列表 来分配内存。
多线程问题:
对象创建在虚拟机中是非常频繁的行 为,即使仅仅修改一个指针所指向 的位置,在并发情况下也并不是线程安全的,可能出现正在给对象 A分 配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内 存的情况。解决这个问题 有两种可选方案:一种是对分配内存空间的动 作进行同步处理——实际上虚拟机是采用CAS配上失败 重试的方式保证更 新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的 空间之中进 行,即每个线程在Java堆中预先分配一小块内存,称为本 地线程分配缓冲(Thread Local Allocation Buffer,TLAB。
4. 初始化:
将分配到的内存空间都初始化为零值; 从上面我们知道,变量初始化的时候:基本类型初始化为其默认值;而 引用类型会被初始化为null;
- 设置对象头信息
主要设置头信息的:GC分代年龄、对象的哈希码 hashCode‘元数据信 息。 - 执行init方法:执行以下静态方法
public class A {
private int a;
private String name;
private boolean isA;
static {
System.out.println("enter static method");
}
public A(){
System.out.println("enter construct");
}
public int getA() {
return a;
}
public void setA(int a) {
System.out.println("enter set");
this.a = a;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
A a = new A();
a.setAget(1);
}
}
先打印出静态方法也就是init方法;然后执行构造器方法,最后执行set普通方法。
2、对象布局
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐(Padding)。
1. 对象头部:包含两类数据:第一类:存储对象自身的运行时数据(如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等);第二类:类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针 来确定该对象是哪个类的实例。
2. 实例数据:是对象真正存储的有效信息(即我们在程序代码里面所定义的各种类型的字段内容)。
3. 对齐填充:于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是 任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者 2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
3、对象的访问
创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象。主流的访问方式主要有使用句柄和直接指针两种。
1. 直接指针访问:
Java堆中本地变量表中:reference中存储的直接就是对象地址。我们通过这个地址;通过这个reference就可以访问我们堆上的实例数据;我们在堆里面也会存对象的类型数据指针;通过这个指针去查找指向方法区的对象类型数据。那么这些class文件肯定是放在方法区里面的。那么堆里面会放:对象的实例数据、对象类型数据的指针。
2. 句柄访问:
reference中存储的就 是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
3. 区别:
相同点:对象类型指针不管是直接内存访问还是句柄访问;其都是在堆内存里面开辟一块内存空间。维护了一个对象实例指针。然后通过这个指针来访问方法区的。
不同点:对象实例数据访问方式:直接访问方式通过局部变量表里面的引用访问到堆里面的对象实例数据;句柄访问方式是通过局部变量表里面的引用来访问句柄池,通过句柄池维护的对象实例指针查找到对象实例数据。
4. 对比:
垃圾回收分析:句柄访问当垃圾回收移动对象时,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址;直接指针访问垃圾回收时需要修改reference中存储的地址。所以句柄访问比直接指针访问效率高。
访问效率分析:直接指针访问优于句柄 访问,因为直接指针访问只进⾏了⼀次指针定位,节省了时间开销,
⽽这也是HotSpot采⽤的实现⽅式
使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访 问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟 机HotSpot而言,它主要使用直接指针访问方式进行对象访问
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。