JVM系列(四)[JMM内存模型,Volatile和Synchronized的实现细节,大厂面试题-对象的内存布局,大小,对象头内容,创建过程,定位,分配]

文章目录

  • Volatile和Synchronized的实现细节
    • JVM级别的规范,保证指令的有序执行(JSR133)
    • Volatile实现细节
    • Synchronized实现细节
  • JMM 和 一些规则
    • Java Memory Model并发内存模型,
    • 8大原子操作
    • happens-before原则
    • as if serial
  • 对象的内存布局
    • 对象的创建过程
    • 对象在内存中的存储布局
      • 对象大小(64位机)
        • 观察虚拟机配置
      • 对象的内存布局
    • Object o = new Object()在内存中占用多少字节?
    • Hotspot开启内存压缩的规则(64位机)
    • 对象头具体包括什么
    • 对象怎么定位
    • 对象怎么分配

Volatile和Synchronized的实现细节

JVM级别的规范,保证指令的有序执行(JSR133)

LoadLoad屏障:

对于这样的语句Load1; LoadLoad; Load2, 
在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:

对于这样的语句Store1; StoreStore; Store2,
在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障:

对于这样的语句Load1; LoadStore; Store2,
在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:

 对于这样的语句Store1; StoreLoad; Load2,
 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

Volatile实现细节

众所周知,volatile是保证可见和有序的,但不保证同步.

  1. 字节码层面,ACC_VOLATILE
    volatile修饰的变量编译后,字节码文件的Access flags中会有个 0x0040,即volatile的修饰符,这个就是告诉JVM,该变量使用了volatile修饰;
    这个我们写点代码,然后用IntelliJ的JclassLib插件就可以看到
  2. JVM层面,volatile内存区的读写 都加屏障
    JVM表示知道了,然后在volatile变量的读写操作前后都加了屏障:
    LoadLoadBarrier volatile读操作 LoadStoreBarrier
    LoadStoreBarrier volatile写操作 StoreLoadBarrier
  3. OS和硬件层面
    JVM当然是依赖操作系统及硬件去实现这个屏障的效果
    参考 https://blog.csdn.net/qq_26222859/article/details/52235930
    hsdis - HotSpot Dis Assembler 这个命令是看JVM编译好的字节码,在CPU级别是用啥指令完成的
    windows lock 指令实现 | MESI实现

Synchronized实现细节

synchronized会保证同步性,附带着保证可见和有序

  1. 字节码层面
    ACC_SYNCHRONIZED
    monitorenter monitorexit

    这里有俩monitorexit是因为,如果发生了异常,synchronized会自动释放锁
    JVM系列(四)[JMM内存模型,Volatile和Synchronized的实现细节,大厂面试题-对象的内存布局,大小,对象头内容,创建过程,定位,分配]_第1张图片

  2. JVM层面
    C C++ 调用了操作系统提供的同步机制

  3. OS和硬件层面
    X86 : lock cmpxchg / xxx
    参考 https://blog.csdn.net/21aspnet/article/details/88571740

JMM 和 一些规则

Java Memory Model并发内存模型,

这个知道了硬件(CPU和主存)的内存模型,这里就很容易理解了,基本一样.
JVM系列(四)[JMM内存模型,Volatile和Synchronized的实现细节,大厂面试题-对象的内存布局,大小,对象头内容,创建过程,定位,分配]_第2张图片

8大原子操作

JVM系列(四)[JMM内存模型,Volatile和Synchronized的实现细节,大厂面试题-对象的内存布局,大小,对象头内容,创建过程,定位,分配]_第3张图片

happens-before原则

JVM系列(四)[JMM内存模型,Volatile和Synchronized的实现细节,大厂面试题-对象的内存布局,大小,对象头内容,创建过程,定位,分配]_第4张图片

as if serial

不管如何重排序,单线程执行结果不会改变

对象的内存布局

大厂的一些面试题:
JVM系列(四)[JMM内存模型,Volatile和Synchronized的实现细节,大厂面试题-对象的内存布局,大小,对象头内容,创建过程,定位,分配]_第5张图片

对象的创建过程

  1. class loading,把class加载到内存中
  2. class linking(verification,preparation,resolution),静态变量默认值
  3. class initializing,静态变量初始值,执行静态语句块
  4. 申请对象内存
  5. 成员变量赋默认值
  6. 调用构造方法
    1. 成员变量顺序赋初始值
    2. 执行构造方法语句(先调用父类的构造方法super())

对象在内存中的存储布局

https://www.zhihu.com/question/52116998/answer/133400077
https://blog.csdn.net/zhaocuit/article/details/100208879

