让我们带着问题一探究竟
一个Object对象占用几个字节,里面内容都是什么(对象占用大小)?在哪里分布呢(内存模型)?什么时候会销毁(GC)?

klass是java中,类的元信息在jvm中的表现形式,就是在c++中,类的元信息是通过klass来表示的

对象占用大小

new Object();会占用多少内存呢?答案是16字节=128比特位=128bits

为什么是16字节呢?而不是17或者15?

这是因为8个字节=1byte,所以只能是8的倍数,那么是8,要么是16、24、32
那为什么是16,而不是24、32?因为这得取决于里面的内容是什么

里面内容是什么呢?

对象头和数据体两大部分

1. 对象头

  1. 前面8个字节为markword,记录对象被上锁的各种状态或者垃圾回收和hashcode相关的信息
    默认无锁的情况如下,有锁的情况参考java锁
  • 64位系统中(64bit)
    未使用:25位
    hashcode: 31位存储对象的System.identityHashCode(),采用延迟加载技术
    未使用:1位
    gc分带年龄: 4位
    偏向锁的标记:1位
    当前对象的锁的状态:2位
    25+31+1+4+1+2=64
  • 32位系统中(也是有64bit,只不过java只用32bit)
    hashcode: 25位存储对象的System.identityHashCode(),采用延迟加载技术
    gc分带年龄: 4位
    偏向锁的标记:1位
    当前对象的锁的状态:2位
    25+4+1+2 = 32
  1. 接下来4个字节(也有可能是8个字节)是Klass Point(类型指针)
    为什么有可能是4有可能是8呢,取决于是否开启了classPointer指针压缩
    虚拟机通过这个指针来确定这个对象是哪个类的实例 一个对象里面的数据都是紧挨着的,因为是紧挨着,无法区分数据断点在哪里,所以必须有class对象的指针,通过对象起始地址和字段的偏移地址(从class获取)读取字段的数据

  2. 如果是数组类型的话对象头还得再加4个或8个字节(取决于是否开启压缩指针)表示数组的长度
    只有当本对象是一个数组对象时才会有这个部分

2. 对象数据体

对象的里面的数据
如果是基本类型则是对应的值占用的空间,如果是引用类型那么大小有可能是8bit或者4bit取决于是否开启压缩指针

3. 对齐填充数据(可选)

根据对象对齐空间进行对齐,默认为8bit
因为必须是8的倍数,不是8的倍数剩下的将要补齐

所以一个new Object()为16字节,8字节为对象头,后面8字节为class对象的指针和数据填充
1G内存大约可以存1024 * 1024 * 1024 / 16 = 67108864(约等于6千7百万多个对象)


java锁

  • java都有哪些锁呢?

    1. 是否阻塞
      悲观(阻塞其他线程-synchronized)、乐观(不阻塞其他线程-ReentrantLock和ReentrantReadWriteLock)

    2. 是否公平
      非公平(synchronized)、公平(ReentrantLock和ReentrantReadWriteLock)

    3. 二次是否能获取
      不可重入锁(jdk没有自带的)、可重入锁(jdk自带的锁都可以重入)

    4. 是否共享
      共享锁(ReentrantReadWriteLock)、排他锁

  • 大体分为synchronized、ReentrantLock和ReentrantReadWriteLock3个阵营

