hello云胜

技术与生活

0%

一次堆外内存溢出故障

今天早上刚泡好茶,还没喝,业务的电话就打过来了。说给大领导的一个上传文件的服务不好用了,领导震怒,责令我们立即修复。

不等我开始排查,直属领导的电话也响起来了。很快领导自觉的站到我的身后,成为正宗的程序员鼓励师。

我们的这个文件服务也是坑,用户调用的接口和我们隔了两层。用户首先调用A网关提供的服务,A网关再转到B网关。B网关之后才到我们rest服务。这个rest接口实际又调了dubbo服务。

话不多说,立马打开postman测试一下。

A网关,哎,好使啊。

客户那边说了,小文件好使,你传个1M的就不行了。

好吧,换个大文件

A网关:失败。

B网关:哎,好使。

好了,我的排查结束了,因为网关不是我负责的。但是作为一个有追求的程序员,怎么会不帮忙看一下网关的问题呢。

找到日志,

image-20220111111625875

很明确的netty报错,OutOfDirectMemoryError

还是领导高明,立即要求重启。问题得到修复。

现在来看,这个A网关产品是有bug的。因为第一现场已经没了,下面的排查命令只能是练手了,打印的信息已经不是出故障时的状态。

看一下这个进程的jvm配置

1
2
[root@APIGATEWAY01 bin]# ./jps -v
123725 core-1.0.0.RELEASE.jar -Xms8192m -Xmx16384m -XX:MaxDirectMemorySize=61440m

限制了最大堆外内存60G。(不要震惊,我们这台服务器内存是256G的,牛逼plus)

image-20220111112002232

32C,256G就问你们怕不怕。题外话,继续。

1
2
3
# ps -p 123725 -o rss,vsz
RSS VSZ
2130228 36014336

rss是进程使用的内存,2G

vxz是进程的虚拟内存大小,36G

MAT

image-20220111143916572

没有一个明显有问题的。

NMT

要监控jvm的堆外内存,首先要打开直接内存追踪参数。

在启动参数上加上-XX:NativeMemoryTracking=detail,之后重启。

然后用

1
jcmd pid VM.native_memory

性能优化

http压测工具

ab 工具(通过 yum -y install httpd-tools 可以快速安装),对 http 请求接口进行测试

可以通过设置 -n 请求数 /-c 并发用户数来模拟线上的峰值请求,再通过 TPS、RT(每秒 响应时间)以及每秒请求时间分布情况这三个指标来衡量接口的性能

jstack检测cpu高

步骤一:查看cpu占用高进程

执行top命令后,按shift+p 按cpu使用量排序

1
2
3
4
5
6
7
8
9
10
11
12
top

Mem: 16333644k total, 9472968k used, 6860676k free, 165616k buffers
Swap: 0k total, 0k used, 0k free, 6665292k cached

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
17850 root 20 0 7588m 112m 11m S 100.7 0.7 47:53.80 java
1552 root 20 0 121m 13m 8524 S 0.7 0.1 14:37.75 AliYunDun
3581 root 20 0 9750m 2.0g 13m S 0.7 12.9 298:30.20 java
1 root 20 0 19360 1612 1308 S 0.0 0.0 0:00.81 init
2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
3 root RT 0 0 0 0 S 0.0 0.0 0:00.14 migration/0

步骤二:查看cpu占用高线程

通过第一步得到了进程号

通过top -Hp 进程号,查看具体的线程情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
top -H -p 17850

top - 17:43:15 up 5 days, 7:31, 1 user, load average: 0.99, 0.97, 0.91
Tasks: 32 total, 1 running, 31 sleeping, 0 stopped, 0 zombie
Cpu(s): 3.7%us, 8.9%sy, 0.0%ni, 87.4%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 16333644k total, 9592504k used, 6741140k free, 165700k buffers
Swap: 0k total, 0k used, 0k free, 6781620k cached

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
17880 root 20 0 7588m 112m 11m R 99.9 0.7 50:47.43 java
17856 root 20 0 7588m 112m 11m S 0.3 0.7 0:02.08 java
17850 root 20 0 7588m 112m 11m S 0.0 0.7 0:00.00 java
17851 root 20 0 7588m 112m 11m S 0.0 0.7 0:00.23 java
17852 root 20 0 7588m 112m 11m S 0.0 0.7 0:02.09 java
17853 root 20 0 7588m 112m 11m S 0.0 0.7 0:02.12 java
17854 root 20 0 7588m 112m 11m S 0.0 0.7 0:02.07 java

步骤三:转换线程ID

通过第二步得到了线程17880有问题,转换成16进制,以便后续使用。

1
2
printf "%x\n" 17880          
45d8

步骤四:定位cpu占用线程

