build(www): 添加 Drone CI 流水线配置

- 新增 .drone.yml 文件用于定义 CI/CD 流程
- 配置了基于 Docker 的部署步骤
- 设置了工作区和卷映射以支持持久化数据
- 添加了构建准备阶段和 Docker 部署阶段
- 定义了环境变量和代理设置
- 配置了 artifacts 目录的处理逻辑
- 添加了 timezone 映射以确保时间同步
- 设置了 docker.sock 映射以支持 Docker in Docker
This commit is contained in:
2025-11-01 13:36:00 +08:00
commit 22e48d9558
272 changed files with 44007 additions and 0 deletions

561
blogs/java/java8/java8-1.md Normal file
View File

@@ -0,0 +1,561 @@
---
title: JDK8 特性(一)
date: 2022-05-23
sidebar: 'auto'
tags:
- JDK8
categories:
- Java
---
## 1 Lambda表达式
### 1.1 Lambda标准格式
- Lambda省去面向对象的条条框框, Lambda的标准格式格式由3个部分组成
- 格式说明:
- `(...args)`: 参数列表
- `->`: 分隔或连接参数与方法体的标识符
- `{...;}`: 方法体, 主要的代码逻辑
- eg:
```java
class Demo{
public static void main(String[] args) {
new Thread(() -> {
System.out.println("线程启动");
}).start();
}
}
```
### 1.2 Lambda初体验
#### 无参无返回值
```java
public class Demo1 {
public static void main(String[] args) {
fishDo(() -> System.out.println("小鱼在欢乐的游着..."));
}
private static void fishDo(Fish fish){
fish.swim();
}
}
/**
* 定义一个接口, 并且该接口只有一个需要实现的方法
**/
interface Fish{
void swim();
}
```
#### 有参有返回值
```java
public class Demo1 {
public static void main(String[] args) {
catEat(foodName -> {
System.out.println("小猫在吃" + foodName);
return 3;
});
}
private static void catEat(Cat cat){
System.out.println("小猫吃了" + cat.eat("🐟") + "分钟");
}
}
/**
* 定义一个接口, 并且该接口只有一个需要实现的方法
**/
interface Cat{
int eat(String foodName);
}
```
#### 小结
以后我们调用方法时,看到参数是接口就可以考虑使用Lambda表达式,Lambda表达式相当于是对接口中抽象方法的重写
### 1.3 Lambda实现原理
现有以下类
- 接口类
```java
public interface Swimable {
void swimming();
}
```
- main入口
```java
public class SwimImplDemo {
public static void main(String[] args) {
goSwimming(new Swimable() {
@Override
public void swimming() {
System.out.println("去匿名内部类游泳了");
}
});
}
private static void goSwimming(Swimable swim){
swim.swimming();
}
}
```
- 将以上的main方法进行执行后, 会编译生成以下字节码文件
- ![](/java8/匿名内部类生成的字节码.png)
- 将内部类的字节码文件通过 [XJad] 进行反编译
- 匿名内部类在编译后会形成一个新的类.$
```java
// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://kpdus.tripod.com/jad.html
// Decompiler options: packimports(3) fieldsfirst ansi space
// Source File Name: SwimImplDemo.java
package com.zhuhjay.lambda;
import java.io.PrintStream;
// Referenced classes of package com.zhuhjay.lambda:
// Swimable, SwimImplDemo
static class SwimImplDemo$1 implements Swimable{
public void swimming(){
System.out.println("去匿名内部类游泳了");
}
SwimImplDemo$1(){}
}
```
- main入口改为使用Lambda表达式
```java
public class SwimImplDemo {
public static void main(String[] args) {
goSwimming(() -> {
System.out.println("去Lambda游泳了");
});
}
private static void goSwimming(Swimable swim){
swim.swimming();
}
}
```
- 将以上的main方法进行执行后, 不会生成多余的字节码文件
- 使用 [XJad] 反编译工具会失败
- 使用JDK工具来对Lambda表达式的字节码进行反汇编
```text
javap -c -p 文件名.class
-c表示对代码进行反汇编 -p显示所有类和成员
```
- 对Lambda表达式的字节码文件进行反汇编
```S
public class com.zhuhjay.lambda.SwimImplDemo {
public com.zhuhjay.lambda.SwimImplDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:swimming:()Lcom/zhuhjay/lambda/Swimable;
5: invokestatic #3 // Method goSwimming:(Lcom/zhuhjay/lambda/Swimable;)V
8: return
private static void goSwimming(com.zhuhjay.lambda.Swimable);
Code:
0: aload_0
1: invokeinterface #4, 1 // InterfaceMethod com/zhuhjay/lambda/Swimable.swimming:()V
6: return
private static void lambda$main$0();
Code:
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String 去Lambda游泳了
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
```
- 通过与源代码的比较后, 发现多了静态方法`lambda$main$0`
- 得出: Lambda表达式会在类中新生成一个私有的静态方法, 命名为`lambda$方法名$序列`
- 通过断点调试Lambda也可以从调用栈中发现该静态方法的生成(当然不会显式的在源码中出现)
- 验证在Lambda表达式运行中会生成一个内部类
- 命令格式: `java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名`
- 执行(需要退出包运行该命令): `java -Djdk.internal.lambda.dumpProxyClasses com.zhuhjay.lambda.SwimImplDemo`
- ![](/java8/获得运行时Lambda内部类.png)
- 查看反编译结果
```java
final class SwimImplDemo$$Lambda$1
implements Swimable {
public void swimming() {
SwimImplDemo.lambda$main$0();
}
private SwimImplDemo$$Lambda$1() {}
}
```
- 对以上结论来推断Lambda生成的字节码文件
```java
public class SwimImplDemo {
public static void main(String[] args) {
// 也是相当于Lambda生成了一个匿名内部类, 来调用Lambda生成的静态方法
goSwimming(new Swimable() {
public void swimming(){
SwimImplDemo.lambda$main$0();
}
});
}
/** Lambda生成的私有静态方法 **/
private static void lambda$main$0(){
System.out.println("去Lambda游泳了");
}
private static void goSwimming(Swimable swim){
swim.swimming();
}
}
```
- 小结
- 匿名内部类在编译的时候会一个class文件
- Lambda在程序运行的时候形成一个类
1. 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码
2. 还会形成一个匿名内部类, 实现接口, 重写抽象方法
3. 在接口的重写方法中会调用新生成的方法.
### 1.4 Lambda省略格式
在Lambda标准格式的基础上使用省略写法的规则为
1. 小括号内参数的类型可以省略
2. 如果小括号内有且仅有一个参数,则小括号可以省略
3. 如果大括号内有且仅有一个语句可以同时省略大括号、return关键字及语句分号
### 1.5 Lambda前提条件
Lambda的语法非常简洁但是Lambda表达式不是随便使用的使用时有几个条件要特别注意
1. 方法的参数或局部变量类型必须为接口才能使用Lambda
2. 接口中有且仅有一个抽象方法(@FunctionalInterface用来检测接口是否为函数式接口)
### 1.6 Lambda和匿名内部类对比
1. 所需的类型不一样
- 匿名内部类,需要的类型可以是类,抽象类,接口
- Lambda表达式,需要的类型必须是接口
2. 抽象方法的数量不一样
- 匿名内部类所需的接口中抽象方法的数量随意
- Lambda表达式所需的接口只能有一个抽象方法
3. 实现原理不同
- 匿名内部类是在编译后会形成class
- Lambda表达式是在程序运行的时候动态生成class
## 2 JDK8接口增强
### 2.1 增强介绍
- JDK8以前的接口:
```text
interface 接口名 {
静态常量;
抽象方法;
}
```
- JDK8的接口:
```text
interface 接口名 {
静态常量;
抽象方法;
默认方法;
静态方法;
}
```
### 2.2 接口默认方法和静态方法的区别
1. 默认方法通过实例调用,静态方法通过接口名调用。
2. 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
3. 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。
## 3 常用内置函数式接口
### 3.1 内置函数式接口的由来
我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名抽象方法名只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便JDK提供了大量常用的函数式接口。
### 3.2 常用函数式接口的介绍
它们主要在`java.util.function`包中。下面是最常用的几个接口。
#### Supplier:生产者
`java.util.function.Supplier<T>`接口,它意味着"供给", 对应的Lambda表达式需要"对外提供"一个符合泛型类型的对象数据。
```java
@FunctionalInterface
public interface Supplier<T> {
public abstract T get();
}
```
#### Consumer:消费者
`java.util.function.Consumer<T>`接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
```java
@FunctionalInterface
public interface Consumer<T> {
public abstract void accept(T t);
/** 该方法使得两个Consumer先后调用 c1.andThen(c2).accept(t) **/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
```
- 默认方法: andThen
- 如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen
- 要想实现组合需要两个或多个Lambda表达式即可而 andThen 的语义正是"一步接一步"操作
#### Function:类型转换
`java.util.function.Function<T,R>`接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。
```java
@FunctionalInterface
public interface Function<T, R> {
public abstract R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
}
```
- 默认方法: andThen
- Function 接口中有一个默认的 andThen 方法,用来进行组合操作
- 该方法同样用于"先做什么,再做什么"的场景,和 Consumer 中的 andThen 差不多
#### Predicate:判断
有时候我们需要对某种类型的数据进行判断从而得到一个boolean值结果。这时可以使用`java.util.function.Predicate<T>`接口。
```java
@FunctionalInterface
public interface Predicate<T> {
public abstract boolean test(T t);
/** && **/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
/** ! **/
default Predicate<T> negate() {
return (t) -> !test(t);
}
/** || **/
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
```
- 默认方法: and
- 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用"与"逻辑连接起来实现"并且"的效果时可以使用default方法 and
- 默认方法: or
- 与 and 的"与"类似,默认方法 or 实现逻辑关系中的"或"
- 默认方法: negate
- "与"、"或"已经了解了,剩下的"非"取反也会简单。它是执行了test方法之后对结果boolean值进行"!"取反而已。一定要在 test 方法调用之前调用 negate 方法
## 4 方法引用
方法引用的注意事项
1. 被引用的方法,参数要和接口中抽象方法的参数一样
2. 当接口抽象方法有返回值时,被引用的方法也必须有返回值
### 4.1 方法引用简化Lambda
使用Lambda表达式求一个数组的和
```java
public class Demo2 {
public static void main(String[] args) {
// 将数组进行求和
printSum((arr) -> {
int sum = 0;
for (int i : arr) {
sum += i;
}
System.out.println("sum = " + sum);
});
// 使用已有的方法进行方法引用(让已实现的方法复用)
// 类名::静态方法
printSum(Demo2::getSum);
}
/** 已有的求和方法 **/
private static void getSum(int[] arr){
int sum = 0;
for (int i : arr) {
sum += i;
}
System.out.println("sum = " + sum);
}
/** 使用消费者函数式接口 **/
private static void printSum(Consumer<int[]> consumer){
int[] arr = new int[]{11, 22, 33, 44};
consumer.accept(arr);
}
}
```
### 4.2 方法引用格式
- 符号表示: `::`
- 符号说明: 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。
- 应用场景: 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用
### 4.3 常见引用方式
方法引用在JDK 8中使用方式相当灵活有以下几种形式
1. `instanceName::methodName` 对象::方法名
2. `ClassName::staticMethodName` 类名::静态方法
3. `ClassName::methodName` 类名::普通方法
4. `ClassName::new` 类名::new 调用的构造器
5. `TypeName[]::new` String[]::new 调用数组的构造器
#### 对象::成员方法
这是最常见的一种用法。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:
```java
public class MethodRefDemo {
public static void main(String[] args) {
Date now = new Date();
// 使用Lambda表达式获取当前时间
Supplier<Long> su1 = () -> now.getTime();
System.out.println(su1.get());
// 使用方法引用获取当前时间
Supplier<Long> su2 = now::getTime;
System.out.println(su2.get());
}
}
```
#### 类名::静态方法
由于在`java.lang.System`类中已经存在了静态方法 currentTimeMillis所以当我们需要通过Lambda来调用该方法时,可以使用方法引用, 写法是:
```java
public class MethodRefDemo {
public static void main(String[] args) {
Supplier<Long> su3 = () -> System.currentTimeMillis();
// 等同于, 调用该类的静态方法
Supplier<Long> su4 = System::currentTimeMillis;
}
}
```
#### 类名::引用实例方法
Java面向对象中类名只能调用静态方法类名引用实例方法是有前提的实际上是拿第一个参数作为方法的调用者。
```java
public class MethodRefDemo {
public static void main(String[] args) {
Function<String, Integer> f1 = (str) -> str.length();
// 等同于, 将参数作为调用者去调用方法, 然后接收对应数据类型的返回值
Function<String, Integer> f2 = String::length;
BiFunction<String, Integer, String> f3 = String::substring;
// 等同于, 将第一个参数作为调用者, 第二个参数作为参数, 然后接收对应数据类型的返回值
BiFunction<String, Integer, String> f4 = (str, index) -> str.substring(index);
}
}
```
#### 类名::new
由于构造器的名称与类名完全一样。所以构造器引用使用`类名称::new`的格式表示
```java
public class MethodRefDemo {
public static void main(String[] args) {
// 使用无参构造器实例一个String类
Supplier<String> s1 = String::new;
// 等同于
Supplier<String> s2 = () -> new String();
s1.get();
// 把具体地调用体现在了接口上
// 使用一个参数的构造器实例一个String类
Function<String, String> s3 = String::new;
// 等同于
Function<String, String> s4 = (str) -> new String(str);
s3.apply("张三");
}
}
```
#### 数组::new
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同
```java
public class MethodRefDemo {
public static void main(String[] args) {
Function<Integer, int[]> f = length -> new int[length];
// 等同于
Function<Integer, int[]> ff = int[]::new;
}
}
```
#### 小结
方法引用是对Lambda表达式符合特定情况下的一种缩写它使得我们的Lambda表达式更加的精简也可以理解为Lambda表达式的缩写形式, 不过要注意的是方法引用只能"引用"已经存在的方法!

