Volatile和Synchronized的实现细节

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

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

StoreStore屏障:

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

LoadStore屏障:

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

StoreLoad屏障:

  1. 对于这样的语句Store1; StoreLoad; Load2,
  2. 在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和硬件层面

    1. JVM当然是依赖操作系统及硬件去实现这个屏障的效果<br /> 参考 [https://blog.csdn.net/qq_26222859/article/details/52235930](https://blog.csdn.net/qq_26222859/article/details/52235930)<br /> hsdis - HotSpot Dis Assembler 这个命令是看JVM编译好的字节码,在CPU级别是用啥指令完成的<br /> windows lock 指令实现 | MESI实现

Synchronized实现细节

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

  1. 字节码层面

    ACC_SYNCHRONIZED
    monitorenter monitorexit
    这里有俩monitorexit是因为,如果发生了异常,synchronized会自动释放锁
    image.png

  2. JVM层面

    C C++ 调用了操作系统提供的同步机制

  3. OS和硬件层面

    X86 : lock cmpxchg / xxx
    参考 https://blog.csdn.net/21aspnet/article/details/88571740

JMM 和 一些规则

Java Memory Model并发内存模型,
这个知道了硬件(CPU和主存)的内存模型,这里就很容易理解了,基本一样.
image.png

8大原子操作

image.png

happens-before原则

image.png

as if serial

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

对象的内存布局

大厂的一些面试题:
image.png

对象的创建过程

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

    1. 6.1 成员变量顺序赋初始值<br /> 6.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

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

对象的内存布局

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

  1. 对象头包括两部分

    1. 第一部分: markword,8字节<br /> 这个对象的运行时数据,哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID等<br /> 第二部分<br /> ClassPointer类型指针: 表示该对象属于哪个类,XX:+UseCompressedClassPointers 4字节,不开启为8字节<br /> 如果对象是数组,还有块记录 数组长度,4字节
  2. 实例数据

    1. 引用类型: -XX:+UseCompressedOops 4字节 不开启为8字节<br /> Oops: Ordinary Object Pointers<br /> 基础类型: byte 1字节, short 2字节, int 4字节, long 8, float 4, double 8,boolean 1, char 2,<br /> String属于引用类型
  3. 对齐填充:对齐成8的倍数,64位的机器读起来效率高一些(一个字节8位,8个字节64位)

    1. Object o = new Object()在内存中占用多少字节?<br /> 答案是16字节<br /> 首先对象头8个字节,如果-XX:+UseCompressedClassPointers,那么ClassPointer 4个字节,然后Padding对齐,结果是 16字节<br /> 如果-XX:-UseCompressedClassPointers,那么ClassPointer 8个字节,结果还是16字节

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

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

    1. 内存并不是越大越好

    对象头具体包括什么

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

对象处于不同的状态时,markword各个bit表示出不同的内容(复用存储空间)
image.png
网上找的一个说明markword的图(32位JVM的)
可以看到分代年龄占用4bit,这也是为什么,GC年龄默认16
image.png
IdentityHashCode的问题:
image.png
当一个对象计算过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. 句柄池

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

    o直接指向该对象,该对象可以指向T.class
    Hotspot使用直接指针的方式
    句柄池方式对GC(三色算法)友好一些

对象怎么分配

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

先尝试往栈上分配,栈弹出时回收对象;
栈上放不下的话,
如果对象很大(阈值),那就直接分配到堆内存(老年代);
如果对象不是特别大,就分配到线程本地,如果线程本地分配不下,就找新生代的Eden区
image.png