在jstack打印的堆栈信息中查询线程相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
jstack 17850|grep 45d8 -A 30
"pool-1-thread-11" #20 prio=5 os_prio=0 tid=0x00007fc860352800 nid=0x45d8 runnable [0x00007fc8417d2000]
java.lang.Thread.State: RUNNABLE
at java.io.FileOutputStream.writeBytes(Native Method)
at java.io.FileOutputStream.write(FileOutputStream.java:326)
at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
- locked <0x00000006c6c2e708> (a java.io.BufferedOutputStream)
at java.io.PrintStream.write(PrintStream.java:482)
- locked <0x00000006c6c10178> (a java.io.PrintStream)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
- locked <0x00000006c6c26620> (a java.io.OutputStreamWriter)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
at java.io.PrintStream.write(PrintStream.java:527)
- eliminated <0x00000006c6c10178> (a java.io.PrintStream)
at java.io.PrintStream.print(PrintStream.java:597)
at java.io.PrintStream.println(PrintStream.java:736)
- locked <0x00000006c6c10178> (a java.io.PrintStream)
at com.demo.guava.HardTask.call(HardTask.java:18)
at com.demo.guava.HardTask.call(HardTask.java:9)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)

"pool-1-thread-10" #19 prio=5 os_prio=0 tid=0x00007fc860345000 nid=0x45d7 waiting on condition [0x00007fc8418d3000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000006c6c14178> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)

微服务架构浅显分享


概念


微服务

Martin Fowler

特别擅长抽象归纳和制造概念

image-20210908144425231


Microservices (martinfowler.com)

微服务是由单一应用程序构成的小服务,拥有自己的进程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用HTTP API通讯。同时,服务会使用最小规模的集中管理 (例如Docker)技术,服务可以用不同的编程语言与数据库等。


单体应用

如果你不知道单体应用的痛,那也不会深刻理解微服务的价值。

早期的应用技术栈大致可分为LAMP(Linux + Apache + MySQL + PHP)和MVC(Spring + iBatis/Hibernate + Tomcat)两大流派。

无论是LAMP还是MVC,都是为单体应用架构设计的。


然而随着业务规模的不断扩大,代码和开发人员的增加,很快出现了一下几个问题:

  1. 部署效率低下
  2. 代码管理,代码冲突
  3. 系统高可用性差
  4. 线上发布慢

为了解决这些问题,服务化几乎就是自然而然产生的。


SOA

面向服务的体系结构 SOA (Service-Oriented Architecture) 听起来和微服务很像,但 SOA 早期均使用了总线模式,这种总线模式是与某种技术栈强绑定的,比如:J2EE。这导致很多企业的遗留系统很难对接,切换时间太长,成本太高,新系统稳定性的收敛也需要一些时间,最终 SOA 看起来很美,但却成为了企业级奢侈品,中小公司都望而生畏。


RPC

Remote Procedure Call,远程过程调用。

解决微服务的跨节点调用问题。

和restful调用的区别:

  1. 协议不同
  2. 调用方式不同。rpc的实现上经常使用本地代理,就像调用一个本地方法。

其实这两者并不是一个维度的概念,总得来说RPC涉及的维度更广。RPC可以基于HTTP协议实现,也可以直接在TCP协议上实现。


微服务框架


img


微服务架构下,服务调用主要依赖以下基本组件:

  • 服务描述

    常用的服务描述方式包括RESTful API、XML配置以及IDL文件三种。

    restful的如springcloud,xml的如dubbo的xml文件,IDL文件方式通常用作Thrift和gRPC这类跨语言服务调用框架

  • 注册中心


  • 服务框架
  • 服务监控
  • 服务追踪
  • 服务治理

Dubbo

阿里,最早只支持java。目前多语言支持。

默认通信协议是Dubbo Protocol,也支持其他的。


//imgs/architecture.png


Tars

腾讯,最早只支持C++,目前支持C++,Java,PHP,Nodejs,Go语言。

通信协议是Tars protocol


架构拓扑图


Motan

新浪微博,java


gRPC

Google,跨语言。

gRPC 本身没有提供注册中心,但为开发者提供了实现注册中心的接口


img


thrift

Facebook,跨语言。


总结:

rpc框架并不神秘,简单来看就是解决如何调用远程服务的问题。所以通过注册中心进行服务的注册和发现。解决服务的负载均衡问题和状态监控。以及对象的序列化传输。但是要做一个高性能的,功能完善的rpc框架就难了。


RPC和微服务的关系

如上所述,远程过程调用。解决微服务的跨节点调用问题。

微服务框架一般都包含了RPC的实现和一系列「服务治理」能力,是一套软件开发框架。我们可以基于这个框架之上实现自己的微服务,方便的利用微服务框架提供的「服务治理」能力和RPC能力,所以微服务框架也被有些人称作RPC框架。


SpringCloud微服务框架

Spring Cloud全家桶,而Dubbo基本上只提供了最基础的RPC框架的功能,其他微服务组件都需要自己去实现。


