Java8-13新特性概述

291 阅读30分钟

来源于一次内部分享。着重讲了下Java8的相关新特性。

Java8新特性

Lambda表达式

1,为什么使用Lambda表达式?

假设你有一个Apple类,它 有一个getColor方法,还有一个变量inventory保存着一个Apples的列表。你可能想要选出所 有的绿苹果,并返回一个列表。通常我们用筛选(filter)一词来表达这个概念。

image-20201229104251123

image-20201229104304216

image-20201229104439332

image-20201229104514578

lambda本质:行为参数化,从传递参数到传递方法

以上写法如果改成Lambda的写法如下所示:

filterApples(inventory, (Apple a) -> "green".equals(a.getColor()) );
filterApples(inventory, (Apple a) -> a.getWeight() > 150 ); 
filterApples(inventory, (Apple a) -> a.getWeight() < 80 || "brown".equals(a.getColor()) ); 

2,Lambda表达式是什么

可传递匿名函数的一种方式 。

  • 匿名——我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写得少而想得多!

  • 函数——我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方 法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表

  • 传递——Lambda表达式可以作为参数传递给方法或存储在变量中.

  • 简洁——无需像匿名类那样写很多模板代码。

3,Lambda表达式如何使用

image-20201229105743863

  • 参数列表——这里它采用了Comparator中compare方法的参数,两个Apple。
  • 箭头——箭头->把参数列表与Lambda主体分隔开。
  • Lambda主体——比较两个Apple的重量。表达式就是Lambda的返回值了。

image-20201229105958640

以下哪个不是有效的Lambda表达式?

(1) () -> {}

(2) () -> "Raoul"

(3) () -> {return "Mario";}

(4) (Integer i) -> return "Alan" + i;

(5) (String s) -> {"IronMan";}

答案:只有4和5是无效的Lambda。

(1) 这个Lambda没有参数,并返回void。它类似于主体为空的方法:public void run() {}。

(2) 这个Lambda没有参数,并返回String作为表达式。

(3) 这个Lambda没有参数,并返回String(利用显式返回语句)。

(4) return是一个控制流语句。要使此Lambda有效,需要使花括号,如下所示: (Integer i) -> {return "Alan" + i;}。

(5)“Iron Man”是一个表达式,不是一个语句。要使此Lambda有效,你可以去除花括号 和分号,

如下所示:(String s) -> "Iron Man"。或者如果你喜欢,可以使用显式返回语 句,

如下所示:(String s)->{return "IronMan";}。

image-20201229110354782

在哪里及如何使用lambda表达式?

答案:函数式接口

List<Apple> greenApples = filter(inventory, (Apple a) -> "green".equals(a.getColor()));

image-20201229110735728

4,函数式接口

函数式接口就是只定义一个抽象方法的接口 。

image-20201229110839969

下面哪些接口是函数式接口?

public interface Adder{ int add(int a, int b); }

public interface SmartAdder extends Adder{ int add(double a, double b); }

public interface Nothing{ }

答案:

只有Adder是函数式接口。

SmartAdder不是函数式接口,因为它定义了两个叫作add的抽象方法(其中一个是从Adder那里继承来的)。

Nothing也不是函数式接口,因为它没有声明抽象方法。

用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。

image-20201229111107445

对于下列函数描述符(即Lambda表达式的签名),你会使用哪些函数式接口?

(1) T->R

(2) (int, int)->int

(3) T->void

(4) ()->T

(5) (T, U)->R

(1) Function<T,R>不错。它一般用于将类型T的对象转换为类型R的对象(比如Function<Apple, Integer>用来提取苹果的重量)。

(2) IntBinaryOperator具有唯一一个抽象方法,叫作applyAsInt,它代表的函数描述符是(int, int) -> int。

(3) Consumer具有唯一一个抽象方法叫作accept,代表的函数描述符是T -> void。

(4) Supplier具有唯一一个抽象方法叫作get,代表的函数描述符是()-> T。或者,Callable具有唯一一个抽象方法叫作call,代表的函数描述符是() -> T。

(5) BiFunction<T, U, R>具有唯一一个抽象方法叫作apply,代表的函数描述符是(T,U) -> R。

5,方法引用

Java8之前引用一个类的方法,必须先实例化一个类,然后使用它的方法。

比如类A,有一个方法

Boolean isHandsome();
A a = new A()
a.isHandsome();

Java 8之后可以这么使用:

A:: isHandsome

