【JVM深度解析】JVM是如何现语法糖(泛型、stream)

本文涉及的产品
云原生大数据计算服务 MaxCompute,5000CU*H 100GB 3个月
云原生大数据计算服务MaxCompute,500CU*H 100GB 3个月
云解析 DNS,旗舰版 1个月
简介: 你了解过JVM是如何实现泛型,常用的stream方法你了解多少?不懂?...一文带你了解语法糖的实现

本文思维导图

image.gif编辑

Java  中的泛型(重点)

泛型是什么

泛型,即“参数化类型”,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。引入一个类型变量 T(其他大写字母都可以,不过常用的就是 T,E,K,V 等等),并且用<>括起来,并放在类名的后面。泛型类是允许有多个类型变量的。

按照约定,类型参数名称命名为单个大写字母,以便可以在使用普通类或接口名称时能够容易地区分类型参数。以下是常用的类型参数名称列表:

E - 元素,主要由 Java 集合(Collections)框架使用。

K - 键,主要用于表示映射中的键的参数类型。

V - 值,主要用于表示映射中的值的参数类型。

N - 数字,主要用于表示数字。

T - 类型,主要用于表示第一类通用型参数。

S - 类型,主要用于表示第二类通用类型参数。

U - 类型,主要用于表示第三类通用类型参数。

V - 类型,主要用于表示第四个通用类型参数。

泛型类和泛型接口

可以为任何类、接口增加泛型声明

/**
 * 泛型类
 * 引入一个类型变量T (其他大写字母都可以,不过常用的就是T,E,K,V等等)
 *
 * @author macfmc
 * @date 2020/8/29-18:06
 */
public class NormalGeneric<T> {
    private T data;
    public NormalGeneric() {
    }
    public NormalGeneric(T data) {
        this();
        this.data = data;
    }
}
/**
 * 泛型接口与泛型类的定义基本相同。
 * 泛型接口
 * 引入一个类型变量T (其他大写字母都可以,不过常用的就是T, E, K,V等等)
 */
public interface Generator<T> {
    public T next();
}

image.gif

泛型类和接口的使用

实现泛型接口的类,有两种实现方法:

1、未传入泛型实参时:在 new 出类的实例时,需要指定具体类型

public static void main(String[] args) {
    ImplGenerator<String> implGenerator = new ImplGenerator<>("") ;
    System.out.println(implGenerator.next());
}

image.gif

2、传入泛型实参:在 new 出类的实例时,和普通的类没区别。

public class ImplGenerator2 implements Generator<String> {
    @0verride
    public String next() {
        return "";
    }
}

image.gif

泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。

/**
 * 泛型方法
 * 引入一个类型变量T (其他大写字母都可以,不过常用的就是T,E, K,V等等)
 */
public class GenericMethod {
    // 泛型方法
    public <T> T genericMethod(T t) { return t; }
    //普通方法
    public void test(int x, int y) {
        System.out.println(x + y);
    }
    public static void main(String[] args) {
        GenericMethod genericMethod = new GenericMethod();
        genericMethod.test(13, 7);
        System.out.println(genericMethod.genericMethod("King"));
        System.out.println(genericMethod.genericMethod(180));
    }
}

image.gif

为什么我们需要泛型?

通过两段代码我们就可以知道为何我们需要泛型,实际开发中,经常有数值类型求和的需求,例如实现 int 类型的加法,有时候还需要实现 long 类型的求和,如果还需要 double 类型的求和,需要重新在重载一个输入是 double 类型的 add 方法。

泛型的好处

1、适用于多种数据类型执行相同的代码

2、泛型中的类型在使用时指定,不需要强制类型转换

虚拟机是如何实现泛型的?

泛型擦除

Java 语言中的泛型,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的 Java 语言来说,ArrayList<int>与 ArrayList<String>就是同一个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

将一段 Java 代码编译成 Class 文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了 Java 泛型出现之前的写法,泛型类型都变回了原生类型(因为)

/**
 * 泛型擦除
 */
public class Theory {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("F","18");
        System.out.println(map.get("F")); // System.out.println((String)map.get("F"));
    }
}

image.gif

使用泛型注意事项(了解即可,装 B 专用)

上面这段代码是不能被编译的,因为参数 List<Integer>和 List<String>编译之后都被擦除了,变成了一样的原生类型 List<E>,

擦除动作导致这两种方法的特征签名变得一模一样(注意在 IDEA 中是不行的,但是 jdk 的编译器是可以,因为 jdk 是根据方法返回值+方法名+参数)。

弱记忆

JVM 版本兼容性问题:JDK1.5 以前,为了确保泛型的兼容性,JVM 除了擦除,其实还是保留了泛型信息(Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息)——弱记忆。

另外,从 Signature属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的 Code 属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。

Stream (了解即可)

是 什么是Stream?

Java8 中,Collection 新增了两个流方法,分别是 Stream() 和 parallelStream()。

