开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

django CBV 与 DRF APIView源码分析 d

发表于 2021-11-11

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

django CBV源码分析

在django框架中,视图层中的逻辑即可以使用函数处理也可以使用类进行处理,如果在视图层中使用函数处理请求,就是FBV(function base views),如果在视图层中使用类处理请求,就是CBV(class base views).这一部分主要介绍一下如何使用CBV处理请求。具体步骤如下:

当浏览器客户端向服务端发送请求时,会先通过路由匹配到对应的视图函数并加括号调用。

在路由层中书写的路由与视图函数的对应关系,路由对应的必须是一个函数地址,因此CBV本质还是FBV,由此可以推断views.MyView.as_view()中的as_view应该是一个类方法,并且该方法加括号调用后返回的结果是一个函数的内存地址。

as_view方法是自定义视图类调用的,但是自定义视图类MyView中并没有该方法,按照面向对象的属性查找顺序,as_view方法是在MyView的父类View中定义的。

当匹配到视图类的时候,会加()运行views.MyView.as_view()

views.MyView.as_view()运行的结果是返回as_view方法内部的view函数的内存地址

在self.setup方法中,会将当次请求的request对象作为属性添加给当前对象

路由对应的是view方法的内存地址,因此当匹配到路由时会加()运行view方法

根据上图可以发现,运行view方法会返回self.dispatch()方法的运行结果

在dispatch方法中会获取对象(MyView的对象)的请求方式然后小写,判断是否在self.http_method_names中,如果在就通过反射将对象的请求方式小写赋值给handler,,最后返回handler加()调用。

CBV视图层中,dispatch方法可以说是前端向后端发送请求的调度员,可以根据不同的请求的方式执行视图类中对应的方法

1
2
3
go复制代码在dispatch方法中会获取对象(`MyView`的对象)的请求方式然后小写,判断是否在`self.http_method_names`中,如果在就通过反射将对象的请求方式小写赋值给handler,,最后返回handler加`()`调用。

CBV视图层中,dispatch方法可以说是前端向后端发送请求的调度员,可以根据不同的请求的方式执行视图类中对应的方法

img

DRF APIView源码分析

对DRF的APIView进行分析都是源于一段代码:

1
2
3
python复制代码from rest_framework.views import APIView

url(r'^books/', views.BookView.as_view()),

url.py中使用的as_view()是APIView类的as_view方法,使用的还是django原生的当次请求的request对象。

在运行该as_view方法时,内部调用了父类(View)的as_view方法。至此request对象还是原生的request对象。

调用父类的as_view方法,就会返回view方法的内存地址,并加()进行调用,返回dispatch方法,注意该dispatch方法不是父类View中的dispatch方法,而是APIView类自己写的dispatch方法。

在该dispatch方法中,通过request = self.initialize_request(request, *args, **kwargs)重新封装了request对象,并重新赋值给原生的request对象。

在initialize_request(request, *args, **kwargs)方法中,返回了Request类实例化的对象,其中一个参数authenticators=self.get_authenticators(),调用了get_authenticators(),该方法是返回了一个列表,该列表中是authentication_classes类实例化得到的对象

authentication_classes在APIView类的配置文件中,我们可以自己写该认证类。

封装完request对象后,回到dispatch方法,继续向下执行到self.initial(request, *args, **kwargs),在该方法中会定义三个认证组件(认证组件 权限组件 限流组件)

再次返回到dispatch方法中,就会根据当此请求的请求方式完成视图类方法内的调度

运行完dispatch方法后回到as_view方法中,最后as_view方法返回了csrf_exempt(view),该方法的作用的就是解除了浏览器请求时的csrf认证,即以后所有的请求方法都不做csrf的认证

img

总计起来APIView源码的运行过程可以总结为以下几步:

  • 通过路由与视图的对应关系,运行了APIView类中的as_view方法,调用了父类的as_view方法,APIView类中的as_view方法最终解除了请求时的csrf认证
  • 通过调用父类的as_view方法,调用了view方法并返回了APIView类中dispath的运行结果。
  • dispath方法中重新封装了request对象
  • dispatch方法定义了三个认证类
  • dispatch方法通过request.method方法获得浏览器客户端请求的方式,进行视图类方法的调度
  • 最后再在django原生response的基础上,进一步封装成drf的Response

结语

文章首发于微信公众号程序媛小庄,同步于掘金。

码字不易,转载请说明出处,走过路过的小伙伴们伸出可爱的小指头点个赞再走吧(╹▽╹)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

spring-security和jwt如何生成token

发表于 2021-11-11

背景:在开始一个项目时,目前生成token的方法主要有两种,一种是spring-security+jwt组合,另一种则是shiro+jwt组合,本文主要阐述spring-security+jwt组合生成token。对这两种框架不熟悉的同学,有兴趣可以点击去官网学习下,不熟悉的也没关系,在生成token的过程中涉及到的知识点,我会一一解答,不熟悉的也不影响阅读本文(建议独自去学习下)。

开始:

废话不多,大家都知道,要使用框架,那必不可少的,首先就是要引入相关依赖啊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码//pom.xml
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>

除此之外,我们还需要在application.yml文件中,给jwt配置一下信息:

1
2
3
4
5
6
7
8
9
js复制代码jwt:
# JWT存储请求头
tokenHeader: Authorization
# JWT加密使用的密钥
secret: juejin-secret
# JWT超期限时间(60*60*24)
expiration: 604800
# JWT负载中拿到开头
tokenHead: Bearer

不管你是老司机,还是正在进阶的同学,我还是在这里啰嗦解析一下其中的字段。tokenHeader是存储的一个请求头,在前端后端接口数据交互中一般都会有,以及tokenHead一般都会存在于请求信息里,secret是jwt加密和解密要使用到的一个密钥,expiration是token的一个失效时间设定,我这里设置是24小时失效。

接下来我们可以去实现一个spring-security+jwt组合生成token的一个工具类JwtTokenUtil,这个类一般都放在包的config下,下面先把代码展示出来:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
js复制代码/java/com.hhk,server.config.security(包路径)

package com.hhk.server.config.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtTokenUtil {
private static final String CLAIM_KEY_USERNAME="sub";
private static final String CLAIM_KEY_CREATED="created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;

/**
* 根据用户信息生成token
*/
public String generateToken(UserDetails userDetails){
Map<String,Object> claims=new HashMap<>();
claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}

// 根据荷载JWT生成token
public String generateToken(Map<String,Object> claims){
// Jwts去生成token
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
}

// 生成token失效时间
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis()+expiration*1000);
}

// 从token中获取登录名
public String getUserNameByToken(String token){
String userName;
try{
Claims claims=getClaimsFromToken(token);
userName=claims.getSubject();
}catch (Exception e){
userName=null;
}
return userName;
}

// 从token中获取荷载
private Claims getClaimsFromToken(String token) {
Claims claims=null;
try {
claims=Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
e.printStackTrace();
}
return claims;
}
// 验证token是否有效
public boolean validateToken(String token,UserDetails userDetails){
String username=getUserNameByToken(token);
return username.equals(userDetails.getUsername())&&!isTokenExpired(token);
}

// 验证token是否失效
private boolean isTokenExpired(String token) {
Date expireDate=getExpiredDateFromToken(token);
return expireDate.before(new Date());
}

// 从token中获取失效时间
private Date getExpiredDateFromToken(String token) {
Claims claims=getClaimsFromToken(token);
return claims.getExpiration();
}
// 判断token是否可以被刷新
public boolean canRefresh(String token){
return !isTokenExpired(token);
}

// 刷新token
public String refreshToken(String token){
Claims claims=getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}
}

下面主要是解析一下,上面整个JwtTokenUtil的源码,自己能阅读的话,不用看下面的源码,下面主要是以方法为单位进行拆分讲解:

1
2
3
4
5
6
7
js复制代码//在类的开始,我们需要定义两个静态字符串,以及获取我们前面在Application.yml中定义好的jwt参数,这里是通过@Value(${""})的方式获取Application.yml文件中的配置信息了
private static final String CLAIM_KEY_USERNAME="sub"; //定义静态用户名(key)CLAIM_KEY_USERNAME
private static final String CLAIM_KEY_CREATED="created";//定义静态生成token的时间
@Value("${jwt.secret}") //获取密钥
private String secret;
@Value("${jwt.expiration}") //获取设定的失效时间
private Long expiration;

到这里在工具类JwtTokenUtil需要使用的静态资源就准备好了,下面我们要开始创建生成token的方法了:

1
2
3
4
5
6
7
8
9
js复制代码/**
* 根据用户信息生成token
*/
public String generateToken(UserDetails userDetails){
Map<String,Object> claims=new HashMap<>(); //定义一个荷载,用于存储用户ing以及token的生成时间
claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername()); //通过。put方法存储CLAIM_KEY_USERNAME和CLAIM_KEY_CREATED的值
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims); //generateToken的方法在下面会编写其源码,主要是根据荷载生成token的
}

承上启下,下面是generateToken(claims):

1
2
3
4
5
6
7
8
js复制代码//    根据荷载JWT生成token
public String generateToken(Map<String,Object> claims){
return Jwts.builder() //Jwts去生成token
.setClaims(claims) //存储带有用户信息的荷载
.setExpiration(generateExpirationDate()) //存储失效时间
.signWith(SignatureAlgorithm.HS256,secret) //使用算法,和密钥
.compact();
}

generateToken(claims)中使用的generateExpirationDate(),贴一下代码:

1
2
3
4
5
js复制代码//    生成token失效时间
private Date generateExpirationDate() {
//System.currentTimeMillis()+expiration*1000===当前时间+失效时间
return new Date(System.currentTimeMillis()+expiration*1000);
}

到这里其实已经实现了生成token了,但是,既然作为工具类,那在项目中,有时候要校验token,就是通过token中的用户名去跟数据库中的用户进行对比,这时就要获取token中的用户名了,也就是源码中的getUserNameByToken(String token):

1
2
3
4
5
6
7
8
9
10
11
js复制代码从token中获取登录名
public String getUserNameByToken(String token){
String userName;
try{ //有时候会存在用户名不存在的情况,所以要对这种异常进行捕获
Claims claims=getClaimsFromToken(token); //因为生成token时,用户名是存储在token中的荷载,所以getClaimsFromToken(token)获取荷载
userName=claims.getSubject(); //claims的getSubject()获取用户名
}catch (Exception e){
userName=null;
}
return userName;
}

getUserNameByToken(String token)中使用的getClaimsFromToken(token)是要从token中获取荷载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码//    从token中获取荷载
private Claims getClaimsFromToken(String token) {
Claims claims=null;
try {
//通过密钥和token从jwts中获取荷载,因为荷载有可能为空,所以需要捕获一下异常,防止程序崩溃
claims=Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
e.printStackTrace();
}
return claims;
}

在日常开发中,或者访问网页中时,我们经常会出现需要重新登录的情况,这是,就需要重新登录,以获取新的token继续访问网页,这个过程,需要验证token是否有效、是否可以被刷新。具体方法如下:

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
js复制代码//    验证token是否有效UserDetails是spring-security框架里的一个类
public boolean validateToken(String token,UserDetails userDetails){
String username=getUserNameByToken(token); //从token中获取用户名
return username.equals(userDetails.getUsername())&&!isTokenExpired(token);
}

// 验证token是否失效
private boolean isTokenExpired(String token) {
Date expireDate=getExpiredDateFromToken(token); //从token中获取失效时间
return expireDate.before(new Date());
}

// 从token中获取失效时间
private Date getExpiredDateFromToken(String token) {
Claims claims=getClaimsFromToken(token); //从token中获取荷载,荷载中存储了失效时间
return claims.getExpiration();
}
// 判断token是否可以被刷新
public boolean canRefresh(String token){
return !isTokenExpired(token);
}

// 刷新token
public String refreshToken(String token){
Claims claims=getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}

到这里,spring-security+jwt生成token,以及获取token中的信息的工具类就写完了,感谢阅读!❤️❤️❤️

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

技术升级 & 行业升级,TiDB + 易车打造超级汽车狂欢节

发表于 2021-11-11

一年一度的双十一又双叒叕来了,给技术人最好的礼物就是****大促技术指南!而经过这些年的发展,大促早已不仅仅局限于电商行业,现在各行各业其实都会采用类似方式做运营活动,汽车界有 818,电商有 618 、11.11 等等,各种各样的大促场景,对包括数据库在内的基础软件提出了很多新挑战,同时也积累了诸多最佳实践。

\

在双十一到来前,PingCAP 与汽车之家、易车网、京东、中通等用户展开一系列深入探讨,希望为大家揭秘逐年飙升的销量背后隐藏着什么样的技术难题?用什么技术架构才能平稳地扛住流量洪峰?本文为「大促背后的数据库专题」的第二篇:TiDB + 易车如何打造超级汽车狂欢节 ?

\

汽车界的 “大促” 狂欢节

成立于 2000 年的易车,是国内最早一批汽车互联网平台企业之一,为汽车用户提供专业、丰富的互联网资讯服务,提升用户在选车、购车、用车和换车过程中的全程体验。

在今年 “818” 期间,易车与浙江卫视联合推出了一台综合汽车工艺秀、明星歌舞演出和明星综艺秀的车界 “春晚” —— “易车超级 818 汽车狂欢夜”。在为汽车用户带来视听盛宴、购车福利的同时,晚会还推出超 150 台半价车的超值福利,观众可边看晚会边抢 5 折售卖的好车,同时还有购车红包、抵扣券、车款直降等多重优惠,得到实实在在的购车福利。截至晚会结束,全平台观看直播人次达 2.24 亿,获得线上订单 4.39 万,累计成交额(GMV)64.2 亿元。\

易车的大促首秀