springcloud_arc


img


注册中心

nacos

zookeeper,etcd,Eureka,Consul


负载均衡

客户端服务均衡

Ribbon –> Feign


全链路监控

skywalking,zipkin,pinpoint,jaeger, springcloud sleuth

Google Dapper:Dapper,大规模分布式系统的跟踪系统 by bigbully

image-20210924132406594


监控

img


分布式事务

阿里开源的 seata


配置中心

Apollo

nacos

springcloud-config


服务网关

springcloud-gateway


服务限流、熔断、降级

推荐使用阿里开源的Sentinel


限流

应对突增流量,对超出自身处理能力的用户请求则予以拒绝,牺牲一部分用户体验,保证系统的稳定运行。


降级

降级就是通过停止系统中的某些非核心的功能,来保证系统高优先级服务的可用性。


熔断

服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。

熔段解决如下几个问题:

  • 当所依赖的对象不稳定时,能够起到快速失败的目的;
  • 快速失败后,能够根据一定的算法动态试探所依赖对象是否恢复

img


断路器。Hystrix

熔断就是把客户端的每一次服务调用用断路器封装起来,通过断路器来监控每一次服务调用。如果某一段时间内,服务调用失败的次数达到一定阈值,那么断路器就会被触发,后续的服务调用就直接返回,也就不会再向服务提供者发起请求了。


  • Closed状态:正常情况下,断路器是处于关闭状态的,偶发的调用失败也不影响。
  • Open状态:当服务调用失败次数达到一定阈值时,断路器就会处于开启状态,后续的服务调用就直接返回,不会向服务提供者发起请求。
  • Half Open状态:当断路器开启后,每隔一段时间,会进入半打开状态,这时候会向服务提供者发起探测调用,以确定服务提供者是否恢复正常。如果调用成功了,断路器就关闭;如果没有成功,断路器就继续保持开启状态,并等待下一个周期重新进入半打开状态。

滑动窗口算法

img


Hystrix通过滑动窗口来对数据进行统计,默认情况下,滑动窗口包含10个桶,每个桶时间宽度为1秒,每个桶内记录了这1秒内所有服务调用中成功的、失败的、超时的以及被线程拒绝的次数。当新的1秒到来时,滑动窗口就会往前滑动,丢弃掉最旧的1个桶,把最新1个桶包含进来。

任意时刻,Hystrix都会取滑动窗口内所有服务调用的失败率作为断路器开关状态的判断依据,这10个桶内记录的所有失败的、超时的、被线程拒绝的调用次数之和除以总的调用次数就是滑动窗口内所有服务的调用的失败率。


微服务的容器化

我们现在在做的事


下一代微服务架构

image-20210917223757390

传统的微服务框架的问题

img


服务消费者这边除了自身的业务逻辑实现外,还需要集成部分服务框架的逻辑,比如服务发现、负载均衡、熔断降级、封装调用等,而服务提供者这边除了实现服务的业务逻辑外,也要集成部分服务框架的逻辑,比如限流降级、服务注册等。


有人把kubernetes,service mesh和serveLess称为云原生架构的三驾马车

Service Mesh–服务网格


A service mesh is a dedicated infrastructure layer for handling service-to-service communication. It’s responsible for the reliable delivery of requests through the complex topology of services that comprise a modern, cloud native application. In practice, the service mesh is typically implemented as an array of lightweight network proxies that are deployed alongside application code, without the application needing to be aware.


和传统的微服务架构的本质区别

  1. 无代码侵入。Service Mesh以轻量级的网络代理的方式与应用的代码部署在一起,应用代码对此无感知。
  2. 完全的语言无关

第一代Service Mesh产品Linkerd

计算机领域有这么一句话:没有什么问题不是抽象出一层解决不了的。如果有,那就再抽象一层。


第一代Service Mesh产品Linkerd

2016年底概念提出,2017年重量级产品出现

img


img


SideCar & Control Plane

Service Mesh实现的关键就在于两点:一个是上面提到的轻量级的网络代理也叫SideCar,它的作用就是转发服务之间的调用;一个是基于SideCar的服务治理也被叫作Control Plane,它的作用是向SideCar发送各种指令,以完成各种服务治理功能。


sidecar

img

服务框架的功能都集中实现在SideCar里,服务消费者和服务提供者只管自己的业务实现。


control plane

img


Service Mesh的代表产品Istio

整体架构

img


Proxy,就是前面提到的SideCar。Istio的Proxy采用的是Envoy。Envoy是Istio中最基础的组件,所有其他组件的功能都是通过调用Envoy提供的API,在请求经过Envoy转发时,由Envoy执行相关的控制逻辑来实现的。

Pilot的作用是实现流量控制,它通过向Envoy下发各种指令来实现流量控制,服务发现和负载均衡。