image-20201229111630072

下列Lambda表达式的等效方法引用是什么?

(1)Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);

(2) BiPredicate<List, String> contains = (list, element) -> list.contains(element);

(1)这个Lambda表达式将其参数传给了Integer的静态方法parseInt。这种方法接受一 个需要解析的String,并返回一个Integer。来重写Lambda表达式,如下所示:

Function<String, Integer> stringToInteger = Integer::parseInt;

(2) 这个Lambda使用其第一个参数,调用其contains方法。由于第一个参数是List类型的,如下所示:

BiPredicate<List<String>, String> contains = List::contains;

这是因为,目标类型描述的函数描述符是 (List,String) -> boolean,而 List::contains可以被解包成这个函数描述符

构造函数引用

对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用: ClassName::new

Supplier<Apple> c1 = Apple::new; 
Apple a1 = c1.get(); 

这就等价于:

Supplier<Apple> c1 = () -> new Apple(); 
Apple a1 = c1.get(); 

如果你的构造函数的签名是Apple(Integer weight),那么它就适合Function接口的签 名,于是你可以这样写

Function<Integer, Apple> c2 = Apple::new; 
Apple a2 = c2.apply(110); 

这就等价于:

Function<Integer, Apple> c2 = (weight) -> new Apple(weight);  
Apple a2 = c2.apply(110); 

6,Lambda小结

• Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。

• Lambda表达式让你可以简洁地传递代码。

• 函数式接口就是仅仅声明了一个抽象方法的接口。

• 只有在接受函数式接口的地方才可以使用Lambda表达式。

• Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。

• Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate、Function<T,R>、Supplier、Consumer和BinaryOperator

• 方法引用让你重复使用现有的方法实现并直接传递它们。

• Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法。

Stream流

流是Java API的新成员,它允许你以声明性方式处理数据集合。你可以把它们看成遍历数据集的高级迭代器 。

流之前的处理方式:

image-20201229134039643

image-20201229134116858

从支持数据处理操作的源生成的元素序列 ,

  • 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList 与 LinkedList)。但流的目的在于表达计算,比如你前面见到的 filter、sorted和map。集合讲的是数据,流讲的是计算。
  • 源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。请注意,从有序集 合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  • 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。
  • 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。这让我们下一章中的一些优化成为可能,如延迟和短路。流水线的操作可以看作对数据源进行数据库式查询。

image-20201229134502559

import static java.util.stream.Collectors.toList;
List<String> threeEighCaloricDishNames = menu.stream().filter(d -> d.getCalories()>300)
   																										.map(Dish::getName) //获取菜名
  																										.limit(3)           //只取前三个
  																										.collect(toList()); //将结果存在另一个List中

1,流 外部迭代与内部迭代

集合:用for-each循环外部迭代:

List<String> names = new ArrayList<>(); 
for(Dish d: menu){ 
	names.add(d.getName()); 
}

集合:用背后的迭代器做外部迭代

List<String> names = new ArrayList<>(); 
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) { 
	Dish d = iterator.next(); 
	names.add(d.getName());
 } 

流:内部迭代

List<String> names = menu.stream() 
			.map(Dish::getName) 
			.collect(toList()); 

image-20201229135303344

image-20201229135337711

image-20201229135351440

由于流是内部操作,所以要是想看流的中间流程,可以通过以下方式:

image-20201229135623604

2,使用流

流的使用一般包括三件事:

  1. 一个数据源(如集合)来执行一个查询;
  2. 一个中间操作链,形成一条流的流水线;
  3. 一个终端操作,执行流水线,并能生成结果

image-20201229140314797

  • 筛选:filter(),distinct()
  • 切片:limit(n)
  • 跳过:skip()
  • 映射:map(), flatmap()
  • 查找与匹配:allMatch、anyMatch、noneMatch、findFirst、findAny

映射

流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一 个新版本”而不是去“修改”)。

提取流中菜肴的名称:

List<String> dishNames = menu.stream().map(Dish::getName) 
				        											.collect(toList()); 

因为getName方法返回一个String,所以map方法输出的流的类型就是Stream。

给定一个单词列表,返回另 一个列表,显示每个单词中有几个字母:

List<String> words = Arrays.asList("Java 8", "Lambdas", "In", "Action");
List<Integer> wordLengths = words.stream().map(String::length)
                                          .collect(toList()); 

流的扁平化

对于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?

image-20201229140844390