在易车的 818 狂欢节中,数据库的应用场景有很多,其中实时数据看板是主要的应用业务之一。看板可以实时展示易车 818 购车节的专题、活动、流量、线索、互动等数据表现,是大数据平台的整体数据输出。

由于易车的这场汽车狂欢夜是台网互动的直播活动,摇一摇(红包、半价车、易车币)和主会场分会场直播节目的投票都是用户参与度最高、数据流量最大的环节。在整个活动过程中,不仅要求数据库能够存储海量数据,同时还要求能够应对高并发、低延迟等场景需求。这里的数据库不仅会作为数据存储的介质,还会作为实时计算的数据源头,配合流量数据,实现秒级数据实时播报。

数据库和 Flink 是整个系统中非常重要的两个组件,Flink 的数据来源包括数据库和业务流量数据,所以数据库不仅要满足数据秒级实时推送,还要支持 Flink 高并发的读写请求。

易车数据库负责人田震坦言,易车网今年是第一次做大促,没有太多经验,量也不好预估,很多需求都是在最后才提出。为了保险起见,DBA 团队在设计大促方案时做了降级方案,但谁都不希望真的实施降级,这对用户的体验太不友好。所以整个 DBA 团队将主要精力放在压测上,并按照平时的两个数量级(100 倍)来规划数据库压测方案。

一开始,易车考虑的首选数据库依然是 MySQL。但在压测过程中,为了保证计算结果的实时性,实时任务会频繁对数据库进行大批量数据写入,MySQL 主从延迟高,极端情况下引起的 MySQL 主从切换,切换时间过长,导致数据库出现短暂不可用状态。同时,实时任务会持续写入大量数据,引起磁盘爆满。在分秒必争的直播过程中这肯定是无法容忍的。

在情势急迫下,田震想到了 TiDB。\

“在游泳中学游泳” TiDB 临危受命

实际上,田震很早就接触过 TiDB ,那时候他一度认为 TiDB 是一款海外产品,了解 TiDB 主要也是从海外社区开始的。但出于谨慎的原因,田震希望将产品研究透彻再正式上线。本次大促给了双方合作一个完美的契机,他形容这一过程就像是 “在游泳中学游泳”。

TiDB 社区的技术支持给了易车 DBA 们非常重要的帮助,从七月正式立项,仅用了不到一个月时间就完成了选型、方案设计、压测、上线部署,并在 “818” 中有惊无险地将大促流量平稳承载过来。

818 汽车狂欢数据看板业务架构图

在整个 818 活动中,TiDB 被用作 818 汽车狂欢节数据看板的核心数据库。易车准备了两套 TiDB 集群,和实时计算的主备方案一一对应。业务研发通过双写的方式把数据同时写入两个集群,一部分业务的查询链接集群 1 ,另一部分业务的查询连接集群 2,当其中一个集群出现问题,应用端就会切换到另外一个集群。两个 TiDB 集群都是部署了 3 个 TiDB Server、3 个 PD Server、6 个 TiKV 节点、2 个 TiFlash 节点。此外,还准备了 4 台机器做扩容以免数据量暴涨集群支撑不了。

最终,易车 818 汽车狂欢节期间数据量达到了平时的 10 倍以上,在直播最后蔡徐坤出场时,数据库流量更是直接翻了四倍,差点启用事先准备好兜底用的一键扩容方案。在整个过程中,818 汽车狂欢数据看板业务 SQL 999 始终控制在 8ms 以内,SQL 99 在 3ms 左右,QPS 达到 62k。

红包摇一摇业务架构图

同时,TiDB 也作为容灾方案被应用在红包摇一摇业务中,避免由于业务流量暴涨引起 MySQL 不可用的情况。一旦发生不可用,业务方可以直接将数据库切换到 TiDB。TiDB 在整个业务中需要作为数据源、实时计算维表和实时计算结果存储引擎三个角色。 TiDB 通过 TiCDC 将数据实时推送到 Kafka 中,为了保证 TiCDC 稳定高效,易车为 TiDB 中的每个库创建了一个 TiCDC 任务,将数据实时推送到指定 Kafka 中,然后 Flink 负责将同一个 TOPIC 中的属于不同库表的数据进行解析,分流到库表对应的 TOPIC 中,提供给实时计算业务使用。实时计算任务消费 Kafka 中的 TiDB 数据进行业务逻辑计算,同时还需要从 TiDB 中查询对应的维度数据,最终将计算结果再输出到 TiDB 中。\

高速增长的挑战:技术栈统一

大促的极限场景总能发现一些平时注意不到的问题,在易车的高速发展中,很多业务为了快速迭代、迅速上线,引入了非常多的技术栈,如 Lambda 、 Kappa 等大数据架构,Kylin、Druid、Clickhouse 等实时数仓等等。但易车 DBA 团队却只有 6 个人,管理如此多的技术栈无疑是一个很大的挑战。

统一技术栈成为易车 DBA 团队的最佳选择,借着这次大促的机会,易车希望用 TiDB 上线取代 Kylin、Druid、Clickhouse ,简化技术栈,DBA 团队也能将注意力放回专职工作上。

TiDB 的 HTAP 架构是一个混合了交易型事务和分析处理的融合架构,由于都是在同一个架构、同一套数据中,解决了易车实时数仓数据流延迟的问题。数据不用再从 OLTP 数据库复制出来,经过漫长的 ETL 清洗等过程进入分析工具。

而 TiDB 对 MySQL 的完美兼容,对 DBA 和开发者意味着不需要做什么改变,只要会 SQL 就能使用。而在以往应用 Hadoop 或 Spark 时,由于学习成本比较高,对使用造成了一定壁垒。

经此一役,易车的业务方对 TiDB 平添了许多期待与信任。未来,易车的广告、媒体平台、网站、投放数据、广告效果都希望能够实时看到,田震希望借用 TiDB 覆盖易车整个混合技术栈的场景,与其他数据流进行打通,这些都需要 TiDB HTAP 对实时数仓进行支持。

大促对于企业而言,除了支持业务创新,也是一次对自身技术架构的大练兵和全链路演练。通过大促的极致考验,企业的 IT 架构、组织流程、人才技能都获得了大幅提升。而在大促中的经验和思考,也会加速企业日常的业务创新节奏,提升技术驱动的创新效率,打造增长新引擎。

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

关于Java 中的 Lambda 表达式你了解多少?

发表于 2021-11-11

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」

🌊 作者主页:海拥

🌊 作者简介:🥇HDZ核心组成员、🏆全栈领域优质创作者、🥈蝉联C站周榜前十

🌊 粉丝福利:进粉丝群每周送四本书(每位都有),每月抽送各种小礼品(掘金搪瓷杯、抱枕、鼠标垫、马克杯等)

这篇文章我们将讨论关于Java 中的 Lambda 表达式,Lambda 表达式是 Java 涉足函数式编程的过程。它接受参数并将其应用于表达式或代码块。以下是语法的基本示例:

1
java复制代码(parameter1, parameter2) => expression

或者

1
java复制代码(parameter1, parameter2) => {code block}

Lambda 表达式非常有限,如果它不是 void,则必须立即返回一个值。他们不能使用诸如 if 或 for 之类的关键字来保持简单性。如果需要更多行代码,则可以改用代码块。

现在在实现 lambda 表达式时,不能只使用表达式。Lambda 是函数式接口的实现。函数式接口是只有一个抽象方法的接口。lambda 的好处是它们允许你实现方法而无需实现接口的类和实例化对象。下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码interface FuncInterface
{
// 抽象函数
void abstractFun(int x);

// 非抽象(或默认)函数
default void normalFun()
{
System.out.println("Hello");
}
}

class Test
{
public static void main(String args[])
{
// 实现上述功能接口的 lambda 表达式。
// 该接口默认实现 abstractFun()
FuncInterface fobj = (int x)->System.out.println(2*x);

// 这会调用上面的 lambda 表达式并打印 10。
fobj.abstractFun(5);
}
}

Lambda 表达式通常用作函数的参数。为了提高可读性,你还可以将 lambda 表达式存储在变量中,只要该类型是一个只有一个方法、相同数量的参数和相同返回类型的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码import java.util.ArrayList;
import java.util.function.Consumer;

public class Main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<Integer>();
numbers.add(5);
numbers.add(9);
numbers.add(8);
numbers.add(1);
Consumer<Integer> method = (n) -> { System.out.println(n); };
numbers.forEach( method );
}
}

lambda 的一个常见用途是创建线程。这是一个使用 lambda 代码块实现 Runnable 对象的示例,供线程执行。

1
2
3
4
5
java复制代码// 可运行的 Lambda
Runnable task2 = () -> { System.out.println("Task #2 is running"); };

// 启动线程
new Thread(task2).start();

作为初学者,我们大多数人都被教导使用 OOP 概念进行编程,因此使用不同的范式(如函数式编程)可能会有些别扭。我自己仍在学习这些概念中。不管怎样我希望这篇文章能让大家学到一些东西。如果你有更多问题或想深入研究该主题,欢迎评论或者查看下方的资料,我提取的代码示例也来自这里。

参考资料:

www.w3schools.com/java/java_l…

www.geeksforgeeks.org/lambda-expr…

www.developer.com/microsoft/s…

www.codejava.net/java-core/t…

写在最后的

作者立志打造一个拥有100个小游戏的摸鱼网站,更新进度:40/100

我已经写了很长一段时间的技术博客,并且主要通过掘金发表,这是我的一篇关于Java 中的 Lambda 表达式你了解多少。我喜欢通过文章分享技术与快乐。你可以访问我的博客: juejin.cn/user/204034… 以了解更多信息。希望你们会喜欢!😊

💌 欢迎大家在评论区提出意见和建议!💌

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

阿里云马涛:因云进化的基础软件

发表于 2021-11-11

简介: 基础软件的云原生化。

**编者按:**2021 年10 月20 日,在2021 云栖大会·云计算产业升级峰会上,阿里云“因云而生”云原生心智大图正式发布,包含弹性计算、云网络、基础产品、基础设施、操作系统、云安全、开放平台等7个 IaaS 领域,以及云原生应用平台、数据库、大数据&AI、智能loT、云效平台、企业服务云、视频云、钉钉等8个 PaaS & SaaS 领域的阿里云核心技术逐一亮相,具体产品解读内容将在「凌云时刻」进行系列发布。本文根据阿里巴巴集团研究员、阿里云基础软件部操作系统负责人马涛的现场分享整理而成。

大家好!我是阿里云操作系统团队的负责人,也是龙蜥操作系统开源社区理事长。我曾写过一篇关于云原生操作系统的文章,对“因云进化的基础软件”有着深刻的经历和理解。在大家的印象中,操作系统是非常基础和传统的课题,可能会免不了好奇:操作系统怎么会跟云原生这么炙手可热的概念扯上关系?我在2013年加入阿里云,期间也在不断思考基础软件如何云原生化,操作系统如何云原生化。云计算最开始使用的是虚拟机模式,后面随着容器、K8s的部署,在软件实施过程中,操作系统起了非常重要的作用。比如说最早的容器,使用操作系统内部的像Cgroup、Namespace 的功能让容器跑得更好更快。现在再来看整个操作系统怎么样云原生化,最核心的点是弹性、稳定性、安全等特性。

阿里巴巴集团研究员、阿里云基础软件部操作系统负责人 马涛

**涉及到弹性,**我们在考虑操作系统为“云”服务,无论是弹性计算的服务,还是云原生的服务,像ECI、函数计算以及其他的实例,如何利用操作系统提供最佳的弹性?于是,我们在阿里创造了云原生的底层系统:袋鼠。

袋鼠都能提供什么功能呢?简单来说是极致的高密和弹性,我们的袋鼠安全容器和阿里云安全沙箱可以在30秒之内弹出3000个容器,同时在一台机器上可以运行超过2000个以上的容器。这在操作系统领域其实是非常巨大的挑战。面向下一个云计算的十年,袋鼠云原生底层系统实现了Serverless极致性能优化,随意弹性。

**再讲到稳定性,**云上的稳定性和我们惯常提到的稳定性是不一样的,有什么不一样呢?阿里云上的稳定性是经过千锤百炼的,是在云上大规模部署、实践得到的稳定性。因此,阿里云上操作系统的稳定性是比任何一家大家已知的最稳定的操作系统要高出一到两倍。脱胎于阿里云十年操作系统技术积累的龙蜥操作系统,先天具有CentOS替换成功经验,以及优秀的稳定性。

刚才讲到我们在阿里云做了很多操作系统上的创新,今天希望把这些创新和业界的合作伙伴分享。今天上午阿里云智能基础产品事业部总经理蒋江伟,宣布阿里云发布全新操作系统“龙蜥”并开源。同时,阿里达摩院操作系统实验室成立。我们将通过龙蜥操作系统和开源社区,把我们在阿里云积累的因云而生的操作系统技术无偿地分享出来,并且提供至少十年技术支持。我们也希望和统信软件、Intel、三大运营商等社区合作伙伴,一起实现基础软件的整体云原生化,影响并推动整体基础软件的进化。

最后我想讲一点,基础软件虽然是非常基础的一个组件,但是在演进过程中受云的影响非常显著。我们非常高兴地看到目前阿里云在因云进化的基础软件领域迈出了一小步,我们也十分希望通过龙蜥操作系统和开源社区,能够和合作伙伴一起在基础软件领域迈向一大步。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

saas疯行的年代,工作流俨然是一种职业素质 前言 环境安装

发表于 2021-11-11

前言

  • 现在越来越多的项目开始使用工作流来满足日常的工作了。今天我们看看Activiti的工作流,我们先入门下BPMN绘画

环境安装

  • 关于流程的BPMN文件主要还是通过eclipse开发工具进行集成绘画的。idea中主要是actiBPM绘画的,但是2014年之后actiBPM就不在维护了,现在直接是无法在新的idea版本中安装使用了。
  • 既然如此你可以idea开发项目,通过eclipse绘制,但是这种情况好像很麻烦。这里推荐另外一个集成工具绘制BPMN流程图示
  • camunda-modeler就是一个第三方的流程设计工具。他的好处是替代了eclipse , 我们可以通过idea的External Tools方式加载进来。