Mixer的作用是实现策略控制和监控日志收集等功能。Istio支持两类的策略控制,一类是对服务的调用进行速率限制,一类是对服务的调用进行访问控制

Citadel的作用是保证服务之间访问的安全

异常体系

img

List、List、List<?> 的三者的区别以及 <? extends T>与<? super T> 的区别

终于搞明白了<? extends T>与<? super T>的区别,网上很多文章说的都是错误的,下面讲下我的理解。

List、List、List<?>

List :完全没有类型限制和赋值限定。
List :看似用法与List一样,但是在接受其他泛型赋值时会出现编译错误。
List:是一个泛型,在没有赋值前,表示可以接受任何类型的集合赋值,但赋值之后不能往里面随便添加元素,但可以remove和clear,并非immutable(不可变)集合。List一般作为参数来接收外部集合,或者返回一个具体元素类型的集合,也称为通配符集合。
代码验证:

image-20200402212520699

image-20200402212551190

<? extends T>与<? super T>

List 最大的问题是只能放置一种类型,为了放置多种受泛型约束的类型,出现了 <? extends T>与<? super T> 两种语法。简单来说, <? extends T> 是Get First,适用于取出集合元素消费的场景;<? super T>是Put First,适用于生产集合元素为主的场景。

: 里面放的是T或T的子类元素,注意理解,是给集合设置了一个界限,不能往里放T以上的类型,但是可以安全的插入T或T子类。因为有上界,可以安全的插入上界类型的子类。但是读取出来时,因为无法确定具体是什么子类型,所以只能统一转为上届类型,泛型丢失。 :里面放的也是T或T的子类。那可能你会疑惑,这又什么区别呢?, 一般是用作方法的入参,用来限制传进来的参数必须只能是某个T类或其子类,这样我的方法可以统一向上强制转型为T进行处理。任何元素都不得添加进集合内。记住不能add元素进去。因为我们并不知道传进来的是什么具体的子类。 代码示例如下,以加菲猫、猫、动物为例,说明extends和super的详细语法差异 ![image-20200402212621339](D:\文章\java\基础\image-20200402212621339.png) ![image-20200402212643981](D:\文章\java\基础\image-20200402212643981.png) ![image-20200402212659097](D:\文章\java\基础\image-20200402212659097.png) 总之,的场景是put功能受限,可以读取元素,一般用来接收参数。 而是可以插入元素。 源码放上:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static void main(String[] args) {

//第一段:泛型出现之前集合定义方式
List a1 =new ArrayList();
a1.add(new Object());
a1.add(new Integer(10));
a1.add(new String("string"));

//第二段:把a1引用赋值给a2,(a2与a1的区别是增加了泛型限制)
List<Object> a2 =a1;
a2.add(new Object());
a2.add(new Integer(20));
a2.add(new String("string2"));
a2.add(25);
//List<Object> 接受其他泛型赋值时,会报异常(因为在下面例子中List<Integer>不能转为List<Object>)
List<Integer> aint = new ArrayList<Integer>();
List<Object> a22 =aint;//Type mismatch: cannot convert from List<Integer> to List<Object>

//第三段:把a1引用赋值给a3,(a3与a1的区别是增加了泛型<Integer>)
List<Integer> a3 = a1; //此时如果遍历a3则会报类型转换异常ClassCastException
a3.add(new Integer(20));
//下面两行编译出错,不允许增加非Integer类型进入集合
a3.add(new Object());//The method add(Integer) in the type List<Integer> is not applicable for the arguments (Object)
a3.add(new String("string2"));

//第四段:把a1引用赋值给a4,a4与a1的区别是增加了通配符
List<?> a4 = a1;
//允许删除和清除元素
a4.remove(0);
a4.clear();
//编译错误,不允许添加任何元素
a4.add(new Object());
a4.add(new Integer(20));
a4.add(new String("string2"));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Animal{}
class Cat extends Animal{}
class Garfield extends Cat{}

//用动物,猫,加菲猫的继承关系说明extends与super在集合中的意义
public class AnimalCatGarfield {
public static void main(String[] args) {
//第一段:声明第三个依次继承的集合:Object>动物>猫>加菲猫 三个泛型集合可以理解为三个不同的笼子
List<Animal> animal = new ArrayList<Animal>(); //动物
List<Cat> cat = new ArrayList<Cat>(); //猫
List<Garfield> garfield = new ArrayList<Garfield>(); //加菲猫

animal.add(new Animal());
cat.add(new Cat());
garfield.add(new Garfield());

//第二段:测试赋值操作 以Cat为核心,因为它既有子类又有父类
//下行编译出错。只能赋值Cat或Cat子类集合
List<? extends Cat> extendsCatFromAnimal = animal;
List<? super Cat> superCatFromAnimal = animal;

List<? extends Cat> extendsCatFromCat = cat;
List<? super Cat> superCatFromCat = cat;

List<? extends Cat> extendsCatFromGarfield = garfield;
//下行编译出错。只能赋值Cat或着Cat父类集合
List<? super Cat> superCatFromGarfield = garfield;

//第三段:测试add方法
//下面三行中所有的<? extends T>都无法进行add操作,编译出错
extendsCatFromCat.add(new Animal());
extendsCatFromCat.add(new Cat());
extendsCatFromCat.add(new Garfield());

//下行编译出错。只能添加Cat或者Cat的子类集合。
superCatFromCat.add(new Animal());
superCatFromCat.add(new Cat());
superCatFromCat.add(new Garfield());

//第四段:测试get方法
//所有的super操作能够返回元素,我们可以安全的确定,superCatFromCat里的元素一定是CAT或其子类
// 所以可以进行一定的强制类型转换
//可以强转为Animal 安全
Animal object = (Animal) superCatFromCat.get(0);
//可以强转为CAT 安全
Cat object3 = (Cat) superCatFromCat.get(0);
//也能强转为加菲猫,但是不安全
Garfield object4 = (Garfield) superCatFromCat.get(0);
//以下extends操作能够返回元素
Animal catExtends3 = extendsCatFromCat.get(0);
Object catExtends2 = extendsCatFromCat.get(0);
Cat catExtends1 = extendsCatFromCat.get(0);
//下行编译错误。虽然Cat集合从Garfield赋值而来,但类型擦除后,是不知道的
// 可以进行强转,但是不安全。
Garfield cat2 = (Garfield) extendsCatFromGarfield.get(0);
}
}

Stream全解析

Lambda表达式

先从基础的lamda表达式开始讲起

java8新增的语言级特性,和javascript等函数式编程语言不同。在java中,lambda表达式依然是一个对象。它必须依附于一种特殊的对象类型functional interface。(称为方法引用或者函数式接口)

语法

1
2
3
(arg1, arg2...) -> { body }

(type1 arg1, type2 arg2...) -> { body }
  1. 简单的说,可以看成是没有访问修饰符、返回值声明和名字的方法

  2. 参数类型可以省略,自动推断

  3. 只有一个参数时,()可以省略

  4. 匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空

  5. 函数体,只有一条语句时,可省略{}

  6. 每个 Lambda 表达式都能隐式地赋值给函数式接口

    比如 Runnable就是一个函数式接口(用@FunctionalInterface注解修饰)

    Runnable r = () -> System.out.println(“hello world”);

  7. 当不指明函数式接口时,编译器会自动解释这种转化

    new Thread(

    () -> System.out.println(“hello world”)

    ).start();

双冒号(::)操作符

另一种将常规方法转化为 Lambda 表达式的方法

与匿名类的区别

一大区别在于关键词的使用。

对于匿名类,关键词 this 解读为匿名类,而对于 Lambda 表达式,关键词 this 解读为使用 Lambda 的外部类。

另一不同在于两者的编译方法。

Java 编译器编译 Lambda 表达式并将他们转化为类里面的私有函数,它使用 Java 7 中新加的 invokedynamic 指令动态绑定该方法

方法引用或函数接口

上面提到了一个注解FunctionalInterface,可翻译为方法引用或函数接口.

java8新增,用于指明该接口类型声明是根据 Java 语言规范定义的函数式接口。函数式接口只能有一个抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//定义一个函数式接口
@FunctionalInterface
public interface WorkerInterface {

public void doSomeWork();

}


public class WorkerInterfaceTest {

public static void execute(WorkerInterface worker) {
worker.doSomeWork();
}

public static void main(String [] args) {

//使用匿名类的写法
execute(new WorkerInterface() {
@Override
public void doSomeWork() {
System.out.println("Worker invoked using Anonymous class");
}
});

//使用lambda表达式
execute( () -> System.out.println("Worker invoked using Lambda expression") );
}

}

常用API

Collection接口的

Collection接口提供了 stream()方法

我们执行的任何操作都不会对源集合造成影响,你可以同时在一个集合上提取出多个 stream 进行操作。

静态方法

of

构造一个Stream对象

1
Stream<String> s1 = Stream.of("a", "b");
empty

创建一个空的 Stream 对象。

contact

连接两个 Stream ,不改变其中任何一个 Steam 对象,返回一个新的 Stream 对象。

1
2
3
Stream<String> s1 = Stream.of("a", "b");
Stream<String> s2 = Stream.of("c", "d");
Stream<String> s3 = Stream.concat(s1, s2);
generate

创建一个无限Stream,一般用于随机数生成

1
Stream<Double> s5 = Stream.generate(Math::random);
iterate

创建一个无限Stream。可以添加初始元素和生产规则

1
Stream<Integer> s4 = Stream.iterate(1, n -> n + 2);

实例方法

返回Stream的
peek

对其中每个元素进行处理,返回的是一个新的Stream

主要用于debug使用,打印下当前元素

map

一般用这个对每个元素进行处理。比如从一个类型,转换为另一个类型

1
List<String> stringList = integerStream.map(n -> "我是" + n).collect(Collectors.toList());
peek和map的区别

peek一般只用于debug,打印一下信息使用

peek的参数的Consumer接口,它是没有返回值的

1
2
3
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);