以上做法没有实现我们想要的效果。可以使用flatmap()

image-20201229141004977

给定一个数字列表,如何返回一个由每个数的平方构成的列表呢?例如,给定[1, 2, 3, 4, 5],应该返回[1, 4, 9, 16, 25]。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream().map(n -> n * n) 
                                        .collect(toList()); 

给定两个数字列表,如何返回所有的数对呢?例如给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]

List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs = numbers1.stream().flatMap(
  																		i -> numbers2.stream().map(
                                        			j -> new int[]{i, j}
                                      ))
                                     .collect(toList());
pairs.forEach(i->System.out.print(Arrays.toString(i)));

规约

对数字列表中的元素求和。以前的做法:

int sum=0;
for (int x : nums){
      sum+=x;	
}

利用reduce:

int sum = numbers.stream().reduce(0, (a, b) -> a + b); 

reduce接受两个参数:

•一个初始值,这里是0;

•一个BinaryOperator来将两个元素结合起来产生一个新值,这里我们用的是lambda (a, b) -> a + b

image-20201229142122457

归约方法的优势与并行化:

相比于前面写的逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的 性能提升!

3,构建流

1,由值创建流

Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");

2,由数组创建流

int[] numbers3 = {2, 3, 5, 7, 11, 13};
int sum1 = Arrays.stream(numbers3).sum();

3,由文件生成流

long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
            uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                               .distinct()
                               .count();
            System.out.print("文件中不同的词有{}个"+uniqueWords);
        }catch(IOException e){
            System.out.print("exception e:"+e);
        }

4,由函数生成流

//创建斐波那契数列(0, 1)(1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21)
System.out.println("创建斐波那契数列-----");
Stream.iterate(new int[]{0, 1},
                t -> new int[]{t[1], t[0]+t[1]})
                .limit(20)
                .forEach(t -> System.out.print("(" + t[0] + "," + t[1] +")"));

Optional取代null

在Java程序开发中使用null会带来理论和实际 操作上的种种问题。

  • 它是错误之源。 NullPointerException是目前Java程序开发中最典型的异常。
  • 它会使你的代码膨胀。 它让你的代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶。
  • 它自身是毫无意义的。 null自身没有任何的语义,尤其是,它代表的是在静态类型语言中以一种错误的方式对 3 缺失变量值的建模。
  • 它破坏了Java的哲学。 Java一直试图避免让程序员意识到指针的存在,唯一的例外是:null指针。
  • 它在Java的类型系统上开了个口子。 null并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题, 原因是当这个变量被传递到系统中的另一个部分后,你将无法获知这个null变量最初的 赋值到底是什么类型。

image-20201229143404510

变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空” 的Optional对象,由方法Optional.empty()返回。Optional.empty()方法是一个静态工厂 方法,它返回Optional类的特定单一实例。你可能还有疑惑,null引用和Optional.empty() 有什么本质的区别吗?从语义上,你可以把它们当作一回事儿,但是实际中它们之间的差别非常 大:如果你尝试解引用一个null,一定会触发NullPointerException,不过使用 Optional.empty()就完全没事儿,它是Optional类的一个有效对象,多种场景都能调用,非 常有用。

//声明一个空的Optional
Optional<Car> optCar = Optional.empty();

//依据一个非空值创建Optional,如果car是一个null,这段代码会立即抛出一个NullPointerException,而不是等到你试图访问car的属性值时才返回一个错误。
Car car=new Car();
Optional<Car> optCar1 = Optional.of(car);

//可接受null的Optional
//如果car是null,那么得到的Optional对象就是个空对象。
Optional<Car> optCar2 = Optional.ofNullable(car);
Insurance insurance = null;
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);

Optional类提供了多种方法读取 Optional实例中的变量值。

  • get()是这些方法中最简单但又最不安全的方法。如果变量存在,它直接返回封装的变量 值,否则就抛出一个NoSuchElementException异常。所以,除非你非常确定Optional 变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于 嵌套式的null检查,也并未体现出多大的改进。
  • orElse(T other)它允许你在 Optional对象不包含值时提供一个默认值。
  • orElseGet(Supplier<? extends T> other)是orElse方法的延迟调用版,Supplier 方法只有在Optional对象不含值时才执行调用。
  • orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常类似, 它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希 望抛出的异常类型。
  • ifPresent(Consumer<? super T>)让你能在变量值存在时执行一个作为参数传入的 方法,否则就不进行任何操作。