image-20211101114751196.png

  • 关于他的配置也是很简单的,点我下载exe。

image-20211101114941598.png

  • 剩下的我们就可以绘制了。

image-20211101115012677.png

扩展

  • 关于类似camunda第三方流程绘制的软件,大多都会提供社区办。他们是以java服务的方式启动的。为什么还需要这种方式呢?因为靠着这种方式我们可以通过调用api的方式通过camunda来绘制流程。在我们的自己的流程平台中就可以绘制了。这样就将流程绘制器接入到我们自己的平台系统中了。

发布流程

  • 首先我们通过camunda进行绘制一个简易的流程。上面我们先通过tomcat形式启动一个camunda平台,将我们制作的流程发布上去。

image-20211101160417731.png

image-20211101160434919.png

服务任务

  • 首先我们的刷卡付款这个动作是服务端操作。这里我们点击后右侧设置栏中设置器服务方式

image-20211102092204749.png

  • 最终服务端是通过监听的方式来获取节点信息的,所以这里我们需要设置下消息类型(Topic)。

image-20211102092301953.png

网关

  • 现在我们的流程还是很简单的。上面我们通过设置Implementation的方式为External表示该节点借助外部服务来共同实现且指定消息主题topic 。 关于消息的监听我们下方通过Java方式来监控信息。在流程中还有一个重要角色就是网关 。 这里的网关就是我们的判断节点。比如我们上面刷卡付款请求存在这么一种需求
  • 付款金额大于等于1000元时需要进行付款审批。小于1000元则可以直接进行付款。
  • 在camunda中有个×的图形,就是我们所说的网关。我们稍微修改下流程图

image-20211102093115972.png

  • 网关我们直接拖拽进来就可以了,他的特色就是存在两条或者更多条分支。那么最终会走哪一个分支就需要我们在每个分支上设置条件了。这里我们通过Java的表达式设置条件。

image-20211102093248255.png

  • amount这就是一个变量。这个变量你可以随意定义,但是在流程开始的时候必须提供这个变量,否则会报错

image-20211102094536939.png

  • 我们可以在网关开始前,先让用户提供先流程的必要信息

image-20211102094627368.png

  • 然后才会根据填写的金额进行扭转流程。付款审批需要根据流程信息进行审批,所以在填写申请单上我还加了item这个参数。为什么加上这个参数呢?假如我是批准付款的负责人。但是我不是很关心金额的事情,或者说我对某些项目就是无脑的通过。这个时候我们可以借助流程的自动化决策来实现该节点的自动审批。
  • 这里我们引入流程的DMN,我们通过camunda新建一个DMN文件。并设置dmn的id

image-20211102095438122.png

  • 然后点击左上角的图标进行设置规则

image-20211102095633826.png

  • 我们的规则很简单,凡是item-xyz项目的我都会审批通过,否则一律不通过。
  • 在这个规则中我们可以设置很多中方式,上面我们的是唯一方式,也就是说每种条件是互斥的,非黑即白的。还有其它的方式我们可以自己调试下
  • 在回到我们主流程中,我们选择Implementation方式为DMN , 并且ref选择上面我们设置的DMN的id

image-20211102095822337.png

Java监控流程信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码<dependency>
<groupId>org.camunda.bpm</groupId>
<artifactId>camunda-external-task-client</artifactId>
<version>7.15.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
  • 我们这里添加依赖后,开始监听我们流程的topic对应流程里的外部服务
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
java复制代码ExternalTaskClient client = ExternalTaskClient.create()
.baseUrl("http://localhost:8080/engine-rest")
.asyncResponseTimeout(10000) // 长轮询超时时间
.build();

// 订阅指定的外部任务
client.subscribe("charge-card")
.lockDuration(1000) // 默认锁定时间为20秒,这里修改为1秒
.handler((externalTask, externalTaskService) -> {
// 将您的业务逻辑写在这

// 获取流程变量
String item = (String) externalTask.getVariable("item");
Long amount = (Long) externalTask.getVariable("amount");

LOGGER.info("Charging credit card with an amount of '" + amount + "'€ for the item '" + item + "'...");

try {
Desktop.getDesktop().browse(new URI("https://docs.camunda.org/get-started/quick-start/complete"));
} catch (Exception e) {
e.printStackTrace();
}

// 完成任务
externalTaskService.complete(externalTask);
})
.open();
  • 此时我们可以查看下日志,

image-20211102100022312.png

Maltcloud集成

  • 工作流在maltcloud版本计划中是排在二期进行的,所以我是在maltcloud完成了基本的功能之后开发计划开发工作流的相关操作。首先就是流程的绘制,上面已经提到了流程的基本绘制及发布。下面我们看看如何将工作流继承到我们的企业级框架maltcloud中吧。
  • 注意在整合的时候需要springboot和camunda版本匹配。官网给我们提供了响应版本的匹配方案

image-20211102165718544.png

  • 这里解释一下
    • 第一列【springboot starter version】表示camunda的springboot starter版本。
    • 第二列【camunda platform version】表示实际的camunda版本
    • 第三列【springboot version】表示我们系统的springboot版本。
  • 结合maltcloud使用的是2.2.2.RELEASE版本,我们可以定位到使用camunda springboot starter版本应该是3.4.x; 然后我们在去maven仓库中查看下相关的版本

image-20211102165953752.png

  • 这里我一开始选择使用3.4.0的版本。后来发现2.2.x.RELEASE的springboot也支持7.13.x , 最终决定使用7.13.0这个版本。