map的参数是Function接口,必须给返回值

1
2
3
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
还有一个疑问
1
2
3
integerStream.peek或者map(item ->
System.out.println("=========")
);

这一句话的打印是不会执行的,没有后面的collect等终止就不会执行.

也就是说,流方法真正执行是在终止方法触发

mapToInt

将元素转换成int类型,后面一般跟sum,max,min,average

mapToLong

mapToDouble

limit

限制个数

distinct

去重功能。判断是根据元素的equals方法和hashCode方法。(两个都需要重写)

基本元素

1
2
3
Stream<Integer> integerStream = Stream.of(2, 5, 100, 5);
List<Integer> collect = integerStream.distinct().collect(Collectors.toList());
System.out.println(JSONUtil.toJsonStr(collect));

[2,5,100]

对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Getter
@Setter
class User {
private String name;
private int age;
}

@Test
public void distinct() {
User a = new User();
a.setName("yun");
a.setAge(20);

User b = new User();
b.setName("yun");
b.setAge(20);

Stream<User> userStream = Stream.of(a, b);
List<User> userList = userStream.distinct().collect(Collectors.toList());
System.out.println(JSONUtil.parse(userList));


}

只写了getter/setter方法时

[{“name”:”yun”,”age”:20},{“name”:”yun”,”age”:20}]

