1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-16 18:10:13 +08:00

[docs update]添加Future相关的3个问题

This commit is contained in:
Guide 2023-02-13 18:18:26 +08:00
parent 274f6b90fa
commit be1b16ffcf
4 changed files with 243 additions and 131 deletions

View File

@ -125,4 +125,5 @@ star: 2
<img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/IMG_3007.jpg" style="margin: 0 auto; " />
</a>
</div>
**真诚欢迎准备面试的小伙伴加入星球一起交流!真心希望能够帮助到更多小伙伴!**

View File

@ -19,6 +19,8 @@ public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
}
```
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/concurrent/completablefuture-class-diagram.jpg)
`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程的能力。
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/image-20210902092441434.png)
@ -31,9 +33,11 @@ public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
- `get()` :等待任务执行完成并获取运算结果。
- `get(long timeout, TimeUnit unit)` :多了一个超时时间。
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/image-20210902093026059.png)
`CompletionStage` 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。
`CompletionStage<T>` 接口中的方法比较多,`CompletableFuture` 的函数式能力就是这个接口赋予的。从这个接口的方法参数你就可以发现其大量使用了 Java8 引入的函数式编程。
`CompletionStage` 接口中的方法比较多,`CompletableFuture` 的函数式能力就是这个接口赋予的。从这个接口的方法参数你就可以发现其大量使用了 Java8 引入的函数式编程。
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/image-20210902093026059.png)
由于方法众多,所以这里不能一一讲解,下文中我会介绍大部分常见方法的使用。

View File