CompletableFuture

下面这段代码展示了Java 8之前使用 Future的一个例子。

image-20201229145939270

1,Future接口的局限性

很难表述Future结果之间的依赖性.比如无法完成如下操作:将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第 一个的结果。

2,CompletableFuture使用

public Future<String> calculateAsync() throws InterruptedException {
    CompletableFuture<String> completableFuture 
      = new CompletableFuture<>();

    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        completableFuture.complete("Hello");
        return null;
    });

    return completableFuture;
}

 Future<String> completableFuture = calculateAsync();
 String result = completableFuture.get();
 System.out.println("result="+result);

上面的代码我们都使用了并发的机制去执行的(比如使用线程池),但是如果我们想要跳过那些无用的模版方法仅仅是简单地异步执行一些code改如何做呢?

runAsync()和supplyAsync()

静态方法例如:runAsync()和supplyAsync(),都允许我们创建一个CompletableFuture 实例,相应地都是Runnable和Supplier实例。

由于Java8的新特性,Runnable和Suplier都允许通过lambda表达式传递他们的实例。

CompletableFuture<String> future= CompletableFuture.supplyAsync(() -> "Hello");
assertEquals("Hello", future.get());

处理异步计算:

CompletableFuture<String> completableFuture= CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future = completableFuture.thenApply(s -> s + " World");
assertEquals("Hello World", future.get());

合并特性:

下面的例子种我们使用thenCompose()方法来依次合并两个Futures。注意,这个方法返回一个CompletableFuture实例。这个方法的参数是上一个计算步骤的结果。这样就允许我们在下一个CompletableFuture的lambda表达式中使用这个值。

CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> "Hello")
                                              .thenCompose(s ->CompletableFuture.supplyAsync(() 																														 -> s + " World"));
assertEquals("Hello World", completableFuture.get());

thenApply()和thenCompose()的使用方法:

thenApply() 被用来处理前一个调用的结果。一个关键的点就是要记住,所有调用的返回值将会被组合。所以这个方法在我们想要转化一个CompletableFuture结果时是有用的。

CompletableFuture<Integer> finalResult = compute().thenApply(s-> s + 1);

thenCompose()和thenApply()类似都会返回一个新的执行阶段。thenCompose()使用前面的阶段作为参数。它将会打平并且返回一个带有结果的Future,而不是像thenApply()那样嵌套的结果。

CompletableFuture<Integer> computeAnother(Integer i){
    return CompletableFuture.supplyAsync(() -> 10 + i);
}
CompletableFuture<Integer> finalResult = compute().thenCompose(this::computeAnother);

已并行的方式运行多个Futures

当我们需要以并行的方式执行多个Futures时,我们通常想要等待他们所有执行完然后处理他们组合的结果。

CompletableFuture的allOf静态方法允许等待所有Futures的完成。

CompletableFuture<String> future1  
  = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2  
  = CompletableFuture.supplyAsync(() -> "Beautiful");
CompletableFuture<String> future3  
  = CompletableFuture.supplyAsync(() -> "World");

CompletableFuture<Void> combinedFuture 
  = CompletableFuture.allOf(future1, future2, future3);

combinedFuture.get();

assertTrue(future1.isDone());
assertTrue(future2.isDone());
assertTrue(future3.isDone());

Java9新特性

模块系统

Java8版本的目录结构

![image-20201229161210544](/Users/yoyocheknow/Library/Application Support/typora-user-images/image-20201229161210544.png)

Java9版本的目录结构

image-20201229161254458

什么是模块?

我们知道,.class文件是JVM看到的最小可执行文件,而一个大型程序需要编写很多Class,并生成一堆.class文件,很不便于管理,所以,jar文件就是class文件的容器。

在Java 9之前,一个大型Java程序会生成自己的jar文件,同时引用依赖的第三方jar文件,而JVM自带的Java标准库,实际上也是以jar文件形式存放的,这个文件叫rt.jar,一共有60多M。

jar只是用于存放class的容器,它并不关心class之间的依赖。

从Java 9开始引入的模块,主要是为了解决“依赖”这个问题。如果a.jar必须依赖另一个b.jar才能运行,那我们应该给a.jar加点说明啥的,让程序在编译和运行的时候能自动定位到b.jar,这种自带“依赖关系”的class容器就是模块。