加上@EqualsAndHashCode

[{“name”:”yun”,”age”:20}]

sorted

排序,基本元素可以使用默认排序方法

1
2
Stream<String> strStream = Stream.of("ba", "bb", "aa", "ab");
strStream.sorted().forEach(item -> System.out.println(item));

aa ab ba bb

也可自定义排序方法

自定义根据第二个字母排序

1
2
3
Stream<String> strStream = Stream.of("ba", "bb", "aa", "ab");
Comparator<String> comparator = (x, y) -> x.substring(1).compareTo(y.substring(1));
strStream.sorted(comparator).forEach(item -> System.out.println(item));

ba aa bb ab

filter

过滤

1
2
Stream<Integer> integerStream = Stream.of(2, 5, 100, 5);
integerStream.filter(item -> item > 10).forEach(item -> System.out.println(item));

100

终止方法
max

获取Stream中的最大值

例子1

1
2
Stream<Integer> integerStream = Stream.of(2, 5, 100, 5);
Integer max = integerStream.max(Integer::compareTo).get();

例子2

1
BigDecimal maxMarketPrice = productsListByGoodsId.stream().max(Comparator.comparing(YcProducts::getMarketPrice)).get().getMarketPrice();
min

获取最小值

findFirst

获取第一个元素

1
Integer i = integerStream.findFirst().get();
findAny

随机获取一个元素。串行情况下,就是第一个。并行不一定,先获取谁就是谁。

count

返回流中元素个数

1
long count = integerStream.count();
collection

将最终的Stream汇总为collection,Collectors已经为我们提供了很多拿来即用的收集器。经常用到Collectors.toList()、Collectors.toSet()、Collectors.toMap()。

另外高级用法还有比如Collectors.groupingBy()用来分组

1
2
3
4
5
// 返回 userId:List<User>
Map<String,List<User>> map = user.stream().collect(Collectors.groupingBy(User::getUserId));

// 返回 userId:每组个数
Map<String,Long> map = user.stream().collect(Collectors.groupingBy(User::getUserId,Collectors.counting()));
toArray

collection是返回列表、map 等,toArray是返回数组

1
2
3
Stream<Integer> integerStream = Stream.of(2, 5, 100, 5);
// Object[] objects = integerStream.toArray();
Integer[] toArray = integerStream.toArray(Integer[]::new);

如果无参,是返回Object数组.可以加参数Integer[]::new,可以返回Integer数组

forEach

也是对每一个元素进行处理

和map的区别是,forEach不会返回Stream,直接消费掉了

forEachOrdered

功能与 forEach是一样的,不同的是,forEachOrdered是有顺序保证的,也就是对 Stream 中元素按插入时的顺序进行消费。

在使用并行的时候,两者会有区别。

reduce

具体可以学习map/reduce计算模型.

1
2
3
Stream<Integer> integerStream = Stream.of(1,2,3);
Integer sum = integerStream.reduce(100, (x, y) -> x + y);
System.out.println(sum);

提供初始值100,然后开始累加

106

我们直接使用reduce较少,但是Collectors好多方法都用到了 reduce,比如 groupingBy、minBy、maxBy等等。