对象大小(64位机)

对象的大小跟虚拟机的实现和设置都有关系
普通对象和数组对象也有不同
这里可以做个实验验证下,自己写个javaagent,java.lang.instrument.Instrumentation.getObjectSize(Object o)

观察虚拟机配置

在终端输入下面命令,其实就是查看Java版本的命令,只不过我们指定了参数,打印出JVM的参数
java -XX:+PrintCommandLineFlags -version
得到下面的结果:
我们比较关心 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops,这俩是默认就有的;
如果想关闭,就把加号改成减号,如-XX:-UseCompressedOops

C:\windows\system32>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=267595520 -XX:MaxHeapSize=4281528320 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version “1.8.0_112”
Java™ SE Runtime Environment (build 1.8.0_112-b15)
Java HotSpot™ 64-Bit Server VM (build 25.112-b15, mixed mode)

对象的内存布局

分为三块:对象头(Header),实例数据(Instance)和对齐填充(Padding)

  1. 对象头包括两部分

    • 第一部分: markword,8字节
      这个对象的运行时数据,哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID等
    • 第二部分
      • ClassPointer类型指针: 表示该对象属于哪个类,XX:+UseCompressedClassPointers 为4字节,不开启为8字节
      • 如果对象是数组,还有块记录 数组长度,4字节
  2. 实例数据
    引用类型: -XX:+UseCompressedOops 为4字节 不开启为8字节
    Oops: Ordinary Object Pointers
    基础类型: byte 1字节, short 2字节, int 4字节, long 8, float 4, double 8,boolean 1, char 2,
    String属于引用类型

  3. 对齐填充:对齐成8的倍数,64位的机器读起来效率高一些(一个字节8位,8个字节64位)

Object o = new Object()在内存中占用多少字节?

答案是16字节
首先对象头8个字节,如果-XX:+UseCompressedClassPointers,那么ClassPointer 4个字节,然后Padding对齐,结果是16字节
如果-XX:-UseCompressedClassPointers,那么ClassPointer 8个字节,结果还是16字节

Hotspot开启内存压缩的规则(64位机)

  1. 4G以下,直接砍掉高32位
  2. 4G - 32G,默认开启内存压缩 ClassPointers Oops
  3. 32G,压缩无效,使用64位
    内存并不是越大越好

对象头具体包括什么

这个很复杂,要看HotSpot的源码,暂时没必要挖太深
很重要的两个东西:
锁标志位和GC标记
严格来说,有3bit表示锁的状态

对象处于不同的状态时,markword各个bit表示出不同的内容(复用存储空间)
JVM系列(四)[JMM内存模型,Volatile和Synchronized的实现细节,大厂面试题-对象的内存布局,大小,对象头内容,创建过程,定位,分配]_第6张图片
网上找的一个说明markword的图(32位JVM的)
可以看到分代年龄占用4bit,这也是为什么,GC年龄最大为15
JVM系列(四)[JMM内存模型,Volatile和Synchronized的实现细节,大厂面试题-对象的内存布局,大小,对象头内容,创建过程,定位,分配]_第7张图片
IdentityHashCode的问题:
JVM系列(四)[JMM内存模型,Volatile和Synchronized的实现细节,大厂面试题-对象的内存布局,大小,对象头内容,创建过程,定位,分配]_第8张图片

当一个对象计算过identityHashCode之后,不能进入偏向锁状态
https://cloud.tencent.com/developer/article/1480590
https://cloud.tencent.com/developer/article/1484167
https://cloud.tencent.com/developer/article/1485795
https://cloud.tencent.com/developer/article/1482500

对象怎么定位

T o = new T(),这个o是怎么找到对应的对象的?
https://blog.csdn.net/clover_lily/article/details/80095580

  1. 句柄池
    o先指向一个间接指针,通过这个间接指针可以找到该对象和T.class
  2. 直接指针
    o直接指向该对象,该对象可以指向T.class

Hotspot使用直接指针的方式
句柄池方式对GC(三色算法)友好一些

对象怎么分配

这里粗略了解下,后面讲GC时详细讲.

先尝试往栈上分配,栈弹出时回收对象;
栈上放不下的话,
如果对象很大(阈值),那就直接分配到堆内存(老年代);
如果对象不是特别大,就分配到线程本地,如果线程本地分配不下,就找新生代的Eden区
JVM系列(四)[JMM内存模型,Volatile和Synchronized的实现细节,大厂面试题-对象的内存布局,大小,对象头内容,创建过程,定位,分配]_第9张图片

你可能感兴趣的:(JVM)