为了表明Java模块化的决心,从Java 9开始,原有的Java标准库已经由一个单一巨大的rt.jar分拆成了几十个模块,这些模块以.jmod扩展名标识,可以在$JAVA_HOME/jmods目录下找到它们。如下图所示:

image-20201229161846352

JShell

Java9,引入了shell。向动态化语言又走进了一步。初学者学Java,更加方便了。

image-20201229162042985

JShell 中默认的上下文:

获取当前的执行线程: Thread.currentThread()

获取当前执行的方法名: Thread.currentThread().getStackTrace()[2].getMethodName()

获取当前执行的类名: Thread.currentThread().getStackTrace()[2].getClassName();

在 JShell 中定义一个方法:

在一行内定义:int doubled(int i){ return i*2;}

在多行定义:

int add(int i,int j){
return i+j;
}

改进JavaDocs

一直以来,Java 生成的文档 JavaDoc 一直使用的都是 HTML 4 格式,这次 Java 9 良心大大的发现,使用了 HTML 5 ,但还不是默认的,如果要输出 HTML 5 格式,还必须在命令行程序中添加 -html5 选项。

集合不可变实例工厂方法

Java 9 为集合接口 ( List 、Set 、Map ) 提供了创建 不可变实例 的工厂方法。这些工厂方法为便利而生,以简介简单的方式创建这些集合.创建可变集合很简单,但是创建不可变集合则先需要创建一个可变集合,然后再使用Collections.unmodifiable{Map,Set,List} 创建不可变集合。

1,创建不可变的列表 ( List ):

static List of(E e1, E e2, E e3); 2,创建不可变的集合 ( Set ):

static Set of(E e1, E e2, E e3);

3,创建不可变的哈希表 ( Hash ):

static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3);

static <K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries)

接口的私有方法

在 Java 8 之前,接口可以有常量变量和抽象方法。我们不能在接口中提供方法实现,如果我们要提供抽象方法和非抽象方法(方法与实现)的组合,那么我们就得使用抽象类。

在 Java 8 接口引入了默认方法和静态方法。我们可以在 Java 8 的接口中编写方法实现,仅仅需要使用default 关键字来定义它们。在 Java 8 中,一个接口中能定义如下几种变量/方法:常量、抽象方法、默认方法、静态方法。

Java 9 不仅像 Java 8 一样支持接口默认方法,同时还支持私有方法。在 Java 9 中,一个接口中能定义如下几种变量/方法:常量、抽象方法、默认方法、静态方法、私有方法、私有静态方法

改进进程管理API

Java 9 对进程管理的改进主要是提供了 ProcessHandle 类。ProcessHandle 可以用于获取进程信息,监听和检查进程的状态,并且可以监听进程的退出。

image-20201229165645962

try-with-resource语句

如果你使用过 Python ,应该对 with 语句不陌生,with 语句会创建一个独立的上下文,当执行流程离开该上下文时,就会立刻释放该上下文中的所有资源