image-20211102192416117.png

  • 因为camunda-bpm-spring-starter中使用的mybatis使用的是3.5.3但是maltcloud中使用的是3.5.6版本。所以我这里还需要解决下冲突。最终pom配置如下
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
xml复制代码<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<camunda.spring-boot.version>7.13.0</camunda.spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.github.zxhtom</groupId>
<artifactId>org.components.datasource</artifactId>
<version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
<version>${camunda.spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-rest</artifactId>
<version>${camunda.spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter</artifactId>
<version>${camunda.spring-boot.version}</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>

</dependencies>
  • maltcloud是笔者设计的一个企业级开发框架,暂时还在开发阶段,待合适的时期开源。这里我引入了maltcloud平台中的datasource模块。这个模块主要就是引入mybatis的配置。读者可以把这段去掉自行配置mybatis就可以了。

细节补充

相关版本文档

image-20211102194207406.png

  • 通过options选择版本。在Installation Procedure中就可以下载指定的版本了。点进去之后会有小版本列表。因为我们maltcloud中选择的是7.13.0 。 所以我们下载对应的平台camunda.

image-20211102194332308.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

GC日志解读,这次别再说看不懂GC日志了 Serial GC

发表于 2021-11-11

测试环境:机器内存16G,JDK 8,12核CPU

测试用例,从网上找的示例,视情况修改即可:

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
58
59
60
61
62
java复制代码import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
/*
演示GC日志生成与解读
*/
public class GCLogAnalysis {
private static Random random = new Random();
public static void main(String[] args) {
// 当前毫秒时间戳
long startMillis = System.currentTimeMillis();
// 持续运行毫秒数; 可根据需要进行修改
long timeoutMillis = TimeUnit.SECONDS.toMillis(1);
// 结束时间戳
long endMillis = startMillis + timeoutMillis;
LongAdder counter = new LongAdder();
System.out.println("正在执行...");
// 缓存一部分对象; 进入老年代
int cacheSize = 2000;
Object[] cachedGarbage = new Object[cacheSize];
// 在此时间范围内,持续循环
while (System.currentTimeMillis() < endMillis) {
// 生成垃圾对象
Object garbage = generateGarbage(100*1024);
counter.increment();
int randomIndex = random.nextInt(2 * cacheSize);
if (randomIndex < cacheSize) {
cachedGarbage[randomIndex] = garbage;
}
}
System.out.println("执行结束!共生成对象次数:" + counter.longValue());
}

// 生成对象
private static Object generateGarbage(int max) {
int randomSize = random.nextInt(max);
int type = randomSize % 4;
Object result = null;
switch (type) {
case 0:
result = new int[randomSize];
break;
case 1:
result = new byte[randomSize];
break;
case 2:
result = new double[randomSize];
break;
default:
StringBuilder builder = new StringBuilder();
String randomString = "randomString-Anything";
while (builder.length() < randomSize) {
builder.append(randomString);
builder.append(max);
builder.append(randomSize);
}
result = builder.toString();
break;
}
return result;
}
}

Serial GC

JVM启动参数:

1
java复制代码java -Xmx512m -Xms512m -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseSerialGC -XX:-UseAdaptiveSizePolicy GCLogAnalysis

GC日志如下:

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
java复制代码Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for windows-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:36:39 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16657944k(1083240k free), swap 33750228k(6246880k free)
CommandLine flags:
-XX:InitialHeapSize=536870912 //初始化堆内存
-XX:MaxHeapSize=536870912 //最大堆内存
-XX:+PrintGC
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:-UseAdaptiveSizePolicy
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:-UseLargePagesIndividualAllocation
-XX:+UseSerialGC
----------------------Minor GC----------------------------
2021-09-09T14:44:04.813+0800: 0.163: [GC (Allocation Failure) 2021-09-09T14:44:04.813+0800: 0.163:
[DefNew: 139776K->17472K(157248K), 0.0164545 secs] 139776K->45787K(506816K), 0.0165501 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
2021-09-09T14:44:04.853+0800: 0.203: [GC (Allocation Failure) 2021-09-09T14:44:04.853+0800: 0.203:
[DefNew: 157248K->17471K(157248K), 0.0192998 secs] 185563K->84401K(506816K), 0.0193485 secs] [Times: user=0.02 sys=0.01, real=0.02 secs]
-----------------------Full GC----------------------------
2021-09-09T14:44:05.240+0800: 0.589: [GC (Allocation Failure) 2021-09-09T14:44:05.240+0800: 0.589:
[DefNew: 157148K->157148K(157248K), 0.0000289 secs]2021-09-09T14:44:05.240+0800: 0.589:
[Tenured: 341758K->308956K(349568K), 0.0459961 secs] 498907K->308956K(506816K),
[Metaspace: 2608K->2608K(1056768K)], 0.0460956 secs] [Times: user=0.05 sys=0.00, real=0.05 secs]
----------------------以下为将Xmx和Xms设置为256m后新产生的Full GC日志----------------------------------
2021-09-09T20:43:46.806+0800: 0.340: [GC (Allocation Failure) 2021-09-09T20:43:46.806+0800: 0.340:
[DefNew: 69734K->69734K(78656K), 0.0000272 secs]2021-09-09T20:43:46.806+0800: 0.340:
[Tenured: 167334K->174631K(174784K), 0.0251452 secs] 237068K->181953K(253440K),
[Metaspace: 2760K->2760K(1056768K)], 0.0253029 secs] [Times: user=0.02 sys=0.00, real=0.03 secs]

2021-09-09T20:43:46.841+0800: 0.375: [Full GC (Allocation Failure) 2021-09-09T20:43:46.841+0800: 0.375:
[Tenured: 174631K->174502K(174784K), 0.0228937 secs] 252975K->195967K(253440K),
[Metaspace: 2760K->2760K(1056768K)], 0.0230051 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]

Minor GC日志解读

1
2
java复制代码2021-09-09T14:44:04.813+0800: 0.163: [GC (Allocation Failure) 2021-09-09T14:44:04.813+0800: 0.163: 
[DefNew: 139776K->17472K(157248K), 0.0164545 secs] 139776K->45787K(506816K), 0.0165501 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
  • 2021-09-09T14:44:04.813-GC事件开始的时间点。+0800表示当前时区为东八区,这只是一个标识。0.163是GC事件相对于JVM启动时间的间隔,单位是秒
  • GC - 用来区分Minor GC还是Full GC的标志。GC表明这是一次小型GC(Minor GC),即年轻代GC。Allocation Failure 表示触发GC的原因。本次GC事件是由于对象分配失败,即年轻代中没有空间来存放新生成的对象引起的。
  • DefNew表示垃圾收集器的名称。这个名称表示:年轻到使用的单线程、标记-复制、STW的垃圾收集器。139776K->17472K 表示在垃圾收集之前和之后的年轻代使用量。(157248K) 表示年轻代的总空间大小。分析可得,GC之后年轻代使用率为11%。
  • 139776K->45787K 表示在垃圾收集前后整个堆内存的使用情况,(506816K)表示整个堆的大小
  • 0.0165501 secs - GC事件的持续时间,单位:秒
  • [Times: user=0.00 sys=0.02, real=0.02 secs] 表示此次GC事件的持续时间,通过三个部分来衡量:user 表示所有GC线程消耗的CPU时间;sys 表示系统调用和系统等待事件消耗的时间;real表示应用程序暂停的时间

总结

从上边的日志中我们可以总结出,本次Minor GC造成系统延迟20ms,这个暂停时间对大部分系统来说都是可以接受的,但对某些延迟敏感的系统就不太理想了。
​

同时可以分析出JVM在GC事件中的内存使用以及变化情况,在此次垃圾收集之前,堆内存总使用量为139776K,其中年轻代使用了139776K。这很明显表示GC之前老年代使用量为0(因为是第一次GC嘛,又没有大对象)。
​

GC前后对比,年轻代的使用量为139776K->17472K,减少了122304K,但是堆内存的总使用量139776K->45787K只减少了93989K,说明从年轻代提升到老年代的对象占122304K - 93989K = 28315K的内存空间,我们也可以通过另一种方式将GC后老年代的使用量算出来,就是GC后堆内存的使用量 - 年轻代的使用量 —- 45787K - 17472K = 28315K。

分析下来,我们关注的主要是两个数据:GC暂停时间,以及GC之后的内存使用量/使用率。
​

Full GC日志解读

Full GC是针对新生代+老年代+方法区进行的垃圾回收

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码2021-09-09T14:44:05.240+0800: 0.589: [GC (Allocation Failure) 2021-09-09T14:44:05.240+0800: 0.589: 
[DefNew: 157148K->157148K(157248K), 0.0000289 secs]2021-09-09T14:44:05.240+0800: 0.589:
[Tenured: 341758K->308956K(349568K), 0.0459961 secs] 498907K->308956K(506816K),
[Metaspace: 2608K->2608K(1056768K)], 0.0460956 secs] [Times: user=0.05 sys=0.00, real=0.05 secs]
----------------------以下为将Xmx和Xms设置为256m后新产生的Full GC日志----------------------------------
2021-09-09T20:43:46.806+0800: 0.340: [GC (Allocation Failure) 2021-09-09T20:43:46.806+0800: 0.340:
[DefNew: 69734K->69734K(78656K), 0.0000272 secs]2021-09-09T20:43:46.806+0800: 0.340:
[Tenured: 167334K->174631K(174784K), 0.0251452 secs] 237068K->181953K(253440K),
[Metaspace: 2760K->2760K(1056768K)], 0.0253029 secs] [Times: user=0.02 sys=0.00, real=0.03 secs]

2021-09-09T20:43:46.841+0800: 0.375: [Full GC (Allocation Failure) 2021-09-09T20:43:46.841+0800: 0.375:
[Tenured: 174631K->174502K(174784K), 0.0228937 secs] 252975K->195967K(253440K),
[Metaspace: 2760K->2760K(1056768K)], 0.0230051 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]

首先看Xmx和Xms为512m下的Full GC日志

  • DefNew 157148K->157148K(157248K), 0.0000289 secs就不多讲了,上边已经讲过了
  • Tenured: 用于清理老年代空间的垃圾收集器,Tenured表明使用的是单线程的STW垃圾收集器,使用的算法为标记-清除-整理算法,341758K->308956K(349568K)表示 GC前后老年代的使用量,以及老年代的空间大小,0.0459961 secs是清理老年代使用的时间
  • 498907K->308956K(506816K) 表示在GC前后整个堆内存部分的使用情况,以及可用的堆空间大小
  • Metaspace: 2760K->2760K(1056768K)表示元空间的变化情况,可以看出此次GC元空间没有变化
  • Times: user=0.05 sys=0.00, real=0.05 secs上边已经讲过了,可以看到这次的GC暂停时间为50毫秒,比起年轻代的GC来说增加了一倍,这个时间与GC后存活对象的总数量关系最大

此次GC之后老年代的使用占比为308956K / 349568K = 88%,已经不算低了,但是也不能说明什么问题,毕竟GC后内侧使用量下降了,还需要后续的观察。。

总结

FullGC主要关注GC后内侧使用量是否下降,其次关注暂停时间,此次GC后老年代使用量大约为301MB左右,耗时50ms,如果内存扩大十倍,那耗时可能就是500ms甚至更高,系统就会有很明显的影响了,这也就是说串行GC性能弱的一个原因,服务端一般是不会采用串行GC的。
​

再看Xmx和Xms设置为256m后新产生的Full GC,并没有回收年轻代,只是回收了老年代和元空间,这是为何呢?个人猜想是由于新生代没有对象可回收并且老年代回收后也没有空间存放新对象,因此JVM就不再回收新生代,而是收集老年代,为的是新生代的对象可以晋升到老年代中,从日志中可以看出,第二次Full GC之前的那次GC日志,年轻代使用率在GC前后没有变化,并且老年代GC后使用率反而升高了达到了老年代容量, 才导致的第二次无新生代的FullGC.

Parallel GC

Java启动命令:

1
java复制代码java -Xmx512m -Xms512m -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseParallelGC -XX:-UseAdaptiveSizePolicy GCLogAnalysis

通过-Xloggc:gc.log将GC日志输出到gc.log文件中,详细GC日志如下

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
java复制代码---------------------第一部分------------------------------
Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for windows-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:36:39 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16645364k(7836692k free), swap 19431936k(6065040k free)
CommandLine flags:
-XX:InitialHeapSize=536870912
-XX:MaxHeapSize=536870912
-XX:+PrintGC
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:-UseAdaptiveSizePolicy
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:-UseLargePagesIndividualAllocation
-XX:+UseParallelGC
---------------------第二部分------------------------------
2021-09-09T22:08:14.204+0800: 0.393:
[GC (Allocation Failure)
[PSYoungGen: 153077K->21497K(153088K)] 446120K->355171K(502784K), 0.0096928 secs] [Times: user=0.05 sys=0.11, real=0.01 secs]
---------------------第三部分------------------------------
..........中间省略部分............
2021-09-09T22:08:14.214+0800: 0.403: [Full GC (Ergonomics) [PSYoungGen: 21497K->0K(153088K)] [ParOldGen: 333673K->249484K(349696K)] 355171K->249484K(502784K), [Metaspace: 2760K->2760K(1056768K)], 0.0326259 secs] [Times: user=0.31 sys=0.00, real=0.03 secs]
..........中间省略部分............
---------------------第四部分------------------------------
Heap
PSYoungGen total 153088K, used 106759K [0x00000000f5580000, 0x0000000100000000, 0x0000000100000000)
eden space 131584K, 81% used [0x00000000f5580000,0x00000000fbdc1ca0,0x00000000fd600000)
from space 21504K, 0% used [0x00000000fd600000,0x00000000fd600000,0x00000000feb00000)
to space 21504K, 0% used [0x00000000feb00000,0x00000000feb00000,0x0000000100000000)
ParOldGen total 349696K, used 331587K [0x00000000e0000000, 0x00000000f5580000, 0x00000000f5580000)
object space 349696K, 94% used [0x00000000e0000000,0x00000000f43d0fb8,0x00000000f5580000)
Metaspace used 2766K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 307K, capacity 386K, committed 512K, reserved 1048576K

新生代Minor GC

1
2
3
java复制代码2021-09-09T22:08:14.204+0800: 0.393:
[GC (Allocation Failure)
[PSYoungGen: 153077K->21497K(153088K)] 446120K->355171K(502784K), 0.0096928 secs] [Times: user=0.05 sys=0.11, real=0.01 secs]
  1. 2021-09-09T22:08:13.985+0800: 0.175代表GC发生的时间
  2. GC – 用来区分 Minor GC 还是 Full GC 的标志。这里是一次 小型GC(Minor GC)
  3. PSYoungGen:垃圾收集器的名称,这个名称表示的是在年轻代中使用的:并行的标记-复制,STW垃圾收集器,
  4. 153077K->21497K(153088K)表示年轻代回收前->回收后(年轻代内存大小),GC后新生代使用率为 21497K / 153088K= 14%
  5. 446120K->355171K(502784K)表示堆内存回收前大小->堆内存回收后大小(堆内存大小),GC后堆内存使用率为 355171K / 502784K = 70%,使用率并不低
  6. [Times: user=0.05 sys=0.11, real=0.01 secs] :GC事件的持续时间,通过三个部分衡量,user表示GC线程所消耗的总CPU时间,sys表示操作系统和系统等待事件所消耗的时间,real则表示应用程序实际暂停时间。由于并不是所有的操作过程都能全部并行,所以在并行GC中,real约等于user+system/GC线程数。

​

通过这部分日志可以简单计算出:
在GC之前,堆内存总使用量为446120K,其中年轻代为153077K,因此可以推算出回收前老年代使用量293043K;
在GC完成后,年轻代使用量减少了153077K- 21497K=131580K,总的堆内存减少了446120K - 355171K=90949K,年轻代减少了 14%,那么可以计算出对象从新生代晋升到老年代131580K- 90949K= 40631K
因此GC完成后,老年代的使用量为293043 K + 40631K =273674K
老年代的大小为堆内存总量 - 年轻代总量 — 502784K - 153088K = 349696K ,使用率为 273674K / 349696K = 78%
​

总结

年轻代GC,我们可以关注暂停时间,以及GC后的内存使用率是否正常,但不用特别关注GC前的使用量,而且只要业务在运行,年轻代的对象分配就少不了,回收量就不会减少

Full GC

1
2
3
4
5
6
7
8
9
java复制代码2021-09-09T22:08:14.204+0800: 0.393:
[GC (Allocation Failure)
[PSYoungGen: 153077K->21497K(153088K)] 446120K->355171K(502784K), 0.0096928 secs] [Times: user=0.05 sys=0.11, real=0.01 secs]

2021-09-09T22:08:14.214+0800: 0.403:
[Full GC (Ergonomics)
[PSYoungGen: 21497K->0K(153088K)]
[ParOldGen: 333673K->249484K(349696K)] 355171K->249484K(502784K),
[Metaspace: 2760K->2760K(1056768K)], 0.0326259 secs] [Times: user=0.31 sys=0.00, real=0.03 secs]

由于Full GC是对新生代+老年代的收集+方法区(元空间),因此GC日志中有新生代、老年代、元空间GC前后及内存大小展示。

  • Full GC是完全GC的标志,表明本次GC清理年轻代+老年代+方法区
  • PSYoungGen 21497K->0K(153088K)和上边一样,不作过多阐述
  • ParOldGen-用于清理老年代的垃圾收集器类型,在这里使用的是名为ParOldGen的垃圾收集器,这是一款并行STW垃圾收集器,算法为标记-清除-整理。333673K->249484KGC前后老年代的内存使用量变化,(349696K)老年代总容量,GC之前老年代使用率为 333673K / 349696K = 95%,GC后老年代使用率为 71%,回收了不少,但是不能计算出有年轻代对象晋升了多少到老年代,因为老年代减少的使用量是被提升的内存抵消过的。
  • Metaspace: 2760K->2760K(1056768K) 元空间并没有被回收掉任何对象
  • 0.0326259 secs]表示GC事件持续的时间,单位:秒
  • Times: user=0.31 sys=0.00, real=0.03 secs:与前边讲的相同

总结

FullGC与MinorGC的唯一区别就是增加了对老年代和方法区(元空间)的回收。
FullGC我们更关注老年代的使用量有没有下降,以及下降了多少,如果FullGC之后内存使用率没有下降并且还很高,那说明系统就有问题了
这里我也把FullGC前一次的GC日志拿出来了,可以看到由于前一次的MinorGC后老年代使用率为78%,才导致的FullGC。

堆内存分布

  • PSYoungGen total 75776K, used 63884K:年轻代占用75776K,,使用了63884K
    • eden space 65024K, 98% used,伊甸区占用了新生代65024K,其中98%被使用
    • from space 10752K, 0% used
    • to space 10752K, 0% used
  • ParOldGen total 180736K, used 180448K,老年代总共180736K,使用了180448K
  • Metaspace used 2766K, capacity 4486K, committed 4864K, reserved 1056768K:元数据区总计使用了2766K,容量是4486K,JVM保证可用的大小是4864K,保留空间1GB左右
    • class space used 307K, capacity 386K, committed 512K, reserved 1048576K,class space使用了307K,容量是386K

CMS GC

Java启动命令

1
java复制代码java  -XX:+UseConcMarkSweepGC  -Xmx512m -Xms512m -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps GCLogAnalysis

通过-Xloggc:gc.log将GC日志输出到gc.log文件中,详细GC日志如下

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
java复制代码--------------------------第一部分----------------------------------
Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for windows-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:36:39 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16657944k(2966240k free), swap 40559696k(11356008k free)
CommandLine flags:
-XX:InitialHeapSize=536870912
-XX:MaxHeapSize=536870912
-XX:MaxNewSize=178958336
-XX:MaxTenuringThreshold=6
-XX:NewSize=178958336
-XX:OldPLABSize=16
-XX:OldSize=357912576
-XX:+PrintGC
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:+UseConcMarkSweepGC
-XX:-UseLargePagesIndividualAllocation
-XX:+UseParNewGC
通过解析第一部分日志,我们可以看到,初始化堆内存大小与最大堆内存大小与并行GC是一样的,指定-XX:+UseConcMarkSweepGC默认会加上-XX:+UseParNewGC作为新生代GC
--------------------------第二部分MinorGC----------------------------------
2021-09-10T10:04:53.039+0800: 0.312: [GC (Allocation Failure) 2021-09-10T10:04:53.039+0800: 0.312:
[ParNew: 157248K->17472K(157248K), 0.0148265 secs] 321753K->222842K(506816K), 0.0148801 secs] [Times: user=0.03 sys=0.03, real=0.01 secs]
--------------------------第三部分Major GC----------------------------------
2021-09-10T10:04:53.054+0800: 0.327: [GC (CMS Initial Mark) [1 CMS-initial-mark: 205370K(349568K)] 223428K(506816K), 0.0001030 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T10:04:53.054+0800: 0.327: [CMS-concurrent-mark-start]
2021-09-10T10:04:53.057+0800: 0.330: [CMS-concurrent-mark: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T10:04:53.057+0800: 0.330: [CMS-concurrent-preclean-start]
2021-09-10T10:04:53.058+0800: 0.331: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T10:04:53.058+0800: 0.331: [CMS-concurrent-abortable-preclean-start]
---------------------------中间穿插着MinorGC-----------------------------
2021-09-10T10:04:53.076+0800: 0.349: [GC (Allocation Failure) 2021-09-10T10:04:53.076+0800: 0.349: [ParNew: 157248K->17472K(157248K), 0.0156403 secs] 362618K->266214K(506816K), 0.0157138 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
2021-09-10T10:04:53.113+0800: 0.387: [GC (Allocation Failure) 2021-09-10T10:04:53.113+0800: 0.387: [ParNew: 157248K->17472K(157248K), 0.0155434 secs] 405990K->308435K(506816K), 0.0155941 secs] [Times: user=0.05 sys=0.02, real=0.02 secs]
2021-09-10T10:04:53.149+0800: 0.423: [GC (Allocation Failure) 2021-09-10T10:04:53.149+0800: 0.423: [ParNew: 157074K->17472K(157248K), 0.0173170 secs] 448038K->353000K(506816K), 0.0173738 secs] [Times: user=0.05 sys=0.00, real=0.02 secs]
2021-09-10T10:04:53.167+0800: 0.440: [CMS-concurrent-abortable-preclean: 0.003/0.109 secs] [Times: user=0.17 sys=0.02, real=0.11 secs]
2021-09-10T10:04:53.167+0800: 0.440: [GC (CMS Final Remark) [YG occupancy: 20972 K (157248 K)]2021-09-10T10:04:53.167+0800: 0.440: [Rescan (parallel) , 0.0002117 secs]2021-09-10T10:04:53.167+0800: 0.440: [weak refs processing, 0.0000083 secs]2021-09-10T10:04:53.167+0800: 0.441: [class unloading, 0.0002509 secs]2021-09-10T10:04:53.168+0800: 0.441: [scrub symbol table, 0.0003192 secs]2021-09-10T10:04:53.168+0800: 0.441: [scrub string table, 0.0001136 secs][1 CMS-remark: 335528K(349568K)] 356500K(506816K), 0.0009630 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T10:04:53.168+0800: 0.441: [CMS-concurrent-sweep-start]
2021-09-10T10:04:53.169+0800: 0.442: [CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T10:04:53.169+0800: 0.442: [CMS-concurrent-reset-start]
2021-09-10T10:04:53.169+0800: 0.442: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

Minor GC

1
2
3
4
java复制代码2021-09-10T10:04:53.039+0800: 0.312: 
[GC (Allocation Failure) 2021-09-10T10:04:53.039+0800: 0.312:
[ParNew: 157248K->17472K(157248K), 0.0148265 secs] 321753K->222842K(506816K), 0.0148801 secs]
[Times: user=0.03 sys=0.03, real=0.01 secs]

前边已经讲过的内容不再重复,只解析未解析过的内容

  • ParNew是垃圾收集器的名称,对应的就是前边打印的-XX:+UseParNewGC,ParNew收集器是年轻代的垃圾收集器,用的是标记-复制算法,专门设计用来配合CMS垃圾收集器, 157248K->17472K表示的是新生代GC前后使用量的变化,(157248K)是年轻代总大小。0.0148265 secs是消耗时间
  • 321753K->222842K(506816K)表示GC前后堆内存的使用量变化,以及堆内存的总大小,消耗时间是0.0148801 secs
  • Times: user=0.03 sys=0.03, real=0.01 secs:表示GC的持续时间,real ~= (user + sys) / GC线程数

结果分析:
GC前,年轻代使用量为 100%,堆内存使用量为 63%,因此可以计算出老年代大小为506816K - 157248 = 349568K ,GC前使用量为321753K - 157428K = 164505K老年代收的使用率为 164505 / 349568 = 47%。
GC后,年轻代使用量为17472K / 157248K = 11%,下降了139776K,堆内存使用量为222842K / 506816K = 43% ,下降了98911K,两值相减就是年轻代提升到老年代的对象大小:40865K,GC后老年代使用率为( 164505K + 40865K ) / 349568K ~= 58%

Major GC

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
java复制代码2021-09-10T10:04:53.054+0800: 0.327: [GC (CMS Initial Mark) [1 CMS-initial-mark: 205370K(349568K)] 223428K(506816K), 0.0001030 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2021-09-10T10:04:53.054+0800: 0.327: [CMS-concurrent-mark-start]
2021-09-10T10:04:53.057+0800: 0.330: [CMS-concurrent-mark: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T10:04:53.057+0800: 0.330: [CMS-concurrent-preclean-start]
2021-09-10T10:04:53.058+0800: 0.331: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T10:04:53.058+0800: 0.331: [CMS-concurrent-abortable-preclean-start]
==================================中间穿插着MinorGC================================
2021-09-10T10:04:53.076+0800: 0.349: [GC (Allocation Failure) 2021-09-10T10:04:53.076+0800: 0.349: [ParNew: 157248K->17472K(157248K), 0.0156403 secs] 362618K->266214K(506816K), 0.0157138 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
2021-09-10T10:04:53.113+0800: 0.387: [GC (Allocation Failure) 2021-09-10T10:04:53.113+0800: 0.387: [ParNew: 157248K->17472K(157248K), 0.0155434 secs] 405990K->308435K(506816K), 0.0155941 secs] [Times: user=0.05 sys=0.02, real=0.02 secs]
2021-09-10T10:04:53.149+0800: 0.423: [GC (Allocation Failure) 2021-09-10T10:04:53.149+0800: 0.423: [ParNew: 157074K->17472K(157248K), 0.0173170 secs] 448038K->353000K(506816K), 0.0173738 secs] [Times: user=0.05 sys=0.00, real=0.02 secs]
==================================================================================
2021-09-10T10:04:53.167+0800: 0.440: [CMS-concurrent-abortable-preclean: 0.003/0.109 secs] [Times: user=0.17 sys=0.02, real=0.11 secs]
----------------------------------最终标记------------------------------------
2021-09-10T10:04:53.167+0800: 0.440: [GC (CMS Final Remark)
[YG occupancy: 20972 K (157248 K)]2021-09-10T10:04:53.167+0800: 0.440:
[Rescan (parallel) , 0.0002117 secs]2021-09-10T10:04:53.167+0800: 0.440:
[weak refs processing, 0.0000083 secs]2021-09-10T10:04:53.167+0800: 0.441:
[class unloading, 0.0002509 secs]2021-09-10T10:04:53.168+0800: 0.441:
[scrub symbol table, 0.0003192 secs]2021-09-10T10:04:53.168+0800: 0.441:
[scrub string table, 0.0001136 secs]
[1 CMS-remark: 335528K(349568K)] 356500K(506816K), 0.0009630 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T10:04:53.168+0800: 0.441: [CMS-concurrent-sweep-start]
2021-09-10T10:04:53.169+0800: 0.442: [CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T10:04:53.169+0800: 0.442: [CMS-concurrent-reset-start]
2021-09-10T10:04:53.169+0800: 0.442: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

这次MajorGC是紧接着前边的MinorGC的,前一次MinorGC后Old区的使用率才58%,怎么就触发CMS MajorGC了呢,前边在将CMS的时候明明说过触发CMS GC的条件是达到了阈值之后才会触发,并且默认阈值为92%,但是这里才58%怎么就触发了呢?
​

原因我也去查了下,大家可以看这篇文章:JVM 源码解读之 CMS GC 触发条件,这里我也做下简单解释,是由于当我们没有配置UseCMSInitiatingOccupancyOnly 时,会根据统计数据动态判断是否需要进行一次CMS GC,判断逻辑是,如果预测CMS GC完成所需要的的时间大于预计老年代将要填满的时间,则进行GC,这些判断是需要历史的CMS统计指标,然后第一次CMS GC时统计数据还没有形成,这时会根据老年代的使用率来判断是否要进行GC,答案就是50%
​

CMS相关介绍可以查看我这篇文章:CMS垃圾收集器
​

接下来进行日志解析
​

阶段1:初始标记
该阶段是STW的阶段,目的是标记所有的根对象包括根对象直接引用的对象,以及被年轻代中所有存活对象引用对象速度很快

1
2
3
4
java复制代码2021-09-10T10:04:53.054+0800: 0.327:
[GC (CMS Initial Mark)
[1 CMS-initial-mark: 205370K(349568K)] 223428K(506816K), 0.0001030 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
  • GC (CMS Initial Mark)—这个阶段的名称为“ Initial Mark”
  • [1 CMS-initial-mark: 205370K(349568K)] 表示老年代的使用量以及老年代的大小
  • 223428K(506816K), 0.0001030 secs–当前堆内存的使用量,以及可用的堆内存大小、GC消耗的时间,时间0.1毫秒,非常短,因为要标记的对象很少
  • [Times: user=0.00 sys=0.00, real=0.00 secs] –初始标记暂停的时间,可以看到被忽略不计了

阶段2:并发标记
并发标记是从“初始标记”阶段编辑的根元素开始,标记所有存活的对象,GC线程与用户线程同时运行。

1
2
java复制代码2021-09-10T10:04:53.054+0800: 0.327: [CMS-concurrent-mark-start]
2021-09-10T10:04:53.057+0800: 0.330: [CMS-concurrent-mark: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  • CMS-concurrent-mark-start表明当前为CMS的并发标记阶段
  • 0.003/0.003 secs-此阶段的持续时间,分别是GC线程消耗的时间和实际消耗的时间
  • [Times: user=0.00 sys=0.00, real=0.00 secs]-对于并发阶段来说这些时间并没有多少意义,因为啥从并发标记时刻计算的,而这段时间应用程序也在执行,所以这个时间至少一个大概的值

阶段3:并发预清理
此阶段也是与用户线程同时运行的,主要是来出来“并发标记”阶段“脏卡”的老年代对象,为了减少Final Remark阶段STW的时间,可通过-XX:-CMSPrecleaningEnabled关闭,默认开启。

1
2
java复制代码2021-09-10T10:04:53.057+0800: 0.330: [CMS-concurrent-preclean-start]
2021-09-10T10:04:53.058+0800: 0.331: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

与“并发标记”阶段相同
​

阶段4:可取消的并发预清理
具体此阶段的流程这里就不细讲了,目的与“并发预清理”一样,也是为了减少Final Remark阶段STW的时间,在进入Final Remark阶段前尽量等到一个Minor GC。具体的可以看我这篇CMS垃圾收集器。

1
2
3
4
5
6
7
java复制代码2021-09-10T10:04:53.058+0800: 0.331: [CMS-concurrent-abortable-preclean-start]
==================================中间穿插着MinorGC================================
2021-09-10T10:04:53.076+0800: 0.349: [GC (Allocation Failure) 2021-09-10T10:04:53.076+0800: 0.349: [ParNew: 157248K->17472K(157248K), 0.0156403 secs] 362618K->266214K(506816K), 0.0157138 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
2021-09-10T10:04:53.113+0800: 0.387: [GC (Allocation Failure) 2021-09-10T10:04:53.113+0800: 0.387: [ParNew: 157248K->17472K(157248K), 0.0155434 secs] 405990K->308435K(506816K), 0.0155941 secs] [Times: user=0.05 sys=0.02, real=0.02 secs]
2021-09-10T10:04:53.149+0800: 0.423: [GC (Allocation Failure) 2021-09-10T10:04:53.149+0800: 0.423: [ParNew: 157074K->17472K(157248K), 0.0173170 secs] 448038K->353000K(506816K), 0.0173738 secs] [Times: user=0.05 sys=0.00, real=0.02 secs]
==================================================================================
2021-09-10T10:04:53.167+0800: 0.440: [CMS-concurrent-abortable-preclean: 0.003/0.109 secs] [Times: user=0.17 sys=0.02, real=0.11 secs]

可以看到预清理过程中,发生了三次Minor GC。
​

阶段5:最终标记
最终标记是CMS GC中的第二次STW,也是最后一次,该阶段重新扫描堆中的对象,因为之前的预清理阶段是并发执行的,有可能GC线程跟不上应用线程的修改速度,该阶段需要扫描**新生代+GC Roots + 被标记为“脏区”的对象,**如果预清理阶段没有做好,这一步扫描新生代会非常耗时

1
2
3
4
5
6
7
8
9
java复制代码2021-09-10T10:04:53.167+0800: 0.440: [GC (CMS Final Remark) 
[YG occupancy: 20972 K (157248 K)]2021-09-10T10:04:53.167+0800: 0.440:
[Rescan (parallel) , 0.0002117 secs]2021-09-10T10:04:53.167+0800: 0.440:
[weak refs processing, 0.0000083 secs]2021-09-10T10:04:53.167+0800: 0.441:
[class unloading, 0.0002509 secs]2021-09-10T10:04:53.168+0800: 0.441:
[scrub symbol table, 0.0003192 secs]2021-09-10T10:04:53.168+0800: 0.441:
[scrub string table, 0.0001136 secs]
[1 CMS-remark: 335528K(349568K)] 356500K(506816K), 0.0009630 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
  • CMS Final Remark -阶段名称,最终标记阶段
  • YG occupancy: 20972 K (157248 K) - 当前年轻代的使用量和总容量
  • Rescan (parallel) , 0.0002117 secs - 在程序暂停后进行重新扫描,已完成存活对象的标记。并行执行,耗时0.0002117 secs
  • weak refs processing, 0.0000083 secs - 第一个子阶段,处理弱引用,耗时0.0002117 secs
  • class unloading, 0.0002509 secs - 第二个子阶段,卸载不使用的类,耗时0.0002509 secs
  • scrub symbol table, 0.0003192 secs - 第三个子阶段,清理符号表,即持有class级别的metadata的符号表(symbol table)
  • 1 CMS-remark: 335528K(349568K) - 此阶段完成后老年代的使用量和总容量
  • 356500K(506816K), 0.0009630 secs - 此阶段完成整个堆内存的使用量和总容量,耗时

​

阶段6:并发清除
此阶段也是与用户线程同时运行的,删除不再使用的对象,并回收他们占用的内存空间,由于是与用户线程并发执行,因此可能会产生“浮动垃圾”

1
2
java复制代码2021-09-10T10:04:53.168+0800: 0.441: [CMS-concurrent-sweep-start]
2021-09-10T10:04:53.169+0800: 0.442: [CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

与前边的阶段类似,不再讲解
​

阶段7:并发重置
此阶段也是与用户线程同时运行的,重置CMS算法相关的内部结构,下次触发GC时就可以直接使用

1
2
java复制代码2021-09-10T10:04:53.169+0800: 0.442: [CMS-concurrent-reset-start]
2021-09-10T10:04:53.169+0800: 0.442: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

具体CMS GC后老年代内存使用量是多少这里并不能分析出来,只能通过后边的Minor GC日志分析,例如本次CMS GC后的Minor GC日志如下

1
2
3
java复制代码2021-09-10T11:20:07.151+0800: 0.615: 
[GC (Allocation Failure) 2021-09-10T11:20:07.151+0800: 0.615:
[ParNew: 157248K->17472K(157248K), 0.0123639 secs] 446745K->354158K(506816K), 0.0124328 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]

计算出来老年代的使用率大约为83%,这个使用率并不度,说明了什么问题呢,一般就是分配的内存小了,毕竟我们才指定了512M最大堆内存
​

Full GC

当并发模式失败(Concurrent mod failure)会触发Full GC。

  • **并发模式失败:**CMS大部分阶段是与用户线程并发执行的,如果在执行垃圾收集时用户线程创建的对象直接往老年代分配,但是没有足够的内存,就会报Concurrent mode failure
  • **晋升失败:**新生代做Minor GC的时候,老年代没有足够的空间用来存放晋升的对象,则会报Concurrent mode failure;如果由于内存碎片问题导致无法分配,就会报晋升失败

可以看到下边的日志,先发生了一次concurrent mode failure,后边紧接着发生了一次Full GC

1
2
3
4
java复制代码CMS2021-09-10T22:33:50.168+0800: 0.562: [CMS-concurrent-abortable-preclean: 0.000/0.017 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
(concurrent mode failure): 331593K->349531K(349568K), 0.0564298 secs] 471258K->361742K(506816K), [Metaspace: 2760K->2760K(1056768K)], 0.0566131 secs] [Times: user=0.05 sys=0.00, real=0.06 secs]

2021-09-10T22:33:50.246+0800: 0.640: [Full GC (Allocation Failure) 2021-09-10T22:33:50.246+0800: 0.640: [CMS: 349531K->349151K(349568K), 0.0528130 secs] 506497K->392533K(506816K), [Metaspace: 2760K->2760K(1056768K)], 0.0529324 secs] [Times: user=0.05 sys=0.00, real=0.05 secs]

G1 GC

Java启动命令

1
java复制代码java  -XX:+UseG1GC  -Xmx512m -Xms512m -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:-UseAdaptiveSizePolicy GCLogAnalysis

详细GC日志如下

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
java复制代码Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for windows-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:36:39 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16657944k(1124124k free), swap 40559696k(8067960k free)
CommandLine flags:
-XX:InitialHeapSize=536870912
-XX:MaxHeapSize=536870912
-XX:+PrintGC
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:-UseAdaptiveSizePolicy
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:+UseG1GC
-XX:-UseLargePagesIndividualAllocation

2021-09-10T14:31:56.590+0800: 0.429: [GC pause (G1 Evacuation Pause) (young), 0.0122670 secs]
[Parallel Time: 2.6 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 437.9, Avg: 437.9, Max: 438.0, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.2, Avg: 0.8, Max: 2.5, Diff: 2.3, Sum: 3.1]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.0, Avg: 1.6, Max: 2.3, Diff: 2.3, Sum: 6.5]
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 0.6]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 2.5, Avg: 2.6, Max: 2.6, Diff: 0.1, Sum: 10.4]
[GC Worker End (ms): Min: 440.5, Avg: 440.5, Max: 440.5, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 9.6 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.3 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 25.0M(25.0M)->0.0B(21.0M) Survivors: 0.0B->4096.0K Heap: 31.2M(512.0M)->13.9M(512.0M)]
[Times: user=0.00 sys=0.00, real=0.01 secs]

----------------------------------------------------------------------
2021-09-10T14:31:57.007+0800: 0.846: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0130786 secs]
[Parallel Time: 12.8 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 845.8, Avg: 845.9, Max: 846.0, Diff: 0.2]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.4]
[Update RS (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.0, Sum: 0.6]
[Processed Buffers: Min: 2, Avg: 2.8, Max: 3, Diff: 1, Sum: 11]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 11.8, Avg: 12.0, Max: 12.4, Diff: 0.6, Sum: 47.8]
[Termination (ms): Min: 0.0, Avg: 0.4, Max: 0.6, Diff: 0.6, Sum: 1.7]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 12.6, Avg: 12.7, Max: 12.8, Diff: 0.2, Sum: 50.6]
[GC Worker End (ms): Min: 858.5, Avg: 858.6, Max: 858.6, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.3 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 118.0M(123.0M)->0.0B(161.0M) Survivors: 12.0M->17.0M Heap: 321.1M(512.0M)->221.0M(512.0M)]
[Times: user=0.00 sys=0.06, real=0.01 secs]


------------------------------------------------------------------
2021-09-10T14:31:57.020+0800: 0.859: [GC concurrent-root-region-scan-start]
2021-09-10T14:31:57.020+0800: 0.859: [GC concurrent-root-region-scan-end, 0.0001075 secs]
2021-09-10T14:31:57.020+0800: 0.859: [GC concurrent-mark-start]
2021-09-10T14:31:57.023+0800: 0.861: [GC concurrent-mark-end, 0.0023080 secs]
2021-09-10T14:31:57.023+0800: 0.862: [GC remark 2021-09-10T14:31:57.023+0800: 0.862: [Finalize Marking, 0.0000914 secs] 2021-09-10T14:31:57.023+0800: 0.862: [GC ref-proc, 0.0000388 secs] 2021-09-10T14:31:57.023+0800: 0.862: [Unloading, 0.0003763 secs], 0.0008956 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T14:31:57.024+0800: 0.863: [GC cleanup 232M->230M(512M), 0.0002672 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T14:31:57.024+0800: 0.863: [GC concurrent-cleanup-start]
2021-09-10T14:31:57.024+0800: 0.863: [GC concurrent-cleanup-end, 0.0000103 secs]
---------------------------------------------------------------------
2021-09-10T14:31:57.063+0800: 0.902: [GC pause (G1 Evacuation Pause) (young) (to-space exhausted), 0.0028671 secs]
[Parallel Time: 2.1 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 902.0, Avg: 902.2, Max: 902.4, Diff: 0.4]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.4]
[Update RS (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 0.8]
[Processed Buffers: Min: 2, Avg: 4.0, Max: 8, Diff: 6, Sum: 16]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 1.6, Avg: 1.6, Max: 1.6, Diff: 0.0, Sum: 6.3]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 1.7, Avg: 1.9, Max: 2.1, Diff: 0.4, Sum: 7.5]
[GC Worker End (ms): Min: 904.1, Avg: 904.1, Max: 904.1, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.7 ms]
[Evacuation Failure: 0.5 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.1 ms]
[Free CSet: 0.0 ms]
[Eden: 161.0M(161.0M)->0.0B(13.0M) Survivors: 17.0M->12.0M Heap: 439.1M(512.0M)->371.4M(512.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]

---------------------------------------------------------------------
2021-09-10T14:31:57.069+0800: 0.908: [GC pause (G1 Evacuation Pause) (mixed), 0.0045144 secs]
[Parallel Time: 4.2 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 908.2, Avg: 908.2, Max: 908.2, Diff: 0.0]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.2, Diff: 0.0, Sum: 0.6]
[Update RS (ms): Min: 0.2, Avg: 0.2, Max: 0.2, Diff: 0.0, Sum: 0.8]
[Processed Buffers: Min: 2, Avg: 3.8, Max: 5, Diff: 3, Sum: 15]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 3.7, Avg: 3.8, Max: 3.8, Diff: 0.1, Sum: 15.0]
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.2]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 4.2, Avg: 4.2, Max: 4.2, Diff: 0.0, Sum: 16.8]
[GC Worker End (ms): Min: 912.3, Avg: 912.4, Max: 912.4, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 13.0M(13.0M)->0.0B(48.0M) Survivors: 12.0M->4096.0K Heap: 389.0M(512.0M)->353.1M(512.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
--------------------------------------------------------------------

Heap
garbage-first heap total 524288K, used 385426K [0x00000000e0000000, 0x00000000e0101000, 0x0000000100000000)
region size 1024K, 4 young (4096K), 2 survivors (2048K)
Metaspace used 2614K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K

以上为G1 GC的部分日志信息,很少一部分。
​

在讲解G1垃圾收集器的文章中,我们讲过G1的垃圾收集分为两个大部分,并且这两部分可以相对独立运行

  • 全局并发标记(Global Concurrent Marking)
  • 拷贝存活对象(Evacuation)

拷贝存活对象阶段又分为两种模式:

  • Yong模式
  • Mix模式

Evacuation Pause Yong(Yong 模式拷贝暂停)

从上边的G1日志中我们就可以找到对应的这两种模式:GC pause (G1 Evacuation Pause) (young) (to-space exhausted),GC pause (G1 Evacuation Pause) (mixed)

当年轻代空间用满后,应用现场会被暂停,年轻代内存快中的存活对象被拷贝到存活区。如果还没有存活区,则任意选择一部分空闲的内存块作为存活区。

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
java复制代码2021-09-10T14:31:57.063+0800: 0.902: 
[GC pause (G1 Evacuation Pause) (young) (to-space exhausted), 0.0028671 secs]
[Parallel Time: 2.1 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 902.0, Avg: 902.2, Max: 902.4, Diff: 0.4]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.4]
[Update RS (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 0.8]
[Processed Buffers: Min: 2, Avg: 4.0, Max: 8, Diff: 6, Sum: 16]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 1.6, Avg: 1.6, Max: 1.6, Diff: 0.0, Sum: 6.3]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 1.7, Avg: 1.9, Max: 2.1, Diff: 0.4, Sum: 7.5]
[GC Worker End (ms): Min: 904.1, Avg: 904.1, Max: 904.1, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.7 ms]
[Evacuation Failure: 0.5 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.1 ms]
[Free CSet: 0.0 ms]
[Eden: 161.0M(161.0M)->0.0B(13.0M)
Survivors: 17.0M->12.0M
Heap: 439.1M(512.0M)->371.4M(512.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
  • GC pause (G1 Evacuation Pause) (young) (to-space exhausted) - G1转移暂停,纯年轻代模式,只清理年轻代空间。这次暂停在JVM启动之后902ms开始,持续的系统时间为0.0028671 secs
  • [Parallel Time: 2.1 ms, GC Workers: 4] - 表明后面的活动由4个Worker线程并行执行,消耗时间为2.1毫秒,worker是一种模式,类似于一个老板指挥多个工人干活儿,中间的内容先不讲,后边会讲解
  • [Code Root Fixup: 0.0 ms] - 释放用于管理并行活动的内部数据,一般都接近于0,这个过程是串行的
  • [Code Root Purge: 0.0 ms] - 清理部分数据,也是非常快的,如非必要基本上等于0,也是串行的
  • [Other: 0.7 ms]- 其他活动消耗的时间,其中大部分是并行执行的,具体内容后边会讲解
  • [Eden: 161.0M(161.0M)->0.0B(13.0M) - 暂停之前和暂停之后,Eden区的使用量/总容量
  • Survivors: 17.0M->12.0M - GC暂停前后,存活区的使用量
  • Heap: 439.1M(512.0M)->371.4M(512.0M)] - 暂停前后整个堆内存的使用量/总容量
  • [Times: user=0.00 sys=0.00, real=0.00 secs] - GC事件的持续时间
    • 说明:系统时间是指一段程序从运行到终止,系统时钟走过的时间,一般系统时间都要比CPU时间略长一些

Worker线程日志解读

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码[GC Worker Start (ms): Min: 902.0, Avg: 902.2, Max: 902.4, Diff: 0.4]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.4]
[Update RS (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 0.8]
[Processed Buffers: Min: 2, Avg: 4.0, Max: 8, Diff: 6, Sum: 16]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 1.6, Avg: 1.6, Max: 1.6, Diff: 0.0, Sum: 6.3]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 1.7, Avg: 1.9, Max: 2.1, Diff: 0.4, Sum: 7.5]
[GC Worker End (ms): Min: 904.1, Avg: 904.1, Max: 904.1, Diff: 0.0]
  • GC Worker Start (ms) - GC的worker线程开始启动时间,相对于pause开始时间的毫秒间隔。如果Min和Max差别很大,则表明本机其他进程所使用的线程数量过多,挤占了GC的可用CPU时间
  • [Ext Root Scanning (ms) - 用了来扫描对外内存(non - heap)的GC Root,如classloader,JNI引用,JVM系统ROOT等。后面显示了运行时间,Sum指的是CPU时间
  • Update RS (ms)、Processed Buffers、Scan RS (ms)这三部分也是类似的,RS是Remember Set的缩写,可以参考我之前写的G1文章
  • Code Root Scanning (ms) - 扫描实际代码中的root用了多长时间:例如线程栈中的局部变量
  • Object Copy (ms) - 用了多长时间来拷贝回收集中的存活对象
  • Termination (ms) - GC的worker线程用了多长时间来确保自身可用安全地停止,在这段时间内什么也不做,完成后GC线程就终止运行了,所以叫终止等待时间
  • Termination Attempts - GC的worker线程尝试多少次try 和terminate。如果worker发现还有一些任务没处理完,则这一次尝试就是失败的,暂时还不能终止
  • GC Worker Other (ms) - 其他小的任务,因为时间很短,在GC日志将他们归结在一起
  • GC Worker Total (ms) - GC的worker线程工作时间总计
  • GC Worker End (ms) - GC的worker线程完成作业时刻,相对于此次GC暂停开始时间的毫秒数。通常涞水这部分数字应该大致相等,否则就说明有太多的线程呗挂起,很可能是因为“坏邻居效应”所导致的

Other日志解读

1
2
3
4
5
6
7
8
java复制代码[Evacuation Failure: 0.5 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.1 ms]
[Free CSet: 0.0 ms]
  • Evacuation Failure: 0.5 ms:拷贝失败耗时
  • Choose CSet:选择回收集耗时
  • Ref Proc - 处理强引用耗时,进行清理或者决定是否需要清理
  • Ref Enq: 0.0 ms - 用来将剩下的non - strong引用排列到合适的ReferenceQueue中
  • Humongous Register、Humongous Reclaim - 大对象相关的比分,后面介绍
  • Free CSet: 0.0 ms - 将回收集中被释放的内存归还所消耗的时间,释放内存以便他们能用来分配新的对象

Concurrent Mark (并发标记)

当堆内存的总体使用比例达到一定数值时,就会触发并发标记。这个默认比例是45%,可以通过参数​
-XX:InitiatingHeapOccupancyPercent来配置。和CMS一样,G1的标记阶段也是多阶段部分并发的。

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
java复制代码2021-09-10T14:31:57.007+0800: 0.846: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0130786 secs]
[Parallel Time: 12.8 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 845.8, Avg: 845.9, Max: 846.0, Diff: 0.2]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.4]
[Update RS (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.0, Sum: 0.6]
[Processed Buffers: Min: 2, Avg: 2.8, Max: 3, Diff: 1, Sum: 11]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 11.8, Avg: 12.0, Max: 12.4, Diff: 0.6, Sum: 47.8]
[Termination (ms): Min: 0.0, Avg: 0.4, Max: 0.6, Diff: 0.6, Sum: 1.7]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 12.6, Avg: 12.7, Max: 12.8, Diff: 0.2, Sum: 50.6]
[GC Worker End (ms): Min: 858.5, Avg: 858.6, Max: 858.6, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.3 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 118.0M(123.0M)->0.0B(161.0M) Survivors: 12.0M->17.0M Heap: 321.1M(512.0M)->221.0M(512.0M)]
[Times: user=0.00 sys=0.06, real=0.01 secs]
2021-09-10T14:31:57.020+0800: 0.859: [GC concurrent-root-region-scan-start]
2021-09-10T14:31:57.020+0800: 0.859: [GC concurrent-root-region-scan-end, 0.0001075 secs]
2021-09-10T14:31:57.020+0800: 0.859: [GC concurrent-mark-start]
2021-09-10T14:31:57.023+0800: 0.861: [GC concurrent-mark-end, 0.0023080 secs]
2021-09-10T14:31:57.023+0800: 0.862: [GC remark 2021-09-10T14:31:57.023+0800: 0.862: [Finalize Marking, 0.0000914 secs] 2021-09-10T14:31:57.023+0800: 0.862: [GC ref-proc, 0.0000388 secs] 2021-09-10T14:31:57.023+0800: 0.862: [Unloading, 0.0003763 secs], 0.0008956 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T14:31:57.024+0800: 0.863: [GC cleanup 232M->230M(512M), 0.0002672 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T14:31:57.024+0800: 0.863: [GC concurrent-cleanup-start]
2021-09-10T14:31:57.024+0800: 0.863: [GC concurrent-cleanup-end, 0.0000103 secs]

阶段1:初始标记
可以在日志中看到(initial-mark)类似这种:

1
java复制代码2021-09-10T14:31:57.007+0800: 0.846: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0130786 secs]

这次并发标记导致的原因是大对象分配失败,还可以看到(yong)字样,由于G1的初始标记是借用Yong GC的暂停做的,可以看到下边随之进行了一次Yong GC。
​

阶段2:Root区扫描
此阶段标记所有从“根区域”可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域

1
2
java复制代码2021-09-10T14:31:57.020+0800: 0.859: [GC concurrent-root-region-scan-start]
2021-09-10T14:31:57.020+0800: 0.859: [GC concurrent-root-region-scan-end, 0.0001075 secs]

阶段3:并发标记
在整个堆中查找标记可以访问的存活对象

1
2
java复制代码2021-09-10T14:31:57.020+0800: 0.859: [GC concurrent-mark-start]
2021-09-10T14:31:57.023+0800: 0.861: [GC concurrent-mark-end, 0.0023080 secs]

阶段4:重新标记
此阶段是STW的,处理在并发标记阶段剩余未处理的SATB写屏障的记录,同时也处理弱引用。

1
2
3
4
5
6
java复制代码2021-09-10T14:31:57.023+0800: 0.862:
[GC remark 2021-09-10T14:31:57.023+0800: 0.862:
[Finalize Marking, 0.0000914 secs] 2021-09-10T14:31:57.023+0800: 0.862:
[GC ref-proc, 0.0000388 secs] 2021-09-10T14:31:57.023+0800: 0.862:
[Unloading, 0.0003763 secs], 0.0008956 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]

阶段5:清理
清理和重置标记状态,与mark - sweep中的sweep阶段类似,但不是在堆上sweep实际对象,会统计每个Region被标记为活的对象有多少,这个阶段如果发现完全没有活对象的Region,就会将其整体回收到可分配Region列表中。

1
2
3
4
java复制代码2021-09-10T14:31:57.024+0800: 0.863: [GC cleanup 232M->230M(512M), 0.0002672 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2021-09-10T14:31:57.024+0800: 0.863: [GC concurrent-cleanup-start]
2021-09-10T14:31:57.024+0800: 0.863: [GC concurrent-cleanup-end, 0.0000103 secs]

标记周期一般只在碰到Region中一个对象没有的时候,才会顺手处理一把,大多数情况下都不释放内存。

Evacuation Pause Mix(混合模式)

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
java复制代码2021-09-10T14:31:57.069+0800: 0.908: [GC pause (G1 Evacuation Pause) (mixed), 0.0045144 secs]
[Parallel Time: 4.2 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 908.2, Avg: 908.2, Max: 908.2, Diff: 0.0]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.2, Diff: 0.0, Sum: 0.6]
[Update RS (ms): Min: 0.2, Avg: 0.2, Max: 0.2, Diff: 0.0, Sum: 0.8]
[Processed Buffers: Min: 2, Avg: 3.8, Max: 5, Diff: 3, Sum: 15]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 3.7, Avg: 3.8, Max: 3.8, Diff: 0.1, Sum: 15.0]
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.2]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 4.2, Avg: 4.2, Max: 4.2, Diff: 0.0, Sum: 16.8]
[GC Worker End (ms): Min: 912.3, Avg: 912.4, Max: 912.4, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 13.0M(13.0M)->0.0B(48.0M) Survivors: 12.0M->4096.0K Heap: 389.0M(512.0M)->353.1M(512.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]

并发标记完成之后,G1将执行一次混合收集,不只清理年轻代,还将收集统计出收益最高的若干old区的Region。
​

简单解读:

  • Update RS (ms) - 因为RSet是并发处理的,必须确保在实际的垃圾收集之前,缓冲区中的card得到处理,如果card数量很多,则GC并发线程的负载可能就会很高
  • Processed Buffers - 各个worker线程处理了多少个本地缓冲区
  • Scan RS (ms) - 扫描RSet的引用耗费的时间
  • [Clear CT: 0.0 ms - 清理 Card Table中cards的时间。清理工作至上简单地删除“脏”状态,此状态用来标识一个字段是否被更新,供RSet使用
  • Redirty Cards - 将card table 中适当位置标记为dirty所花费的时间。“适当的位置”是由GC本身执行的堆内存改变所决定的,例如引用排队等

Full GC

如果Mixed GC无法跟上程序分配内存的速度,导致Old区域填满无法分配内存时,就会切换到Serial GC来回收整个堆(Yong + Old + 方法区)

1
2
3
4
5
6
java复制代码2021-09-10T22:28:58.211+0800: 0.899: 
[Full GC (Allocation Failure) 404M->389M(512M), 0.0213584 secs]
[Eden: 0.0B(25.0M)->0.0B(25.0M)
Survivors: 0.0B->0.0B
Heap: 404.0M(512.0M)->389.3M(512.0M)],
[Metaspace: 2760K->2760K(1056768K)]

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【设计模式系列】适配器模式

发表于 2021-11-11

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

前言

我之前有专门写过一起单例设计模式的实现,最近准备重新整理一下常用的设计模式,分别都在什么场景下该如何使用。

  • 单例模式

今天我们的主题是适配器模式,争取后面能将工作中常用的都写到,如果觉得对你有帮助,点个赞我会更有动力。

适配器模式定义

适配器设计模式是结构化设计模式之一,它的作用主要是让两个不兼容的接口可以一起工作。连接这些不相关接口的对象称为Adapter。

场景

使用显示中最经典的一个适配器模式的例子,就是我们手机的充电器,我们手机充电一般都是使用3V,5V的电压进行充电,但是我们的普通插座的电压都是220V或者250V,而手机充电器就是作为普通插座和手机之间的一个适配器,适配器的作用就是将插座上的电压转换为手机可以充电的5V电压。

代码实现

在本期内容中,我将使用适配器设计模式来实现手机充电器(适配器)的功能。

首先,创建两个类分别代表电压Volt和插座Scoket:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class Volt {

private int volts;

public Volt(int v){
this.volts=v;
}

public int getVolts() {
return volts;
}

public void setVolts(int volts) {
this.volts = volts;
}

}

表示插座的类Scoket,我们这个插座默认它是220V的:

1
2
3
4
5
java复制代码public class Socket {
public Volt getVolt(){
return new Volt(220);
}
}

现在我想构建一个将220V电压适配为5V电压的充电器,首先,我们将用这些方法创建一个适配器接口。

1
2
3
java复制代码public interface SocketAdapter {
public Volt get5Volt();
}

在实现适配器模式时,有类适配器和对象适配器两种方法,这两种方法产生的结果是相同的。

类适配器 使用java继承并扩展了源接口,在我们的例子中是Socket类。

对象适配器 使用Java组合方式,适配器包含源对象。

首先我们用类适配器的方式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// 适配器继承了Socket
public class SocketClassAdapterImpl extends Socket implements SocketAdapter{

@Override
public Volt get5Volt() {
Volt v= getVolt();
return convertVolt(v,5);
}

private Volt convertVolt(Volt v, int i) {
if(v.getVolts()==i){
return v;
}
return new Volt(i);
}
}

这种实现方式把适配器比喻为一个转换插座可能更容易理解。

对象适配器实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class SocketObjectAdapterImpl implements SocketAdapter{
// 将插座作为属性组合
private Socket sock = new Socket();

@Override
public Volt get5Volt() {
Volt v= sock.getVolt();
return convertVolt(v,5);
}

private Volt convertVolt(Volt v, int i) {
if(v.getVolts()==i){
return v;
}
return new Volt(i);
}
}

这两种适配器实现方式几乎是相同的,它们都实现了是SocketAdapter接口。

下面是使用适配器设计模式实现的测试程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class AdapterPatternTest {

public static void main(String[] args) {
testClassAdapter();
testObjectAdapter();
}

private static void testObjectAdapter() {
SocketAdapter sockAdapter = new SocketObjectAdapterImpl();
Volt v5 = sockAdapter.get5Volt();
System.out.println("v5 volts using Object Adapter="+v3.getVolts());
}

private static void testClassAdapter() {
SocketAdapter sockAdapter = new SocketClassAdapterImpl();
Volt v5 = sockAdapter.get5Volt();
System.out.println("v5 volts using Object Adapter="+v3.getVolts());
}
}

输出结果:

1
2
shell复制代码v5 volts using Class Adapter=5
v5 volts using Object Adapter=5

适配器模式类图

JDK中的适配器模式

JDK中的适配器模式还是挺多的:

  • java.util.Arrays.asList()
  • java.io.InputStreamReader(InputStream) (returns a Reader)
  • java.io.OutputStreamWriter(OutputStream) (returns a Writer)

适配器模式的优点

  • 可以让任何两个没有关联的类一起运行
  • 提高了类的复用
  • 增加了类的透明度
  • 灵活性好

当然适配器也有缺点,过多的使用会让代码非常凌乱,比如命名调用的是A接口,但是内部调用了B接口,这种情况太多不易于管理。

适配器的使用一般是在一个已有的正常运行的功能上进行扩展时使用,而不是在一开始设计时就考虑。


以上就是本期的全部内容,如果对你有帮助点个赞是对我最大的鼓励。我是小黑,下期见。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

听说你还在写双层 for 循环解两数之和?

发表于 2021-11-11

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

一、写在前面

大家好,我是爱猫爱技术的老表。

  • 初衷
    想刷leetcode也不是一两天的事情了,之前也有很多人给过建议,于是乎,就给安排上了,一来算法的确是很重要的一块,需要好好学,为了提升自己,再者,这也可以作为掘金分享的一块,给大家分享,当然,最重要的是这个过程中会结交到很多志趣相投,有想法的朋友。
  • 安排
    目前打算一个星期刷2-3个题,推文分享进度可能会慢一点,但一个星期至少也会有两篇,期待大家参与。

二、今日题目

给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。
你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。

示例:

1
2
3
4
python复制代码给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

三、 分析

这个题目看似很简单,在一个列表(nums)里面找到两数,满足和为事先指定好的数(target ),其实有陷阱,第一:很多人一看到题目自然想到两层for循环解决问题,but这种人人都想到的问题你若是也这么做,如果你就这种程度,面试失败也不算亏;第二:这题的返回值到底是什么?你看清了吗?它的返回值是一个列表,列表里是int型的数据,这个数据并不是我们找到的满足算式的数,而是这个数在列表里对应的下标,你中招了吗?第三:双for循环极易出错的地方,不能出现自己加自己的情况。

四、解题

  • 方法一:
    又蠢又笨的双重for循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码class Solution(object):
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
l = len(nums)
for a in range(l):
for b in range(l):
if a != b:
if nums[a]+nums[b] == target:
return [a,b]
nums = [2, 7, 11, 15]
target = 9
test_o = Solution()
result = test_o.twoSum(nums,target)
print(result)
  • 提交结果:
    方法一运行结果
  • 方法二:比双for聪明一点

我们作图分析易发现,其实直接双重for循环进行运算是有一半的运算是没有意义的,比如a+b和b+a其实是一模一样的,如下图分析:
小聪明
如何把重复的去掉减少计算机运行量来提升运行速度呢?
我想的比较简单,利用标识位,建立一个和给定整数列表一样长的数组,初始值为全为0,第一层for循环运行一次,该位对应的标识值由0变成1,在第二层for运算时先判断对应的数据位上的标识符是否都为0,为0 则进行运算比较,否则说明互相之间已经运算过,就continue,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码class Solution(object):
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
l = len(nums)
flags = [0 for i in range(l)]
for a in range(l):
flags[a]=1
for b in range(l):
if flags[b]==0:
if nums[a]+nums[b] == target:
return [a,b]
# 另一种简单方法
# l = len(nums)
# for a in range(l):
# for b in range(a+1,l):
# if nums[a] + nums[b] == target:
# return [a, b]
  • 运行结果:
    方法二运行结果
  • 方法三 一层循环(偷瞄了小詹学长的方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码class Solution(object):
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
l = len(nums)
for a in range(l):
one_unm = nums[a]
other_one = target - one_unm
if other_one in nums:
b = nums.index(other_one)
if a != b:
if a>b:
return [b,a]
return [a,b]
  • 运行结果:
    方法三运行结果
  • 方法四:一层for循环优化(小詹读者【村前河水流】提供)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码class Solution(object):
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
l = len(nums)
dict_nums = {nums[i]:i for i in range(l)}
for a in range(l):
one_unm = nums[a]
other_one = target - one_unm
if other_one in dict_nums and a!= dict_nums[other_one]:
return [a,dict_nums[other_one]]
  • 运行结果:
    方法四运行结果

五、疑惑

方法一和二耗时长,双重for循环,时间复杂度高,耗时长没话说,可方法三和方法四的差别,怎么也这么大呢?
我仔细研究了一下,方法三和四最大的差别就是:前者是列表遍历查找,后者是字典遍历查找,那么关键点来了,到底是不是这个问题呢?

  • 列表与字典遍历性能测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码import time

list_01 = [str(i) for i in range(1000)]
start_time = time.time()
if 'a' in list_01 :
a = 0
end_time = time.time()
print("列表遍历耗时:"+str(end_time-start_time))

dict_01 = {str(i):i for i in range(1000)}
start_time2 = time.time()
if 'a' in dict_01 :
a = 0
end_time2 = time.time()
print("字典遍历耗时:"+str(end_time2-start_time2))
  • 测试结果:
数据量 列表耗时 字典耗时
1000 0 0
10000 0 0
50000 0.996ms 0
100000 1.995ms 0
1000000 20.944ms 0
10000000 208.443ms 0

注:0表示的是所花时间极少,我所测的数据可以精确到微秒,极少的意思可表示为少于1微秒。
通过上表很容易看出,列表遍历与字典遍历的天大差别了,不过值得一提的还有,字典生成上要比列表生成慢很多,大家可以自测一下。

六、结语

啥也不说,刷题就是要坚持。

思想很复杂,

实现很有趣,

只要不放弃,

终有成名日。

—《老表打油诗》

下期见,我是爱猫爱技术的老表,如果觉得本文对你学习有所帮助,欢迎点赞、评论、关注我!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

网易云音乐音视频算法的 Serverless 探索之路 现状

发表于 2021-11-11

简介: 网易云音乐最初的音视频技术大多都应用在曲库的数据处理上,基于音视频算法服务化的经验,云音乐曲库团队与音视频算法团队一起协作,一起共建了网易云音乐音视频算法处理平台,为整个云音乐提供统一的音视频算法处理平台。本文将分享我们如何通过 Serverless 技术去优化我们整个音视频处理平台。

作者:廖祥俐

策划:望宸

网易云音乐最初的音视频技术大多都应用在曲库的数据处理上,基于音视频算法服务化的经验,云音乐曲库团队与音视频算法团队一起协作,一起共建了网易云音乐音视频算法处理平台,为整个云音乐提供统一的音视频算法处理平台。本文将分享我们如何通过 Serverless 技术去优化我们整个音视频处理平台。

本文将从三个部分向大家介绍:

  • 现状:音视频技术在网易云音乐的应用情况,引入 Serverless 技术之前遇到的问题;
  • 选型:调研 Serverless 方案时的考虑点;
  • 落地和展望:我们进行了哪些改造,最终的落地效果和未来规划。

现状

作为一家以音乐为主体的公司,音视频技术被广泛应用于网易云音乐的众多业务场景里,为了更形象的让大家感受到,这里列举了 5 个常见的场景:

  1. 默认情况下,用户听到的是我们采用音频转码算法预先转好的标准化码率的音质,但由于流量有限或自身对于音质更高的要求,想要切换到差一些或更好的音质。
  2. 用户可以使用云音乐 APP 里面的听歌识曲功能去识别环境中的音乐,这背后使用到了音频指纹提取及识别技术。
  3. 在平台上的一些 VIP 歌曲,为了能给用户更好的试听体验,我们会做副歌检测,让试听直接定位到高潮片段,这里用到了副歌检测算法。
  4. 在云音乐的 K 歌场景里,我们需要对音频的音高进行展示并辅助打分,这里我们用到了音高生成算法去完善 K 歌的基础数据。
  5. 为了更好的满足云音乐平台上,小语种用户的听歌体验,我们为日语、粤语等提供了音译歌词,这里用到了自动罗马音的算法。

从上面的场景可以看到,音视频技术被广泛应用于云音乐的不同场景里面,发挥了重要的作用。

从我们的音视频技术做一个简单划分,可以分为三大类:分析理解、加工处理、创作生产,这些一部分是以端上 SDK 的方式,在端上进行处理;而更多的部分,是通过算法工程化的方式,采用后端集群部署管理,以服务的形式提供通用的音视频能力,而这部分是我们今天分享的重点。

音视频算法的服务化部署工作中,需要了解很多相关音视频算法的特点,如部署环境、执行时间、能否支持并发处理等,随着我们落地算法的增加,我们总结了以下规律:

  1. 算法的执行时间长:执行时间往往与原始音频的时长成正比,云音乐很多场景下音频、视频的时长 Range 范围是很大的,基于这个特点,我们在执行单元的设计上,往往都采用异步化的模式。
  2. 音视频算法具有多语言特性:云音乐的算法包括了 C++、Python 等语言,对接环境上下文会带来极大的困扰,为了解决这个问题,我们采用标准化约定及镜像交付的方式,解耦各类环境准备的工作,所以后续对于能否支持镜像部署,会成为我们技术选型的一个重点考察。
  3. 弹性的诉求正在变大:云音乐平台的歌曲,从我入职时候的 500w,到现在在线超过 6000w,增量 vs 存量的 gap 越来越大,当我们快速实施一个算法时,不仅要考虑增量的接入,更要考虑存量的快速处理,所以在系统设计中,会单独把执行单元的最小粒度剥离出来,便于快速的扩容。

基于我们对工程化的理解,及音视频算法处理的特点,云音乐的音视频处理平台的整体架构如下:

对于不同音视频算法处理的共同部分,我们做了统一的设计,包括算法处理的可视化、监控、快速试用和处理数据统计等,对于资源的分配也设计了统一可配置的管理模式,让整个系统的公共部分可以尽可能抽象并复用。

整个音视频算法处理平台最关键的,也是今天的分享重点,是执行单元的交互与设计。云音乐通过统一的对接标准、采用镜像交付的方式,解决了很多对接和部署上的效率问题。针对资源的使用,由于我们不断有新算法、存量/增量服务的存在,在上云之前,用的是内部私有云云主机申请/回收、内容容器化的方式。

为了更好的描述云音乐执行单元的运行流程,我们将它更细化下,如下图所示:

通过消息队列去解耦了执行单元与其他系统的交互,在执行单元内部,我们通过控制消息队列的并发度去适配不同并发性能的算法,尽量控制执行单元的主要工作仅用于算法的计算,这样最终在系统扩容的时候,我们能够做到最小粒度的扩容。

在这个模式下,我们落地了 60 多种音视频算法,尤其是在近一年来,服务化的算法占到了一半,这些算法向云音乐 100+ 的业务场景提供了服务能力。但更复杂的算法、更多的业务场景,对我们的服务化效率、运维部署和弹性能力都提出了更高的要求,在我们上云之前,在内部已经用到了 1000 台以上不同规格的云主机及物理机。

选型

随着业务场景和算法复杂度的增加,虽然通过了很多方式去简化了内部业务场景、算法等的对接,但越来越多夹杂存量、增量处理的算法,不同流量的业务场景规模,以及不同业务场景可能会复用同一类算法的,让我们在处理机器资源的时间,远比我们在开发的时间更多。

这个也促使我们开始去考虑更多的方式方法,去解决我们遇到的问题,最直接的有三个痛点。

第一个是存量和增量的差异变大,和新算法落地的增多,我们花在处理存量和增量的资源协调时间越来越多;其次是随着算法复杂度的增高,我们在申请/采购机器的时候,需要关注机器的整体规格、利用率等;最后是,我们希望存量的处理能够加快,在处理存量的时候有足够大的资源,在海量音视频数据处理时候,能够压缩存量与增量不一致的时间。总的来讲,我们希望能够有足够大规模的弹性资源,让音视频算法服务不用再多去关注机器管理。

然而,实际改造不仅仅是关注最终服务能力,还需要综合考虑投入的 ROI。具体来看:

  • 成本:包含两方面,改造的实施成本和计算资源的成本。前者可以结合具体方案进行评估,得到所需投入的人日,此外,改造后在未来的灵活拓展性,也是我们需要考虑的点。后者可以通过云厂商官方给出的费用计算模型,结合我们的执行数据,估算出来。我们在成本方面的选型关键是,在改造成本能够接受的情况下,未来的 IT 成本不会大额的增加。
  • 运行环境的支持:前面提到过,云音乐的运行环境比较多样化,是以镜像交付的方式进行部署的;团队内部都有相对完善的 CICD 支持,这个要求未来的升级、部署事务,例如规格配置上,是否能够简化开发人员对于机器等的关注。我们希望在改造后,不需要在此类事项上花费过多的时间和精力,更多的关注算法执行本身。
  • 弹性能力:除了云厂商提供的计算资源池的规模,我们还会关注弹性算力的启动速度,是否能够对固定场景进行实例预留,以及是否提供更符合业务诉求的灵活弹性能力,以更好的支持业务的发展。

这些其实都符合 Serverless 的定义,构建和运行应用程序都不需要对服务器进行管理、弹性能力出众等。综合以上的考量,我们选择了公有云函数计算的方式,它能直观的映射我们目前的计算执行过程,同时也能满足后续想尝试通过 Schema 进行算法的编排。下面我会重点分享下引入函数计算 FC 的过程。

落地

我们在一周内快速试用了函数计算 FC,然而一个完整的、高可靠的架构,需要考虑更多的因素。因此我们的改造重点是只把算力任务通过函数计算 FC 弹出去,系统在整体的对外输入输出上仍保持不变,并且系统拥有流量控制能力,能够在遇到特殊情况时,降级到私有云进行处理,保障系统的高可靠性,具体的架构改造如下图所示:

云音乐的开发环境与函数计算的适配是改造的重点,我们重点针对部署、监控和混合云支持进行了改造。部署上,我们充分应用了函数计算在 CICD 上的支持及镜像部署的支持,实现了镜像的自动化拉取;在监控设计上,一方面利用云上的监控报警功能,另一方面把它转化为我们内部已有监控系统的参数,让整体的开发运维处理能够维持一致性,最后是从代码设计上,考虑能够兼容混合云部署的实现,最终完成了我们音视频处理平台的 Serverless 改造。

从函数计算的计费策略上,我们可以看到,有三大因素在影响最终费用,内存的规格、触发计算的次数,以及公网出流量的费用。直接从技术架构上看,大家可能更关注前两者,实际上流量费用也是一笔不小的费用,这个对于我们来讲,也是关注的一个重点。

我们根据函数计算的费用特性,在存储体系仍然使用网易私有云的情况下,在第一阶段,首先选取的是公网出流量比较少的音视频算法。关于公网出流量比较少,我举个例子,对音频进行特征提取,如一个音频进去,提取一个 256 维的数组,获取的结果就只是一个 256 维数组,它是远远小于音频自身的流量,因此出公网的流量费用会比较少。

在引入函数计算的第一阶段,特征提取类的算法得到了 10 倍速的提升;稀疏类的算法,可以理解为日常使用率很低的算法,在成本上得到了极大的节约。除此之外,通过函数计算的镜像缓存加速能力,优化了我们节点的启动速度,让所有的服务拉起可以在秒级完成。这些工作,降低了算法运维处理中大量的运维成本,让我们能够更聚焦关注在算法及业务自身。

上方右边这幅图是云音乐其中一个算法的运行示例,可以看到,我们在弹性上的变化范围是非常大的,而函数计算很好的满足了这个诉求。

未来,我们希望能够更进一步通过 Serverless 技术去解放我们在运维上的人力投入,并将从存储上进行尝试,进而解决公网出流量的问题,让更多场景的音视频算法可以自然的实现;其次,随着算法复杂度的进一步提升,使得计算资源上使用的更加复杂,希望通过 GPU 实例来优化计算过程;最后,在云音乐的业务场景中,实时音视频处理的场景也越来越多,同样的,它也有明显的高峰、低谷的波动特点,我们希望沉淀更多的 Serverless 服务使用经验,最终助力云音乐实时音视频技术的发展。

作者:廖祥俐,2015年加入网易云音乐,云音乐曲库研发负责人。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…373374375…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%