synchronized

  • 特性:阻塞、非公平、可重入、排他锁
  • 原理
    jdk1.6引入了偏向锁和轻量级锁,1.6之前都是重量级锁,意思就是互斥等待都需要内核态完成(操作系统),开销非常的大,所以优化之后的synchronized可以进行锁升级
    每一个Java对象就有一把看不见的锁,存放在对象头中叫markword,占用8个字节(动态的内容)
    当创建一个对象时,会通过Klass的prototype_header来初始化该对象的markword,prototype_header包含了lock(锁标识-适用于所有的实例)、epoch(偏向时间戳-用来控制锁是否失效的版本号)
    无锁的情况下里面存放默认的数据
    偏向锁、轻量级锁、重量级锁都是根据markword里面的数据来标识当前锁的状态(所以markword里面的内容都是动态的),一个对象锁升级之后不可回退
    在32位jvm中占用空间如下所示(4*8=32bit)
    如图
    在64位中占用(8*8=64bit)如下所示
    |------------------------------------------------------------------------------|--------------------|  
    |                                  Mark Word (64 bits)                         |       State        |  
    |------------------------------------------------------------------------------|--------------------|  
    | unused:25 |  identity_hashcode:31  |unused:1|  age:4  |biased_lock:1| lock:2 |       Normal       |  
    |------------------------------------------------------------------------------|--------------------|  
    |       threadId:54      |  epoch:2  |unused:1|  age:4  |biased_lock:1| lock:2 |       Biased       |  
    |------------------------------------------------------------------------------|--------------------|  
    |                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |  
    |------------------------------------------------------------------------------|--------------------|  
    |                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |  
    |------------------------------------------------------------------------------|--------------------|  
    |                                                                     | lock:2 |    Marked for GC   |  
    |------------------------------------------------------------------------------|--------------------|  
    
    无锁、偏向锁、轻量级锁、重量级锁、gc标记,这5种状态都是通过锁标志位(lock)来判断的,但是2bit只能表示4种状态,如上图
    所以无锁和偏向锁的锁标志位都为01,然后在通过额外的1bit(biased_lock)用来区分当前对象是否为偏向锁
    只有匿名偏向时(lock=01,biased_lock=1,threadId=null)才可以使用偏向锁,否则从轻量级锁开始

    无锁

    锁标志位为lock=01,偏向锁标识为biased_lock=0
    对于无锁状态的锁对象,如果有竞争,会直接进入到轻量级锁,所以如果没有关闭偏向锁那么JVM在启动4秒之后(-XX:BiasedLockingStartupDelay=4000),会将所有加载的Klass的prototype_header修改为匿名偏向锁

    为什么要延迟之后修改呢?
    JVM启动时必不可免会有大量sync的操作,而偏向锁竞争时会STW并升级为轻量级锁,锁升级不可回退。如果开启了偏向锁,会发生大量锁撤销和锁升级操作,大大降低JVM启动效率

    偏向锁

    因为偏向锁会占用hashcode空间,如果该对象已被产生hashcode则不能使用偏向锁,如果在偏向时产生hashcode,那么会升级为轻量级锁

    JVM默认的计算identity hash code的方式得到的是一个随机数,如果不想为随机数可参考, 因而我们必须要保证一个对象的identity hash code只能被底层JVM计算一次
    Identity hash code是未被覆写的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值

    只有匿名偏向时才可以使用偏向锁
    匿名偏向:锁标志位为lock=01,偏向锁标识为biased_lock=1, 且threadId为空(未偏向任何线程),代表当前为匿名偏向
    markword里面有54(64位jvm)或者23(32位jvm)bit表示偏向锁偏向的线程id,未偏向时此值为空,上锁时依赖一次CAS原子指令,设置对应的线程的id,默认情况下已上锁的threadId不会自动释放(减少CAS指令的调用),如果上锁失败或已偏向其他线程,则会进行重偏或竞争升级为轻量级锁
    epoch的作用是记录偏向锁的版本号,用来判断偏向锁是否失效(实例的epoch != klass的epoch),epoch默认是有效的,如果失效可以重偏,什么时候会失效呢?参考-XX:BiasedLockingBulkRebiasThreshold=20

    偏向锁上锁大致过程为:
    在匿名偏向状态才可以上锁,CAS设置threadId成功代表上锁成功,失败则会进行锁升级为轻量级锁
    如果epoch失效,则会重偏(非重偏的逻辑只要有竞争就会升级为轻量级锁),CAS设置threadId和epoch最新值,失败则会进行锁升级
    每次上锁或者重入时只需要检查epoch和threadId是否有效即可,并且会在当前的栈侦添加一条锁记录LockRecord(记录被锁对象的地址和被锁对象的markword),用来计算重入的次数(偏向锁LockRecord的MarkWord为空,称之为Displaced MarkWord),执行完同步代码块之后会销毁LockRecord

    偏向锁锁升级为轻量级锁的大致流程为:
    其他线程请求锁,以被锁对象和新线程作为参数构造一个VM_Operation vo,新线程被挂起,在全局安全点(STW)时,VM_Thread去执行vo
    vo内部逻辑是去检查持有偏向锁的线程状态,如果对应线程已经销毁则或者对应线程执行的代码在同步块之外(通过遍历持有锁的线程的栈,判断是否有指向被锁对象的lockRecord),则设置对象为无锁状态(无锁上锁会升级为轻量级锁) ,如果还在同步块之内则把当前偏向锁升级为轻量级锁,然后让新线程以轻量级锁的状态去竞争
    至此偏向锁流程解析完毕

    轻量级锁

    锁标志位为lock=00
    偏向锁失效(超过-XX:BiasedLockingBulkRevokeThreshold=40默认的次数)或偏向锁竞争时,就会使用轻量级锁

ReentrantLock

特性:非阻塞和阻塞、非公平和公平、可重入、排他锁

ReentrantReadWriteLock

特性:非阻塞和阻塞、非公平和公平、可重入、共享锁