Skip to content

Commit

Permalink
Update Blog content
Browse files Browse the repository at this point in the history
  • Loading branch information
yanggl committed Mar 3, 2024
1 parent b2a6d9d commit f3f02ef
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 2 deletions.
136 changes: 134 additions & 2 deletions src/note/java/jvm/2311221518.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,149 @@ tag:

# JIT(即时编译)深入理解

Java是一门解释型语言,通过编译器(javac)将源代码编译成平台无关的Java字节码文件(.class)。然后JVM解释执行这些字节码文件,实现平台无关性。
但是,解释执行的速度相对较慢。为了提高执行速度,引入了JIT技术。JIT是JUST IN TIME的缩写,意味着**即时编译**
<!-- more -->

## 热点探测技术

当虚拟机发现某个方法或代码块运行特别频繁时,就会将这些代码标记为**热点代码(Hot Spot Code)**。热点探测的目的是识别出这些热点代码,以便进行即时编译(JIT)优化。

这种探测方法周期性地检查各个线程的虚拟机栈的栈顶。如果某个方法经常出现在栈顶,就认为这个方法是热点方法。基于采样的热点探测是一种常见的热点代码判定方式。

对于热点方法,JIT会触发标准编译,将其编译成机器码,以提高执行效率。对于循环体,JIT可能触发OSR(On-Stack
Replacement)栈上替换,直接切换到本地代码执行。

热点探测技术的触发,只需要满足以下一个条件即可:

- 方法调用计数器
- 方法计数器用于记录方法被调用的次数。当某个方法被多次调用时,方法计数器的值会逐渐增加
- 在Client模式下,默认的方法计数器阈值是1500次。
- 在Server模式下,默认的方法计数器阈值是10000次。
- 回边计数器
- 回边计数器用于判断循环是否热点。当循环体被多次执行时,回边计数器的值会增加。
- 在Client模式下,回边计数器的阈值计算公式为:OSRP = 方法调用计数器阈值 * 933 / 100。
- 在Server模式下,回边计数器的阈值计算公式为:OSRP = (方法调用计数器阈值 * 140 - 监控解释器比率) / 100。

```java
public class HotSpotDetectionExample {

public static void main(String[] args) {
// Simulate some method calls
for (int i = 0; i < 15000; i++) {
hotMethod();
}
}

// A "hot" method (called frequently)
private static void hotMethod() {a
System.out.println("Hot method called!");
}
}
```

> 在这个示例中定义了hotMethod()方法。hotMethod()
> 被频繁调用,VM会根据方法调用计数器来判断哪些方法是热点方法。在这里,hotMethod()会被标记为热点方法,因为它被调用的次数较多。
## 方法内联

方法内联的优化行为是把目标方法的代码复制到发起调用的方法之中,避免发生真是的调用,以此来避免方法调用带来的栈帧生成、参数压入、栈帧弹出等开销。

方法内联的触发需要满足以下条件:

- 触发热点探测技术:方法被频繁调用,达到一定次数(通常是1500次或10000次)。
- 方法体大小合适:过大的方法不适合内联(方法体大小小于325字节,可通过 -XX:FreqInlineSize=N 来设置),因为会影响代码缓存(CodeCache)的使用。
- 不是经常执行的方法,默认情况下方法大小小于35字节才会进行内联(可通过 -XX:MaxInlineSize=N 来重置)

```java
public class MethodInliningExample {

public static boolean flag = true;
public static int value0 = 0;
public static int value1 = 1;

public static int foo(int value) {
int result = bar(flag);
if (result != 0) {
return result;
} else {
return value;
}
}

public static int bar(boolean flag) {
return flag ? value0 : value1;
}

public static void main(String[] args) {
for (int i = 0; i < 20000; i++) {
foo(i);
}
}
}
```

> 在这个示例中,foo方法会调用bar方法。我们期望编译器能够内联这两个方法,从而减少方法调用的开销。执行上述代码时,你会发现foo方法被触发了即时编译(JIT),并且bar方法成功内联到了foo方法中。
## 锁消除

通过对运行上下文的扫描,JVM可以去除那些不可能存在共享资源竞争的锁。这种优化可以节省毫无意义的请求锁时间。

锁消除主要是为了处理那些在代码上要求同步,但实际上并不存在共享数据竞争情况的锁。例如,如果局部变量在运行过程中没有出现逃逸,那么就可以认为这段代码是线程安全的,无需加锁。

锁消除的触发条件主要是基于**逃逸分析**。如果JVM判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。

```java
public class LockClearTest {
public static void main(String[] args) {
LockClearTest test = new LockClearTest();
for (int i = 0; i < 100000; i++) {
test.append("aaa", "bbb");
}
}

public void append(String str1, String str2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}
}
```

>在这个例子中,StringBuffer的append是一个同步方法,但是在LockClearTest中的StringBuffer是一个局部变量,不可能从该方法中逃逸出去(即stringBuffer的引用没有传递到该方法外,不会被其他线程引用),因此其实这个过程是线程安全的,可以将锁消除。
## 锁粗化

在一系列连续的操作中,如果对同一个对象反复加锁和解锁,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

锁粗化主要是为了解决频繁的加锁、解锁带来的性能损耗问题。在一些情况下,锁的粒度粗一些反而更好,两次加锁解锁之间,间隙非常小,此时,不如就直接一次大锁搞定得了。

锁粗化的触发条件是,如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

```java
public class StringBufferTest {
StringBuffer stringBuffer = new StringBuffer();
public void append(){
stringBuffer.append("a").append("b").append("c");
}
}
```