@ -468,6 +468,99 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内
- **[Hippo-4](https://github.com/opengoofy/hippo4j)** :一款强大的动态线程池框架,解决了传统线程池使用存在的一些痛点比如线程池参数没办法动态修改、不支持运行时变量的传递、无法执行优雅关闭。除了支持动态修改线程池参数、线程池任务传递上下文,还支持通知报警、运行监控等开箱即用的功能。
- **[Dynamic TP](https://github.com/dromara/dynamic-tp)** 轻量级动态线程池内置监控告警功能集成三方中间件线程池管理基于主流配置中心已支持Nacos、ApolloZookeeper、Consul、Etcd可通过SPI自定义实现
## Future
### Future 类有什么用?
`Future` 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 `Future` 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。
这其实就是多线程中经典的 **Future 模式**,你可以将其看作是一种设计模式,核心思想是异步调用,主要用在多线程领域,并非 Java 语言独有。
在 Java 中,`Future` 类只是一个泛型接口,位于 `java.util.concurrent` 包下,其中定义了 5 个方法,主要包括下面这 4 个功能:
- 取消任务;
- 判断任务是否被取消;
- 判断任务是否已经执行完成;
- 获取任务执行结果。
```java
// V 代表了Future执行的任务返回值的类型
public interface Future<V> {
// 取消任务执行
// 成功取消返回 true否则返回 false
boolean cancel(boolean mayInterruptIfRunning);
// 判断任务是否被取消
boolean isCancelled();
// 判断任务是否已经执行完成
boolean isDone();
// 获取任务执行结果
V get() throws InterruptedException, ExecutionException;
// 指定时间内没有返回计算结果就抛出 TimeOutException 异常
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutExceptio
}
```
简单理解就是:我有一个任务,提交给了 `Future` 来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 `Future` 那里直接取出任务执行结果。
### Callable 和 Future 有什么关系?
我们可以通过 `FutureTask` 来理解 `Callable``Future` 之间的关系。
`FutureTask` 提供了 `Future` 接口的基本实现,常用来封装 `Callable``Runnable`,具有取消任务、查看任务是否执行完成以及获取任务执行结果的方法。`ExecutorService.submit()` 方法返回的其实就是 `Future` 的实现类 `FutureTask`
```java
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
```
`FutureTask` 不光实现了 `Future`接口,还实现了`Runnable` 接口,因此可以作为任务直接被线程执行。
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/concurrent/completablefuture-class-diagram.jpg)
`FutureTask` 有两个构造函数,可传入 `Callable` 或者 `Runnable` 对象。实际上,传入 `Runnable` 对象也会在方法内部转换为`Callable` 对象。
```java
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW;
}
public FutureTask(Runnable runnable, V result) {
// 通过适配器RunnableAdapter来将Runnable对象runnable转换成Callable对象
this.callable = Executors.callable(runnable, result);
this.state = NEW;
}
```
`FutureTask`相当于对`Callable` 进行了封装,管理着任务执行的情况,存储了 `Callable``call` 方法的任务执行结果。
### CompletableFuture 类有什么用?
`Future` 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。
Java 8 才被引入`CompletableFuture` 类可以解决`Future` 的这些缺陷。`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。
下面我们来简单看看 `CompletableFuture` 类的定义。
```java
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
}
```
可以看到,`CompletableFuture` 同时实现了 `Future``CompletionStage` 接口。
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/concurrent/completablefuture-class-diagram.jpg)
`CompletionStage` 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。
`CompletionStage` 接口中的方法比较多,`CompletableFuture` 的函数式能力就是这个接口赋予的。从这个接口的方法参数你就可以发现其大量使用了 Java8 引入的函数式编程。
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/image-20210902093026059.png)
## AQS
### AQS 是什么?
@ -842,5 +935,6 @@ public int await() throws InterruptedException, BrokenBarrierException {
- 《实战 Java 高并发程序设计》
- 带你了解下 SynchronousQueue并发队列专题https://juejin.cn/post/7031196740128768037
- 阻塞队列 — DelayedWorkQueue 源码分析https://zhuanlan.zhihu.com/p/310621485
- Java多线程——FutureTask/CompletableFuturehttps://www.cnblogs.com/iwehdio/p/14285282.html
- Java 并发之 AQS 详解https://www.cnblogs.com/waterystone/p/4920797.html
- Java 并发包基石-AQS 详解https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html

View File

@ -1,34 +1,33 @@
---
title: 大白话带你认识 JVM
category: Java
tag:
- JVM
---
# 大白话带你认识JVM
> 来自掘金用户:[说出你的愿望吧丷](https://juejin.im/user/5c2400afe51d45451758aa96)投稿原文地址https://juejin.im/post/5e1505d0f265da5d5d744050#heading-28
> 来自[说出你的愿望吧丷](https://juejin.im/user/5c2400afe51d45451758aa96)投稿原文地址https://juejin.im/post/5e1505d0f265da5d5d744050 。
## 前言
如果在文中用词或者理解方面出现问题,欢迎指出。此文旨在提及而不深究,但会尽量效率地把知识点都抛出来
## 一、JVM的基本介绍
## 一、JVM 的基本介绍
JVM 是 Java Virtual Machine 的缩写,它是一个虚构出来的计算机,一种规范。通过在实际的计算机上仿真模拟各类计算机功能实现···
其实抛开这么专业的句子不说就知道JVM其实就类似于一台小电脑运行在windows或者linux这些操作系统环境下即可。它直接和操作系统进行交互与硬件不直接交互而操作系统可以帮我们完成和硬件进行交互的工作。
好,其实抛开这么专业的句子不说,就知道 JVM 其实就类似于一台小电脑运行在 windows 或者 linux 这些操作系统环境下即可。它直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作。
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/d947f91e44c44c6c80222b49c2dee859-new-image19a36451-d673-486e-9c8e-3c7d8ab66929.png)
### 1.1 Java文件是如何被运行的
### 1.1 Java 文件是如何被运行的
比如我们现在写了一个 HelloWorld.java 好了,那这个 HelloWorld.java 抛开所有东西不谈,那是不是就类似于一个文本文件,只是这个文本文件它写的都是英文,而且有一定的缩进而已。
那我们的 **JVM** 是不认识文本文件的,所以它需要一个 **编译** ,让其成为一个它会读二进制文件的 **HelloWorld.class**
那我们的 **JVM** 是不认识文本文件的,所以它需要一个 **编译** ,让其成为一个它会读二进制文件的 **HelloWorld.class**
#### ① 类加载器
如果 **JVM** 想要执行这个 **.class** 文件,我们需要将其装进一个 **类加载器** 中,它就像一个搬运工一样,会把所有的 **.class** 文件全部搬进JVM里面来。
如果 **JVM** 想要执行这个 **.class** 文件,我们需要将其装进一个 **类加载器** 中,它就像一个搬运工一样,会把所有的 **.class** 文件全部搬进 JVM 里面来。
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/81f1813f371c40ffa1c1f6d78bc49ed9-new-image28314ec8-066f-451e-8373-4517917d6bf7.png)
@ -46,7 +45,7 @@ JVM 是 Java Virtual Machine 的缩写,它是一个虚构出来的计算机,
**栈** 这是我们的代码运行空间。我们编写的每一个方法都会放到 **栈** 里面运行。
我们会听说过 本地方法栈 或者 本地方法接口 这两个名词不过我们基本不会涉及这两块的内容它俩底层是使用C来进行工作的和Java没有太大的关系。
我们会听说过 本地方法栈 或者 本地方法接口 这两个名词,不过我们基本不会涉及这两块的内容,它俩底层是使用 C 来进行工作的,和 Java 没有太大的关系。
#### ⑤ 程序计数器
@ -56,9 +55,9 @@ JVM 是 Java Virtual Machine 的缩写,它是一个虚构出来的计算机,
#### 小总结
1. Java文件经过编译后变成 .class 字节码文件
1. Java 文件经过编译后变成 .class 字节码文件
2. 字节码文件通过类加载器被搬运到 JVM 虚拟机中
3. 虚拟机主要的5大块方法区堆都为线程共享区域有线程安全问题栈和本地方法栈和计数器都是独享区域不存在线程安全问题而 JVM 的调优主要就是围绕堆,栈两大块进行
3. 虚拟机主要的 5 大块:方法区,堆都为线程共享区域,有线程安全问题,栈和本地方法栈和计数器都是独享区域,不存在线程安全问题,而 JVM 的调优主要就是围绕堆,栈两大块进行
### 1.2 简单的代码例子
@ -66,57 +65,58 @@ JVM 是 Java Virtual Machine 的缩写,它是一个虚构出来的计算机,
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/29046a721c2548e0a0680ec5baf4ea95-new-imageb0b42e5e-8e25-409e-b7b9-6586a39a0b8d.png)
一个main方法
一个 main 方法
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/a3d34d33eab74f6f8743ecf62807445c-new-image08506a9e-5101-4f30-b0bc-3abbcb8f1894.png)
执行main方法的步骤如下:
执行 main 方法的步骤如下:
1. 编译好 App.java 后得到 App.class 后,执行 App.class系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内,这个过程叫做 App 类的加载
2. JVM 找到 App 的主程序入口执行main方法
3. 这个main中的第一条语句为 Student student = new Student("tellUrDream") ,就是让 JVM 创建一个Student对象但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把 Student 类的信息放到方法区中
2. JVM 找到 App 的主程序入口,执行 main 方法
3. 这个 main 中的第一条语句为 Student student = new Student("tellUrDream") ,就是让 JVM 创建一个 Student 对象,但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把 Student 类的信息放到方法区中
4. 加载完 Student 类后JVM 在堆中为一个新的 Student 实例分配内存,然后调用构造函数初始化 Student 实例,这个 Student 实例持有 **指向方法区中的 Student 类的类型信息** 的引用
5. 执行student.sayName();时JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址。
6. 执行sayName()
5. 执行 student.sayName();时JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址。
6. 执行 sayName()
其实也不用管太多,只需要知道对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法。找方法就在方法表中找。
## 二、类加载器的介绍
之前也提到了它是负责加载.class文件的它们在文件开头会有特定的文件标示将class文件字节码内容加载到内存中并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载而是否能够运行则由 Execution Engine 来决定
之前也提到了它是负责加载.class 文件的,它们在文件开头会有特定的文件标示,将 class 文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,并且 ClassLoader 只负责 class 文件的加载,而是否能够运行则由 Execution Engine 来决定
### 2.1 类加载器的流程
从类被加载到虚拟机内存中开始到释放内存总共有7个步骤加载验证准备解析初始化使用卸载。其中**验证,准备,解析三个部分统称为连接**
从类被加载到虚拟机内存中开始,到释放内存总共有 7 个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中**验证,准备,解析三个部分统称为连接**
#### 2.1.1 加载
1. 将class文件加载到内存
1. 将 class 文件加载到内存
2. 将静态数据结构转化成方法区中运行时的数据结构
3. 在堆中生成一个代表这个类的 java.lang.Class对象作为数据访问的入口
3. 在堆中生成一个代表这个类的 java.lang.Class 对象作为数据访问的入口
#### 2.1.2 链接
1. 验证:确保加载的类符合 JVM 规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实就是一个安全检查
2. 准备为static变量在方法区中分配内存空间设置变量的初始值例如 static int a = 3 (注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的)
3. 解析虚拟机将常量池内的符号引用替换为直接引用的过程符号引用比如我现在import java.util.ArrayList这就算符号引用直接引用就是指针或者对象地址注意引用对象一定是在内存进行
2. 准备:为 static 变量在方法区中分配内存空间,设置变量的初始值,例如 static int a = 3 (注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的)
3. 解析:虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用比如我现在 import java.util.ArrayList 这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)
#### 2.1.3 初始化
初始化其实就是执行类构造器方法的`<clinit>()`的过程,而且要保证执行前父类的`<clinit>()`方法执行完毕。这个方法由编译器收集顺序执行所有类变量static修饰的成员变量显式初始化和静态代码块中语句。此时准备阶段时的那个 `static int a` 由默认初始化的0变成了显式初始化的3。 由于执行顺序缘故,初始化阶段类变量如果在静态代码块中又进行了更改,会覆盖类变量的显式初始化,最终值会为静态代码块中的赋值。
>注意:字节码文件中初始化方法有两种,非静态资源初始化的`<init>`和静态资源初始化的`<clinit>`,类构造器方法`<clinit>()`不同于类的构造器这些方法都是字节码文件中只能给JVM识别的特殊方法。
初始化其实就是执行类构造器方法的`<clinit>()`的过程,而且要保证执行前父类的`<clinit>()`方法执行完毕。这个方法由编译器收集顺序执行所有类变量static 修饰的成员变量)显式初始化和静态代码块中语句。此时准备阶段时的那个 `static int a` 由默认初始化的 0 变成了显式初始化的 3。 由于执行顺序缘故,初始化阶段类变量如果在静态代码块中又进行了更改,会覆盖类变量的显式初始化,最终值会为静态代码块中的赋值。
> 注意:字节码文件中初始化方法有两种,非静态资源初始化的`<init>`和静态资源初始化的`<clinit>`,类构造器方法`<clinit>()`不同于类的构造器,这些方法都是字节码文件中只能给 JVM 识别的特殊方法。
#### 2.1.4 卸载
GC将无用对象从内存中卸载
GC 将无用对象从内存中卸载
### 2.2 类加载器的加载顺序
加载一个Class类的顺序也是有优先级的类加载器从最底层开始往上的顺序是这样的
加载一个 Class 类的顺序也是有优先级的,类加载器从最底层开始往上的顺序是这样的
1. BootStrap ClassLoaderrt.jar
2. Extension ClassLoader: 加载扩展的jar包
3. App ClassLoader指定的classpath下面的jar包
2. Extension ClassLoader: 加载扩展的 jar
3. App ClassLoader指定的 classpath 下面的 jar
4. Custom ClassLoader自定义的类加载器
### 2.3 双亲委派机制
@ -142,118 +142,123 @@ public class String {
### 3.1 本地方法栈和程序计数器
比如说我们现在点开Thread类的源码会看到它的start0方法带有一个native关键字修饰而且不存在方法体这种用native修饰的方法就是本地方法这是使用C来实现的然后一般这些方法都会放到一个叫做本地方法栈的区域。
比如说我们现在点开 Thread 类的源码,会看到它的 start0 方法带有一个 native 关键字修饰,而且不存在方法体,这种用 native 修饰的方法就是本地方法,这是使用 C 来实现的,然后一般这些方法都会放到一个叫做本地方法栈的区域。
程序计数器其实就是一个指针它指向了我们程序中下一句需要执行的指令它也是内存区域中唯一一个不会出现OutOfMemoryError的区域而且占用内存空间小到基本可以忽略不计。这个内存仅代表当前线程所执行的字节码的行号指示器字节码解析器通过改变这个计数器的值选取下一条需要执行的字节码指令。
程序计数器其实就是一个指针,它指向了我们程序中下一句需要执行的指令,它也是内存区域中唯一一个不会出现 OutOfMemoryError 的区域,而且占用内存空间小到基本可以忽略不计。这个内存仅代表当前线程所执行的字节码的行号指示器,字节码解析器通过改变这个计数器的值选取下一条需要执行的字节码指令。
如果执行的是native方法那这个指针就不工作了。
如果执行的是 native 方法,那这个指针就不工作了。
### 3.2 方法区
方法区主要的作用是存放类的元数据信息,常量和静态变量···等。当它存储的信息过大时,会在无法满足内存分配时报错。
### 3.3 虚拟机栈和虚拟机堆
一句话便是:栈管运行,堆管存储。则虚拟机栈负责运行代码,而虚拟机堆负责存储数据。
#### 3.3.1 虚拟机栈的概念
它是Java方法执行的内存模型。里面会对局部变量动态链表方法出口栈的操作入栈和出栈进行存储且线程独享。同时如果我们听到局部变量表那也是在说虚拟机栈
它是 Java 方法执行的内存模型。里面会对局部变量,动态链表,方法出口,栈的操作(入栈和出栈)进行存储,且线程独享。同时如果我们听到局部变量表,那也是在说虚拟机栈
```java
public class Person{
int a = 1;
public void doSomething(){
int b = 2;
}
}
```
#### 3.3.2 虚拟机栈存在的异常
如果线程请求的栈的深度大于虚拟机栈的最大深度,就会报 **StackOverflowError** 这种错误经常出现在递归中。Java虚拟机也可以动态扩展但随着扩展会不断地申请内存当无法申请足够内存时就会报错 **OutOfMemoryError**
如果线程请求的栈的深度大于虚拟机栈的最大深度,就会报 **StackOverflowError** 这种错误经常出现在递归中。Java 虚拟机也可以动态扩展,但随着扩展会不断地申请内存,当无法申请足够内存时就会报错 **OutOfMemoryError**
#### 3.3.3 虚拟机栈的生命周期
对于栈来说,不存在垃圾回收。只要程序运行结束,栈的空间自然就会释放了。栈的生命周期和所处的线程是一致的。
这里补充一句8种基本类型的变量+对象的引用变量+实例方法都是在栈里面分配内存。
这里补充一句8 种基本类型的变量+对象的引用变量+实例方法都是在栈里面分配内存。
#### 3.3.4 虚拟机栈的执行
我们经常说的栈帧数据说白了在JVM中叫栈帧放到Java中其实就是方法它也是存放在栈中的。
我们经常说的栈帧数据,说白了在 JVM 中叫栈帧,放到 Java 中其实就是方法,它也是存放在栈中的。
栈中的数据都是以栈帧的格式存在它是一个关于方法和运行期数据的数据集。比如我们执行一个方法a就会对应产生一个栈帧A1然后A1会被压入栈中。同理方法b会有一个B1方法c会有一个C1等到这个线程执行完毕后栈会先弹出C1后B1,A1。它是一个先进后出后进先出原则。
栈中的数据都是以栈帧的格式存在,它是一个关于方法和运行期数据的数据集。比如我们执行一个方法 a就会对应产生一个栈帧 A1然后 A1 会被压入栈中。同理方法 b 会有一个 B1方法 c 会有一个 C1等到这个线程执行完毕后栈会先弹出 C1 B1,A1。它是一个先进后出后进先出原则。
#### 3.3.5 局部变量的复用
局部变量表用于存放方法参数和方法内部所定义的局部变量。它的容量是以Slot为最小单位一个slot可以存放32位以内的数据类型。
局部变量表用于存放方法参数和方法内部所定义的局部变量。它的容量是以 Slot 为最小单位,一个 slot 可以存放 32 位以内的数据类型。
虚拟机通过索引定位的方式使用局部变量表,范围为[0,局部变量表的slot的数量]。方法中的参数就会按一定顺序排列在这个局部变量表中至于怎么排的我们可以先不关心。而为了节省栈帧空间这些slot是可以复用的当方法执行位置超过了某个变量那么这个变量的slot可以被其它变量复用。当然如果需要复用那我们的垃圾回收自然就不会去动这些内存。
虚拟机通过索引定位的方式使用局部变量表,范围为[0,局部变量表的 slot 的数量]。方法中的参数就会按一定顺序排列在这个局部变量表中,至于怎么排的我们可以先不关心。而为了节省栈帧空间,这些 slot 是可以复用的,当方法执行位置超过了某个变量,那么这个变量的 slot 可以被其它变量复用。当然如果需要复用,那我们的垃圾回收自然就不会去动这些内存。
#### 3.3.6 虚拟机堆的概念
JVM内存会划分为堆内存和非堆内存堆内存中也会划分为**年轻代**和**老年代**,而非堆内存则为**永久代**。年轻代又会分为**Eden**和**Survivor**区。Survivor也会分为**FromPlace**和**ToPlace**toPlace的survivor区域是空的。EdenFromPlace和ToPlace的默认占比为 **8:1:1**。当然这个东西其实也可以通过一个 -XX:+UsePSAdaptiveSurvivorSizePolicy 参数来根据生成对象的速率动态调整
JVM 内存会划分为堆内存和非堆内存,堆内存中也会划分为**年轻代**和**老年代**,而非堆内存则为**永久代**。年轻代又会分为**Eden**和**Survivor**区。Survivor 也会分为**FromPlace**和**ToPlace**toPlace survivor 区域是空的。EdenFromPlace ToPlace 的默认占比为 **8:1:1**。当然这个东西其实也可以通过一个 -XX:+UsePSAdaptiveSurvivorSizePolicy 参数来根据生成对象的速率动态调整
堆内存中存放的是对象垃圾收集就是收集这些对象然后交给GC算法进行回收。非堆内存其实我们已经说过了就是方法区。在1.8中已经移除永久代,替代品是一个元空间(MetaSpace)最大区别是metaSpace是不存在于JVM中的它使用的是本地内存。并有两个参数
堆内存中存放的是对象,垃圾收集就是收集这些对象然后交给 GC 算法进行回收。非堆内存其实我们已经说过了,就是方法区。在 1.8 中已经移除永久代,替代品是一个元空间(MetaSpace),最大区别是 metaSpace 是不存在于 JVM 中的,它使用的是本地内存。并有两个参数
MetaspaceSize初始化元空间大小控制发生GC
MaxMetaspaceSize限制元空间大小上限防止占用过多物理内存。
移除的原因可以大致了解一下融合HotSpot JVM和JRockit VM而做出的改变因为JRockit是没有永久代的不过这也间接性地解决了永久代的OOM问题。
移除的原因可以大致了解一下:融合 HotSpot JVM JRockit VM 而做出的改变,因为 JRockit 是没有永久代的,不过这也间接性地解决了永久代的 OOM 问题。
#### 3.3.7 Eden年轻代的介绍
#### 3.3.7 Eden 年轻代的介绍
当我们new一个对象后会先放到Eden划分出来的一块作为存储空间的内存但是我们知道对堆内存是线程共享的所以有可能会出现两个对象共用一个内存的情况。这里JVM的处理是为每个线程都预先申请好一块连续的内存空间并规定了对象存放的位置而如果空间不足会再申请多块内存空间。这个操作我们会称作TLAB有兴趣可以了解一下。
当我们 new 一个对象后,会先放到 Eden 划分出来的一块作为存储空间的内存,但是我们知道对堆内存是线程共享的,所以有可能会出现两个对象共用一个内存的情况。这里 JVM 的处理是为每个线程都预先申请好一块连续的内存空间并规定了对象存放的位置,而如果空间不足会再申请多块内存空间。这个操作我们会称作 TLAB有兴趣可以了解一下。
当Eden空间满了之后会触发一个叫做Minor GC就是一个发生在年轻代的GC的操作存活下来的对象移动到Survivor0区。Survivor0区满后触发 Minor GC就会将存活对象移动到Survivor1区此时还会把from和to两个指针交换这样保证了一段时间内总有一个survivor区为空且to所指向的survivor区为空。经过多次的 Minor GC后仍然存活的对象**这里的存活判断是15次对应到虚拟机参数为 -XX:MaxTenuringThreshold 。为什么是15因为HotSpot会在对象头中的标记字段里记录年龄分配到的空间仅有4位所以最多只能记录到15**)会移动到老年代。老年代是存储长期存活的对象的占满时就会触发我们最常听说的Full GC期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。
Eden 空间满了之后,会触发一个叫做 Minor GC就是一个发生在年轻代的 GC的操作存活下来的对象移动到 Survivor0 区。~~Survivor0 区满后触发 Minor GC就会将存活对象移动到 Survivor1 ~~,此时还会把 from to 两个指针交换,这样保证了一段时间内总有一个 survivor 区为空且 to 所指向的 survivor 区为空。经过多次的 Minor GC 后仍然存活的对象(**这里的存活判断是 15 次,对应到虚拟机参数为 -XX:MaxTenuringThreshold 。为什么是 15因为 HotSpot 会在对象头中的标记字段里记录年龄,分配到的空间仅有 4 位,所以最多只能记录到 15**)会移动到老年代。
而且当老年区执行了full gc之后仍然无法进行对象保存的操作就会产生OOM这时候就是虚拟机中的堆内存不足原因可能会是堆内存设置的大小过小这个可以通过参数-Xms、-Xmx来调整。也可能是代码中创建的对象大且多而且它们一直在被引用从而长时间垃圾收集无法收集它们。
> 🐛 修正 :当 Eden 区内存空间满了的时候,就会触发 Minor GCSurvivor0 区满不会触发 Minor GC 。
>
> **那 Survivor0 区 的对象什么时候垃圾回收呢?**
>
> 假设 Survivor0 区现在是满的,此时又触发了 Minor GC ,发现 Survivor0 区依旧是满的,存不下,此时会将 S0 区与 Eden 区的对象一起进行可达性分析,找出活跃的对象,将它复制到 S1 区并且将 S0 区域和 Eden 区的对象给清空,这样那些不可达的对象进行清除,并且将 S0 区 和 S1 区交换。
老年代是存储长期存活的对象的,占满时就会触发我们最常听说的 Full GC期间会停止所有线程等待 GC 的完成。所以对于响应要求高的应用应该尽量去减少发生 Full GC 从而避免响应超时的问题。
而且当老年区执行了 full gc 之后仍然无法进行对象保存的操作,就会产生 OOM这时候就是虚拟机中的堆内存不足原因可能会是堆内存设置的大小过小这个可以通过参数-Xms、-Xmx 来调整。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/c02ecba3c33f43429a765987b928e423-new-image93b46f3d-33f9-46f9-a825-ec7129b004f6.png)
补充说明:关于-XX:TargetSurvivorRatio参数的问题。其实也不一定是要满足-XX:MaxTenuringThreshold才移动到老年代。可以举个例子如对象年龄5的占30%年龄6的占36%年龄7的占34%加入某个年龄段如例子中的年龄6总占用超过Survivor空间*TargetSurvivorRatio的时候从该年龄段开始及大于的年龄对象就要进入老年代即例子中的年龄6对象就是年龄6和年龄7晋升到老年代这时候无需等到MaxTenuringThreshold中要求的15
补充说明:关于-XX:TargetSurvivorRatio 参数的问题。其实也不一定是要满足-XX:MaxTenuringThreshold 才移动到老年代。可以举个例子:如对象年龄 5 的占 30%,年龄 6 的占 36%,年龄 7 的占 34%,加入某个年龄段(如例子中的年龄 6总占用超过 Survivor 空间\*TargetSurvivorRatio 的时候,从该年龄段开始及大于的年龄对象就要进入老年代(即例子中的年龄 6 对象,就是年龄 6 和年龄 7 晋升到老年代),这时候无需等到 MaxTenuringThreshold 中要求的 15
#### 3.3.8 如何判断一个对象需要被干掉
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/1c1d85b5fb8b47239af2a5c0436eb2d7-new-image0cd10827-2f96-433c-9b16-93d4fe491d88.png)
图中程序计数器、虚拟机栈、本地方法栈3个区域随着线程的生存而生存的。内存分配和回收都是确定的。随着线程的结束内存自然就被回收了因此不需要考虑垃圾回收的问题。而Java堆和方法区则不一样各线程共享内存的分配和回收都是动态的。因此垃圾收集器所关注的都是堆和方法这部分内存。
图中程序计数器、虚拟机栈、本地方法栈3 个区域随着线程的生存而生存的。内存分配和回收都是确定的。随着线程的结束内存自然就被回收了,因此不需要考虑垃圾回收的问题。而 Java 堆和方法区则不一样,各线程共享,内存的分配和回收都是动态的。因此垃圾收集器所关注的都是堆和方法这部分内存。
在进行回收前就要判断哪些对象还存活,哪些已经死去。下面介绍两个基础的计算方法
1.引用计数器计算给对象添加一个引用计数器每次引用这个对象时计数器加一引用失效时减一计数器等于0时就是不会再次使用的。不过这个方法有一种情况就是出现对象的循环引用时GC没法回收。
1.引用计数器计算:给对象添加一个引用计数器,每次引用这个对象时计数器加一,引用失效时减一,计数器等于 0 时就是不会再次使用的。不过这个方法有一种情况就是出现对象的循环引用时 GC 没法回收。
2.可达性分析计算这是一种类似于二叉树的实现将一系列的GC ROOTS作为起始的存活对象集从这个节点往下搜索搜索所走过的路径成为引用链把能被该集合引用到的对象加入到集合中。搜索当一个对象到GC Roots没有使用任何引用链时则说明该对象是不可用的。主流的商用程序语言例如JavaC#等都是靠这招去判定对象是否存活的
2.可达性分析计算:这是一种类似于二叉树的实现,将一系列的 GC ROOTS 作为起始的存活对象集,从这个节点往下搜索,搜索所走过的路径成为引用链,把能被该集合引用到的对象加入到集合中。搜索当一个对象到 GC Roots 没有使用任何引用链时,则说明该对象是不可用的。主流的商用程序语言,例如 JavaC#等都是靠这招去判定对象是否存活的
了解一下即可在Java语言汇总能作为GC Roots的对象分为以下几种
(了解一下即可)在 Java 语言汇总能作为 GC Roots 的对象分为以下几种:
1. 虚拟机栈(栈帧中的本地方法表)中引用的对象(局部变量)
2. 方法区中静态变量所引用的对象(静态变量)
3. 方法区中常量引用的对象
4. 本地方法栈即native修饰的方法中JNI引用的对象JNI是Java虚拟机调用对应的C函数的方式通过JNI函数也可以创建新的Java对象。且JNI对于对象的局部引用或者全局引用都会把它们指向的对象都标记为不可回收
5. 已启动的且未终止的Java线程
4. 本地方法栈(即 native 修饰的方法)中 JNI 引用的对象JNI Java 虚拟机调用对应的 C 函数的方式,通过 JNI 函数也可以创建新的 Java 对象。且 JNI 对于对象的局部引用或者全局引用都会把它们指向的对象都标记为不可回收)
5. 已启动的且未终止的 Java 线程
这种方法的优点是能够解决循环引用的问题可它的实现需要耗费大量资源和时间也需要GC它的分析过程引用关系不能发生变化所以需要停止所有进程
这种方法的优点是能够解决循环引用的问题,可它的实现需要耗费大量资源和时间,也需要 GC它的分析过程引用关系不能发生变化所以需要停止所有进程
#### 3.3.9 如何宣告一个对象的真正死亡
首先必须要提到的是一个名叫 **finalize()** 的方法
finalize()是Object类的一个方法、一个对象的finalize()方法只会被系统自动调用一次经过finalize()方法逃脱死亡的对象,第二次不会再调用。
finalize()是 Object 类的一个方法、一个对象的 finalize()方法只会被系统自动调用一次,经过 finalize()方法逃脱死亡的对象,第二次不会再调用。
补充一句:并不提倡在程序中调用 finalize()来进行自救。建议忘掉 Java 程序中该方法的存在。因为它执行的时间不确定甚至是否被执行也不确定Java 程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用)。在 Java9 中已经被标记为 **deprecated** ,且 `java.lang.ref.Cleaner`(也就是强、软、弱、幻象引用的那一套)中已经逐步替换掉它,会比 `finalize` 来的更加的轻量及可靠。
补充一句并不提倡在程序中调用finalize()来进行自救。建议忘掉Java程序中该方法的存在。因为它执行的时间不确定甚至是否被执行也不确定Java程序的不正常退出而且运行代价高昂无法保证各个对象的调用顺序甚至有不同线程中调用。在Java9中已经被标记为 **deprecated** ,且 `java.lang.ref.Cleaner`(也就是强、软、弱、幻象引用的那一套)中已经逐步替换掉它,会比 `finalize` 来的更加的轻量及可靠。
  
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/c807dab33f8b42329c1910d609e7ed21-new-image565aeab2-6d3e-4c2c-80f6-7a7b0f629fda.png)
判断一个对象的死亡至少需要两次标记
1. 如果对象进行可达性分析之后没发现与GC Roots相连的引用链那它将会第一次标记并且进行一次筛选。判断的条件是决定这个对象是否有必要执行finalize()方法。如果对象有必要执行finalize()方法则被放入F-Queue队列中。
2. GC对F-Queue队列中的对象进行二次标记。如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。
1. 如果对象进行可达性分析之后没发现与 GC Roots 相连的引用链,那它将会第一次标记并且进行一次筛选。判断的条件是决定这个对象是否有必要执行 finalize()方法。如果对象有必要执行 finalize()方法,则被放入 F-Queue 队列中。
2. GC F-Queue 队列中的对象进行二次标记。如果对象在 finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。
如果确定对象已经死亡,我们又该如何回收这些垃圾呢
@ -265,7 +270,7 @@ finalize()是Object类的一个方法、一个对象的finalize()方法只会被
标记清除算法就是分为“标记”和“清除”两个阶段。标记出所有需要回收的对象,标记结束后统一回收。这个套路很简单,也存在不足,后续的算法都是根据这个基础来加以改进的。
其实它就是把已死亡的对象标记为空闲内存然后记录在一个空闲列表中当我们需要new一个对象时内存管理模块会从空闲列表中寻找空闲的内存来分给新的对象。
其实它就是把已死亡的对象标记为空闲内存,然后记录在一个空闲列表中,当我们需要 new 一个对象时,内存管理模块会从空闲列表中寻找空闲的内存来分给新的对象。
不足的方面就是标记和清除的效率比较低下。且这种做法会让内存中的碎片非常多。这个导致了如果我们需要使用到较大的内存块时,无法分配到足够的连续内存。比如下图
@ -275,13 +280,13 @@ finalize()是Object类的一个方法、一个对象的finalize()方法只会被
#### 3.4.2 复制算法
为了解决效率问题复制算法就出现了。它将可用内存按容量划分成两等分每次只使用其中的一块。和survivor一样也是用from和to两个指针这样的玩法。fromPlace存满了就把存活的对象copy到另一块toPlace上然后交换指针的内容。这样就解决了碎片的问题。
为了解决效率问题,复制算法就出现了。它将可用内存按容量划分成两等分,每次只使用其中的一块。和 survivor 一样也是用 from to 两个指针这样的玩法。fromPlace 存满了,就把存活的对象 copy 到另一块 toPlace 上,然后交换指针的内容。这样就解决了碎片的问题。
这个算法的代价就是把内存缩水了,这样堆内存的使用效率就会变得十分低下了
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/fc349fbb9b204495a5321febe27818d4-new-image45920a9a-552c-4656-94d6-e3ca45ff9b76.png)
不过它们分配的时候也不是按照1:1这样进行分配的就类似于Eden和Survivor也不是等价分配是一个道理。
不过它们分配的时候也不是按照 1:1 这样进行分配的,就类似于 Eden Survivor 也不是等价分配是一个道理。
#### 3.4.3 标记整理算法
@ -291,57 +296,57 @@ finalize()是Object类的一个方法、一个对象的finalize()方法只会被
#### 3.4.4 分代收集算法
这种算法并没有什么新的思想只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中每次垃圾收集时都发现有大批对象死去只有少量存活那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保就必须使用“标记-清除”或者“标记-整理”算法来进行回收。
这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。
说白了就是八仙过海各显神通,具体问题具体分析了而已。
### 3.5 (了解)各种各样的垃圾回收器
HotSpot VM中的垃圾回收器以及适用场景
HotSpot VM 中的垃圾回收器,以及适用场景
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/11e9dcd0f1ee4f25836e6f1c47104c51-new-image69e1c56a-1d40-493a-9901-6efc647a01f3.png)
到jdk8为止默认的垃圾收集器是Parallel Scavenge 和 Parallel Old
jdk8 为止,默认的垃圾收集器是 Parallel Scavenge 和 Parallel Old
从jdk9开始G1收集器成为默认的垃圾收集器
目前来看G1回收器停顿时间最短而且没有明显缺点非常适合Web应用。在jdk8中测试Web应用堆内存6G新生代4.5G的情况下Parallel Scavenge 回收新生代停顿长达1.5秒。G1回收器回收同样大小的新生代只停顿0.2秒。
jdk9 开始G1 收集器成为默认的垃圾收集器
目前来看G1 回收器停顿时间最短而且没有明显缺点,非常适合 Web 应用。在 jdk8 中测试 Web 应用,堆内存 6G新生代 4.5G 的情况下Parallel Scavenge 回收新生代停顿长达 1.5 秒。G1 回收器回收同样大小的新生代只停顿 0.2 秒。
### 3.6 了解JVM的常用参数
### 3.6 了解JVM 的常用参数
JVM的参数非常之多这里只列举比较重要的几个通过各种各样的搜索引擎也可以得知这些信息。
JVM 的参数非常之多,这里只列举比较重要的几个,通过各种各样的搜索引擎也可以得知这些信息。
| 参数名称 | 含义 | 默认值 | 说明 |
|------|------------|------------|------|
| -Xms | 初始堆大小 | 物理内存的1/64(<1GB) |默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%JVM就会增大堆直到-Xmx的最大限制.
| -Xmx | 最大堆大小 | 物理内存的1/4(<1GB) | 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%JVM会减少堆直到 -Xms的最小限制
| -Xmn | 年轻代大小(1.4or later) | |注意此处的大小是eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。整个堆大小=年轻代大小 + 老年代大小 + 持久代(永久代)大小.增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
| -XX:NewSize | 设置年轻代大小(for 1.3/1.4) | |
| -XX:MaxNewSize | 年轻代最大值(for 1.3/1.4) | |
| -XX:PermSize | 设置持久代(perm gen)初始值 | 物理内存的1/64 |
| -XX:MaxPermSize | 设置持久代最大值 | 物理内存的1/4 |
| -Xss | 每个线程的堆栈大小 | | JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.根据应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右一般小的应用 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大需要严格的测试。校长和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了
| -XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) | |-XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5Xms=Xmx并且设置了Xmn的情况下该参数不需要进行设置。
| -XX:SurvivorRatio | Eden区与Survivor区的大小比值 | |设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
| -XX:+DisableExplicitGC | 关闭System.gc() | |这个参数需要严格的测试
| -XX:PretenureSizeThreshold | 对象超过多大是直接在旧生代分配 | 0 |单位字节 新生代采用Parallel ScavengeGC时无效另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.
| -XX:ParallelGCThreads | 并行收集器的线程数 | |此值最好配置与处理器数目相等 同样适用于CMS
| -XX:MaxGCPauseMillis | 每次年轻代垃圾回收的最长时间(最大暂停时间) | |如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值.
| 参数名称 | 含义 | 默认值 | 说明 |
| -------------------------- | ------------------------------------------------------------ | --------------------- | ------------------------------------------------------------ |
| -Xms | 初始堆大小 | 物理内存的 1/64(<1GB) | 默认(MinHeapFreeRatio 参数可以调整)空余堆内存小于 40%JVM 就会增大堆直到-Xmx 的最大限制. |
| -Xmx | 最大堆大小 | 物理内存的 1/4(<1GB) | 默认(MaxHeapFreeRatio 参数可以调整)空余堆内存大于 70%JVM 会减少堆直到 -Xms 的最小限制 |
| -Xmn | 年轻代大小(1.4or later) | | 注意此处的大小是eden+ 2 survivor space).与 jmap -heap 中显示的 New gen 是不同的。整个堆大小=年轻代大小 + 老年代大小 + 持久代(永久代)大小.增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun 官方推荐配置为整个堆的 3/8 |
| -XX:NewSize | 设置年轻代大小(for 1.3/1.4) | | |
| -XX:MaxNewSize | 年轻代最大值(for 1.3/1.4) | | |
| -XX:PermSize | 设置持久代(perm gen)初始值 | 物理内存的 1/64 | |
| -XX:MaxPermSize | 设置持久代最大值 | 物理内存的 1/4 | |
| -Xss | 每个线程的堆栈大小 | | JDK5.0 以后每个线程堆栈大小为 1M,以前每个线程堆栈大小为 256K.根据应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右一般小的应用, 如果栈不是很深, 应该是 128k 够用的 大的应用建议使用 256k。这个选项对性能影响比较大需要严格的测试。校长 threadstacksize 选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了 |
| -XX:NewRatio | 年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值(除去持久代) | | -XX:NewRatio=4 表示年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5Xms=Xmx 并且设置了 Xmn 的情况下,该参数不需要进行设置。 |
| -XX:SurvivorRatio | Eden 区与 Survivor 区的大小比值 | | 设置为 8,则两个 Survivor 区与一个 Eden 区的比值为 2:8,一个 Survivor 区占整个年轻代的 1/10 |
| -XX:+DisableExplicitGC | 关闭 System.gc() | | 这个参数需要严格的测试 |
| -XX:PretenureSizeThreshold | 对象超过多大是直接在旧生代分配 | 0 | 单位字节 新生代采用 Parallel ScavengeGC 时无效另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象. |
| -XX:ParallelGCThreads | 并行收集器的线程数 | | 此值最好配置与处理器数目相等 同样适用于 CMS |
| -XX:MaxGCPauseMillis | 每次年轻代垃圾回收的最长时间(最大暂停时间) | | 如果无法满足此时间,JVM 会自动调整年轻代大小,以满足此值. |
其实还有一些打印及CMS方面的参数这里就不以一一列举了
其实还有一些打印及 CMS 方面的参数,这里就不以一一列举了
## 四、关于JVM调优的一些方面
## 四、关于 JVM 调优的一些方面
根据刚刚涉及的jvm的知识点我们可以尝试对JVM进行调优主要就是堆内存那块
根据刚刚涉及的 jvm 的知识点,我们可以尝试对 JVM 进行调优,主要就是堆内存那块
所有线程共享数据区大小=新生代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m。所以java堆中增大年轻代后将会减小年老代大小因为老年代的清理是使用fullgc所以老年代过小的话反而是会增多fullgc的。此值对系统性能影响较大Sun官方推荐配置为java堆的3/8。
所有线程共享数据区大小=新生代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为 64m。所以 java 堆中增大年轻代后,将会减小年老代大小(因为老年代的清理是使用 fullgc所以老年代过小的话反而是会增多 fullgc 。此值对系统性能影响较大Sun 官方推荐配置为 java 堆的 3/8。
### 4.1 调整最大堆内存和最小堆内存
-Xmx Xms指定java堆最大值默认值是物理内存的1/4(<1GB)和初始java堆最小值默认值是物理内存的1/64(<1GB))
-Xmx Xms指定 java 堆最大值(默认值是物理内存的 1/4(<1GB)和初始 java 堆最小值默认值是物理内存的 1/64(<1GB))
默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时JVM就会增大堆直到-Xmx的最大限制.,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时JVM会减少堆直到 -Xms的最小限制。简单点来说你不停地往堆内存里面丢数据等它剩余大小小于40%了JVM就会动态申请内存空间不过会小于-Xmx如果剩余大小大于70%又会动态缩小不过不会小于Xms。就这么简单
默认(MinHeapFreeRatio 参数可以调整)空余堆内存小于 40%时JVM 就会增大堆直到-Xmx 的最大限制.,默认(MaxHeapFreeRatio 参数可以调整)空余堆内存大于 70%时JVM 会减少堆直到 -Xms 的最小限制。简单点来说,你不停地往堆内存里面丢数据,等它剩余大小小于 40%了JVM 就会动态申请内存空间不过会小于-Xmx如果剩余大小大于 70%又会动态缩小不过不会小于Xms。就这么简单
开发过程中,通常会将 -Xms 与 -Xmx两个参数配置成相同的值其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。
开发过程中,通常会将 -Xms 与 -Xmx 两个参数配置成相同的值,其目的是为了能够在 java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。
我们执行下面的代码
@ -351,23 +356,23 @@ System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 10
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); //当前可用的总空间
```
注意此处设置的是Java堆大小也就是新生代大小 + 老年代大小
注意:此处设置的是 Java 堆大小,也就是新生代大小 + 老年代大小
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/5e7b352c16d74c789c665af46d3a2509-new-imagedd645dae-307d-4572-b6e2-b5a9925a46cd.png)
设置一个VM options的参数
设置一个 VM options 的参数
-Xmx20m -Xms5m -XX:+PrintGCDetails
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/fe99e355f4754fa4be7427cb65261f3d-new-imagebb5cf485-99f8-43eb-8809-2a89e6a1768e.png)
再次启动main方法
再次启动 main 方法
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/300539f6560043dd8a3fe085d28420e6-new-image3c581a2e-196f-4b01-90f1-c27731b4610b.png)
这里GC弹出了一个Allocation Failure分配失败这个事情发生在PSYoungGen也就是年轻代中
这里 GC 弹出了一个 Allocation Failure 分配失败,这个事情发生在 PSYoungGen也就是年轻代中
这时候申请到的内存为18M空闲内存为4.214195251464844M
这时候申请到的内存为 18M空闲内存为 4.214195251464844M
我们此时创建一个字节数组看看,执行下面的代码
@ -379,11 +384,9 @@ System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 10
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
```
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/bdd717d0a3394be7a733760052773374-new-image371b5d59-0020-4091-9874-603c0ab0073d.png)
此时free memory就又缩水了不过total memory是没有变化的。Java会尽可能将total mem的值维持在最小堆内存大小
此时 free memory 就又缩水了,不过 total memory 是没有变化的。Java 会尽可能将 total mem 的值维持在最小堆内存大小
```java
byte[] b = new byte[10 * 1024 * 1024];
@ -395,7 +398,7 @@ System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 /
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/0fd7550ae2144adca8ed2ede12d5fb96-new-image0c31ff20-289d-4088-8c67-a846d0c5d1e0.png)
这时候我们创建了一个10M的字节数据这时候最小堆内存是顶不住的。我们会发现现在的total memory已经变成了15M这就是已经申请了一次内存的结果。
这时候我们创建了一个 10M 的字节数据,这时候最小堆内存是顶不住的。我们会发现现在的 total memory 已经变成了 15M这就是已经申请了一次内存的结果。
此时我们再跑一下这个代码
@ -407,20 +410,20 @@ System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 /
```
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/4cc44b5d5d1c40c48640ece6a296b1ac-new-image4b57baf6-085b-4150-9c60-ac51b0f815d7.png)
此时我们手动执行了一次fullgc此时total memory的内存空间又变回5.5M了,此时又是把申请的内存释放掉的结果。
此时我们手动执行了一次 fullgc此时 total memory 的内存空间又变回 5.5M 了,此时又是把申请的内存释放掉的结果。
### 4.2 调整新生代和老年代的比值
-XX:NewRatio --- 新生代eden+2*Survivor和老年代不包含永久区的比值
-XX:NewRatio --- 新生代eden+2\*Survivor和老年代不包含永久区的比值
例如:-XX:NewRatio=4表示新生代:老年代=1:4即新生代占整个堆的1/5。在Xms=Xmx并且设置了Xmn的情况下该参数不需要进行设置。
例如:-XX:NewRatio=4表示新生代:老年代=1:4即新生代占整个堆的 1/5。在 Xms=Xmx 并且设置了 Xmn 的情况下,该参数不需要进行设置。
### 4.3 调整Survivor区和Eden区的比值
### 4.3 调整 Survivor 区和 Eden 区的比值
-XX:SurvivorRatio幸存代--- 设置两个Survivor区和eden的比值
-XX:SurvivorRatio幸存代--- 设置两个 Survivor 区和 eden 的比值
例如8表示两个Survivor:eden=2:8即一个Survivor占年轻代的1/10
例如8表示两个 Survivor:eden=2:8即一个 Survivor 占年轻代的 1/10
### 4.4 设置年轻代和老年代的大小
@ -428,41 +431,42 @@ System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 /
-XX:MaxNewSize --- 设置年轻代最大值
可以通过设置不同参数来测试不同的情况反正最优解当然就是官方的Eden和Survivor的占比为8:1:1然后在刚刚介绍这些参数的时候都已经附带了一些说明感兴趣的也可以看看。反正最大堆内存和最小堆内存如果数值不同会导致多次的gc需要注意。
可以通过设置不同参数来测试不同的情况,反正最优解当然就是官方的 Eden Survivor 的占比为 8:1:1然后在刚刚介绍这些参数的时候都已经附带了一些说明感兴趣的也可以看看。反正最大堆内存和最小堆内存如果数值不同会导致多次的 gc需要注意。
### 4.5 小总结
根据实际事情调整新生代和幸存代的大小官方推荐新生代占java堆的3/8幸存代占新生代的1/10
根据实际事情调整新生代和幸存代的大小,官方推荐新生代占 java 堆的 3/8幸存代占新生代的 1/10
在OOM时记得Dump出堆确保可以排查现场问题通过下面命令你可以输出一个.dump文件这个文件可以使用VisualVM或者Java自带的Java VisualVM工具。
OOM 时,记得 Dump 出堆,确保可以排查现场问题,通过下面命令你可以输出一个.dump 文件,这个文件可以使用 VisualVM 或者 Java 自带的 Java VisualVM 工具。
-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=你要输出的日志路径
一般我们也可以通过编写脚本的方式来让OOM出现时给我们报个信可以通过发送邮件或者重启程序等来解决。
一般我们也可以通过编写脚本的方式来让 OOM 出现时给我们报个信,可以通过发送邮件或者重启程序等来解决。
### 4.6 永久区的设置
-XX:PermSize -XX:MaxPermSize
初始空间默认为物理内存的1/64和最大空间默认为物理内存的1/4。也就是说jvm启动时永久区一开始就占用了PermSize大小的空间如果空间还不够可以继续扩展但是不能超过MaxPermSize否则会OOM。
初始空间(默认为物理内存的 1/64和最大空间默认为物理内存的 1/4。也就是说jvm 启动时,永久区一开始就占用了 PermSize 大小的空间,如果空间还不够,可以继续扩展,但是不能超过 MaxPermSize否则会 OOM。
tips如果堆空间没有用完也抛出了OOM有可能是永久区导致的。堆空间实际占用非常少但是永久区溢出 一样抛出OOM。
tips如果堆空间没有用完也抛出了 OOM有可能是永久区导致的。堆空间实际占用非常少但是永久区溢出 一样抛出 OOM。
### 4.7 JVM的栈参数调优
### 4.7 JVM 的栈参数调优
#### 4.7.1 调整每个线程栈空间的大小
可以通过-Xss调整每个线程栈空间的大小
JDK5.0以后每个线程堆栈大小为1M以前每个线程堆栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的不能无限生成经验值在3000~5000左右
JDK5.0 以后每个线程堆栈大小为 1M以前每个线程堆栈大小为 256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右
#### 4.7.2 设置线程栈的大小
-XXThreadStackSize
设置线程栈的大小(0 means use default stack size)
这些参数都是可以通过自己编写程序去简单测试的这里碍于篇幅问题就不再提供demo了
这些参数都是可以通过自己编写程序去简单测试的,这里碍于篇幅问题就不再提供 demo
### 4.8 (可以直接跳过了)JVM其他参数介绍
### 4.8 (可以直接跳过了)JVM 其他参数介绍
形形色色的参数很多,就不会说把所有都扯个遍了,因为大家其实也不会说一定要去深究到底。
@ -476,11 +480,13 @@ JDK5.0以后每个线程堆栈大小为1M以前每个线程堆栈大小为256
-XX:+UseFastAccessorMethods
设置原始类型的快速优化
#### 4.8.3 设置关闭手动GC
#### 4.8.3 设置关闭手动 GC
-XX:+DisableExplicitGC
设置关闭System.gc()(这个参数需要严格的测试)
#### 4.8.4 设置垃圾最大年龄
-XX:MaxTenuringThreshold
设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代.
对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,
@ -488,32 +494,39 @@ JDK5.0以后每个线程堆栈大小为1M以前每个线程堆栈大小为256
增加在年轻代即被回收的概率。该参数只有在串行GC时才有效.
#### 4.8.5 加快编译速度
-XX:+AggressiveOpts
加快编译速度
#### 4.8.6 改善锁机制性能
-XX:+UseBiasedLocking
#### 4.8.7 禁用垃圾回收
-Xnoclassgc
#### 4.8.8 设置堆空间存活时间
-XX:SoftRefLRUPolicyMSPerMB
设置每兆堆空闲空间中SoftReference的存活时间默认值是1s。
#### 4.8.9 设置对象直接分配在老年代
-XX:PretenureSizeThreshold
设置对象超过多大时直接在老年代分配默认值是0。
#### 4.8.10 设置TLAB占eden区的比例
-XX:TLABWasteTargetPercent
设置TLAB占eden区的百分比默认值是1% 。
#### 4.8.10 设置 TLAB 占 eden 区的比例
-XX:TLABWasteTargetPercent
设置TLAB占eden区的百分比默认值是1% 。
#### 4.8.11 设置是否优先 YGC
#### 4.8.11设置是否优先YGC
-XX:+CollectGen0First
设置FullGC时是否先YGC默认值是false。
## finally
真的扯了很久这东西参考了多方的资料有极客时间的《深入拆解虚拟机》和《Java核心技术面试精讲》也有百度也有自己在学习的一些线上课程的总结。希望对你有所帮助谢谢。
真的扯了很久这东西参考了多方的资料有极客时间的《深入拆解虚拟机》和《Java 核心技术面试精讲》,也有百度,也有自己在学习的一些线上课程的总结。希望对你有所帮助,谢谢。