例子:BigDecimal的求和

1
BigDecimal add = list.stream().map(User::getHeight).reduce(BigDecimal.ZERO, BigDecimal::add);

并行方法

创建并行Stream

1
2
3
4
Stream.of(1,2,3).parallel();

List<Integer> src = Arrays.asList(1, 2, 3);
Stream<Integer> integerStream = src.parallelStream();

并行 Stream和普通Stream支持的api基本一样

并行 Stream 默认使用 ForkJoinPool线程池,当然也支持自定义,不过一般情况下没有必要。ForkJoin 框架的分治策略与并行流处理正好契合。

虽然并行这个词听上去很厉害,但并不是所有情况使用并行流都是正确的,很多时候完全没这个必要。

比如

1
2
3
Stream<Integer> integerStream = Stream.of(1,2,3).parallel();
Integer sum = integerStream.reduce(100, (x, y) -> x + y);
System.out.println(sum);

这个结果就变成306了

HashMap的初始容量与扩容问题

对于HashMap,有经验的开发人员都比较熟悉,也是日常工作和面试时常遇到的点。

但是有些细节你可能还不清楚。

默认初始容量

HashMap的有默认的大小,是16。从源码和代码都可以验证。

1
2
3
4
5
6
7
8
9
@Test
public void testHashMap() throws Exception {
HashMap<String, String> users = new HashMap();
Class<? extends HashMap> mapClass = users.getClass();
Method capacity = mapClass.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity:" + capacity.invoke(users));
System.out.println("size:" + users.size());
}

结果:

1
2
capacity:16
size:0

使用默认容量的问题就是,HashMap的自动扩容机制。

默认情况下,扩容系数是0.75。也就是size到13时,users的capacity会自动扩一倍到36

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testHashMap2() throws Exception {
HashMap<String, String> users = new HashMap();
users.put("1", "1");
users.put("2", "1");
users.put("3", "1");
users.put("4", "1");
users.put("5", "1");
users.put("6", "1");
users.put("7", "1");
users.put("8", "1");
users.put("9", "1");
users.put("10", "1");
users.put("11", "1");
users.put("12", "1");
Class<? extends HashMap> mapClass = users.getClass();
Method capacity = mapClass.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity:" + capacity.invoke(users));
System.out.println("size:" + users.size());
users.put("13", "1");
System.out.println("capacity:" + capacity.invoke(users));
System.out.println("size:" + users.size());
}

结果:

1
2
3
4
capacity:16
size:12
capacity:32
size:13

在扩容时,会进行rehash,这是一个比较耗时的操作。所以要求初始化HashMap时必须指定初始化容量值。

设置初始化容量

这里,我就要问一个问题了

如果HashMap<String, String> users = new HashMap(10),那么users的容量是多少?

1
2
3
4
5
6
7
8
9
@Test
public void testHashMap() throws Exception {
HashMap<String, String> users = new HashMap(10);
Class<? extends HashMap> mapClass = users.getClass();
Method capacity = mapClass.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity:" + capacity.invoke(users));
System.out.println("size:" + users.size());
}

答案是

1
2
capacity:16
size:0

为什么是16,而不是10

原因是,HashMap会根据用户的传值,选择大于这个值的第一个2的幂作为容量。保证容量是2的幂进行哈希寻址最高效。

如果让你来找大于一个Int值的最近的2的幂的数,你会怎么做?

2的幂是什么特征,那就是只有一个位是1,其余全是0。

如果我们能将一个数,从它的最高位开始全部置为1,然后再加1,就可以得到这个2的幂。

看一下HashMap的实现

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

他是用了对这个数进行无符号右移,然后进行或运算。

经过5次之后,一定会把从第一位不是0的位开始后面所有的位全部置为1。

再进行加1操作,就可以得到一个2的幂

精妙又高效。

初始化容量应该设置为多大

如前所述,我们已经知道HashMap会根据传值自动设置为大于该值的2的幂。但是这个规则比较死板。并不是很合理。

比如说,你现在需要一个容量为7的map,你确定只会有7个。

根据默认的规则,HashMap会创建一个容量为8的Map。那么可以知道这个Map的扩展值是6。所以当放进第7个元素时,map会自动扩展到16.这其实并不是我们希望的。

我们设置初始容量值的目的就是避免自动扩展。

所以设置多大合适呢?有一个公式是

1
return (int) ((float) expectedSize / 0.75F + 1.0F);

就是除以扩展因子,然后+1。

比如这里 7/0.75 + 1 = 10。这样会创建一个容量为16的map。放进第7个元素时,不会再扩展。

这其实就是用内存空间换效率。

这个公式其实是在guava中的newHashMapWithExpectedSize方法。

关于容器内JVM的实验

默认的,jvm的初始内存Xms为服务器内存的1/64,最大内存Xmx为内存的1/4

