第一行汇编码,申请空间,并初始化成员变量为默认值(半初始化) 第二行:dup 复制 (因为第三行的invokespecial会消耗一个引用,所以必须先复制一个) 第三行:调用T的 构造方法,初始化m为8 第四行:astore-1 把t和new的对象连接起来
第一种写法:一来先new一个private static 的对象,将构造方法设为 private ,设置一个public函数getInstance来获取这个对象 第二种写法: 要在需要的时候才new这个对象,先不new,等到需要的时候new。 getInstance的时候判断对象是否为空,不是空就直接get. 但是这种方法是线程不安全(多线程同时访问时可能会有多个new)
解决方法:getInstance上锁(synchronized):整个方法加锁,锁粒度太粗。因为整个方法都拒绝多线程访问,若方法中含有较多业务代码,效率会很低。 解决方法,只对对象为空时的代码同步: 但是在一个线程判断对象为空后,获得锁之前,可能已经有另外一个线程把对象给new出来了。所以有最后的写法(DCL, double check lock): 判断两次INSTANCE是否为空(上锁前后)
回到问题:DCL中需不需要加volatile?
volatile 的两个功能:
线程可见性禁止指令重排序回顾一下新建对象时发生的:调用构造器初始化和建立链接的两条指令如果发生重排序会怎么样?
线程1在链接时会连到一个半初始化的对象上(t不为空了), 线程二来判断是否为空(不为空,已经半初始化了),那么就直接get了这个半初始化的对象m=0 所以要加 volatile !
对齐:前面三个部分加起来的bit数不能被8整除,就补齐 用ClassLayout里的parseInstance函数将对象存储布局打印 执行结果(这里没有实例数据 0Bytes):
一共16个字节
锁升级的过程:synchronized 是对对象上锁而不是对代码上锁 打印输出上锁前后的对象存储布局: 可以看出markword里存储的就是锁信息: 对于一个刚刚new出来的对象,先上的是偏向锁,再是自旋锁(无锁,lock-free,轻量级锁),再是重量级锁
偏向锁(biased lock):第一个线程来时,只需要贴上名片,因为synchronized是可重入的,又没有锁竞争,效率很高 当第二个线程来要与第一个线程竞争锁,先把偏向锁撤销,然后两个线程用CAS(compare and swap)的方式竞争锁
CAS的过程:每个线程要对这个对象做操作时先读出来,再计算,如果写回去的时候检查它还是原来读出来的数,那说明这中间没线程动过它,就成功了,如果写回去的时候发现它已经不是原来那个值了,那就重新读出来,再计算,再写回(比较)
这样就不用加锁了 但是会有ABA问题,可以用加版本的方式解决ABA问题
句柄方式唯一的优势:对象小,垃圾回收时不用频繁改动t 缺点:两次访问
优先在栈中分配(声明周期短,不需要GC,效率大概比在堆中分配快一倍) 可以进行栈上分配的条件:1.逃逸分析 DoEscapeAnalysis(没有别的方法用这个对象)2.可以进行标量替换EliminateAllocation(这个对象可以用在栈中存的成员变量代替)
不能放入栈中的先判断大不大,大的话直接放入old区老年代 不大的话,判断是否符合线程本地分配(Thread Local Allocat Buffer), 如果符合,那么放入线程自己独有的一小块内存中,不需要锁。如果不符合,那么需要加锁竞争内存,但都是分配进堆中的EDEN区
分代年龄,存在markword中
Object o = new Object; o, 普通对象指针 Ordinary object pointers (OOPS) 占4个字节 不考虑o,一个Object也不一定是16个字节 查看Java的命令参数 因为在64位的机器上,类指针应该是8字节,但是由于默认开启UseCompressedClassPointers 这个指针被压缩到4字节,同时实例数据里如果有普通对象指针,比如成员变量有一个字符串或者数组,这个指针原本也是8字节,但是默认UseCompressedOops,就压缩到4字节 -XX:-UseCompressedClassPointers, 去掉压缩类指针
那么什么情况下不压缩? 答:4个字节的指针,能寻址的最大空间是多大:32G 如果堆内存空间超过32G,压缩自动不起作用?(这里没讲清楚)
(文中截图来源马士兵老师公开课)