810
blogs/java/java8/java8-2.md Normal file
View File

@@ -0,0 +1,810 @@
---
title: JDK8 特性(二)
date: 2022-05-24
sidebar: 'auto'
tags:
- JDK8
categories:
- Java
---
## 1 Stream流
### 1.1 集合处理数据的弊端
- 案例引入:
1. 首先筛选所有姓张的人;
2. 然后筛选名字有三个字的人;
3. 最后进行对结果进行打印输出。
- 传统实现
```java
public class StreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");
// 1. 首先筛选所有姓张的人;
List<String> zhangList = new ArrayList<>();
for (String name : list) {
if(name.contains("张")){
zhangList.add(name);
}
}
// 2. 然后筛选名字有三个字的人;
List<String> threeList = new ArrayList<>();
for (String name : zhangList) {
if(name.length() == 3){
threeList.add(name);
}
}
// 3. 最后进行对结果进行打印输出。
for (String name : threeList) {
System.out.println(name);
}
}
}
```
- Stream流式实现
```java
public class StreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");
// 1. 首先筛选所有姓张的人; 2. 然后筛选名字有三个字的人; 3. 最后进行对结果进行打印输出
list.stream()
.filter(str -> str.contains("张"))
.filter(str -> str.length() == 3)
.forEach(System.out::println);
}
}
```
### 1.2 Stream流式思想概述
**注意Stream和IO流(InputStream/OutputStream)没有任何关系请暂时忘记对传统IO流的固有印象**
Stream流式思想类似于工厂车间的“生产流水线”Stream流不是一种数据结构不保存数据而是对数据进行加工处理。Stream可以看作是流水线上的一个工序。在流水线上通过多个工序让一个原材料加工成一个商品。
![](/java8/Stream流式思想.png)
![](/java8/Stream流式思想1.png)
Stream API能让我们快速完成许多复杂的操作如筛选、切片、映射、查找、去除重复统计匹配和归约。
### 1.3 获取Stream流的两种方式
- 方式一
- Collection集合接口中有默认方法`default Stream<E> stream(){...}`
- 也就是说实现该接口的集合类都可以直接调用stream()方法获取流对象
- 方式二
- Stream中的静态方法`static<T> Stream<T> of(T t){...}`
- 可以调用该方法获取流对象
### 1.4 了解Stream流的常用方法和注意事项
- Stream常用方法
| 方法名 | 方法作用 | 返回值类型 | 方法种类 |
| ------- | ---------- | ---------- | -------- |
| count | 统计个数 | long | 终结 |
| forEach | 逐一处理 | void | 终结 |
| filter | 过滤 | Stream | 函数拼接 |
| limit | 取用前几个 | Stream | 函数拼接 |
| skip | 跳过前几个 | Stream | 函数拼接 |
| map | 映射 | Stream | 函数拼接 |
| concat | 组合 | Stream | 函数拼接 |
- 终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。
- 非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结方法。)
- Stream注意事项
1. Stream只能操作一次
2. Stream方法返回的是新的流
3. Stream不调用终结方法中间的操作不会执行
#### forEach
- forEach用来遍历流中的数据 -> 终结方法
```text
void forEach(Consumer<? super T> action);
```
- 该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。
```java
public class StreamTest {
private List<String> list;
@Before
public void before(){
list = new ArrayList<>();
Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");
}
@Test
public void forEach(){
list.stream().forEach(System.out::println);
}
}
```
#### count
- count方法来统计其中的元素个数 -> 终结方法
```text
long count();
```
- 该方法返回一个long值代表元素个数。基本使用
```java
public class StreamTest {
private List<String> list;
@Before
public void before(){
list = new ArrayList<>();
Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");
}
@Test
public void count(){
System.out.println(list.stream().count());
}
}
```
#### filter
- filter用于过滤数据返回符合过滤条件的数据 -> 非终结方法
```text
Stream<T> filter(Predicate<? super T> predicate);
```
- ![](/java8/filter.png)
- 该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
```java
public class StreamTest {
private List<String> list;
@Before
public void before(){
list = new ArrayList<>();
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
}
@Test
public void filter(){
// 计算名字长度为3的人数
System.out.println(list.stream().filter(name -> name.length() == 3).count());
}
}
```
#### limit
- limit 方法可以对流进行截取只取用前n个 -> 非终结方法
```text
Stream<T> limit(long maxSize);
```
- 参数是一个long型如果集合当前长度大于参数则进行截取。否则不进行操作
```java
public class StreamTest {
private List<String> list;
@Before
public void before(){
list = new ArrayList<>();
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
}
@Test
public void limit(){
list.stream().limit(3).forEach(System.out::println);
}
}
```
#### skip
- 如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流 -> 非终结方法
```text
Stream<T> skip(long n);
```
- 如果流的当前长度大于n则跳过前n个; 否则将会得到一个长度为0的空流。
```java
public class StreamTest {
private List<String> list;
@Before
public void before(){
list = new ArrayList<>();
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
}
@Test
public void skip(){
list.stream().skip(3).forEach(System.out::println);
}
}
```
#### map
- 如果需要将流中的元素映射到另一个流中,可以使用 map 方法 -> 非终结方法
```text
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
```
- 该接口需要一个 Function 函数式接口参数可以将当前流中的T类型数据转换为另一种R类型的流
```java
public class StreamTest {
private List<String> list;
@Before
public void before(){
list = new ArrayList<>();
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
}
@Test
public void map(){
list.stream()
// 类型增强
.map(name -> "name: " + name)
.forEach(System.out::println);
}
}
```
#### sorted
- 如果需要将数据排序,可以使用 sorted 方法。 -> 非终结方法
```text
Stream<T> sorted(); // 根据元素的自然顺序排序
Stream<T> sorted(Comparator<? super T> comparator); // 自定义比较器排序
```
- 该方法可以有两种不同地排序实现
```java
public class StreamTest {
private List<String> list;
@Before
public void before(){
list = new ArrayList<>();
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
}
@Test
public void sorted(){
list.stream()
// 默认自然序
.sorted()
// 执行比较器进行排序
.sorted(((o1, o2) -> o1.length() - o2.length()))
.forEach(System.out::println);
}
}
```
#### distinct
- 如果需要去除重复数据,可以使用 distinct 方法 -> 非终结方法
- 自定义类型是根据对象的 hashCode 和 equals 来去除重复元素的
```text
Stream<T> distinct();
```
- 基本使用
```java
public class StreamTest {
private List<String> list;
@Before
public void before(){
list = new ArrayList<>();
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
}
@Test
public void distinct(){
list.add("迪丽热巴");
list.add("张无忌");
list.stream()
.distinct()
.forEach(System.out::println);
}
}
```
#### match
- 如果需要判断数据是否匹配指定的条件,可以使用 Match 相关方法 -> 终结方法
```text
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
```
- 基本使用
```java
public class StreamTest {
private List<String> list;
@Before
public void before(){
list = new ArrayList<>();
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
}
@Test
public void match(){
// allMatch 所有元素都需要满足
System.out.println(list.stream()
.allMatch(name -> name.contains("张")));
// anyMatch 如果有元素满足即可
System.out.println(list.stream()
.anyMatch(name -> name.contains("张")));
// noneMatch 所有元素都不满足
System.out.println(list.stream()
.noneMatch(name -> name.contains("男")));
}
}
```
#### find
- 如果需要找到某些数据,可以使用 find 相关方法 -> 终结方法
```text
Optional<T> findFirst();
Optional<T> findAny();
```
- 基本使用
```java
public class StreamTest {
private List<String> list;
@Before
public void before(){
list = new ArrayList<>();
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
}
@Test
public void find(){
// 都是获取第一个元素
Optional<String> optional = list.stream().findAny();
Optional<String> optional1 = list.stream().findFirst();
System.out.println(optional.get());
System.out.println(optional1.get());
}
}
```
#### max && min
- 如果需要获取最大和最小值,可以使用 max 和 min 方法 -> 终结方法
```text
Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(Comparator<? super T> comparator);
```
- 基本使用
```java
public class StreamTest {
@Test
public void min_max(){
// 传入一个比较器, 排序完后获取最后一个
Optional<Integer> max = Stream.of(4, 2, 7, 1).max((o1, o2) -> o1 - o2);
System.out.println(max.get());
// 传入一个比较器, 排序完后获取第一个
Optional<Integer> min = Stream.of(4, 2, 7, 1).min((o1, o2) -> o1 - o2);
System.out.println(min.get());
}
}
```
#### reduce
- 如果需要将所有数据归纳得到一个数据,可以使用 reduce 方法 -> 终结方法
```text
T reduce(T identity, BinaryOperator<T> accumulator);
```
- 基本使用
```java
public class StreamTest {
@Test
public void reduce(){
Integer reduce = Stream.of(4, 5, 3, 9)
// T identity: 默认值
// BinaryOperator<T> accumulator: 对数据的处理方式
// 获取最大值, 初始值为0
.reduce(0, Integer::max);
System.out.println(reduce);
reduce = Stream.of(4, 5, 3, 9)
// T identity: 默认值
// BinaryOperator<T> accumulator: 对数据的处理方式
// 获取加和结果, 初始值为0
.reduce(0, Integer::sum);
System.out.println(reduce);
}
}
```
![](/java8/reduce执行流程.png)
#### map && reduce结合
- 使用案例
```java
public class StreamTest {
@Test
public void reduceAndMap(){
// 获取最大年龄
Optional<Integer> maxAge = Stream.of(
new Person("张三", 18),
new Person("李四", 20),
new Person("王五", 17),
new Person("小红", 19),
new Person("小明", 18))
.map(Person::getAge)
.reduce(Integer::max);
System.out.println(maxAge.get());
// 获取年龄总和
Optional<Integer> totalAge = Stream.of(
new Person("张三", 18),
new Person("李四", 22),
new Person("王五", 17),
new Person("小红", 19),
new Person("小明", 21))
.map(Person::getAge)
.reduce(Integer::sum);
System.out.println(totalAge.get());
}
}
```
#### mapToInt
- 如果需要将Stream中的Integer类型数据转成int类型可以使用 mapToInt 方法 -> 终结方法
```text
IntStream mapToInt(ToIntFunction<? super T> mapper);
```
- ![](/java8/filter.png)
- 该接口需要一个 Function 函数式接口参数可以将当前流中的T类型数据转换为另一种R类型的流
```java
public class StreamTest {
@Test
public void mapToInt(){
// Integer占用的内存比int多, 在Stream流中会自动拆箱装箱
Stream<Integer> stream = Stream.of(1, 2, 3, 4);
// 将Integer类型转换为int类型, 可以节省空间
IntStream intStream = stream
.mapToInt(Integer::intValue);
}
}
```
#### concat
- 如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat -> 终结方法
- 这是一个静态方法,与 `java.lang.String` 当中的 concat 方法是不同的
```text
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b){...}
```
- 基本使用
```java
public class StreamTest {
@Test
public void concat(){
Stream<String> streamA = Stream.of("张三");
Stream<String> streamB = Stream.of("李四");
// 将以上流合并, 合并的流就不能够继续操作了
Stream.concat(streamA, streamB).forEach(System.out::println);
}
}
```
### 1.5 Stream综合案例
- 现在有两个 ArrayList 集合存储队伍当中的多个成员姓名要求使用Stream实现若干操作步骤
1. 第一个队伍只要名字为3个字的成员姓名
2. 第一个队伍筛选之后只要前3个人
3. 第二个队伍只要姓张的成员姓名;
4. 第二个队伍筛选之后不要前2个人
5. 将两个队伍合并为一个队伍;
6. 根据姓名创建 Person 对象;
7. 打印整个队伍的Person对象信息。
```java
public class StreamCaseDemo {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
List<String> two = new ArrayList<>();
Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七公");
Collections.addAll(two, "古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱", "张三");
Stream<String> streamOne = one.stream()
// 1. 第一个队伍只要名字为3个字的成员姓名
.filter(name -> name.length() == 3)
// 2. 第一个队伍筛选之后只要前3个人
.limit(3);
Stream<String> streamTwo = two.stream()
// 3. 第二个队伍只要姓张的成员姓名
.filter(name -> name.startsWith("张"))
// 4. 第二个队伍筛选之后不要前2个人
.skip(2);
// 5. 将两个队伍合并为一个队伍
Stream<String> stream = Stream.concat(streamOne, streamTwo);
// 6. 根据姓名创建 Person 对象
stream.map(Person::new)
// 7. 打印整个队伍的Person对象信息
.forEach(System.out::println);
}
}
```
### 1.6 收集Stream流中的结果
- 对流操作完成之后,如果需要将流的结果保存到数组或集合中,可以收集流中的数据
#### 收集结果到集合中
- Stream流提供 collect 方法,其参数需要一个`java.util.stream.Collector<T,A, R>`接口对象来指定收集到哪种集合中。`java.util.stream.Collectors`类提供一些方法可以作为Collector`接口的实例:
- `public static <T> Collector<T, ?, List<T>> toList()`: 转换为 List 集合
- `public static <T> Collector<T, ?, Set<T>> toSet()`: 转换为 Set 集合
- `public static <T, C extends Collection<T>>
Collector<T, ?, C> toCollection(Supplier<C> collectionFactory)`: 收集到指定的集合中
#### 收集结果到数组中
- Stream提供 toArray 方法来将结果放到一个数组中
```text
Object[] toArray(); // 返回Object数组
<A> A[] toArray(IntFunction<A[]> generator); // 返回指定数组
```
#### 对流中数据进行聚合计算
- 当我们使用Stream流处理数据后可以像数据库的聚合函数一样对某个字段进行操作。比如获取最大值获取最小值求总和平均值统计数量。
```java
public class StreamCollectTest {
@Test
public void aggregation(){
List<Person> personList = Stream.of(
new Person("张三", 18),
new Person("李四", 22),
new Person("王五", 17),
new Person("小红", 19),
new Person("小明", 21))
.collect(Collectors.toList());
// 获取最大值
Optional<Person> optional = personList.stream()
.collect(Collectors.maxBy((o1, o2) -> o1.getAge() - o2.getAge()));
System.out.println("最大值: " + optional.get());
// 获取最小值
Optional<Person> optionalMin = personList.stream()
.collect(Collectors.minBy((o1, o2) -> o1.getAge() - o2.getAge()));
System.out.println("最小值: " + optionalMin.get());
// 求总和
Integer totalAge = personList.stream()
.collect(Collectors.summingInt(Person::getAge));
System.out.println("总和: " + totalAge);
// 平均值
Double avg = personList.stream()
.collect(Collectors.averagingInt(Person::getAge));
System.out.println("平均值: " + avg);
// 统计数量
Long count = personList.stream()
.collect(Collectors.counting());
System.out.println("总共: " + count);
// 以上方法聚合
IntSummaryStatistics summaryStatistics = personList.stream()
.collect(Collectors.summarizingInt(Person::getAge));
System.out.println("上面所有方法的聚合: " + summaryStatistics);
}
}
```
#### 对流中数据进行分组
- 当我们使用Stream流处理数据后可以根据某个属性将数据分组
```java
public class StreamCollectTest {
@Test
public void group(){
List<Person> personList = Stream.of(
new Person("张三", 18),
new Person("李四", 22),
new Person("王五", 18),
new Person("小红", 18),
new Person("小明", 21))
.collect(Collectors.toList());
// 通过年龄来进行分组
personList.stream()
.collect(Collectors.groupingBy(Person::getAge))
// 分组结果为键值对
.forEach((key, value) -> System.out.println(key + "::" + value));
// 将年龄大于19的分为一组, 小于19分为一组
personList.stream()
.collect(Collectors.groupingBy(s -> {
if(s.getAge() > 19){
return ">=19";
}else{
return "<19";
}
}))
// 分组结果为键值对, 键为groupingBy返回的数据
.forEach((k, v) -> System.out.println(k + "::" + v));
}
}
```
#### 对流中数据进行多级分组
- 还可以对数据进行多级分组:
```java
public class StreamCollectTest {
@Test
public void multiGroup(){
List<Person> personList = Stream.of(
new Person("张三丰", 18),
new Person("迪丽热巴", 22),
new Person("古力娜扎", 18),
new Person("迪迦奥特曼", 18),
new Person("宇宙无敌法外狂徒张三", 21))
.collect(Collectors.toList());
// 先根据年龄分组, 每组中再根据名字的长度分组
personList.stream()
.collect(
// 根据年龄分组
Collectors.groupingBy(p -> {
if(p.getAge() > 19){
return "大于19";
}else{
return "小于等于19";
}
},
// 根据名字长度分组
Collectors.groupingBy(p -> {
if(p.getName().length() > 4){
return "较长的名字";
}else{
return "较短的名字";
}
}))
)
// 结果的类型为: Map<String, Map<String, Person>>
.forEach((oneK, oneV) -> {
System.out.println("年龄" + oneK);
oneV.forEach((twoK, twoV) -> {
System.out.println("\t" + twoK + "::" + twoV);
});
});
/*
result -> {
年龄小于等于19
较长的名字::[Person{name='迪迦奥特曼', age=18}]
较短的名字::[Person{name='张三丰', age=18}, Person{name='古力娜扎', age=18}]
年龄大于19
较长的名字::[Person{name='宇宙无敌法外狂徒张三', age=21}]
较短的名字::[Person{name='迪丽热巴', age=22}]
}
*/
}
}
```
#### 对流中数据进行分区
- `Collectors.partitioningBy`会根据值是否为true把集合分割为两个列表一个true列表一个false列表
- ![](/java8/流数据分区.png)
```java
public class StreamCollectTest {
@Test
public void partition(){
List<Person> personList = Stream.of(
new Person("张三", 18),
new Person("李四", 22),
new Person("王五", 18),
new Person("小红", 18),
new Person("小明", 21))
.collect(Collectors.toList());
personList.stream()
// 将结果分为, true和false两个分区
.collect(Collectors.partitioningBy(p -> p.getAge() > 19))
.forEach((k, v) -> System.out.println(k + "::" + v));
}
}
```
#### 对流中数据进行拼接
- `Collectors.joining`会根据指定的连接符,将所有元素连接成一个字符串
```java
public class StreamCollectTest {
@Test
public void join(){
List<Person> personList = Stream.of(
new Person("张三", 18),
new Person("李四", 22),
new Person("王五", 18),
new Person("小红", 18),
new Person("小明", 21))
.collect(Collectors.toList());
// 根据一个字符拼接
String names = personList.stream()
.map(Person::getName)
.collect(Collectors.joining("-"));
System.out.println(names);
// 根据三个字符拼接
names = personList.stream()
.map(Person::getName)
// 参数说明: 分隔符, 前缀, 后缀
.collect(Collectors.joining(",", "{", "}"));
System.out.println(names);
}
}
```

258
blogs/java/java8/java8-3.md Normal file
View File

@@ -0,0 +1,258 @@
---
title: JDK8 特性(三)
date: 2022-05-25
sidebar: 'auto'
tags:
- JDK8
categories:
- Java
---
## 1 Stream流-续
### 1.7 并行的Stream流
- 串行的Stream流
- 目前我们使用的Stream流是串行的就是在一个线程上执行。
- 并行的Stream流
- parallelStream其实就是一个并行执行的流。它通过默认的ForkJoinPool可能提高多线程任务的速度。
#### 获取并行Stream流的两种方式
1. 直接获取并行的流
```java
public class StreamParallelTest {
@Test
public void parallel(){
ArrayList<Integer> list = new ArrayList<>();
// 直接获取并行的流
Stream<Integer> stream = list.parallelStream();
}
}
```
2. 将串行流转成并行流
```java
public class StreamParallelTest {
@Test
public void serialToParallel(){
Stream.of(9, 1, 34, 5, 3, 21, 56, 9)
// 转成并行流
.parallel()
.filter(i -> {
System.out.println(Thread.currentThread() + "::" + i);
return i > 50;
})
.forEach(System.out::println);
}
}
```
#### 串行和并行效率的比较
- 使用for循环串行Stream流并行Stream流来对10亿个数字求和。看消耗的时间。
```java
public class StreamParallelTest {
private static final int TIMES = 1000000000;
long start;
@Before
public void init(){
start = System.currentTimeMillis();
}
@Test
public void useFor(){
// 消耗时间: 339ms
int sum = 0;
for (int i = 0; i < TIMES; i++) {
sum += i;
}
}
@Test
public void useSerialStream(){
// 获取足够长度的串行流进行加和
// 消耗时间: 631ms
LongStream.rangeClosed(0, TIMES)
.reduce(0, Long::sum);
}
@Test
public void useParallelStream(){
// 获取足够长度的并行流进行加和
// 消耗时间: 317ms
LongStream.rangeClosed(0, TIMES)
.parallel()
.reduce(0, Long::sum);
}
@After
public void destroy(){
System.out.println("消耗时间: " +
(System.currentTimeMillis() - start) +
"ms");
}
}
```
- 我们可以看到parallelStream的效率是最高的。
- Stream并行处理的过程会分而治之也就是将一个大任务切分成多个小任务这表示每个任务都是一个操作。
#### ParallelStream线程安全问题
```java
public class StreamParallelTest {
@Test
public void parallelStreamNotice(){
List<Integer> list = new ArrayList<>(1000);
// 并行线程不安全
// list.size() = 947
IntStream.range(0, 1000)
.parallel()
.forEach(i -> list.add(i));
System.out.println("list.size() = " + list.size());
// 解决线程安全问题方案一: 使用同步代码块
Object lock = new Object();
IntStream.range(0, 1000)
.parallel()
.forEach(i -> {
synchronized (lock){
list.add(i);
}
});
System.out.println("list.size() = " + list.size());
// 解决线程安全问题方案二: 使用线程安全的集合
// 使用 Vector<Integer> 集合
Vector<Integer> v = new Vector<>();
IntStream.range(0, 1000)
.parallel()
.forEach(i -> v.add(i));
System.out.println("v.size() = " + v.size());
// 解决线程安全问题方案二: 使用线程安全的集合
// 使用集合工具类提供的线程安全的集合
List<Integer> synchronizedList = Collections.synchronizedList(list);
IntStream.range(0, 1000)
.parallel()
.forEach(i -> synchronizedList.add(i));
System.out.println("synchronizedList.size() = " + synchronizedList.size());
// 解决线程安全问题方案三: 调用Stream流的collect/toArray
// 使用Stream流的collect
List<Integer> collect = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());
System.out.println("collect.size() = " + collect.size());
// 解决线程安全问题方案三: 调用Stream流的collect/toArray
// 使用Stream流的toArray
Integer[] integers = IntStream.range(0, 1000)
.parallel()
.boxed()
.toArray(Integer[]::new);
System.out.println("integers.length = " + integers.length);
}
}
```
### 1.8 Fork/Join框架介绍
- parallelStream使用的是Fork/Join框架。Fork/Join框架自JDK 7引入。Fork/Join框架可以将一个大任务拆分为很多小 任务来异步执行。 Fork/Join框架主要包含三个模块
1. 线程池ForkJoinPool
2. 任务对象ForkJoinTask
3. 执行任务的线程ForkJoinWorkerThread
![](/java8/ForkJoin一览.png)
#### Fork/Join原理-分治法
- ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序那么会将这个任务分割成 两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推对于500万的数据也会做出同样的分割处理到最后会设置一个阈值来规定当数据规模到多少时停止这样的分割处理。比如当元素的数量小于10时会停止分割转而使用插入排序对它们进行排序。那么到最后所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。
![](/java8/ForkJoin-分治法.png)
#### Fork/Join原理-工作窃取算法
- Fork/Join最核心的地方就是利用了现代硬件设备多核在一个操作时候会有空闲的cpu那么如何利用好这个空闲的cpu就成了提高性能的关键而这里我们要提到的工作窃取work-stealing算法就是整个Fork/Join框架的核心理念 Fork/Join工作窃取work-stealing算法是指某个线程从其他队列里窃取任务来执行。
![](/java8/ForkJoin-工作窃取算法.png)
- 那么为什么需要使用工作窃取算法呢假如我们需要做一个比较大的任务我们可以把这个任务分割为若干互不依赖的子任务为了减少线程间的竞争于是把这些子任务分别放到不同的队列里并为每个队列创建一个单独的线程来执行队列里的任务线程和队列一一对应比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的 任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任 务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永 远从双端队列的尾部拿任务执行。
- 工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
- 上文中已经提到了在Java 8引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行也就是我们使用了ForkJoinPool的ParallelStream。
- 对于ForkJoinPool通用线程池的线程数量通常使用默认值就可以了即运行时计算机的处理器数量。可以通过设置系统属性`java.util.concurrent.ForkJoinPool.common.parallelism=N`N为线程数量来调整ForkJoinPool的线程数量可以尝试调整成不同的参数来观察每次的输出结果。
#### Fork/Join案例
- 需求使用Fork/Join计算1-10000的和当一个任务的计算数量大于3000时拆分任务数量小于3000时计算。
![](/java8/ForkJoin案例.png)
```java
public class ForkJoinDemo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
SumRecursiveTask task = new SumRecursiveTask(1, 10000L);
Long result = pool.invoke(task);
System.out.println("result = " + result);
long end = System.currentTimeMillis();
System.out.println("消耗的时间为: " + (end - start));
}
}
class SumRecursiveTask extends RecursiveTask<Long> {
private static final long THRESHOLD = 3000L;
private final long start;
private final long end;
public SumRecursiveTask(long start, long end){
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end - start;
if (length <= THRESHOLD) {
// 任务不用再拆分了.可以计算了
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
// 数量大于预定的数量,任务还需要再拆分
long middle = (start + end) / 2;
SumRecursiveTask left = new SumRecursiveTask(start, middle);
left.fork();
SumRecursiveTask right = new SumRecursiveTask(middle + 1, end);
right.fork();
return left.join() + right.join();
}
}
}
```
### 1.9 小结
1. parallelStream是线程不安全的
2. parallelStream适用的场景是CPU密集型的只是做到别浪费CPU假如本身电脑CPU的负载很大那还到处用并行流那并不能起到作用
3. I/O密集型磁盘I/O、网络I/O都属于I/O操作这部分操作是较少消耗CPU资源一般并行流中不适用于I/O密集型的操作就比如使用并流行进行大批量的消息推送涉及到了大量I/O使用并行流反而慢了很多
4. 在使用并行流的时候是无法保证元素的顺序的,也就是即使你用了同步集合也只能保证元素都正确但无法保证其中的顺序

445
blogs/java/java8/java8-4.md Normal file
View File

@@ -0,0 +1,445 @@
---
title: JDK8 特性(四)
date: 2022-05-26
sidebar: 'auto'
tags:
- JDK8
categories:
- Java
---
## 1 Optional
### 1.1 Optional介绍
- Optional是一个没有子类的工具类Optional是一个可以为null的容器对象。它的作用主要就是为了解决避免Null检查防止NullPointerException。
### 1.2 Optional基本使用
- Optional类的创建方式
```text
Optional.of(T t) : 创建一个 Optional 实例, 不能为空
Optional.empty() : 创建一个空的 Optional 实例
Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
```
- Optional类的常用方法
```text
isPresent() : 判断是否包含值,包含值返回true不包含值返回false
get() : 如果Optional有值则将其返回否则抛出NoSuchElementException
orElse(T t) : 如果调用对象包含值返回该值否则返回参数t
orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
map(Function f): 如果有值对其处理并返回处理后的Optional否则返回 Optional.empty()
```
### 1.3 Optional高级使用
```java
public class OptionalTest {
@Test
public void ifPresent(){
// Optional<User> op = Optional.of(new User("张三", 18));
Optional<User> op = Optional.empty();
// 如果存在就做点什么, 没有就不做了
op.ifPresent(user -> {
System.out.println(user.toString());
});
}
@Test
public void map(){
User user = new User("迪丽热巴", 20);
// 判断如果存在, 那么就将名字重新拼接
Optional<User> optional = Optional.of(user);
System.out.println(getNewName(optional));
}
private String getNewName(Optional<User> optional){
return optional.map(User::getUsername)
.map(str -> str.substring(2))
.orElse("null");
}
}
```
## 2 新的日期时间API
- 旧版日期时间API存在的问题
1. 设计很差:在`java.util`和`java.sql`的包中都有日期类,`java.util.Date`同时包含日期和时间,而`java.sql.Date`仅包含日期。此外用于格式化和解析的类在`java.text`包中定义。
2. 非线程安全:`java.util.Date`是非线程安全的所有的日期类都是可变的这是Java日期类最大的问题之一。
3. 时区处理麻烦日期类并不提供国际化没有时区支持因此Java引入了`java.util.Calendar`和 `java.util.TimeZone`类,但他们同样存在上述所有的问题。
### 2.1 新的日期时间API介绍
- JDK 8中增加了一套全新的日期时间API这套API设计合理是线程安全的。新的日期及时间API位于 `java.time`包中,下面是一些关键类。
- LocalDate :表示日期,包含年月日,格式为 2019-10-16
- LocalTime :表示时间,包含时分秒,格式为 16:38:54.158549300
- LocalDateTime :表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750
- DateTimeFormatter :日期时间格式化类。
- Instant时间戳表示一个特定的时间瞬间。
- Duration用于计算2个时间(LocalTime时分秒)的距离
- Period用于计算2个日期(LocalDate年月日)的距离
- ZonedDateTime :包含时区的时间
- Java中使用的历法是ISO 8601日历系统它是世界民用历法也就是我们所说的公历。平年有365天闰年是366天。此外Java 8还提供了4套其他历法分别是
- ThaiBuddhistDate泰国佛教历
- MinguoDate中华民国历
- JapaneseDate日本历
- HijrahDate伊斯兰历
### 2.2 日期时间类
- LocalDate、LocalTime、LocalDateTime类的实例是不可变的对象分别表示使用 ISO-8601 日历系统的日期、时 间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
- LocalDate: 获取日期的信息
```java
public class DateTest {
@Test
public void localDate(){
// 创建指定日期
LocalDate localDate = LocalDate.of(2022, 11, 11);
System.out.println("localDate = " + localDate); // localDate = 2022-11-11
// 得到当前日期
LocalDate now = LocalDate.now();
System.out.println("now = " + now); // now = 2022-05-23
// 获取日期信息
System.out.println("年: " + now.getYear()); // 年: 2022
System.out.println("月: " + now.getMonthValue()); // 月: 5
System.out.println("日: " + now.getDayOfMonth()); // 日: 23
System.out.println("星期: " + now.getDayOfWeek()); // 星期: MONDAY
}
}
```
- LocalTime: 获取时间信息
```java
public class DateTest {
@Test
public void localTime(){
// 得到指定的时间
LocalTime time = LocalTime.of(12, 12, 12);
System.out.println("time = " + time); // time = 12:12:12
// 得到当前时间
LocalTime now = LocalTime.now();
System.out.println("now = " + now); // now = 11:43:10.495
// 获取时间信息
System.out.println("小时: " + now.getHour()); // 小时: 11
System.out.println("分钟: " + now.getMinute()); // 分钟: 43
System.out.println("秒: " + now.getSecond()); // 秒: 10
System.out.println("纳秒: " + now.getNano()); // 纳秒: 495000000
}
}
```
- LocalDateTime: 获取日期时间类型
```java
public class DateTest {
@Test
public void localDateTime(){
LocalDateTime time = LocalDateTime.of(2022, 11, 11, 12, 12, 12);
System.out.println("time = " + time); // time = 2022-11-11T12:12:12
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now); // now = 2022-05-23T11:47:29.308
// 获取日期信息
System.out.println("年: " + now.getYear()); // 年: 2022
System.out.println("月: " + now.getMonthValue()); // 月: 5
System.out.println("日: " + now.getDayOfMonth()); // 日: 23
System.out.println("时: " + now.getHour()); // 时: 11
System.out.println("分: " + now.getMinute()); // 分: 47
System.out.println("秒: " + now.getSecond()); // 秒: 29
System.out.println("纳秒: " + now.getNano()); // 纳秒: 308000000
}
}
```
- 对日期时间的修改对已存在的LocalDate对象创建它的修改版最简单的方式是使用withAttribute方法。 withAttribute方法会创建对象的一个副本并按照需要修改它的属性。以下所有的方法都返回了一个修改属性的对象他们不会影响原来的对象。
```java
public class DateTest {
@Test
public void localDateTimeUpdate(){
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now); // now = 2022-05-23T12:47:18.640
// 修改年份, 每份修改后的数据都是一个新的对象, 不会与原对象冲突
LocalDateTime setYear = now.withYear(2025);
System.out.println("修改年份后 = " + setYear); // 修改年份后 = 2025-05-23T12:47:18.640
System.out.println("setYear == now? " + (setYear == now)); // setYear == now? false
System.out.println("修改月份: " + now.withMonth(1)); // 修改月份: 2022-01-23T12:47:18.640
System.out.println("修改日: " + now.withDayOfMonth(1)); // 修改日: 2022-05-01T12:47:18.640
System.out.println("修改小时: " + now.withMonth(12)); // 修改小时: 2022-12-23T12:47:18.640
// 在当前对象的基础上加上或减去指定的时间
LocalDateTime localDateTime = now.plusDays(5);
System.out.println("五天后: " + localDateTime); // 五天后: 2022-05-28T12:47:18.640
System.out.println("10年后: " + now.plusYears(10)); // 10年后: 2032-05-23T12:47:18.640
System.out.println("20年后: " + now.plusYears(20)); // 20年后: 2042-05-23T12:47:18.640
System.out.println("3个月前: " + now.minusMonths(3)); // 3个月前: 2022-02-23T12:47:18.640
System.out.println("3分钟前: " + now.minusMinutes(3)); // 3分钟前: 2022-05-23T12:44:18.640
}
}
```
- 日期比较
```java
public class DateTest {
@Test
public void dateEqual(){
// 在JDK8中LocalDate类中使用isBefore()、isAfter()、equals()方法来比较两个日期,可直接进行比较。
LocalDate now = LocalDate.now();
LocalDate date = LocalDate.of(2011, 11, 11);
System.out.println(now.isBefore(date)); // false
System.out.println(date.isBefore(now)); // true
}
}
```
### 2.3 日期格式化与解析
- 通过`java.time.format.DateTimeFormatter`类可以进行日期时间解析与格式化。
```java
public class DateTest {
@Test
public void dateFormatAndParse(){
// 获得当前的日期
LocalDateTime now = LocalDateTime.now();
// 自定义日期格式
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd & HH:mm:ss");
// 将日期转换为对应的格式
String format = dateTimeFormatter.format(now);
System.out.println("format = " + format);
// 将字符串解析为时间
LocalDateTime time = LocalDateTime.parse(format, dateTimeFormatter);
System.out.println("time = " + time);
}
}
```
### 2.4 Instant类
- Instant 时间戳/时间线内部保存了从1970年1月1日 00:00:00以来的秒和纳秒。
- 只能操作秒以下的级别, 用来程序做一些统计的类, 比如说计算运行时间等, 不是给用户使用的
```java
public class InstantTest {
@Test
public void instant(){
Instant now = Instant.now();
System.out.println("当前时间戳 = " + now);
// 获取从1970 01 01 00:00:00的秒
System.out.println(now.getNano());
System.out.println(now.getEpochSecond());
System.out.println(now.toEpochMilli());
System.out.println(System.currentTimeMillis());
System.out.println("5s以后: " + now.plusSeconds(5));
Instant instant = Instant.ofEpochSecond(5);
System.out.println("instant = " + instant);
}
}
```
### 2.5 计算日期时间差的类
- Duration/Period类: 计算日期时间差。
1. Duration用于计算2个时间(LocalTime时分秒)的距离
2. Period用于计算2个日期(LocalDate年月日)的距离
- 注意: 两个类的计算都是第二个参数减去第一个参数, 所以当参数对调时会产生负数的情况
```java
public class DifferenceDateTest {
@Test
public void test(){
// 获取当前时间
LocalTime now = LocalTime.now();
LocalTime of = LocalTime.of(12, 12, 12);
// 使用Duration计算时间的间隔
Duration duration = Duration.between(of, now);
System.out.println("相差的天数" + duration.toDays());
System.out.println("相差的小时数" + duration.toHours());
System.out.println("相差的分钟数" + duration.toMinutes());
System.out.println("相差的秒数" + duration.toMillis());
// 获取当前的日期
LocalDate date = LocalDate.now();
LocalDate localDate = LocalDate.of(2021, 2, 2);
// 使用Period计算日期的间隔
Period period = Period.between(localDate, date);
System.out.println("相差的年" + period.getYears());
System.out.println("相差的月" + period.getMonths());
System.out.println("相差的日" + period.getDays());
}
}
```
### 2.6 时间校正器
- 有时我们可能需要获取例如:将日期调整到“下一个月的第一天”等操作。可以通过时间校正器来进行。
- TemporalAdjuster : 时间校正器。
- TemporalAdjusters : 该类通过静态方法提供了大量的常用TemporalAdjuster的实现。
```java
public class Demo{
@Test
public void temporalAdjuster(){
LocalDateTime now = LocalDateTime.now();
// 自定义时间调整器
LocalDateTime with = now.with(temporal -> {
LocalDateTime dateTime = (LocalDateTime) temporal;
// 修改到下个月1号
return dateTime.plusMonths(1).withDayOfMonth(1);
});
System.out.println("with = " + with);
}
}
```
### 2.7 设置日期时间的时区
- Java8 中加入了对时区的支持LocalDate、LocalTime、LocalDateTime是不带时区的带时区的日期时间类分别为ZonedDate、ZonedTime、ZonedDateTime。
- 其中每个时区都对应着IDID的格式为 “区域/城市” 。例如 Asia/Shanghai 等。
- ZoneId该类中包含了所有的时区信息。
```java
public class Demo{
@Test
public void test(){
// 获取所有的时区ID
ZoneId.getAvailableZoneIds().forEach(System.out::println);
// 不带时区获取计算机的当前时间
// 中国默认时区东八区, 比标准时间快8h
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now);
// 操作带时区的类
// 创建世界标准时间, 不带参数则使用计算机默认的时区
ZonedDateTime zonedDateTime = ZonedDateTime.now(Clock.systemUTC());
System.out.println("zonedDateTime = " + zonedDateTime);
System.out.println("指定时区获取时间: " + ZonedDateTime.now(ZoneId.of("Europe/London")));
}
}
```
### 2.8 小结
- 详细学习了新的日期是时间相关类LocalDate表示日期,包含年月日,LocalTime表示时间,包含时分 秒,`LocalDateTime = LocalDate + LocalTime`,时间的格式化和解析,通过DateTimeFormatter类型进行.
- 学习了Instant类,方便操作秒和纳秒,一般是给程序使用的.学习Duration/Period计算日期或时间的距离,还使用时间调整器方便的调整时间,学习了带时区的3个类ZoneDate/ZoneTime/ZoneDateTime
- JDK 8新的日期和时间API的优势
1. 新版的日期和时间API中日期和时间对象是不可变的。操纵的日期不会影响老值而是新生成一个实例。
2. 新的API提供了两种不同的时间表示方式有效地区分了人和机器的不同需求。
3. TemporalAdjuster可以更精确的操纵日期还可以自定义日期调整器。
4. 是线程安全的
## 3 重复注解与类型注释
### 3.1 重复注解的使用
- 自从Java 5中引入注解以来注解开始变得非常流行并在各个框架和项目中被广泛使用。不过注解有一个很大的限制是在同一个地方不能多次使用同一个注解。JDK 8引入了重复注解的概念允许在同一个地方多次使用同一个注解。在JDK 8中使用`@Repeatable`注解定义重复注解。
- 重复注解的使用步骤:
1. 定义重复的注解容器注解
2. 定义一个可以重复的注解
3. 配置多个重复的注解
4. 解析得到指定注解
```java
/** 3. 配置多个重复的注解 **/
@MyTest("AAA")
@MyTest("BBB")
@MyTest("CCC")
public class RepeatableTest {
/** 3. 配置多个重复的注解 **/
@Test
@MyTest("DD")
@MyTest("EE")
public void test(){}
public static void main(String[] args) throws NoSuchMethodException {
// 4. 解析得到指定注解
// getAnnotationsByType是新增API用来获取重复注解的
// 获取类上的注解
MyTest[] annotation = RepeatableTest.class.getAnnotationsByType(MyTest.class);
for (MyTest myTest : annotation) {
System.out.println("myTest = " + myTest.value());
}
// 获取方法上的注解
MyTest[] tests = RepeatableTest.class.getMethod("test").getAnnotationsByType(MyTest.class);
for (MyTest myTest : tests) {
System.out.println("myTest = " + myTest.value());
}
}
}
/**
* 1. 定义重复的注解容器注解
*/
@Retention(RetentionPolicy.RUNTIME)
@interface MyTests{
MyTest[] value();
}
/**
* 2. 定义一个可以重复的注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(MyTests.class)
@interface MyTest{
String value();
}
```
### 3.2 类型注解的使用
- JDK 8为@Target元注解新增了两种类型TYPE_PARAMETERTYPE_USE 。
- TYPE_PARAMETER :表示该注解能写在类型参数的声明语句中。
- TYPE_USE :表示注解可以再任何用到类型的地方使用。
```java
public class TypeAnnotationDemo<@TypeParam T> {
private @NotNull int one = 10;
public <@TypeParam E extends Integer> void test(@NotNull E ele){}
public static void main(String[] args) {
@NotNull int x = 2;
@NotNull String str = new @NotNull String();
}
}
/**
* 该注解能用到类型前面
*/
@Target(ElementType.TYPE_USE)
@interface NotNull {}
/**
* 该注解能写在泛型上
*/
@Target(ElementType.TYPE_PARAMETER)
@interface TypeParam {}
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,885 @@
---
title: JVM之垃圾回收
date: 2022-12-01
sidebar: 'auto'
tags:
- JVM
categories:
- Java
---
## 1 如何判断对象可以回收
### 1.1 引用计数法
当一个对象被引用时则会将该对象进行引用计数。数值的大小代表被引用的数量当数值为0时则会被当作垃圾进行回收。
**弊端**:当有对象形成互相引用的关系,那么则会造成内存泄漏的问题。
**Java虚拟机 不使用该方式来进行垃圾回收判断**
### 1.2 可达性分析算法
- Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着 GC Root对象为起点的引用链找到该对象找不到表示可以回收
- 哪些对象可以作为 GC Root ?
> Tip
>
> 使用 Eclipse 官方提供的可视化工具 Memory Analyzer 进行内存分析
>
> 官网下载地址https://www.eclipse.org/mat/downloads.php
>
> - 使用说明
> 1. 使用 `jps` 命令定位到进程id
> 2. 使用 `jmap -dump:format=b,live,file=<文件名.bin> 进程id`
> - `format=b`:转储文件格式
> - `live`:存活对象,会在快照前进行一次垃圾回收
> - `file=<文件名.bin>`:将内存快照存放到文件中
>
>
>
> 使用案例
>
> ```java
> public class Demo2_1 {
> public static void main(String[] args) throws IOException {
> List<Object> list = new ArrayList<>();
> list.add("a");
> list.add("b");
> System.out.println(1);
> System.in.read();
>
> list = null;
> System.out.println(2);
> System.in.read();
> }
> }
> ```
>
> 对以上代码进行运行时内存抓取,来查看控制台输出 1 和 2 时,内存中数据的不同之处
>
> 1. 运行代码后,使用 `jps` 命令获取进程id然后 dump 此时的内存状态,使用 Memory Analyzer 读取内存信息
>
> ![1669368873122](/jvm/1669368873122.png)
>
> 2. dump 第二次内存状态,使用 Memory Analyzer 读取内存信息
>
> ![1669369052512](/jvm/1669369052512.png)
### 1.3 四种引用
1. 强引用
- 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
2. 软引用SoftReference
- 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
- 可以配合引用队列来释放软引用自身
3. 弱引用WeakReference
- 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
- 可以配合引用队列来释放弱引用自身
4. 虚引用PhantomReference
- 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存
5. 终结器引用FinalReference
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
#### 1.3.1 软引用
案例代码用来演示内存不够时GC 垃圾回收会做什么事情,需要添加虚拟机参数 `-Xmx20m -XX:+PrintGCDetails -verbose:gc`
```java
public class Demo2_2 {
private static final int _4MB = 1024 * 1024 * 4;
public static void main(String[] args) throws IOException {
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
}
}
```
分析以上代码结果循环向数组中添加5次大小为 4M 的字节数组,因为将虚拟机堆的大小设置为 20M所以该程序的结果就是发生了几次 GC垃圾回收后抛出异常 `java.lang.OutOfMemoryError: Java heap space`
使用软引用解决这个问题,代码修改为:将 byte[] 数据通过一个 SoftReference 软引用对象来实例
```java
public class Demo2_2 {
private static final int _4MB = 1024 * 1024 * 4;
public static void main(String[] args) throws IOException {
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new SoftReference<>(new byte[_4MB]));
list.forEach(ref -> System.out.print(ref.get() + "\t"));
System.out.println();
}
}
}
```
运行结果如下,可以发现
- 在加入第四个对象的时候,已经发生了内存空间不充裕的情况,调用了 Minor GC清理了新生代的内存空间
- 在加入第五个对象的时候,使用 Minor GC 后发现根本没有垃圾可以清理,就调用了 Full GC 进行深度清理 ,再使用 Minor GC 尝试再次清理新生代,发现还是不起作用,再次使用 Full GC 时就进行了软引用内存的清理,使得前面存入的软引用数据全部清空了,但是软引用对象 `SoftReference` 不会进行释放
- 印证了:当内存实在不充裕的情况下,会将软引用数据清空
```tex
[B@404b9385
[B@404b9385 [B@6d311334
[B@404b9385 [B@6d311334 [B@682a0b20
[GC (Allocation Failure) [PSYoungGen: 3804K->496K(6144K)] 16092K->13164K(19968K), 0.0012198 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@404b9385 [B@6d311334 [B@682a0b20 [B@3d075dc0
[GC (Allocation Failure) --[PSYoungGen: 4704K->4704K(6144K)] 17372K->17436K(19968K), 0.0008617 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 4704K->4523K(6144K)] [ParOldGen: 12732K->12697K(13824K)] 17436K->17221K(19968K), [Metaspace: 4212K->4212K(1056768K)], 0.0059000 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) --[PSYoungGen: 4523K->4523K(6144K)] 17221K->17245K(19968K), 0.0006258 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 4523K->0K(6144K)] [ParOldGen: 12721K->781K(8704K)] 17245K->781K(14848K), [Metaspace: 4212K->4206K(1056768K)], 0.0073492 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
null null null null [B@214c265e
```
现在还有一个问题,软引用数据已经清空了,那软引用对象 `SoftReference` 什么时候、又是怎么被清空的?
- 需要使用 `ReferenceQueue` 队列来存储对应的需要进行垃圾回收的软引用对象,然后手动进行对应数组数据的删除,之后的软引用回收交给 GC垃圾回收 去做
```java
public class Demo2_2 {
private static final int _4MB = 1024 * 1024 * 4;
public static void main(String[] args) throws IOException {
List<SoftReference<byte[]>> list = new ArrayList<>();
// 软引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联软引用队列,将来软引用对象关联的数据 byte[] 被清空了之后,软引用自己会加入到队列中
list.add(new SoftReference<>(new byte[_4MB], queue));
list.forEach(ref -> System.out.print(ref.get() + "\t"));
System.out.println();
}
// 去除没用的软引用对象
System.out.println("=====华丽分界线=====");
Reference<? extends byte[]> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
}
}
```
运行结果可以发现所有空的软引用对象都从数组中删除了
```tex
[B@404b9385
[B@404b9385 [B@6d311334
[B@404b9385 [B@6d311334 [B@682a0b20
[B@404b9385 [B@6d311334 [B@682a0b20 [B@3d075dc0
null null null null [B@214c265e
=====华丽分界线=====
[B@214c265e
```
#### 1.3.2 弱引用
使用弱引用来存放数据,需要添加虚拟机参数 `-Xmx20m -XX:+PrintGCDetails -verbose:gc`
```java
public class Demo2_3 {
private static final int _4MB = 1024 * 1024 * 4;
public static void main(String[] args) throws IOException {
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new WeakReference<>(new byte[_4MB]));
list.forEach(ref -> System.out.print(ref.get() + "\t"));
System.out.println();
}
}
}
```
运行结果如下分析这几次GC的结果
1. 执行 Minor GC清除新生代获得内存空间
2. 执行 Minor GC清除新生代将上一次还未放入老年代的弱引用数据清除获得内存空间
3. (3) -> (6) 同上步骤来获取内存空间
4. 直到第 (7) 的时候Minor GC 已经缓解不了当前内存空间不足的窘境,发起了 Full GC将所有的弱引用数据全部清空获取更多的内存空间
![1669385236575](/jvm/1669385236575.png)
从上述引出了一个问题,不是说 `仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 `
是因为有些弱引用对象存活到了老年代中,而触发 Minor GC 却清理不到老年代中的内存,所以只有当触发了 Full GC 的时候才能进行老年代中的弱引用对象数据的清理。
弱引用对象的释放同软引用的释放方式,需要一个 `WeakReferenceQueue` 队列来进行弱引用对象的绑定。
## 2 垃圾回收算法
### 2.1 标记清除
定义: Mark Sweep
- 速度较快
凡是没有被任何 GC Root 引用的对象,都是没有用的数据,则会被清除
缺点:内存碎片过多,使得当有更大的内存需要使用时导致内存溢出
![1669386211266](/jvm/1669386211266.png)
### 2.2 标记整理
定义Mark Compact
- 速度慢
在内存清理时,顺便将内存碎片进行整理
![1669386395288](/jvm/1669386395288.png)
### 2.3 复制
定义Copy
- 不会有内存碎片
- 需要占用双倍内存空间
总有一半的内存空间用来进行内存重排
![1669386619876](/jvm/1669386619876.png)
## 3 分代垃圾回收
![1669386799712](/jvm/1669386799712.png)
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 minor gc伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
- minor gc 会引发 stop the worldSTW暂停其它用户的线程等垃圾回收结束用户线程才恢复运行
- 当对象寿命超过阈值时会晋升至老年代最大寿命是154bit
- 当老年代空间不足,会先尝试触发 minor gc如果之后空间仍不足那么触发 full gcSTW的时间更长
### 3.1 相关 VM 参数
| 描述 | 命令 |
| ------------------ | ----------------------------------------------------------- |
| 堆初始大小 | -Xms |
| 堆最大大小 | -Xmx \| -XX:MaxHeapSize=size |
| 新生代大小 | -Xmn \| -XX:NewSize=size + -XX:MaxNewSize=size |
| 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:UseAdaptiveSizePolicy |
| 幸存区比例 | -XX:SurvivorRatio=ratio |
| 晋升阈值 | -XX:MaxTenuringThreshold=threshold |
| 晋升详情 | -XX:+PrintTenuringDistribution |
| GC详情 | -XX:+PrintGCDetails -verbose:gc |
| FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
### 3.2 GC分析
案例代码,需要使用虚拟机参数 `-Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc`
```java
public class Demo2_4 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) {
// TODO
}
}
```
运行以上案例代码,来分析一下打印的数据
- `def new generation`新生代的信息total 表示最大内存大小(伊甸园区 + form区used 表示已使用,后面的十六进制代表地址
- `eden space`:伊甸园,占比 80%
- `from space`from 分区占比10%
- `to space`to 分区占比10%
- `tenured generation`老年代的信息total 表示最大内存大小uesd 表示已使用
- `Metaspace`:元数据信息,并不在堆中,只是方便查看,目前不关心这块
```tex
Heap
def new generation total 9216K, used 2020K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 24% used [0x00000000fec00000, 0x00000000fedf9048, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3164K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 345K, capacity 388K, committed 512K, reserved 1048576K
```
#### 3.2.1 分析1
修改案例代码为以下代码,将大小为 7MB 的数据放入数组
```java
public class Demo2_4 {
private static final int _7MB = 7 * 1024 * 1024;
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
}
}
```
运行结果进行分析,发现
- 执行了一次 Minor GC伊甸园最大空间为 8MB开辟一块 7MB 的内存需要整理堆空间,将一部分数据放入了 from 分区
- 伊甸园占用了 94% 空间,此时还没用上老年代的空间
```tex
[GC (Allocation Failure) [DefNew: 1855K->600K(9216K), 0.0025878 secs] 1855K->600K(19456K), 0.0032786 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 8342K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 94% used [0x00000000fec00000, 0x00000000ff38f7a0, 0x00000000ff400000)
from space 1024K, 58% used [0x00000000ff500000, 0x00000000ff596228, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3223K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
```
#### 3.2.2 分析2
在 分析1 的基础上再添加 512KB
```java
public class Demo2_4 {
private static final int _512KB = 512 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
}
}
```
运行结果进行分析,发现
- 执行了一次 Minor GC在 7MB 的基础上添加 512KB 伊甸园已经把内存用完了,此时老年代还未用上
```tex
[GC (Allocation Failure) [DefNew: 1685K->599K(9216K), 0.0011811 secs] 1685K->599K(19456K), 0.0012264 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 8791K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 100% used [0x00000000fec00000, 0x00000000ff400000, 0x00000000ff400000)
from space 1024K, 58% used [0x00000000ff500000, 0x00000000ff595d10, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3225K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
```
#### 3.2.3 分析3
在 分析2 的基础上再添加 512KB
```java
public class Demo2_4 {
private static final int _512KB = 512 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
list.add(new byte[_512KB]);
}
}
```
运行结果进行分析,发现
- 执行了两次 Minor GC在第二次添加 512KB 内存时发生的 Minor GC 将新生代中的数据全部提升到了老年代(虽然此时还没到年龄阈值),使得伊甸园中的使用量大大减小
```tex
[GC (Allocation Failure) [DefNew: 1676K->598K(9216K), 0.0011451 secs] 1676K->598K(19456K), 0.0011917 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 8769K->540K(9216K), 0.0053774 secs] 8769K->8303K(19456K), 0.0054079 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 1218K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 8% used [0x00000000fec00000, 0x00000000feca9748, 0x00000000ff400000)
from space 1024K, 52% used [0x00000000ff400000, 0x00000000ff487368, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 7762K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 75% used [0x00000000ff600000, 0x00000000ffd949f8, 0x00000000ffd94a00, 0x0000000100000000)
Metaspace used 3230K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
```
#### 3.2.4 分析4-oom
当有以下情况时,直接添加 8MB 的数据
```java
public class Demo2_4 {
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
}
```
运行结果如下,发现
- 不会进行垃圾回收,伊甸园内存 8MB该数据的大小已经在伊甸园中放不下了JVM 将其直接晋升到老年代进行存储
```tex
Heap
def new generation total 9216K, used 2183K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee21fb8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
Metaspace used 3276K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 355K, capacity 388K, committed 512K, reserved 1048576K
```
#### 3.2.5 分析5
那如果在 分析4 的情况下再添加 8MB 的数据
```java
public class Demo2_4 {
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}
}
```
运行结果如下,发现
- 在抛出 `java.lang.OutOfMemoryError: Java heap space` 异常前进行了两次垃圾回收,这两次垃圾回收是 JVM 尝试自救而触发的,当这两次的垃圾回收还无法解决时,就直接抛出了异常
```tex
[GC (Allocation Failure) [DefNew: 2019K->607K(9216K), 0.0018340 secs][Tenured: 8192K->8798K(10240K), 0.0032842 secs] 10211K->8798K(19456K), [Metaspace: 3254K->3254K(1056768K)], 0.0057250 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 8798K->8780K(10240K), 0.0015195 secs] 8798K->8780K(19456K), [Metaspace: 3254K->3254K(1056768K)], 0.0015379 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66800, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 8780K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 85% used [0x00000000ff600000, 0x00000000ffe932e8, 0x00000000ffe93400, 0x0000000100000000)
Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 362K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.zhuhjay.Demo2_4.main(Demo2_4.java:19)
```
#### 3.2.6 分析6
当一个线程发生了 OOM会不会导致主线程停止运行呢
```java
public class Demo2_4 {
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep start...");
Thread.sleep(1000L);
System.out.println("sleep end...");
}
}
```
主线程仍然能够正常的运行,并且线程抛出异常后,会将线程使用的资源进行释放
```tex
sleep start...
[GC (Allocation Failure) [DefNew: 4023K->840K(9216K), 0.0021618 secs][Tenured: 8192K->9030K(10240K), 0.0028407 secs] 12215K->9030K(19456K), [Metaspace: 4205K->4205K(1056768K)], 0.0050747 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 9030K->8973K(10240K), 0.0024870 secs] 9030K->8973K(19456K), [Metaspace: 4205K->4205K(1056768K)], 0.0025340 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at com.zhuhjay.Demo2_4.lambda$main$0(Demo2_4.java:20)
at com.zhuhjay.Demo2_4$$Lambda$1/1324119927.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
sleep end...
Heap
def new generation total 9216K, used 1540K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 18% used [0x00000000fec00000, 0x00000000fed81218, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 8973K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 87% used [0x00000000ff600000, 0x00000000ffec37f8, 0x00000000ffec3800, 0x0000000100000000)
Metaspace used 4722K, capacity 4836K, committed 4992K, reserved 1056768K
class space used 527K, capacity 592K, committed 640K, reserved 1048576K
```
## 4 垃圾回收器
1. 串行
- 单线程
- 堆内存较小,适合个人电脑
2. 吞吐量优先
- 让单位时间内STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
3. 响应时间优先
- 多线程
- 堆内存较大,多核 cpu
- 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
### 4.1 串行
`-XX:+UseSerialGC` 通过以下部分组成
- `Serial`: 工作在新生代,是 `复制` 算法
- `SerialOld`: 工作在老年代,是 `标记整理` 算法
![1669854149735](/jvm/1669854149735.png)
### 4.2 吞吐量优先
`-XX:+UseParallelGC`JDK1.8 默认开启,使用复制算法,工作在新生代) ~ `-XX:+UseParallelOldGC`使用标记整理算法工作在老年代只要开启了其中一个另一个就会联动开启垃圾回收的线程数与CPU核数有关。在垃圾回收时会瞬间把CPU拉满
- `-XX:+UseAdaptiveSiizePolicy`:动态调整新生代中伊甸园与幸存区的大小
- `-XX:GCTimeRatio=ratio` :吞吐量目标 `1/(1+radio)` ,默认 991/100=0.01 -> 100分钟内有1分钟的垃圾回收
- `-XX:MaxGCPauseMillis=ms`:最大暂停毫秒数,默认 200与上面的 `GCTimeRatio` 互斥,需要折中考虑
- `-XX:ParallelGCThreads=n`:控制运行时的线程数
![1669854271896](/jvm/1669854271896.png)
### 4.3 响应时间优先
`-XX:+UseConcMarkSweepGC`(并发标记清除算法,工作在老年代的 CMS垃圾回收器 ~ `-XX:+UseParNewGC`(基于复制算法的垃圾回收器,工作在新生代) ~ `SerialOld` (当 CMS 垃圾回收器并发失败时,会自动退化到该垃圾回收器)只有在 `初始标记``重新标记` 阶段才会触发 STW
- `-XX:ParallelGCThreads=n`并行线程数与CPU核数有关 ~ `-XX:ConcGCThreads=threads` (并发线程数)
- `-XX:CMSInitiatingOccupancyFraction=percent`CMS 垃圾回收触发比例当老年代内存占用到预定值后进行垃圾回收值越小CMS 垃圾回收越频繁
- 浮动垃圾:在并发清理的同时会产生新的垃圾,这些垃圾只能等到下一次垃圾清理时才能进行回收,需要预留一些空间
- `-XX:+CMSScavengeBeforeRemark`:在重新标记阶段,新生代对象可能会引用老年代的对象,这时候去做可达性分析需要扫描整个堆,为了避免这个耗时的操作,可以先对新生代进行一次垃圾回收,然后再进行老年代的可达性分析
![1669854379036](/jvm/1669854379036.png)
> **Tip:**
>
> 在使用 CMS 进行垃圾回收时,会造成内存碎片过多而使得并发失败的问题,这时候会将垃圾回收器退化到 SerialOld进行一次串行的标记整理后才能恢复 CMS 垃圾回收,这时候垃圾回收的时间会变长。
### 4.4 G1
定义Garbage First
- 2004 论文发布
- 2009 JDK 6u14 体验
- 2012 JDK 7u4 官方支持
- 2017 JDK 9 默认
适用场景
- 同时注重吞吐量Throughput和低延迟Low latency默认的暂停目标是 200 ms
- 超大堆内存,会将堆划分为多个大小相等的 `Region`(区)
- 整体上是 `标记整理` 算法,两个区域之间是 `复制` 算法
相关 JVM 参数
- `-XX:+UseG1GC`:使用 G1 垃圾回收JDK9 开始才默认开启
- `-XX:G1HeapRegionSize=size`G1 堆分区的大小需要是2的指数次
- `-XX:MaxGCPauseMillis=time`:暂停目标 默认 200ms
#### 1) G1 垃圾回收阶段
![1669854602888](/jvm/1669854602888.png)
#### 2) Young Collection
新生代的垃圾收集
- 会 STW
![1669907703582](/jvm/1669907703582.png)
当内存紧张了进行垃圾回收时,使用 `复制算法` 将伊甸园中幸存的对象放置到幸存区中
![1669907818221](/jvm/1669907818221.png)
过了一段时间或者内存紧张后进行垃圾回收,会将幸存区中到达年龄的对象会晋升到老年代,而其他对象会被复制到另一个新的幸存区中
![1669907993824](/jvm/1669907993824.png)
#### 3) Young Collection + CM
- 在 Young GC 时会进行 GC Root 的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW由下面的 JVM 参数决定
`-XX:InitiatingHeapOccupancyPercent=percent`默认45%
![1669908372614](/jvm/1669908372614.png)
#### 4) Mixed Collection
会对 E、S、O 进行全面垃圾回收
- 最终标记Remark会 STW
- 拷贝存活Evacuation会 STW
`-XX:MaxGCPauseMillis=ms`:最大暂停时间
G1 垃圾回收器会根据最大暂停时间进行有选择的、回收价值较高的垃圾回收
![1669908437948](/jvm/1669908437948.png)
#### 5) Full GC
- SerialGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- ParallelGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- CMS
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足
- G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足(两种情况)
- 垃圾产生速度低于垃圾回收速度(有能力进行回收):并发垃圾回收
- 垃圾产生速度高于垃圾回收速度(没有能力进行回收):串行垃圾回收,这时才是真正的 full gc
#### 6) Young Collection 跨代引用
- 新生代回收的跨代引用(老年代引用新生代)问题
- 将老年代引用新生代的对象标记为 脏卡
![1669909462580](/jvm/1669909462580.png)
- 卡表与 Remembered Set新生代对象记录脏卡对象
- 在引用变更时通过 post-write barrier + dirty card queue写屏障+脏卡队列,通过异步的方式让一个线程来维护)
- concurrent refinement threads 更新 Remembered Set
![1669909491746](/jvm/1669909491746.png)
#### 7) Remark
在并发标记阶段当对象引用改变时jvm会加入一个写屏障引用改变写屏障指令就会被执行会将该对象加入一个队列中将对象标记为待处理状态等到整个并发标记结束进入重新标记阶段会STW对象出队列进行标记检查
- `pre-write barrier`(预写屏障) + `satb_mark_queue`(混合标记队列)
#### 8) JDK 8u20 字符串去重
- 优点:节省大量内存
- 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
`-XX:+UseStringDeduplication` (默认打开)
- 将所有新分配的字符串放入一个队列
- 当新生代回收时G1并发检查是否有字符串重复
- 如果它们值一样,让它们引用同一个 char[]
- 注意,与 String.intern() 不一样
- String.intern() 关注的是字符串对象
- 而字符串去重关注的是 char[]
- 在 JVM 内部,使用了不同的字符串表
#### 9) JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,**当一个类加载器的所有类都不再使用,则卸载它所加载的所有类**
- `-XX:+ClassUnloadingWithConcurrentMark` 默认启用
#### 10) JDK 8u60 回收巨型对象
- 一个对象大于 region 的一半时,称之为巨型对象
- G1 不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉
#### 11) JDK 9 并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为 FullGC
- JDK 9 之前需要使用 `-XX:InitiatingHeapOccupancyPercent`
- JDK 9 可以动态调整
- `-XX:InitiatingHeapOccupancyPercent` 用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空档空间
#### 12) JDK 9 更高效的回收
- 250+增强
- 180+bug修复
- https://docs.oracle.com/en/java/javase/12/gctuning
## 5 调优
### 5.1 调优领域
- 内存
- 锁竞争
- cpu 占用
- io
### 5.2 确认目标
- 【低延迟】还是【高吞吐量】,选择合适的回收器
- CMSG1ZGC
- ParallelGC
- Zingjava虚拟机几乎零延时的垃圾回收
### 5.3 最快的GC
最快的 GC 是不发生 GC
- 查看 FullGC 前后的内存占用,考虑下面几个问题
- 数据是不是太多?
- `resultSet = statement.executeQuery("select * from 大表 limit n")`
- 数据表示是否太臃肿?
- 对象图
- 对象大小占用16字节Integer对象占用24字节int类型占用4字节
- 是否存在内存泄漏?
- `static Map map = ... `,只存不释放,死数据过多
- 使用软引用
- 使用弱引用
- 第三方缓存实现
### 5.4 新生代调优
- 新生代的特点
- 所有的 new 操作的内存分配非常廉价
- `TLABthread-local allocation buffer` :线程局部分配缓冲区,为每一个线程分配专门的伊甸园空间,避免了线程安全问题,加快了对象创建的速度
- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC 的时间远远低于 Full GC
- 新生代堆内存分配越大越好?
新生代堆内存分配过大,会导致老年代的堆内存占用相对变小,使得当老年代内存紧张时,不断触发 Full GC使得暂停时间变长影响性能。Oracle 建议新生代内存大小占整个堆的 25%-50% 之间较为合适。
> **-Xmn**
>
> Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.
- 新生代能容纳所有 `并发量 * (请求-响应)` 的数据
- 幸存区大到能保留 `当前活跃对象+需要晋升对象`
- 晋升阈值配置得当,让长时间存活对象尽快晋升
- `-XX:MaxTenuringThreshold=threshold`:调整晋升阈值
- `-XX:+PrintTenuringDistribution`:打印晋升详情信息
### 5.5 老年代调优
以 CMS 为例
- CMS 的老年代内存越大越好
- 先尝试不做调优,如果没有 Full GC 那么已经...,否则先尝试调优新生代
- 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
- `-XX:CMSInitiatingOccupancyFraction=percent`

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff