Skip to content

Commit

Permalink
Update Blog content
Browse files Browse the repository at this point in the history
  • Loading branch information
JSD-qr committed Nov 9, 2023
1 parent c4320ab commit eff79a0
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 2 deletions.
88 changes: 86 additions & 2 deletions src/note/java/jvm/2311071055.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,18 @@ Elimination)、锁粗化(Lock Coarsening)等技术来减少锁操作的性

管程中引入了条件变量的概念,每一个条件变量都对应又一个等待队列。条件变量和等待队列的作用就是解决线程之间的同步问题。

> 注意:对于使用**wait()**时,唤醒的时间和获取到锁继续执行的时间是存在差异的,线程被唤醒之后再次执行时条件可能又不满足了。因此,对于MESA管程来说,使用wait()时会又一个编程范式:
> 注意:对于使用**wait()**
>
时,唤醒的时间和获取到锁继续执行的时间是存在差异的,线程被唤醒之后再次执行时条件可能又不满足了。因此,对于MESA管程来说,使用wait()
> 时会又一个编程范式:
```java
while (条件不满足) {
wait();
}
```

通常情况下,我们在编程时尽量使用**notifyAll()**来唤醒线程,若满足以下三个条件,可使用**notify()**
通常情况下,我们在编程时尽量使用 **notifyAll()** 来唤醒线程,若满足以下三个条件,可使用 **notify()**

1. 只需要唤醒一个线程
2. 所有等待线程拥有i相同的等待条件
Expand All @@ -138,10 +141,91 @@ Java管程实现参考了MESA模型,语言内置的管程(synchronized)对