Java8 中添加了一个新的接口类 Stream,相当于高级版的 Iterator,它可以通过 Lambda 表达式对集合进行 大批量数据操作,或者各种非常便利、高效的 聚合数据操作。

为什么要使用Stream?

在 Java8 之前,我们通常是通过 for 循环或者 Iterator 迭代来重新排序合并数据,又或者通过重新定义 Collections.sorts 的

Comparator 方法来实现,这两种方式对于大数据量系统来说,效率并不是很理想。

Stream 的聚合操作与数据库 SQL 的聚合操作 sorted、filter、map 等类似。我们在应用层就可以高效地实现类似数据库 SQL 的

聚合操作了,而在数据操作方面,Stream 不仅可以通过串行的方式实现数据操作,还可以通过并行的方式处理大批量数据, 提高数据的处理效率。

Stream  操作分类

官方将 Stream 中的操作分为两大类:终结操作(Terminal operations)和中间操作(Intermediate operations)

image.gif编辑

中间操作会返回一个新的流,一个流可以后面跟随 零个或多个 中间操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后会返回一个新的流,交给下一个操作使用。 这类操作都是惰性化的 (lazy ),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。而是在终结操作开始的时候才真正开始执行。

中间操作又可以分为 无状态(Stateless)与 有状态(Stateful)操作,无状态是指元素的处理不受之前元素的影响,有状态是指该操作只有拿到所有元素之后才能继续下去。

终结操作是指返回最终的结果。一个流只能有一个终结操作,当这个操作执行后,这个流就被使用“光”了,无法再被操作。所以

这必定这个流的最后一个操作。 终结操作的执行才会真正开始流的遍历,并且会生成一个结果。

终结操作又可以分为 短路(Short-circuiting)与 非短路(Unshort-circuiting)操作,

短路是指遇到某些符合条件的元素就可以得到最终结果,

非短路是指必须处理完所有元素才能得到最终结果。

因为 Stream 操作类型非常多,总结一下常用的

map():将流中的元素进行再次加工形成一个新流,流中的每一个元素映射为另外的元素。

filter():返回结果生成新的流中只包含满足筛选条件的数据

limit():返回指定数量的元素的流。返回的是 Stream 里前面的 n 个元素。

skip():和 limit()相反,将前几个元素跳过(取出)再返回一个流,如果流中的元素小于或者等于 n,就会返回一个空的流。

sorted():将流中的元素按照自然排序方式进行排序。

distinct():将流中的元素去重之后输出。

peek():对流中每个元素执行操作,并返回一个新的流,返回的流还是包含原来流中的元素。

Stream 的底层实现

Stream操作叠加

一个 Stream 的各个操作是由处理管道组装,并统一完成数据处理的。

我们知道 Stream 有中间操作和终结操作,那么对于一个写好的 Stream 处理代码来说,中间操作是通过 AbstractPipeline 生成了一个中间操作 Sink 链表

当我们调用终结操作时,会生成一个最终的 ReducingSink,通过这个 ReducingSink 触发之前的中间操作,从最后一个 ReducingSink 开始,递归产生一个 Sink链。如下图所示:

image.gif编辑

Stream  源码实现(不重要)

stream 方法

因为 names 是 ArrayList 集合,所以 names.stream() 方法将会调用集合类基础接口 Collection 的 Stream 方法。

filter 方法

再调用 filter,这个方法都是无状态的中间操作,所以执行 filter 时,并没有进行任何的操作,而是分别创建了一个 Stage 来标识用户的每一次操作。

而通常情况下 Stream 的操作又需要一个回调函数,所以一个完整的 Stage 是由数据来源、操作、回调函数组成的三元组来表示。如下图所示,分别是 ReferencePipeline 的 filter 方法和 map 方法:

max 方法

当 Sink 链表生成完成后,Stream 开始执行,通过 spliterator 迭代集合,执行 Sink 链表中的具体操作。

java8 中的 Spliterator 的 forEachRemaining 会迭代集合,每迭代一次,都会执行一次 filter 操作,如果 filter 操作通过,就会触发 map操作,然后将结果放入到临时数组 object 中,再进行下一次的迭代。完成中间操作后,就会触发终结操作 max。

Stream  并发 Stream  源码实现(不重要)

并发的处理函数对比 parallelStream()方法,这里的并行处理指的是,Stream 了 结合了 ForkJoin  框架,对 Stream 处理进行了分片,Splititerator 中的 estimateSize 方法会估算出分片的数据量。

通过预估的数据量获取最小处理单元的阈值,如果当前分片大小大于最小处理单元的阈值,就继续切分集合。每个分片将会生成一个Sink 链表,当所有的分片操作完成后,ForkJoin 框架将会合并分片任何结果集。

Stream  的性能

100  的性能对比 image.gif编辑

常规的迭代 > Stream  并行迭代> Stream  串行迭代

为什么这样:

1、常规迭代代码简单,越简单的代码执行效率越高。