实验验证下docker中jvm的内存使用情况

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@SpringBootApplication
public class OomApplication {

public static void main(String[] args) {
System.out.println("===============");
SpringApplication.run(OomApplication.class, args);
System.out.println("********************");
oom();
}

private static void oom() {
Runtime rt = Runtime.getRuntime();
//返回java虚拟机中的初始内存总量Xms,默认应该是机器内存的1/64
long totalMemory = rt.totalMemory();
//返回java虚拟机可以使用的最大内存量Xms,默认应该是机器的1/4
long maxMemory = rt.maxMemory();
System.out.println("Total_Memory(-Xms ) = " + totalMemory + " 字节 " + (totalMemory / (double) 1024 / 1024) + "MB");
System.out.println("Max_Memory(-Xmx ) = " + maxMemory + " 字节 " + (maxMemory / (double) 1024 / 1024) + "MB");

List l = new ArrayList<>();
while (true) {
byte b[] = new byte[1048576];
l.add(b);
System.out.println("使用的内存:"+ rt.totalMemory() / (double) 1024 / 1024 + "M,free memory: " + rt.freeMemory() / (double) 1024 / 1024);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}

限制docker容器的内存

Dockerfile

1
2
3
4
5
FROM openjdk:8u191-jdk-alpine3.9
COPY ./oom-1.0.0.jar /
RUN apk add --no-cache tini
ENTRYPOINT ["tini"]
CMD ["java", "-jar", "oom-1.0.0.jar"]
1
docker run -d -m 1000m --memory-swap 1000m --name myjdk  myjdk:1.0.0

-m 限制容器可用内存为1000m

–memory-swap是容器可以使用的物理内存和可以使用的 swap 之和,设置的和memory一样大,就是不允许使用swap

设置为0,或者不设置,表示swap为memory的2倍

-1,它表示容器程序使用内存的受限,而可以使用的 swap 空间使用不受限制(宿主机有多少 swap 容器就可以使用多少)。

打印的日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Total_Memory(-Xms ) =  16252928 字节  15.5MB
Max_Memory(-Xmx ) = 253427712 字节 241.6875MB
使用的内存:15.5M,free memory: 8.396034240722656
使用的内存:15.5M,free memory: 7.396018981933594
使用的内存:15.5M,free memory: 6.396003723144531
使用的内存:15.5M,free memory: 5.395988464355469
使用的内存:15.5M,free memory: 4.454139709472656
使用的内存:15.5M,free memory: 3.4541244506835938
使用的内存:15.5M,free memory: 2.4541091918945312
使用的内存:15.5M,free memory: 1.4540939331054688
使用的内存:31.47265625M,free memory: 17.306068420410156
使用的内存:31.47265625M,free memory: 16.306053161621094
...
使用的内存:31.47265625M,free memory: 2.3054122924804688
使用的内存:67.7265625M,free memory: 37.36358642578125
使用的内存:67.7265625M,free memory: 36.36357116699219
...
使用的内存:67.7265625M,free memory: 3.3696060180664062
使用的内存:149.89453125M,free memory: 84.09593200683594
使用的内存:149.89453125M,free memory: 83.09591674804688
...
使用的内存:149.89453125M,free memory: 8.10455322265625
使用的内存:149.89453125M,free memory: 7.1045379638671875
使用的内存:241.6875M,free memory: 98.41573333740234
使用的内存:241.6875M,free memory: 98.41573333740234
...
使用的内存:241.6875M,free memory: 0.5089797973632812
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:107)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88)
Caused by: java.lang.OutOfMemoryError: Java heap space
at com.yunsheng.demo.OomApplication.oom(OomApplication.java:30)
at com.yunsheng.demo.OomApplication.main(OomApplication.java:16)
... 8 more

看到初始Xms确实为1G内存的1/64,Xmx为1G内存的1/4

然后内存耗尽之后,申请的基本上是当前内存的一倍还多一点。

验证jdk命令

进入容器,实现jps,jstat等jdk命令可以使用

jmap进行堆dump也是可以的

image-20210928144320898

验证异常退出dump

Dockerfile

1
2
3
4
5
6
7
8
FROM openjdk:8u191-jdk-alpine3.9
COPY ./oom-1.0.0.jar /
COPY ./start.sh /
WORKDIR /
RUN apk add --no-cache tini
ENTRYPOINT ["tini"]
#CMD ["java", "-jar", "oom-1.0.0.jar"]
CMD sh start.sh

在程序jvm的启动参数中增加异常退出时进行堆转储

1
nohup java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/my-heap-dump.hprof -jar oom-1.0.0.jar > log.log 2>&1 &

image-20210928164531723

验证ok

测试s2i方式打镜像启动

同样的代码

同样的容器启动命令

发现s2i方式启动的java进行,其内存的设置是依赖的物理机内存