![JAVA管程模型](https://qiniu.yanggl.cn/image/2311080958212.png)

#### 锁实现原理

![锁实现原理](https://qiniu.yanggl.cn/image/202311090953123.png)

当一个线程去获取锁的时候,先将当前线程插入到_cxq队列(FILO)的头部

释放锁时默认策略(QMode=0)是:

- _EntityList为空,则将_cxq中的元素按原有顺序插入到_EntityList,并唤醒第一个线程。意味着当_EntityList为空时,后面来获取锁的线程先去获取锁
- _EntityList不为空直接从_EntityList中唤醒线程

> 思考:让后面来获取锁的线程先去获取锁,这样的设计可以让已经睡眠的线程继续,而新来的直接去获取,降低线程的唤醒/睡眠操作
## 锁实现

JVM锁标识是记录再对象的对象头内的,关于对象内存布局以及对象头中锁标识的记录可查看[JVM对象内存布局](./2311091006.md)文章。

在Hotspot中将锁标记分为了:**无锁****偏向锁****轻量级锁****重量级锁**,锁标记枚举如下:

```c
enum {
loced_value =0, //00 轻量级锁
unlocked_value =1, //001 无锁
monitor_value =2, //10 重量级锁
marked_value =3, //11 GC标记
biased_lock_pattern =5 //101 偏向锁锁
}
```

### 偏向锁

偏向锁是一种针对加锁操作的优化手段。在多数场景下,锁是不存在多线程竞争的,总是由同一线程多次获得。为了消除对象在没有竞争的情况下锁重入(CAS操作)的开销而引入偏向锁。

#### 延迟偏向

偏向锁机制存在偏向锁延迟机制,HotSpot虚拟机在启动后会有4s的延迟才会对每个新建的对象开启偏向锁模式。

这是因为JVM启动时会进行一系列复杂活动,比如类装载配置,系统类初始化等。在这个过程中会使用大量synchronized关键字为对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延迟加载偏向锁。

例子:

```java
public static void main(String[] args) throws InterruptedException {
System.err.println(ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
System.err.println(ClassLayout.parseInstance(new Object()).toPrintable());
}
```

输出结果:

```shell
#--------------首次创建,处于无锁状态--------------
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

#--------------延迟4s后,处于偏向锁--------------
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
```

#### 匿名偏向

当JVM启用了偏行锁模式(JDK6后默认启用),新创建对象的对行头重ThreadId为0,说明此时处于可偏向但是未偏向任何线程,
也叫做 **匿名偏向** 状态(anonymously biased)。

#### 偏向锁撤销-调用HashCode



#### 偏向锁撤销-调用wait/notify




### 轻量锁

### 重量锁
113 changes: 113 additions & 0 deletions src/note/java/jvm/2311091006.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
isOriginal: true
date: 2023-11-09
index: true
category:

- Java

tag:

- JVM

---

# JVM对象内存布局

Hotspot虚拟机中,将对象在内存中存储的布局分为三块:**对象头(Header)****实例数据(Instance Data)****对齐填充(Padding)**

<!-- more -->

![对象内存布局](https://qiniu.yanggl.cn/image/202311091034325.png)

## 对象头

HotSpot主要将对象头划分为:**MarkWord****KlassPointer****数组长度**,记录了对象Hash码、对象所属年代、对象锁、锁状态、偏向锁的线程ID、偏向时间、数组长度等等信息

### MarkWord

MarkWord用于存储对象自身运行时的数据,例如哈希码(HashCode)、GC分代年龄、锁状态、持有锁的线程、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位的虚拟机中分别占32bit和64bit。

- **32位MarkWord**

![32位MarkWord](https://qiniu.yanggl.cn/image/202311091048332.png)

- **64位MarkWord**

![64位MarkWord](https://qiniu.yanggl.cn/image/202311091048664.png)

> MarkWord结构搞得那么复杂,是因为需要节省内存,让同一内存区域在不同锁阶段有不同的用处
- **hash**:保存对象的哈希码,在运行期间调用System.identityHashCode()进行计算,这是一个延迟计算,并将结果赋值到hash内存中
- **age**:保存对象的分代年龄。记录对象被GC的次数,当该次数到达阈值后就会由年轻代转入老年代
- **biased_lock**:偏向锁标识位。由于无锁和偏向锁的锁标识都是记01,为了区分引入了一位来标识是否为偏向锁
- **lock**:锁状态标识。区分锁状态,比如00时表示轻量锁,只有最后2位锁标识(00)有效
- **JavaThread**:保存持有偏向锁的线程ID。当处于偏行模式时,有线程持有对象,则对象的这里就会保存持有线程的ID,后续再获取锁时就无需再进行尝试获取锁的动作
- **epoch**:保存偏向时间戳。偏向锁再CAS锁操作过程中,偏向性标识,标识对象更偏向哪个锁

### KlassPointer

KlassPointer又称类型指针,是指向对象的类元数据的指针。虚拟机可以通过这个指针来确定这个对象是那个类的实例。

在32位的JVM内,类型指针占4byte
在64位的JVM内,类型指针正常占8byte,若开启**指针压缩**或者**最大堆内存小于32G**时占4byte。JDK8后默认开启指针压缩

### 数组长度

如果这个对象是一个数组,则对象头中会有一块4byte长度数数据区用于记录数组的长度,如果不是数组则这部分长度为0

![Header内存占用](https://qiniu.yanggl.cn/image/202311091135325.png)

## 内存布局查看实战

为了验证对象内存布局,可使用Java对象的内部布局工具**JOL(JAVA OBJECT LAYOUT)**
,用此工具可以查看new出来的一个java对象的内部布局,以及一个普通的java对象占用多少字节。

Maven依赖引入

```xml
<!-- JAVA对象布局、大小查看 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
```

案例

```java
public class Main {
public static void main(String[] args) {
System.err.println(ClassLayout.parseInstance(new Object()).toPrintable());
}
}
```

- 开启指针压缩

```shell
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
```

- 未开启指针压缩

```shell
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 8 (object header: class) 0x000000001beb1c00
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
```

> - OFF:偏移地址(Byte)
> - SZ:内存占用大小(Byte)
> - TYPE DESCRIPTION:类型描述,object header为对象头,object alignment gap为对齐补充
> - VALUE:对应内存中当前存储的值

0 comments on commit eff79a0

Please sign in to comment.