with open('hello.txt') as f:
    print(f.read()

try-with-resources 首先是一个 try 语句,其次,该语句包含一个或多个正式声明的资源。这些资源是一个对象,当不再需要时就应该关闭它。

try-with-resources 语句可以确保在需求完成后关闭每个资源,当然了,这些可以自动关闭的资源也是有条件的,那就是必须实现java.lang.AutoCloseable 或 java.io.Closeable 接口

Java10新特性

##局部变量的类型推断 var关键字

var list = new ArrayList<String>(); // ArrayList<String>
var stream = list.stream(); // Stream<String>

并行全垃圾回收器 G1

初衷:

在G1提出之前,经典的垃圾收集器主要有三种类型:串行收集器、并行收集器和并发标记清除收集器,这三种收集器分别可以是满足Java应用三种不同的需求:内存占用及并发开销最小化、应用吞吐量最大化和应用GC暂停时间最小化,但是,上述三种垃圾收集器都有几个共同的问题:

(1)所有针对老年代的操作必须扫描整个老年代空间;

(2)新生代和老年代是独立的连续的内存块,必须先决定年轻代和老年代在虚拟地址空间的位置。

设计目标:

G1是一种服务端应用使用的垃圾收集器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。

serial, parallel, CMS垃圾收集器作用于堆上的三个部分:年轻代,老年代,永久代

image-20201229184604709

G1 垃圾收集器采取了另外一种方式:

image-20201229184641488

每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。在物理上不需要连续,则带来了额外的好处——有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。

###CMS回收器

CMS垃圾收集器运作过程大致划分为以下五个步骤:

1,初始标记 Initial Mark(暂停用户线程):标记GC roots能直接关联到的对象,速度很快,暂停时间很短。

2,并发标记 Concurrent Marking:在Java应用运行期间从永久代对象图遍历可达的对象。不需要暂停用户线程。

3,重新标记 Remark (暂停用户线程):为了修正并发标记期间,因用户程序继续运作而错过的一部分对象。

4,并发清除 concurrent sweep:清理删除标记期间判断为死亡的对象。由于不需要移动存活对象,所以这个阶段也是可以和用户线程并发进行的。

5,重置线程:为下一次并发收集做准备。

image-20201229184849419

CMS收集器的堆结构:

1,堆被分在三个部分。

2,年轻代被分为Eden区 和两个Survivor区。

3,老年代是一块连续的空间。

4,直到一次full GC才会压缩一次空间。

image-20201229185131966

Yong GC 在CMS收集器下是怎么工作的?

1,绿色部分是年轻代。

2,蓝色部分是老年代。

3,灰色部分是没有分配的空间。

4,CMS收集器下老年代对象会被直接释放,不需要移动。直到一次full GC才会压缩一次空间。

image-20201229185301516

年轻代收集过程:

1,年轻代Eden区和Survivor区存活的对象会复制到另一个Survivor区。

2,到达年龄阈值的对象会晋升到老年代

image-20201229185533259

一次Yong GC后:

1,Eden区和其中一块Survivor被释放空间。

2,图中的深蓝色表示晋升到老年代的对象。

3,绿色的部分表示年轻代存活的但是没有晋升的对象。image-20201229185656526

CMS下 老年代的收集:

1,初始标记是一个短暂的过程,这个过程存活的可达对象会被标记。

2,并发标记会找到程序继续执行期间存活的对象。

3,重新标记会找到上个阶段被错过的存活对象。

image-20201229185759136

CMS下并发清除:

1,在前几个阶段没有被标记的对象,将会被释放。但是不会移动压缩空间。

image-20201229190259644

CMS下并发清除后:

1,会看到大量的空间被释放,也没有压缩空间。

image-20201229190332268

G1回收器

G1 回收器的堆空间结构:

1,从右图可以看到只有一块空间,被分为大小不同的逻辑块。

2,JVM产生了大约2000块 1~32Mb的小块。

image-20201229190412446

G1 回收器的堆空间分配:

1,每个分区都可能是年轻代也可能是老年代,但是在同一 时刻只能属于某个代。

2,年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。在物理上不需要连续,则带来了额外的好处——有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。

3,灰色部分称为大对象块,这些块主要被设计用来存放超过标准块50%或者更大的对象,他们会存放在连续的空间。

image-20201229190519809

Yong GC 后,年轻代存活的对象会被复制或者移动到存活块。达到年龄阈值的对象会被晋升为老年代块。这是一个暂停用户线程的过程。年轻代是由不连续的逻辑块组成,这使得年轻代更容易被扩展。

image-20201229190547797

image-20201229190608785

G1 垃圾收集器运作过程大致划分为以下四个步骤:

image-20201229190717672

image-20201229190733848

初始标记:

初始标记是对年轻代存活对象的标记。

image-20201229190805259

并发标记:

如果一个空的块被发现,它们会在重新标记阶段被立即清空掉。

image-20201229190829679

image-20201229190837664

复制清除阶段:

G1会优先收集低生存率的逻辑块。复制清除后,这些块会被收集,移动到如第二张图的深绿色和深蓝色部分。

image-20201229190915263

image-20201229190924252

老年代GC总结:

并发标记阶段:

•当程序运行时存活信息会同时计算。

•一个逻辑块在暂停期间被认为是最合适回收的时候,存活信息会被确定下来。

•没有像CMS阶段的清除阶段。

重新标记阶段:

•使用Snapshot-at-the-Beginning (SATB) 算法,比CMS更快。

•完全空的逻辑块会被回收。

复制/清除阶段:

•Yong GC 和老年代GC会同时被回收

•老年代逻辑块回收是基于存活率的

Java11新特性

基于嵌套的访问控制

标准 HTTP Client 升级

简化启动单个源代码文件的方法

此功能允许使用 Java 解释器直接执行 Java 源代码。源代码在内存中编译,然后由解释器执行。

唯一的约束在于所有相关的类必须定义在同一个 Java 文件中。

对于 Java 初学者并希望尝试简单程序的人特别有用,并且能和 jshell 一起使用

一定能程度上增强了使用 Java 来写脚本程序的能力

// 编译 javac Javastack.java

// 运行 java Javastack

String 类中新的 API

public String strip() 去除前后的空格

public String stripLeading() 去除前面的空格

public String stripTrailing() 去除后面的空格

public boolean isBlank() 判断是否为空,或者只含有空格

public Stream lines() 依据 line terminators (\n \r \r\n) 来进行分割

public String repeat(int count) 将字符串重复n次

用于 Lambda 参数的局部变量语法

低开销的 Heap Profiling

提供一种低开销的Java堆分配采样方法,可通过JVMTI访问。

目标 :提供一种可以从JVM中获取堆分配信息的方法。

1,开销足够低,可持续。

2,可以通过定义好的,参数化的接口获得。

3,可以采样所有分配(不局限于某一个特定堆 region的分配)

4,与GC的算法和VM的实现无关。

5,可以给出存活或者死亡对象的信息。

支持 TLS 1.3 协议

ZGC:可伸缩低延迟垃圾收集器

ZGC:A Scalable Low Latency Garbage Collector 一款可伸缩、低延迟的GC。

Epsilon垃圾收集器:A NoOp Garbage Collectors没有操作的垃圾收集器。

ZGC设计目标:

image-20201229191602277

•GC暂停时间不会超过10ms。

•既能处理几百兆的小堆,也能几T的大堆(OMG)和

•G1相比,应用吞吐能力不会下处理降超过15%

•为未来的GC功能和利用colord指针以及Load barriers 优化奠定基础。

ZGC和其他收集器的对比:

image-20201229191648034

ZGC堆内存布局:

image-20201229191712039

染色指针技术

HotSpot虚拟机的标记实现方案有如下几种:

1,把标记直接记录在对象头上(如Serial收集器);

2,把标记记录在与对象相互独立的数据结构上(如G1、Shenandoah使用了一种相当于堆内存的1/64

大小的,称为BitMap的结构来记录标记信息);

3,直接把标记信息记在引用对象的指针上(如ZGC)

染色指针是一种直接将少量额外的信息存储在指针上的技术。

image-20201229191821836

三色标记

在并发的可达性分析算法中我们使用三色标记(Tri-color Marking)来标记对象是否被收集器访问过:

白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。

黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象

灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

image-20201229191919883

读屏障

当对象从堆中加载的时候,就会使用到读屏障(Load Barrier)。这里使用读屏障的主要作用就是检查指针上的三色标记位,根据标记位判断出对象是否被移动过,如果没有可以直接访问,如果移动过就需要进行“自愈”(对象访问会变慢,但也只会有一次变慢),当“自愈”完成后,后续访问就不会变慢了。

ZGC运作过程:

![image-20201229192037093](/Users/yoyocheknow/Library/Application Support/typora-user-images/image-20201229192037093.png)

并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记和最终标记也会出现短暂的停顿,整个标记阶段只会更新染色指针中的Marked 0、Marked 1标志位。

并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。

并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。

并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。

飞行记录器

Java飞行记录仪(Java Flight Recorder)已经变成Java 11的一部分了,之前它是一个商业功能,但是伴随JEP 328的 Java 11发布,它从OracleJDK开源到了OpenJDK。Java飞行记录器类似飞机的黑盒子,可以将OS系统和JVM中发生的事件记录下来,然后就可以使用Java Mission Control(JMC)进行性能侦测和分析了。启用JFR可以最大限度地降低工具本身对JVM性能的影响,JVM其他性能监测工具对应用运行性能都有影响,因此很少在生产环境一直启用,而JFR则可以在生产环境部署启用。

目标:提供低开销的数据收集框架,用于对Java应用程序和HotSpot JVM进行故障排除。

动态类文件常量

删除 Java EE 和 CORBA 模块

Java12新特性

一个低停顿垃圾收集器

Switch 表达式扩展(预览功能)

引入 JVM 常量 API

改进 AArch64 实现

使用默认类数据共享(CDS)存档

改善 G1 垃圾收集器,使其能够中止混合集合

增强 G1 垃圾收集器,使其能自动返回未用堆内存给操作系统

Java13新特性

动态应用程序类-数据共享

增强 ZGC 释放未使用内存

Socket API 重构

Switch 表达式扩展(预览功能)

文本块(预览功能)

参考书籍:《Java8实战》,JDK官网