> 在这个示例中,每次调用 stringBuffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append 方法时进行加锁,最后一次 append 方法结束后进行解锁。这样就避免了频繁用户态到内核态的状态转换,从而提高了性能。
## 逃逸分析技术

## 标量替换
逃逸分析是编译程序优化理论中的一种方法,用于确定指针动态范围,即分析在程序的哪些地方可以访问到指针。当一个变量(或者对象)在方法中被分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸。

逃逸分析的主要作用是帮助Java Hotspot编译器分析出一个新的对象的引用的使用范围,从而决定是否需要将这个对象分配到堆上。逃逸分析可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

逃逸分析的优化技术包括:

- **栈上分配:** 如果一个对象不会在方法体内,或线程内发生逃逸,那么可以使用栈上的空间,那么大量的对象将随方法的结束而销毁,减轻了GC压力2。
- **同步消除:** 如果你定义的类的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行2。
- **标量替换:** 如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替2。

逃逸的方式主要有两种:

## 栈上分配
- **方法逃逸:** 在一个方法体内,定义一个局部变量,而它可能被外部方法引用,比如作为调用参数传递给方法,或作为对象直接返回。
- **线程逃逸:** 这个对象被其他线程访问到,比如赋值给了实例变量,并被其他线程访问到了。

59 changes: 59 additions & 0 deletions src/note/java/jvm/2403031236.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
isOriginal: true
order: 9
date: 2024-03-03
index: true
category:
- Java
tag:
- JVM
- GC
---

# 垃圾收集器及原理

JVM垃圾收集器负责回收无用对象,以防止内存泄漏。常见的收集器有**Serial、Parallel Scavenge、ParNew、Serial Old、Parallel Old、CMS和G1及ZGC**等。 本文主要简单记录垃圾收集器常用的垃圾收集算法、串行以及并行垃圾收集器的相关知识。

![垃圾收集器](https://qiniu.yanggl.cn/image/2403031236_1.png)

<!-- more -->

## GC算法

- **分代收集算法:** 这种算法根据对象存活周期的不同将内存划分为几块,一般是**新生代和老年代**。新生代中的对象一般存活时间较短,所以选择复制算法,而老年代中的对象存活时间长,空间大,所以选择标记-清理或者标记-整理算法。
- **复制算法:** 这种算法将内存分为两块,每次只使用其中一块。当这一块内存用完后,就将还存活的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉。这种算法的主要缺点是内存利用率只有50%。
- **标记-清除算法:** 这种算法首先标记出所有需要回收的对象,然后进行清除。这种算法的主要缺点是效率不高,并且清除后会产生大量的内存碎片。
- **标记-整理算法:** 这种算法是在标记-清除算法的基础上进行的改进,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

## Serial收集器

Serial收集器是Java虚拟机(JVM)中最基本,也是历史最悠久的垃圾收集器。此收集器是一个单线程的收集器,它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束,这种机制被称为**Stop-The-World(STW)**

Serial收集器的优点在于是单线程运行,没有线程交互的开销,对内存的消耗小,可以专心做垃圾收集,因此可以获得很高的单线程收集效益。缺点就是在垃圾收集过程中其他CPU资源被闲置,必须暂停其他所有的工作线程(STW),这会影响到应用程序的响应性能。

![Serial收集器](https://qiniu.yanggl.cn/image/2403031236_2.png)

**Serial Old收集器被用作Serial收集器的老年代版本**,它同样是一个单线程收集器。它主要有两大用途:

- 在JDK1.5版本及以前的Parallel Scavenge收集器搭配使用
- 作为CMS收集器的后备方案

Serial收集器主要用于新生代的垃圾回收,采用的是**复制算法**。 Serial Old收集器使用的是**标记-整理算法**

## Parallel收集器

Parallel收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集以外,其行为(参数控制、收集算法、回收策略等)和Serial相似。

**Parallel收集器主要用于多核处理器的服务器中,旨在提高垃圾收集的吞吐量(高效的利用CPU)**,所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总耗时的比值。**在注重吞吐量以及CPU资源的场合,都可以优先考虑Parallel收集器配合Parallel Old收集器来进行垃圾收集(JDK1.8默认垃圾收集组合)**

![Parallel收集器](https://qiniu.yanggl.cn/image/2403031236_3.png)

**Parallel Old收集器是Parallel收集器的老年代版本**,同样是使用多线程来进行垃圾收集。Parallel采用的是**复制算法**。 Parallel Old收集器使用的是**标记-整理算法**

## ParNew收集器

ParNew垃圾收集器是一种用于新生代的并行垃圾回收器,与Parallel收集器类似,都是并行收集器。它的名字中的 “Par” 是 “Parallel” 的缩写,表示并行,而 “New” 表示它主要处理新生代的垃圾回收。

![ParNew收集器](https://qiniu.yanggl.cn/image/2403031236_3.png)

ParNew收集器的诞生主要是为了配合CMS(Concurrent Mark-Sweep)垃圾回收器一起使用,用于处理新生代的垃圾回收。这是因为在新生代中,回收次数频繁,使用并行方式更高效。此外,目前只有**Serial和ParNew可以与CMS进行配合垃圾收集**
14 changes: 14 additions & 0 deletions src/note/java/jvm/2403031505.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
isOriginal: true
order: 10
date: 2024-03-03
index: true
category:
- Java
tag:
- JVM
- GC
---

# 垃圾收集之CMS

13 changes: 13 additions & 0 deletions src/note/java/jvm/2403032005.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
isOriginal: true
order: 11
date: 2024-03-03
index: true
category:
- Java
tag:
- JVM
- GC
---

# 垃圾收集之G1

0 comments on commit f3f02ef

Please sign in to comment.