java对象的内存布局及创建过程

java | 2019-09-13 10:02:39

一、对象的内存布局 
   对象的内存结构又可以被分为:对象头,实例数据,对象填充 
   
   对象头:对象头结构在32位JVM与64位JVM中的实现细节是不同的 
32bit: 

1.png

64bit: 

2.png

实例数据:对象真正存储的有效信息,也是在程序代码中定义的各种类型字段内容。无论是从父类继承下来的还是子类定义的,都需要记录下来。 

   对象填充:没有实际意义,仅仅起着占位符的作用。以为对象的大小必须是8字节的整数倍。 

二、对象的访问定位 

对象的访问定位分为两种: 

通过句柄访问对象:

3.gif

通过指针访问对象:

4.jpg

两种对象访问方式各有优势,使用句柄访问最大好处是reference中存储的是稳定的句柄地址,在对象被移动是只会改变句柄中的实例数据指针,而reference本身不需要修改。 
    使用直接指针访问方式的最大好处是速度快,他节省了一次指针定位的时间开销。 

三、对象的创建过程 

这里主要讨论实例化普通对象的过程,不包括数组和Class对象。 

1、检查指令的参数是否可以在常量池中定位到一个类的符号引用 

2、检查这个符号引用代表的类是否被加载,解析初始化过 

3.1、如果被加载并初始化了继续下一步 

3.2、如果没有没有初始化,那么先执行类的加载过程 

4、位新生对象分配内存(两种方法) 

   1)指针碰撞 
        如果java 堆当中的内存是规则的,已使用内存在一边,未使用内存在另外一边, 中间  放着一个指针作为分界点的指示器,如果需要分配内存就只需要把指针向空闲空间那边挪动一段与对象大小相等的距离 

   2)空闲列表 
        如果已用内存和空闲内存是交互存在的,这样虚拟机就必须维护一个列表,记录哪块内存是可用的,在分配内存的时候从列表中找到一个足够大的空间划分给对象实例,并且更新空闲列表。 

    分配内存时可能出现的问题:并发创建对象会出现指针没来得及修改导致的重复地址覆盖对象 
    解决办法: 
   



  •    

  • 分配内存动作同步-采用 CAS配上失败重试方式保证更新操作的原子性
       

  • 预先给 Java 线程在堆中分配一小块内存,称为本地线程分配缓冲(TLAB),只有当该线程用完才需要同步锁。是否使用 TLAB 可以通过 -XX:+/-UseTLAB
       



5、将分配到的内存空间都初始化为零值(不包括对象头) 

6、对对象进行必要的设置 
   例如: 
  



  •   

  • 设置这个对象是哪个类的实例
      

  • 如何才能找到类的元数据信息
      

  • 对象的hash码
      

  • 对象的GC分代年龄
      

  • 是否启用偏向锁
      



7、执行<init>方法,可以根据一段代码来讨论变量的具体初始化过程:

class Parent {
        private int p_01 = getP_01();
        private static int p_02 = getP_02();
        {
                System.out.println("I am Parent Local code block");
        }
        static {
                System.out.println("I am Parent static code block");
        }
        public Parent() {
                System.out.println("I am Parent Constructor!");
        }
        private int getP_01() {
                System.out.println("p_01 is initialized!");
                return 13;
        }
        private static int getP_02() {
                System.out.println("static p_02 is initialized!");
                return 14;
        }
}
class Son extends Parent {
        private int s_01 = getS_01();
        private static int s_02 = getS_02();
        {
                System.out.println("I am Son Local code block");
        }
        static {
                System.out.println("I am Son static code block");
        }
        public Son() {
                System.out.println("I am Son Constructor!");
        }
        private int getS_01() {
                System.out.println("s_01 is initialized!");
                return 13;
        }
        private static int getS_02() {
                System.out.println("static s_02 is initialized!");
                return 14;
        }
}
public class CreateObject {
        public static void main(String[] args) {
                Son s = new Son();
        }
}

执行结果如下: 

static p_02 is initialized!
I am Parent static code block
static s_02 is initialized!
I am Son static code block
p_01 is initialized!
I am Parent Local code block
I am Parent Constructor!
s_01 is initialized!
I am Son Local code block
I am Son Constructor!


然后可以总结得出<init>初始化过程如下: 
   1)父类静态变量,静态代码块执行初始化(静态变量,静态代码按顺序执行,属于同一优先级) 
   2)子类静态变量,静态代码块执行初始化 
   3)父类全局变量,代码块被初始化(全局变量,代码按顺序执行,属于同一优先级) 
   4)父类构造函数执行 
   5)子类全局变量,代码块被初始化 
   6)子类构造函数被执行 

  误区:其实并不是真的子类构造函数在父类构造函数之后执行,而是在子类构造器的第一行隐式调用了父类的构造函数.

登录后即可回复 登录 | 注册
    
关注编程学问公众号