2、Stream 串行迭代,使用了复杂的设计,导致执行速度偏低。所以是性能最低的。

3、Stream 并行迭代 使用了 Fork-Join 线程池,所以效率比 Stream 串行迭代快,但是对比常规迭代还是要慢(毕竟设计和代码复杂)

大数据迭代

一亿的数组性能对比(默认线程池)image.gif编辑

为什么这样:

1 、Stream 并行迭代 使用了 Fork-Join 线程池, 而线程池线程数为 cpu 的核心数(我的电脑为 12 核),大数据场景下,能够利用多线程机制,所以效率比 Stream 串行迭代快,同时多线程机制切换带来的开销相对来说还不算多,所以对比常规迭代还是要快(虽然设计和代码复杂)

2、常规迭代代码简单,越简单的代码执行效率越高。

3、Stream 串行迭代,使用了复杂的设计,导致执行速度偏低。所以是性能最低的。

一亿的数组性能对比(线程池数量=2 )image.gif编辑

Stream 并行迭代 使用了 Fork-Join 线程池,大数据场景下,虽然利用多线程机制,但是线程池线程数为 2,所以多个请求争抢着执行任务,想象对请求来说任务是被交替执行完成,所以对比常规迭代还是要慢(虽然用到了多线程技术)

一亿的数组性能对比(线程池数量=240)image.gif编辑

Stream 并行迭代 使用了 Fork-Join 线程池, 而线程池线程数为 240,大数据场景下,虽然利用多线程机制,但是线程太多,线程的上下文切换成本过高,所以导致了执行效率反而没有常规迭代快。

如何合理使用 Stream?

我们可以看到:在循环迭代次数较少的情况下,常规的迭代方式性能反而更好;而在大数据循环迭代中, parallelStream(合理的线程池数上)有一定的优势。

但是由于所有使用并行流 parallelStream 的地方都是使用同一个 Fork-Join 线程池,而线程池线程数仅为 cpu 的核心数。切记,如果对底层不太熟悉的话请不要乱用并行流 parallerStream(尤其是你的服务器核心数比较少的情况下)

相关文章
|
1月前
|
存储 安全 Java
JVM锁的膨胀过程与锁内存变化解析
在Java虚拟机(JVM)中,锁机制是确保多线程环境下数据一致性和线程安全的重要手段。随着线程对共享资源的竞争程度不同,JVM中的锁会经历从低级到高级的膨胀过程,以适应不同的并发场景。本文将深入探讨JVM锁的膨胀过程,以及锁在内存中的变化。
42 1
|
2月前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
117 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
3月前
|
Java API
Java 8新特性:Lambda表达式与Stream API的深度解析
【7月更文挑战第61天】本文将深入探讨Java 8中的两个重要特性:Lambda表达式和Stream API。我们将首先介绍Lambda表达式的基本概念和语法,然后详细解析Stream API的使用和优势。最后,我们将通过实例代码演示如何结合使用Lambda表达式和Stream API,以提高Java编程的效率和可读性。
|
3月前
|
C# 开发者 Windows
震撼发布:全面解析WPF中的打印功能——从基础设置到高级定制,带你一步步实现直接打印文档的完整流程,让你的WPF应用程序瞬间升级,掌握这一技能,轻松应对各种打印需求,彻底告别打印难题!
【8月更文挑战第31天】打印功能在许多WPF应用中不可或缺,尤其在需要生成纸质文档时。WPF提供了强大的打印支持,通过`PrintDialog`等类简化了打印集成。本文将详细介绍如何在WPF应用中实现直接打印文档的功能,并通过具体示例代码展示其实现过程。
322 0
|
3月前
|
运维 监控 Java
【JVM 调优秘籍】实战指南:JVM 调优参数全解析,让 Java 应用程序性能飙升!
【8月更文挑战第24天】本文通过一个大型在线零售平台的例子,深入探讨了Java虚拟机(JVM)性能调优的关键技术。面对应用响应延迟的问题,文章详细介绍了几种常用的JVM参数调整策略,包括堆内存大小、年轻代配置、垃圾回收器的选择及日志记录等。通过具体实践(如设置`-Xms`, `-Xmx`, `-XX:NewRatio`, `-XX:+UseParallelGC`等),成功降低了高峰期的响应时间,提高了系统的整体性能与稳定性。案例展示了合理配置JVM参数的重要性及其对解决实际问题的有效性。
102 0
|
3月前
|
存储 Java 测试技术
解析 -XX:+UseCompressedOops JVM 选项
【8月更文挑战第21天】
53 0
|
3月前
|
存储 Java 索引
32 位和 64 位 JVM 中 int 变量的大小解析
【8月更文挑战第21天】
213 0
|
3月前
|
存储 Java 索引
64 位 JVM 中 int 的大小解析
【8月更文挑战第21天】
55 0
|
11天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
39 2
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
70 0
下一篇
无影云桌面