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

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


  • 首页

  • 归档

  • 搜索

Logback踩坑

发表于 2021-11-05

Logback的学习最好到Logback官方文档上去,上边很细,很全。

配置

错误的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码    <!-- 日志文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 文件名 -->
<File>${LOG_HOME}/${APP_NAME}/user.log</File>
<!-- 当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/${APP_NAME}/user.log.%d{yyyy-mm-dd}.gz</FileNamePattern>
<!-- 如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true -->
<!-- 最大保存天数 -->
<MaxHistory>10</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread{30}] %logger{50} : %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>

这个配置在日志文件大于10MB的时候会触发归档,但是FileNamePattern 配置的时间没有到达所以并不会触发,并且日志文件已经停止输出了。

正确的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码    <!-- 日志文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志激活输出文件名 -->
<File>${LOG_HOME}/${APP_NAME}/auth.log</File>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>

<!-- 当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 归档文件名 aux:auxiliary 辅助的 即有多个地方使用了%d,需表明主要的 .gz 或 .zip自动开启压缩 -->
<FileNamePattern>${LOG_HOME}/${APP_NAME}/%d{yyyy-MM-dd, aux}/auth.%d{yyyy-MM-dd}.%i.log.gz</FileNamePattern>
<!-- 如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true -->
<!-- 每个文件最大2MB,最大保存60天数据 总大小3GB
单个文件超过2MB,触发滚动策略
时间触发
-->
<MaxHistory>60</MaxHistory>
<maxFileSize>2MB</maxFileSize>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
</appender>

该配置使用了基于时间和大小的归档策略,当单个日志文件大于2MB的时候,会触发归档,在归档文件名后边添加索引。并且归档后的文件总大小最大3GB,最多保存60天。

有一个小坑,maxFileSize 的值少写一个B会检测到错误。如下:

1
xml复制代码<maxFileSize>2M</maxFileSize>

pic1.png

appender

RollingFileAppender 可以归档日志文件,有2个重要的子组件RollingPolicy和TriggeringPolicy。TriggeringPolicy负责触发的时间,RollingPolicy负责触发时做什么。

TimeBasedRollingPolicy

  1. TimeBasedRollingPolicy 实现了 RollingPolicy 和TriggeringPolicy,所以使用它的时候可以不用写TriggeringPolicy
  2. TimeBasedRollingPolicy的配置有一个必输的属性fileNamePattern,用来定义归档文件的名称,并且推断出归档的时间。
  3. TimeBasedRollingPolicy支持自动文件压缩,只要文件以.gz 或 .zip结尾。

SizeAndTimeBasedRollingPolicy

SizeAndTimeBasedRollingPolicy 可以通过时间归档文件的同时限制文件的大小。
这个时候 %i 是必须要有的,他是用在在当前按时间归档前,大小已经超出限制的时候,每个归档文件的序号

如下图:

pic2.png

FixedWindowRollingPolicy

摘自官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>test.log</file>

<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>tests.%i.log.zip</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
</rollingPolicy>

<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>

该配置当单个日志文件超过5MB时触发FixedWindowRollingPolicy策略,该策略如其名像滑动窗口一样。维护一个1-3的日志文件,tests.1.log.zip,tests.2.log.zip,tests.3.log.zip。
如果test.log超过5MB,会将tests.3.log.zip 删掉,tests.2.log.zip 重命名为tests.3.log.zip,tests.1.log.zip 重命名为 tests.2.log.zip,test.log 重命名为 tests.1.log.zip,然后新的日志继续写test.log,如此循环

SizeBasedTriggeringPolicy

他会检测激活的日志文件,一旦他超过设定的阈值,就会触发rollingPolicy来归档

本文转载自: 掘金

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

Go的宕机与宕机恢复 拜一拜,永不宕机 宕机 宕机恢复 你以

发表于 2021-11-05

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

拜一拜,永不宕机

宕机

话说这是一个风和日丽的上午,拿到了外卖小哥送来的我最心爱的麻辣烫,我打开了因为昨天刚充了三毛钱电费而恢复了使用的电脑,登上了满是美女好友的微信,熟悉的声音马上响彻在了我这三百平米的卧室中,我这该死的魅力,点开这闪烁的美女头像,一行文字映入眼帘,项目出bug了,直接宕机了,五分钟处理了,不然麻辣烫给你倒了…… 真好
在这里插入图片描述
跑题了……废话不多说,上货
在这里插入图片描述

啥是宕机

我认为go语言中的宕机和报错差不多,数组访问越界、空指针引用等,这些运行时错误都会引起宕机,宕机后程序就会停止,编译器就会输出对应的报错信息,包括 panic value 和函数调用的堆栈跟踪信息,panic value 通常是某种错误信息
看上去宕机好像没什么好处,但其实有时合理的宕机是一种非常明智的止损方式。
在这里插入图片描述

手动触发宕机

类似其他语言,比如java手动 throw 一个 error 一样,Go也有手动触发宕机的方式——panic()

1
2
3
4
5
6
7
8
9
go复制代码package main

import "fmt"
func main() {
//xxxxxxx
fmt.Println("我上面有一万行代码")
panic("一万行也没用,我出毛病了")
fmt.Println("我不好使了")
}

在这里插入图片描述
Go语言程序在宕机时,会将堆栈和 goroutine 信息输出到控制台,所以宕机也可以方便地知晓发生错误的位置。上面就展示第7行发生了错误

如果我们想让方法最后宕机,不导致其他语句的执行可以使用defer

1
2
3
4
5
6
7
8
9
10
go复制代码package main

import "fmt"
func main() {

//xxxxxxx
fmt.Println("我上面有一万行代码")
defer panic("一万行也没用,我出毛病了")
fmt.Println("哎,我又好使了")
}

在这里插入图片描述
为什么会有两个错误信息呢,因为defer会在当前行加载,所以产生了第一次报错,后在方法后进行执行,产生了第二个报错。

宕机恢复

如果我们想在宕机后仍让程序继续执行,可以使用recover ,跟java中try/catch作用差不多,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

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
go复制代码package main

import "fmt"

func test () {
defer func() {
err := recover()
if err != nil {
fmt.Println("我说好使了你信不信")
}
}()//调用匿名函数
demo ()
}

func demo () {
fmt.Println("一万行也没用,我出毛病了,下面都不好使")
defer panic("我还能说啥,说啥都不好使了")
}
func main() {

//xxxxxxx
fmt.Println("我上面有一万行代码")
test()
fmt.Println("哎,我又好使了")

}

在这里插入图片描述
panic 和 recover 的组合有如下特性:
有 panic 没 recover,程序宕机。
有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。

ps

虽然 panic/recover 能模拟其他语言的异常机制,但并不建议在编写普通函数时也经常性使用这种特性
在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛,直到程序整体崩溃
如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置

你以为结束了

搞明白了如何恢复宕机的我拿着recover直接将导致宕机的方法捕捉,然后上去就是一顿乌鸦坐飞机,这回大功告成,赶紧回复处理完了
不一会收到回复,写的什么玩意,你就这么处理的嘛,留的坑给谁填,不仅这个月麻辣烫没了,下个月也没了
真是美好的一天……
在这里插入图片描述
大家看完发现有什么错误,写在下面吧!跟我黑虎阿福比划比划!
在这里插入图片描述

本文转载自: 掘金

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

maven项目部署在tomcat下中文乱码 1、

发表于 2021-11-05

1、

tomcat安装目录下的server.xml文件下:添上了这么一句 URIEncoding=“UTF-8”

image.png

二

在D:\apache-tomcat-8.5\bin\catalina.bat下,添加
set JAVA_OPTS=-Xms512m -Xms1024m -XX:MaxPermSize=1024m -Dfile.encoding=UTF-8

三

在 tomcat / conf 目录下,设置 logging.properties ,增加或修改参数 java.util.logging.ConsoleHandler.encoding = GBK

image.png

本文转载自: 掘金

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

【Mybatis-源码解析】11 启动过程中,Mapper

发表于 2021-11-05

启动过程中,Mapper接口的扫描与代理注册

整体流程

简易流程图

整体流程创建扫描器实例扫描器根据配置注册过滤器扫描器进行扫描扫描后获取bean定义交给Spring进行加载spring通过MapperFactoryBean获取到Mapper的代理对象结束

文本概括

  1. 创建扫描器实例
    • 如何使用的是@MapperScan,@MapperScan中配置了@Import(MapperScannerRegistrar.class)注解 会使MapperScannerRegistrar类在启动时自动加载 (待-见spring学习)
      • MapperScannerRegistrar 并加载 registerBeanDefinitions 方法
        • 创建扫描器实例
          • 设置resourceLoader属性(资源加载器)
          • 设置Annotation属性(被修饰的注解)
          • 设置markerInterface属性(继承的父类)
          • 设置ameGenerator属性(名字生成器)
          • 设置factoryBean属性 (工厂bean对象)
          • 设置SqlSessionTemplateBeanName属性 (指定使用的SqlSessionTemplate、通常多个数据源使用)
          • 设置basePackages属性 (要扫描的包)
    • 如果在mapper接口上配置@Mapper注解后,MybatisAutoConfiguration中的内部类AutoConfiguredMapperScannerRegistrar
      • 执行registerBeanDefinitions方法
        • 创建扫描器实例
          • 设置resourceLoader属性(资源加载器)
          • 设置Annotation属性(被修饰的注解)设置值为Mapper.class
  2. 扫描器注册过滤器
    • 判断是否设置了Annotation类型,如果设置了添加指定注解类型的过滤器
    • 判断是否设置了markerInterface属性,如果设置了添加指定父类的过滤器
    • 如果以上属性没有被设置,添加默认的过滤器(直接返回true - 不过滤)
    • 添加 package-info.java 过滤器
  3. 扫描器进行扫描
    • 将包路径交给父类扫描(org.springframework.context.annotation.ClassPathBeanDefinitionScanner)扫描
    • 对扫描后得到的bean定义持有者们进行遍历
      • 获取bean定义
      • 设置bean定义的 原类型和bean类(mapperFactoryBean)
      • bean定义中添加属性 addToConfig
      • 如果设置了sqlSessionTemplate 或者 设置了sqlSessionFactory的话,将bean定义自定注入类型设置为 AUTOWIRE_BY_TYPE (根据类型)
  4. bean定义修改好后,将bean定义交给 springbean的注册实例方法 进行实例化
    • 最终调用org.mybatis.spring.mapper.MapperFactoryBean#getObject获取mapper的代理实例
      • 调用SqlSession获取Mapper实例
      • 在SqlSession中获取configuration对象,通过configuration获取Mapper实例
      • 在configuration中获取mapperRegistry对象,在mapperRegistry中获取Mapper实例
      • 通过mapperRegistry中的knownMappers(Key(接口class),value该接口的代理工厂对象)获取到代理工厂对象 (在启动时解析mapper.xml中sql的时候存储的 方法 -> xmlMapperBuilder.parse())
      • 通过代理工厂对象获取代理实例
      • 通过SqlSession创建代理对象
      • 将代理对象交给代理,代理的类为mapper接口

springboot启动类配置@MapperScan

源码解析(MapperScannerRegistrar.registerBeanDefinitions)

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
scss复制代码  @Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

// 获取MapperScan对象
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
// 通过bean注册表获取 mapper类路径扫描器
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

// 设置 资源加载器
if (resourceLoader != null) {
scanner.setResourceLoader(resourceLoader);
}

// 在MapperScan中获取annotationClass属性 如果不为Annotation类型 扫描器中设置 AnnotationClass
Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
if (!Annotation.class.equals(annotationClass)) {
scanner.setAnnotationClass(annotationClass);
}

// 在MapperScan中获取markerInterface属性 如果不为Class类型 扫描器中配置markerInterface
Class<?> markerInterface = annoAttrs.getClass("markerInterface");
if (!Class.class.equals(markerInterface)) {
scanner.setMarkerInterface(markerInterface);
}

// 在MapperScan中获取nameGenerator属性 如果不为BeanNameGenerator类型 扫描器中配置generatorClass
Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
if (!BeanNameGenerator.class.equals(generatorClass)) {
scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass));
}

// 在MapperScan中获取factoryBean属性 如果不为MapperFactoryBean类型 扫描器中配置mapperFactoryBeanClass
Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
}

// 扫描器中配置SqlSessionTemplateBeanName 和 SqlSessionFactoryBeanName
scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));

// 设置要扫描的包
List<String> basePackages = new ArrayList<String>();
for (String pkg : annoAttrs.getStringArray("value")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
for (String pkg : annoAttrs.getStringArray("basePackages")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
for (Class<?> clazz : annoAttrs.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}

// 扫描器注册过滤器
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(basePackages));
}

接口上配置@mapper注解

在MybatisAutoConfiguration中有一个内部类:AutoConfiguredMapperScannerRegistrar,内部类AutoConfiguredMapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,在spring项目启动时会执行registerBeanDefinitions方法

源码解析(AutoConfiguredMapperScannerRegistrar#registerBeanDefinitions)

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
typescript复制代码    @Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

// 通过bean注册表获取 mapper类路径扫描器
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

try {
// 在类扫描器中中 设置 resourceLoader
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}

// 会获取到启动类所在的包,作为扫描包的根路径
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
for (String pkg : packages) {
logger.debug("Using auto-configuration base package '{}'", pkg);
}
}

// 设置要扫描的注解为 Mapper
scanner.setAnnotationClass(Mapper.class);
// 注册过滤器
scanner.registerFilters();
// 进行扫描
scanner.doScan(StringUtils.toStringArray(packages));

} catch (IllegalStateException ex) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", ex);
}
}

通用方法

源码解析(registerFilters)

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复制代码public void registerFilters() {
// 是否全接口注入标记
boolean acceptAllInterfaces = true;

// 如果配置了 annotationClass,新增一个注解过滤器,并且设置不全注入
if (this.annotationClass != null) {
addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
acceptAllInterfaces = false;
}

// 如果配置了 markerInterface,新增一个父类接口过滤器,并且设置不全注入
if (this.markerInterface != null) {
addIncludeFilter(new AssignableTypeFilter(this.markerInterface) {
@Override
protected boolean matchClassName(String className) {
return false;
}
});
acceptAllInterfaces = false;
}

// 如果全接口注入
if (acceptAllInterfaces) {
// 新增一个默认的过滤器,比较永远返回true
addIncludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return true;
}
});
}

// 排除 package-info.java 类
addExcludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
String className = metadataReader.getClassMetadata().getClassName();
return className.endsWith("package-info");
}
});
}

源码解析(doScan)

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
kotlin复制代码  @Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 使用spring的扫描器获取到 bean定义的持有者
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

// 如果bean定义为空,打印警告日志 否则 处理bean的持有者定义
if (beanDefinitions.isEmpty()) {
logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}

return beanDefinitions;
}


private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
// 声明一个 GenericBeanDefinition 实例
GenericBeanDefinition definition;

// 循环所有的bean定义持有者
for (BeanDefinitionHolder holder : beanDefinitions) {
// 获取持有者的bean定义
definition = (GenericBeanDefinition) holder.getBeanDefinition();


// 设置bean定义的 原类型和bean类(mapperFactoryBean)
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
definition.setBeanClass(this.mapperFactoryBean.getClass());

// bean定义中添加属性 addToConfig
definition.getPropertyValues().add("addToConfig", this.addToConfig);

// 声明 是否有使用的bean工厂标识 默认 没有,当有设置SessionFactoryBeanName或者当前sqlSessionFactory不为空是 添加属性sqlSessionFactory 并将标识改为true
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}

// 如果设置了 SessionTemplateBeanName 或者 当前类有 sqlSessionTemplate 时添加属性 sqlSessionTemplate 并将 并将 是否有使用的bean工厂标识 改为true
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}

// 如果 有使用的bean工厂标识 设置自动装配模式为 根据类型装配
if (!explicitFactoryUsed) {
if (logger.isDebugEnabled()) {
logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
}
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}

获取代理BEAN

源码解析(MapperFactoryBean#getObject)

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
typescript复制代码  @Override
public T getObject() throws Exception {
// 通过SqlSession获取Mapper实例
return getSqlSession().getMapper(this.mapperInterface);
}


@Override
public <T> T getMapper(Class<T> type) {
// 通过configuration获取Mapper实例
return configuration.<T>getMapper(type, this);
}

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 调用mapper注册器获取mapper实例
return mapperRegistry.getMapper(type, sqlSession);
}

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// knownMappers 为mybatis启动时扫描mapper接口,存放到mapperRegistry的knownMappers的mapperProxyFactory(代理工厂对象)
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);

// 如果代理工厂对象为空 标识该接口没有被注册
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}


try {
/**
* 通过代理工厂获取到代理对象实例
*
* 1. 根据sqlSession创建MapperProxy代理类(设置sqlSession、mapperInterface、methodCache属性)
* 2. 将MapperProxy交由代理(Proxy.newProxyInstance)代理接口为mapperInterface
*/
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}

文章链接

  • 【Myabtis】- 1. 整合spring boot后启动流程
  • 【Mybatis】- 1.1 启动过程中,Mapper接口的扫描与代理注册
  • 【Mybatis】- 1.2 启动过程中,Mapper.xml的解析
  • 【Mybatis】- 2. SQL语句的执行过程
  • 【Mybatis-附件1】- Mapper.xml 参数配置以及含义

本文转载自: 掘金

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

一次内存溢出的排查心得 前言 MAT 总结

发表于 2021-11-05

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

前言

今天突然收到了服务容器告警,内存达到了百分之九十的告警线了。正好前几天刚上了一个大需求,第一反应就是这个新模块的代码有内存泄漏点。本文将从如何发现内存泄漏点、如何定位内存泄漏点的代码到最后如何解决内存泄漏,说一说心得。

MAT

目前分析jvm内存问题最好的工具,我认为是Memory Analyzer。支持html页面查看内存情况。

image.png
通过这张预览图,如果有泄漏点的话,会有Problem Suspect提示出来,告诉你占了百分之多少。这里我们一眼就能看出是friend-thread-pool这个线程池里出了问题。这里推荐大家一个项目代码如果有多个业务模块的话,建议每个业务模块定义自己的线程池,这样不仅可以做到线程隔离,也方便了我们后期排查问题。这样我们就一下子把代码范围缩小到使用friend-thread-pool线程池的业务模块了。

点击“Details »”,查看详细信息

image.png
可以看出MemberSubDTO这个类有125,018个,ArrayList和CopyOnWriteArrayList占了大量的内存。不难分析出是List<MemberDTO>这块代码出了问题。这样就可以定位出准确的代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码private void initAppAndHistorySub(Long accountId, List<MemberDTO> subList) {
List<MemberDTO> historySubList = Lists.newCopyOnWriteArrayList();
List<MemberDTO> onlineSubList = Lists.newCopyOnWriteArrayList();
subList.parallelStream().forEach(memberDTO -> {
MemberDTO btUserDTO = UserFeignImpl.get().getBtEntryInfoByAccountId(memberDTO.getAccountId(), false);
if (Objects.nonNull(btUserDTO)) {
historySubList.add(memberDTO);
} else {
onlineSubList.add(memberDTO);
}
});
List<MemberDTO> intersectionAppAndBtSubList = new ArrayList<>(onlineSubList);
initOnlineSub(accountId, onlineSubList, intersectionAppAndBtSubList);
initHistorySub(accountId, historySubList);
}

这里用了并发流处理list,考虑到线程安全问题,就用了CopyOnWriteArrayList。无视了CopyOnWriteArrayList的使用场景。正如CopyOnWriteArrayList的名字一样,是满足 CopyOnWrite 的 ArrayList,所谓 CopyOnWrite 的意思:就是对一块内存进行修改时,不直接在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后,再将原来指向的内存指针指到新的内存,原来的内存就可以被回收。当写操作多时,就会一直内存拷贝,自然就OOM了。所以CopyOnWriteArrayList并不适合写多读少的场景,我们这里就是写多读少。

知道原因后就可以修改代码了,不用parallelStream就不存在线程安全,自然就不需要用CopyOnWriteArrayList了。

总结

在使用CopyOnWriteArrayList一定要考虑使用场景,不能一味了遇到线程安全问题就用CopyOnWriteArrayList。另外,每个业务模块定义自己专属的线程池,也是一个百利无一害的小技巧。

本文转载自: 掘金

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

【golang】 slice 深度解析,踩坑记录

发表于 2021-11-05

大家好,我是「云舒编程」,今天我们来聊聊【golang】 slice。

Go 语言的 slice 很好用,不过也有一些坑。在初学golang中,作者也在slice上踩了很多坑。为了避免以后继续踩坑,也为了能够更加深入了解slice的原理,于是有了本文。

可以先看下以下几个案例,如果你可以正确回答,并且能够说出为什么,那么恭喜你,你对slice已经很了解了。

案例一(slice传参):

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
go复制代码//情况一
func main() {
slice := make([]int,0,4)
slice = append(slice,1,2,3)
TestSlice(slice)
fmt.Println(slice)
}

func TestSlice(slice []int) {
slice = append(slice,4)
}

//情况二
func main() {
slice := make([]int,0,4)
slice = append(slice,1,2,3)
TestSlice(slice)
fmt.Println(slice)
}

func TestSlice(slice []int) {
slice = append(slice,4)
slice[0] = 10
}

//情况三
func main() {
slice := make([]int,0,3)
slice = append(slice,1,2,3)
TestSlice(slice)
fmt.Println(slice)
}

func TestSlice(slice []int) {
slice = append(slice,4)
slice[0] = 10
}

情况一:输出[1,2,3]

情况二:输出[10,2,3]

情况三:输出[1,2,3]

这里需要明确两个点:

1、golang中只有值传递

2、golang中slice是一个struct,结构如下:

image-20211105144537337.png

情况一和情况二:

外部的slice 传参到TestSlice,这里发生了复制,结构如下:
image-20211105144606073.png

由于slice中持有的是数组的指针,所以这里两个slice指向的是同一个数组。所以改变同一个数组会影响到两个slice。

但是由于打印slice是受len控制的,所以这里情况一就会打印[1,2,3]。但是情况二就会打印[10,1,2]。

通过强行修改len,可以打印出1,2,3,4

1
2
3
4
5
6
7
8
9
10
11
go复制代码func main() {
slice := make([]int,0,4)
slice = append(slice,1,2,3)
TestSlice(slice)
(*reflect.SliceHeader)(unsafe.Pointer(&slice)).Len = 4 //强制修改slice长度
fmt.Println(slice)
}

func TestSlice(slice []int) {
slice = append(slice,4)
}

情况三:

情况三跟一、二的区别在于,情况三的初始容量是3,并且随后放入了1,2,3三个元素。所以在传参前slice就已经满了。

然后在函数里发生了append,导致数组发生了扩容。

扩容逻辑:

1、根据策略申请一个更大的数组空间(slice容量的扩容规则:当原slice的cap小于1024时,新slice的cap变为原来的2倍;原slice的cap大于1024时,新slice变为原来的1.25倍)

2、copy 旧数组中的数据到新数组

3、添加新增的数据

4、将数组的指针复制给slice

扩容后,结构如下:

image-20211105144857810.png
所以函数里改变数组对原始的slice没有任何改变

可以通过下列方式看出slice底层的数组地址变化,可以发现前两个输出值一样,第三个输出不一样。证明指向的数组产生了变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func main() {
slice := make([]int,0,3)
slice = append(slice,1,2,3)
fmt.Println(unsafe.Pointer(&slice[0]))
TestSlice(slice)
fmt.Println(slice)
}

func TestSlice(slice []int) {
fmt.Println(unsafe.Pointer(&slice[0]))
slice = append(slice,4)
slice[0] = 10
fmt.Println(unsafe.Pointer(&slice[0]))
}

案例二(slice append):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码//情况一
func main() {
slice1 := make([]int, 0, 4)
slice1 = append(slice1, 1, 2, 3)

slice2 := append(slice1, 4)
slice2[0] = 10

fmt.Println(slice1)
fmt.Println(slice2)
}

//情况二
func main() {
slice1 := make([]int, 0, 4)
slice1 = append(slice1, 1, 2, 3)

slice2 := append(slice1, 4,5)
slice2[0] = 10

fmt.Println(slice1)
fmt.Println(slice2)
}

情况一:输出[10,2,3] [10,2,3,4]

情况二:输出[1,2,3] [10,2,3,4,5]

原理是类似的,append过程如果没有发生扩容,那么两个slice就指向同一个数组,如果发生扩容就会分别指向不同的数组。

案例三(切片):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码//情况一
func main() {
slice1 := make([]int, 0, 4)
slice1 = append(slice1, 1, 2, 3)

slice2 := slice1[:len(slice1)-1]
slice2[0] = 10

fmt.Println(slice1)
fmt.Println(slice2)
}

//情况二
func main() {
slice1 := make([]int, 0, 4)
slice1 = append(slice1, 1, 2, 3)

slice2 := slice1[:len(slice1)-1]
slice2 = append(slice2,11,12,13,14,15)
slice2[0] = 10

fmt.Println(slice1)
fmt.Println(slice2)
}

情况一:输出[10,2,3] [10,2]

情况二:输出[1 2 3] [10 2 11 12 13 14 15]

原理是类似的,append过程如果没有发生扩容,那么两个slice就指向同一个数组,如果发生扩容就会分别指向不同的数组。

深拷贝

可以看出,golang的slice操作默认都是浅拷贝。触发发生扩容才会让两个slice指向不同的数组。在实际业务中,很多场景是需要深拷贝的,这个时候可以使用copy函数

1
go复制代码copy(newSlice,oldSlice)

本文转载自: 掘金

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

5 年迭代 5 次,抖音基于 Flink 的推荐系统演进历程

发表于 2021-11-05

本文基于字节跳动推荐系统基础服务方向负责人郭文飞在 5 月 22 日 Apache Flink Meetup 分享的《Flink 在字节跳动推荐特征体系中的落地实践》整理,主要内容包括:

  1. 业务背景
  2. 新一代系统架构
  3. 后续规划

2021 年,字节跳动旗下产品总 MAU 已超过 19 亿。在以抖音、今日头条、西瓜视频等为代表的产品业务背景下,强大的推荐系统显得尤为重要。Flink 提供了非常强大的 SQL 模块和有状态计算模块。目前在字节推荐场景,实时简单计数特征、窗口计数特征、序列特征已经完全迁移到 Flink SQL 方案上。结合 Flink SQL 和 Flink 有状态计算能力,我们正在构建下一代通用的基础特征计算统一架构,期望可以高效支持常用有状态、无状态基础特征的生产。

一、业务背景

图片

对于今日头条、抖音、西瓜视频等字节跳动旗下产品,基于 Feed 流和短时效的推荐是核心业务场景。而推荐系统最基础的燃料是特征,高效生产基础特征对业务推荐系统的迭代至关重要。

1. 主要业务场景

图片

  • 抖音、火山短视频等为代表的短视频应用推荐场景,例如 Feed 流推荐、关注、社交、同城等各个场景,整体在国内大概有 6 亿 + 规模 DAU;
  • 头条、西瓜等为代表的 Feed 信息流推荐场景,例如 Feed 流、关注、子频道等各个场景,整体在国内有数亿规模 DAU;

2. 业务痛点和挑战

图片

目前字节跳动推荐场景基础特征的生产现状是“百花齐放”。离线特征计算的基本模式都是通过消费 Kafka、BMQ、Hive、HDFS、Abase、RPC 等数据源,基于 Spark、Flink 计算引擎实现特征的计算,而后把特征的结果写入在线、离线存储。各种不同类型的基础特征计算散落在不同的服务中,缺乏业务抽象,带来了较大的运维成本和稳定性问题。

而更重要的是,缺乏统一的基础特征生产平台,使业务特征开发迭代速度和维护存在诸多不便。如业务方需自行维护大量离线任务、特征生产链路缺乏监控、无法满足不断发展的业务需求等。

图片

在字节的业务规模下,构建统一的实时特征生产系统面临着较大挑战,主要来自四个方面:

巨大的业务规模:抖音、头条、西瓜、火山等产品的数据规模可达到日均 PB 级别。例如在抖音场景下,晚高峰 Feed 播放量达数百万 QPS,客户端上报用户行为数据高达数千万 IOPS。 业务方期望在任何时候,特征任务都可以做到不断流、消费没有 lag 等,这就要求特征生产具备非常高的稳定性。

较高的特征实时化要求:在以直播、电商、短视频为代表的推荐场景下,为保证推荐效果,实时特征离线生产的时效性需实现常态稳定于分钟级别。

更好的扩展性和灵活性:随着业务场景不断复杂,特征需求更为灵活多变。从统计、序列、属性类型的特征生产,到需要灵活支持窗口特征、多维特征等,业务方需要特征中台能够支持逐渐衍生而来的新特征类型和需求。

业务迭代速度快:特征中台提供的面向业务的 DSL 需要足够场景,特征生产链路尽量让业务少写代码,底层的计算引擎、存储引擎对业务完全透明,彻底释放业务计算、存储选型、调优的负担,彻底实现实时基础特征的规模化生产,不断提升特征生产力;

3. 迭代演进过程

在字节业务爆发式增长的过程中,为了满足各式各样的业务特征的需求,推荐场景衍生出了众多特征服务。这些服务在特定的业务场景和历史条件下较好支持了业务快速发展,大体的历程如下:

图片

在这其中 2020 年初是一个重要节点,我们开始在特征生产中引入 Flink SQL、Flink State 技术体系,逐步在计数特征系统、模型训练的样本拼接、窗口特征等场景进行落地,探索出新一代特征生产方案的思路。

二、新一代系统架构

结合上述业务背景,我们基于 Flink SQL 和 Flink 有状态计算能力重新设计了新一代实时特征计算方案。新方案的定位是:解决基础特征的计算和在线 Serving,提供更加抽象的基础特征业务层 DSL。

在计算层,我们基于 Flink SQL 灵活的数据处理表达能力,以及 Flink State 状态存储和计算能力等技术,支持各种复杂的窗口计算。极大地缩短业务基础特征的生产周期,提升特征产出链路的稳定性。新的架构里,我们将特征生产的链路分为数据源抽取 / 拼接、状态存储、计算三个阶段。Flink SQL 完成特征数据的抽取和流式拼接,Flink State 完成特征计算的中间状态存储。

有状态特征是非常重要的一类特征,其中最常用的就是带有各种窗口的特征,例如统计最近 5 分钟视频的播放 VV 等。对于窗口类型的特征在字节内部有一些基于存储引擎的方案,整体思路是“轻离线重在线”,即把窗口状态存储、特征聚合计算全部放在存储层和在线完成。离线数据流负责基本数据过滤和写入,离线明细数据按照时间切分聚合存储(类似于 micro batch),底层的存储大部分是 KV 存储、或者专门优化的存储引擎,在线层完成复杂的窗口聚合计算逻辑,每个请求来了之后在线层拉取存储层的明细数据做聚合计算。

我们新的解决思路是“轻在线重离线”,即把比较重的 时间切片明细数据 状态存储和窗口聚合计算全部放在离线层。窗口结果聚合通过 离线窗口触发机制 完成,把特征结果 推到 在线 KV 存储。在线模块非常轻量级,只负责简单的在线 serving,极大地简化了在线层的架构复杂度。在离线状态存储层。我们主要依赖 Flink 提供的 原生状态存储引擎 RocksDB,充分利用离线计算集群本地的 SSD 磁盘资源,极大减轻在线 KV 存储的资源压力。

对于长窗口的特征(7 天以上窗口特征),由于涉及 Flink 状态层明细数据的回溯过程,Flink Embedded 状态存储引擎没有提供特别好的外部数据回灌机制(或者说不适合做)。因此对于这种“状态冷启动”场景,我们引入了中心化存储作为底层状态存储层的存储介质,整体是 Hybrid 架构。例如 7 天以内的状态存储在本地 SSD,7~30 天状态存储到中心化的存储引擎,离线数据回溯可以非常方便的写入中心化存储。

除窗口特征外,这套机制同样适用于其他类型的有状态特征(如序列类型的特征)。

1. 实时特征分类体系

图片

2. 整体架构

图片

带有窗口的特征,例如抖音视频最近 1h 的点赞量(滑动窗口)、直播间用户最近一个 session 的看播时长(session 窗口)等;

2.1 数据源层

在新的一体化特征架构中,我们统一把各种类型数据源抽象为 Schema Table,这是因为底层依赖的 Flink SQL 计算引擎层对数据源提供了非常友好的 Table Format 抽象。在推荐场景,依赖的数据源非常多样,每个特征上游依赖一个或者多个数据源。数据源可以是 Kafka、RMQ、KV 存储、RPC 服务。对于多个数据源,支持数据源流式、批式拼接,拼接类型包括 Window Join 和基于 key 粒度的 Window Union Join,维表 Join 支持 Abase、RPC、HIVE 等。具体每种类型的拼接逻辑如下:

图片

三种类型的 Join 和 Union 可以组合使用,实现复杂的多数据流拼接。例如 (A union B) Window Join (C Lookup Join D)。

图片

另外,Flink SQL 支持复杂字段的计算能力,也就是业务方可以基于数据源定义的 TableSchema 基础字段实现扩展字段的计算。业务计算逻辑本质是一个 UDF,我们会提供 UDF API 接口给业务方,然后上传 JAR 到特征后台加载。另外对于比较简单的计算逻辑,后台也支持通过提交简单的 Python 代码实现多语言计算。

2.2 业务 DSL

从业务视角提供高度抽象的特征生产 DSL 语言,屏蔽底层计算、存储引擎细节,让业务方聚焦于业务特征定义。业务 DSL 层提供:数据来源、数据格式、数据抽取逻辑、数据生成特征类型、数据输出方式等。

图片

2.3 状态存储层

图片

如上文所述,新的特征一体化方案解决的主要痛点是:如何应对各种类型(一般是滑动窗口)有状态特征的计算问题。对于这类特征,在离线计算层架构里会有一个状态存储层,把抽取层提取的 RawFeature 按照切片 Slot 存储起来 (切片可以是时间切片、也可以是 Session 切片等)。切片类型在内部是一个接口类型,在架构上可以根据业务需求自行扩展。状态里面其实存储的不是原始 RawFeature(存储原始的行为数据太浪费存储空间),而是转化为 FeaturePayload 的一种 POJO 结构,这个结构里面支持了常见的各种数据结构类型:

  • Int:存储简单的计数值类型 (多维度 counter);
  • HashMap<int, int>:存储二维计数值,例如 Action Counter,key 为 target_id,value 为计数值;
  • SortedMap<int, int>: 存储 topk 二维计数 ;
  • LinkedList

:存储 id_list 类型数据;

  • HashMap<int, List

:存储二维 id_list;

  • 自定义类型,业务可以根据需求 FeaturePayload 里面自定义数据类型

状态层更新的业务接口:输入是 SQL 抽取 / 拼接层抽取出来的 RawFeature,业务方可以根据业务需求实现 updateFeatureInfo 接口对状态层的更新。对于常用的特征类型内置实现了 update 接口,业务方自定义特征类型可以继承 update 接口实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
markdown复制代码/**
* 特征状态 update 接口
*/
public interface FeatureStateApi extends Serializable {
/**
* 特征更新接口, 上游每条日志会提取必要字段转换为 fields, 用来更新对应的特征状态
*
* @param fields
* context: 保存特征名称、主键 和 一些配置参数 ;
* oldFeature: 特征之前的状态
* fields: 平台 / 配置文件 中的抽取字段
* @return
*/
FeaturePayLoad assign(Context context,FeaturePayLoad feature, Map<String, Object> rawFeature);
}

当然对于无状态的 ETL 特征是不需要状态存储层的。

2.4 计算层

特征计算层完成特征计算聚合逻辑,有状态特征计算输入的数据是状态存储层存储的带有切片的 FeaturePayload 对象。简单的 ETL 特征没有状态存储层,输入直接是 SQL 抽取层的数据 RawFeature 对象,具体的接口如下:

有状态特征聚合接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
markdown复制代码/**
* 有状态特征计算接口
*/
public interface FeatureStateApi extends Serializable {

/**
* 特征聚合接口,会根据配置的特征计算窗口, 读取窗口内所有特征状态,排序后传入该接口
*
* @param featureInfos, 包含 2 个 field
* timeslot: 特征状态对应的时间槽
* Feature: 该时间槽的特征状态
* @return
*/
FeaturePayLoad aggregate(Context context, List<Tuple2<Slot, FeaturePayLoad>> slotStates);

}

无状态特征计算接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码/**
* 无状态特征计算接口
*/
public interface FeatureConvertApi extends Serializable {

/**
* 转换接口, 上游每条日志会提取必要字段转换为 fields, 无状态计算时,转换为内部的 feature 类型 ;
*
* @param fields
* fields: 平台 / 配置文件 中的抽取字段
* @return
*/
FeaturePayLoad convert(Context context, FeaturePayLoad featureSnapshot, Map<String, Object> rawFeatures);

}

另外通过触发机制来触发特征计算层的执行,目前支持的触发机制主要有:

图片

3. 业务落地

目前在字节推荐场景,新一代特征架构已经在抖音直播、电商、推送、抖音推荐等场景陆续上线了一些实时特征。主要是有状态类型的特征,带有窗口的一维统计类型、二维倒排拉链类型、二维 TOPK 类型、实时 CTR/CVR Rate 类型特征、序列类型特征等。

在业务核心指标达成方面成效显著。在直播场景,依托新特征架构强大的表达能力上线了一批特征之后,业务看播核心指标、互动指标收益非常显著。在电商场景,基于新特征架构上线了 400+ 实时特征。其中在直播电商方面,业务核心 GMV、下单率指标收益显著。在抖音推送场景,基于新特征架构离线状态的存储能力,聚合用户行为数据然后写入下游各路存储,极大地缓解了业务下游数据库的压力,在一些场景中 QPS 可以下降到之前的 10% 左右。此外,抖音推荐 Feed、评论等业务都在基于新特征架构重构原有的特征体系。

值得一提的是,在电商和抖音直播场景,Flink 流式任务状态最大已经达到 60T,而且这个量级还在不断增大。预计不久的将来,单任务的状态有可能会突破 100T,这对架构的稳定性是一个不小的挑战。

4. 性能优化

4.1 Flink State Cache

目前 Flink 提供两类 StateBackend:基于 Heap 的 FileSystemStateBackend 和基于 RocksDB 的 RocksDBStateBackend。对于 FileSystemStateBackend,由于数据都在内存中,访问速率很快,没有额外开销。而 RocksDBStateBackend 存在查盘、序列化 / 反序列化等额外开销,CPU 使用量会有明显上升。在字节内部有大量使用 State 的作业,对于大状态作业,通常会使用 RocksDBStateBackend 来管理本地状态数据。RocksDB 是一个 KV 数据库,以 LSM 的形式组织数据,在实际使用的过程中,有以下特点**:**

  1. 应用层和 RocksDB 的数据交互是以 Bytes 数组的形式进行,应用层每次访问都需要序列化 / 反序列化;
  2. 数据以追加的形式不断写入 RocksDB 中,RocksDB 后台会不断进行 compaction 来删除无效数据。

业务方使用 State 的场景多是 get-update,在使用 RocksDB 作为本地状态存储的过程中,出现过以下问题:

  1. 爬虫数据导致热 key,状态会不断进行更新 (get-update),单 KV 数据达到 5MB,而 RocksDB 追加更新的特点导致后台在不断进行 flush 和 compaction,单 task 出现慢节点(抖音直播场景)。
  2. 电商场景作业多数为大状态作业 (目前已上线作业状态约 60TB),业务逻辑中会频繁进行 State 操作。在融合 Flink State 过程中发现 CPU 的开销和原有~ 的 基于内存或 abase 的实现有 40%~80% 的升高。经优化后,CPU 开销主要集中在序列化 / 反序列化的过程中。

针对上述问题,可以通过在内存维护一个对象 Cache,达到优化热点数据访问和降低 CPU 开销的目的。通过上述背景介绍,我们希望能为 StateBackend 提供一个通用的 Cache 功能,通过 Flink StateBackend Cache 功能设计方案达成以下目标:

  1. 减少 CPU 开销 : 通过对热点数据进行缓存,减少和底层 StateBackend 的交互次数,达到减少序列化 / 反序列化开销的目的。
  2. 提升 State 吞吐能力 : 通过增加 Cache 后,State 吞吐能力应比原有的 StateBackend 提供的吞吐能力更高。理论上在 Cache 足够大的情况下,吞吐能力应和基于 Heap 的 StateBackend 近似。
  3. Cache 功能通用化 : 不同的 StateBackend 可以直接适配该 Cache 功能。目前我们主要支持 RocksDB,未来希望可以直接提供给别的 StateBackend 使用,例如 RemoteStateBackend。

经过和字节基础架构 Flink 团队的合作,在实时特征生产升级 ,上线 Cache 大部分场景的 CPU 使用率大概会有高达 50% 左右的收益;

4.2 PB IDL 裁剪

在字节内部的实时特征离线生成链路当中,我们主要依赖的数据流是 Kafka。这些 Kafka 都是通过 PB 定义的数据,字段繁多。公司级别的大 Topic 一般会有 100+ 的字段,但大部分的特征生产任务只使用了其中的部分字段。对于 Protobuf 格式的数据源,我们可以完全通过裁剪数据流,mask 一些非必要的字段来节省反序列化的开销。PB 类型的日志,可以直接裁剪 idl,保持必要字段的序号不变,在反序列化的时候会跳过 unknown field 的解析,这 对于 CPU 来说是更节省的,但是网络带宽不会有收益, 预计裁剪后能节省非常多的 CPU 资源。在上线了 PB IDL 裁剪之后,大部分任务的 CPU 收益在 30% 左右。

5. 遇到的问题

新架构特征生产任务本质就是一个有状态的 Flink 任务,底层的状态存储 StateBackend 主要是本地的 RocksDB。主要面临两个比较难解的问题,一是任务 DAG 变化 Checkpoint 失效,二是本地存储不能很好地支持特征状态历史数据回溯。

  • 实时特征任务不能动态添加新的特征:对于一个线上的 Flink 实时特征生产任务,我们不能随意添加新的特征。这是由于引入新的特征会导致 Flink 任务计算的 DAG 发生改变,从而导致 Flink 任务的 Checkpoint 无法恢复,这对实时有状态特征生产任务来说是不能接受的。目前我们的解法是禁止更改线上部署的特征任务配置,但这也就导致了线上生成的特征是不能随便下线的。对于这个问题暂时没有找到更好的解决办法,后期仍需不断探索。
  • 特征状态冷启动问题:目前主要的状态存储引擎是 RocksDB,不能很好地支持状态数据的回溯。

三、后续规划

当前新一代架构还在字节推荐场景中快速演进,目前已较好解决了实时窗口特征的生产问题。

出于实现统一推荐场景下特征生产的目的,我们后续会继续基于 Flink SQL 流批一体能力,在批式特征生产发力。此外也会基于 Hudi 数据湖技术,完成特征的实时入湖,高效支持模型训练场景离线特征回溯痛点。规则引擎方向,计划继续探索 CEP,推动在电商场景有更多落地实践。在实时窗口计算方向,将继续深入调研 Flink 原生窗口机制,以期解决目前方案面临的窗口特征数据退场问题。

  • 支持批式特征:这套特征生产方案主要是解决实时有状态特征的问题,而目前字节离线场景下还有大量批式特征是通过 Spark SQL 任务生产的。后续我们也会基于 Flink SQL 流批一体的计算能力,提供对批式场景特征的统一支持,目前也初步有了几个场景的落地;
  • 特征离线入湖:基于 Hudi On Flink 支持实时特征的离线数仓建设,主要是为了支持模型训练样本拼接场景离线特征回溯;
  • Flink CEP 规则引擎支持:Flink SQL 本质上就是一种规则引擎,目前在线上我们把 Flink SQL 作为业务 DSL 过滤语义底层的执行引擎。但 Flink SQL 擅长表达的 ETL 类型的过滤规则,不能表达带有时序类型的规则语义。在直播、电商场景的时序规则需要尝试 Flink CEP 更加复杂的规则引擎。
  • Flink Native Windowing 机制引入:对于窗口类型的有状态特征,我们目前采用上文所述的抽象 SlotState 时间切片方案统一进行支持。另外 Flink 本身提供了非常完善的窗口机制,通过 Window Assigner、Window Trigger 等组件可以非常灵活地支持各种窗口语义。因此后续我们也会在窗口特征计算场景引入 Flink 原生的 Windowing 机制,更加灵活地支持窗口特征迭代。
  • Flink HybridState Backend 架构:目前在字节的线上场景中,Flink 底层的 StateBackend 默认都是使用 RocksDB 存储引擎。这种内嵌的存储引擎不能通过外部机制去提供状态数据的回灌和多任务共享,因此我们需要支持 KV 中心化存储方案,实现灵活的特征状态回溯。
  • 静态属性类型特征统一管理:通过特征平台提供统一的 DSL 语义,统一管理其他外部静态类型的特征服务。例如一些其他业务团队维度的用户分类、标签服务等。

更多 Flink 相关技术问题,可扫码加入社区钉钉交流群
第一时间获取最新技术文章和社区动态,请关注公众号~

image.png

本文转载自: 掘金

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

爱奇艺自研QAV1编码器,将在4K高清画质为用户节省366

发表于 2021-11-05

灵动的眼神、清晰的皮肤纹理、真实的风景色彩……能够在手机、电脑、电视随时体验更沉浸的4K超高清画质,是每一位追剧人的心愿。

随着网络电影、网剧的专业化发展,各大网络视频平台纷纷推出自己的“高品质视听“技术和内容,打造随时随地沉浸式影院体验也成了网络视频平台的技术探索方向。

那么,如何用更少的流量的同时实现超高清视频自由?

保证用户观看品质内容的同时帮助用户节省大量流量一直是爱奇艺的使命。想要在流媒体平台上,观看一部超高质量的视频内容,流媒体的高效编码压缩、高传输效率等方面是尤为要关注的技术优化方向。

在网络视频传输时,需要经过采集上传、编码、解码等处理,而编码压缩则是能够直接影响视频画质及宽带大小的环节。

当下5G基础设施已经日趋完善和普及,新的视频应用场景不断在拓展,视频编码压缩技术也在持续优化和迭代。新技术需要考虑诸多方面,比如用户体验、平台的存储、算力和带宽等各项因素,还需要在视频画质、码率、带宽和计算资源之间做平衡。爱奇艺持续探索先进的编解码技术,助力4K/8K视频内容的普及。

**今年的MSU世界编码器大赛中,爱奇艺成为唯一在快速档(30fps)赛道中,使用新一代AV1标准的参赛团队,取得了基于SSIM指标慢速/快速综合性能第一名的好成绩。**并且在能在保证画面质量的同时,降低视频文件体积约20%,这也就意味着带宽降低了20%,用户观看视频所需要的流量也降低了20%,从而达到了更高的观看性价比。

就像下图中,盒子A代表视频体积,在不保持同一个画质的标准下,通过视频编码技术压缩处理后,盒子A变成了盒子B,可以看到,盒子B的体积在压缩后变小,这样既保证了用户观看视频的质量,又达到了降低了带宽成本。

早在2020年爱奇艺率先在安卓移动端和PC Web端上线了AV1视频编码格式的内容。经过最近一年的技术积累,爱奇艺充分利用了前沿的深度学习技术对 QAV1 编码器进行深度的优化,在编码速度方面取得重要突破,在保持高压缩率的同时,充分降低了编码复杂度。

编码效率方面,QAV1目前已经达到了和x265 ultrafast(60fps)同样的编码速度,且压缩率相较 x265 有大幅提升。这意味着 QAV1 可以应用到所有 x265 能用的场景,包括更加注重压缩效率的离线编码场景,以及更加注重实时性的直播场景等。

编码压缩率方面,QAV1遵循保证视频质量的情况下提高压缩率:

  • 1080P快速编码场景(30fps)下,QAV1编码视频可以比x265节省29.9%的带宽。
  • 1080P慢速编码场景(1fps)下,QAV1编码视频可以比x265节省32.8%的带宽。
  • 4K8bit/10bit快速编码场景(30fps)下,QAV1编码视频可以比x265节省36.6%的带宽。
  • 4K8bit/10bit慢速编码场景(1fps)下,QAV1编码视频可以比x265节省27.2%的带宽。

编码复杂度低特性方面,也使得 QAV1 可以在直播场景中使用,从而让用户在更多的视频体验场景下获得更好的视频质量。

爱奇艺 QAV1 编码器率先参加了今年 MSU 比赛 ultrafast 赛道(1080p@60fps),并且在ultrafast场景(1080p@60fps)下,QAV1编码1080p视频可以比x265节省42.6%的码率。该场景下编码资源消耗(单线程编码速度)与AOM最快的参数(–cpu-used=9 –rt –lag-in-frames=0)相同,且节省37.7%的带宽。此外,QAV1 编码器还完善了 8K 编码、高帧率、HDR 编码等多项编码功能。

除了QAV1的优异成绩,爱奇艺QAVS3也是MSU世界编码比赛中首个基于AVS3标准参赛的编码器。AVS3标准是中国AVS工作组开发的最新视频编码标准,压缩率相较于H.265/HEVC也有显著的提升。

爱奇艺将持续探索新技术,突破新标准复杂度高的瓶颈,让自研编码器可以应用于更多场景,更加高效地持续提升用户体验。希望用户在不同终端视频体验能够得到更高标准的保证。

本文转载自: 掘金

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

MySQL之事务及锁机制 MySQL事务 锁机制

发表于 2021-11-05

MySQL事务

事务基本要素—-ACID

事务是由一组SQL语句组成的逻辑处理单元,具有4个属性,通常简称为事务的ACID属性。

  • A (Atomicity) 原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • C (Consistency) 一致性:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
  • I (Isolation)隔离性:一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • D (Durability) 持久性:在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。

并发事务处理带来的问题

  • 更新丢失(Lost Update): 事务A和事务B选择同一行,然后基于最初选定的值更新该行时,由于两个事务都不知道彼此的存在,就会发生丢失更新问题
  • 脏读(Dirty Reads):事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
  • 不可重复读(Non-Repeatable Reads):事务 A 多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
  • 幻读(Phantom Reads):幻读与不可重复读类似。它发生在一个事务A读取了几行数据,接着另一个并发事务B插入了一些数据时。在随后的查询中,事务A就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

幻读和不可重复读的区别:

  • 不可重复读的重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样。(因为中间有其他事务提交了修改)
  • 幻读的重点在于新增或者删除:在同一事务中,同样的条件,,第一次和第二次读出来的记录数不一样。(因为中间有其他事务提交了插入/删除)

并发事务处理带来的问题的解决办法:

  • “更新丢失”通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
  • “脏读” 、“不可重复读”和“幻读” ,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决:

事务隔离级别

数据库事务的隔离级别有4种,由低到高分别为

  • READ-UNCOMMITTED(读未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL的默认事务隔离级别。
  • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 。

这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是Next-Key Lock 算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)已经可以完全保证事务的隔离性要求,即达到了 SQL标准的 SERIALIZABLE(可串行化) 隔离级别,而且保留了比较好的并发性能。

MVCC多版本并发控制

MySQL的大多数事务型存储引擎实现都不是简单的行级锁。基于提升并发性考虑,一般都同时实现了多版本并发控制(MVCC),只是实现机制各不相同。

可以认为 MVCC 是行级锁的一个变种,但它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只是锁定必要的行。

MVCC 的实现是通过保存数据在某个时间点的快照来实现的。也就是说不管需要执行多长时间,每个事物看到的数据都是一致的。

MVCC实现原理

MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。所以我们先来看看这个三个 point 的概念

3个隐式字段

表中每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID 三个字段

  • DB_TRX_ID: 最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID
  • DB_ROLL_PTR: 回滚指针,配合undo日志指向这条记录的上一个版本
  • DB_ROW_ID: 隐含的自增 ID(隐藏主键)

undo日志

undo log 主要分为两种:

  • insert undo log
    代表事务在 insert 新记录时产生的 undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  • update undo log
    事务在进行 update 或 delete 时产生的 undo log ; 不仅在事务回滚时需要,在快照读时也需要

Read View 读视图

什么是 Read View,说白了 Read View 就是事务进行快照读操作的时候生产的读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID (当每个事务开启时,都会被分配一个 ID , 这个 ID 是递增的,所以最新的事务,ID 值越大)

什么是当前读和快照读?

当前读:就是读取的是记录的最新版本,读取时还会对读取的记录进行锁定保证其不被其他并发事务修改;

select lock in share mode , select for update; update; insert; delete这些操作都是一种当前读

快照读:不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的实现是基于多版本并发控制,既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

我们可以把 Read View 简单的理解成有三个全局属性

  • list: 用于维护 Read View 生成时刻系统 正活跃的事务 ID 列表
  • up_limit_id: 当前活跃事务ID的最小值
  • low_limit_id: 系统尚未分配的下一个事务 ID
  1. 首先比较DB_TRX_ID < up_limit_id,如果小于,则当前事务能看到DB_TRX_ID所在的记录,如果大于等于进入下一个判断
  2. 接下来判断DB_TRX_ID >= low_limit_id,如果大于等于则代表DB_TRX_ID所在的记录在Read View生成后才出现的,那么对于当前事务肯定不可见,如果小于,则进入下一步判断
  3. 判断DB_TRX_ID是否在活跃事务中,如果在,则代表在Read View生成的时刻,这个事务还是活跃状态,还没有commit,修改的数据,当前事务也是看不到,如果不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的

MVCC 只在 COMMITTED READ(读提交)和REPEATABLE READ(可重复读)两种隔离级别下工作。

RC和RR级别下快照读有什么区别?

RC和RR级别下都是用了MVCC机制,为什么RC只能做到避免脏读问题而不能避免重复读问题,但是RR可以做到?因为生成Read View的时机不同!

  • RC:每次执行快照读的时候生成新的Read View
  • RR:第一次执行快照读的时候生成Read View,并且之后都是这个版本

锁机制

锁是计算机协调多个进程或线程并发访问某一资源的机制。数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。

首先对mysql锁进行划分:

按照锁的粒度划分:行锁、表锁、页锁

按照锁的使用方式划分:共享锁、排它锁(悲观锁的一种实现)

还有两种思想上的锁:悲观锁、乐观锁。

行锁

行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。 行级锁按照使用方式分为共享锁和排他锁。

共享锁用法:

select … lock in share mode;

排它锁用法:

select … for update

表锁

表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁,但是InnoDB默认的是行级锁。

共享锁用法:

LOCK TABLE table_name READ

排它锁用法:

LOCK TABLE table_name WRITE

解锁用法:

unlock tables;

页锁

开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

适用:从锁的角度来说,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用。

行锁 表锁 页锁
MyISAM √
BDB √ √
InnoDB √ √
Memory √

乐观锁与悲观锁

无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。

  • 悲观锁:简单来说就是从数据库层面上进行并发控制,如InnoDB引擎的行锁、表锁等都是悲观锁的体现。
  • 乐观锁:是从应用层面系统层面上去做并发控制,一般默认数据发生并发冲突的情况较少,所以常见的是在数据表中增加版本号字段,每次更新数据时都对版本号进行校验,再进行更新

InnoDB行锁

InnoDB 实现了共享锁和排它锁两种类型的行锁。

同时为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:

  • 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
  • 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。

意向锁不会与行级的共享 / 排他锁互斥!!!

  • Record Locks
    单个行记录上的锁。对索引项(唯一索引或主键索引)加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;
  • Gap locks
    当我们使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁。对于键值在条件范围内但并不存在的记录,叫做“间隙”。

InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。

间隙锁基于非唯一索引,它锁定一段范围内的索引记录。间隙锁基于下面将会提到的Next-Key Locking 算法,请务必牢记:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。

  • Next-key locks

Next-Key 可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法。通过临建锁可以解决幻读的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。

在根据非唯一索引 对记录行进行 UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE 操作时,InnoDB 会获取该记录行的 临键锁 ,并同时获取该记录行下一个区间的间隙锁。

本文转载自: 掘金

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

第4篇-JVM终于开始调用Java主类的main()方法啦

发表于 2021-11-05

在前一篇 第3篇-CallStub新栈帧的创建 中我们介绍了generate_call_stub()函数的部分实现,完成了向CallStub栈帧中压入参数的操作,此时的状态如下图所示。

继续看generate_call_stub()函数的实现,接来下会加载线程寄存器,代码如下:

1
2
scss复制代码__ movptr(r15_thread, thread);
__ reinit_heapbase();

生成的汇编代码如下:

1
2
perl复制代码mov    0x18(%rbp),%r15  
mov 0x1764212b(%rip),%r12 # 0x00007fdf5c6428a8

对照着上面的栈帧可看一下0x18(%rbp)这个位置存储的是thread,将这个参数存储到%r15寄存器中。

如果在调用函数时有参数的话需要传递参数,代码如下:

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
scss复制代码Label parameters_done;
// parameter_size拷贝到c_rarg3即rcx寄存器中
__ movl(c_rarg3, parameter_size);
// 校验c_rarg3的数值是否合法。两操作数作与运算,仅修改标志位,不回送结果
__ testl(c_rarg3, c_rarg3);
// 如果不合法则跳转到parameters_done分支上
__ jcc(Assembler::zero, parameters_done);

// 如果执行下面的逻辑,那么就表示parameter_size的值不为0,也就是需要为
// 调用的java方法提供参数
Label loop;
// 将地址parameters包含的数据即参数对象的指针拷贝到c_rarg2寄存器中
__ movptr(c_rarg2, parameters);
// 将c_rarg3中值拷贝到c_rarg1中,即将参数个数复制到c_rarg1中
__ movl(c_rarg1, c_rarg3);
__ BIND(loop);
// 将c_rarg2指向的内存中包含的地址复制到rax中
__ movptr(rax, Address(c_rarg2, 0));
// c_rarg2中的参数对象的指针加上指针宽度8字节,即指向下一个参数
__ addptr(c_rarg2, wordSize);
// 将c_rarg1中的值减一
__ decrementl(c_rarg1);
// 传递方法调用参数
__ push(rax);
// 如果参数个数大于0则跳转到loop继续
__ jcc(Assembler::notZero, loop);

这里是个循环,用于传递参数,相当于如下代码:

1
2
3
4
5
6
scss复制代码while(%esi){
rax = *arg
push_arg(rax)
arg++; // ptr++
%esi--; // counter--
}

生成的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
perl复制代码// 将栈中parameter size送到%ecx中
mov 0x10(%rbp),%ecx
// 做与运算,只有当%ecx中的值为0时才等于0
test %ecx,%ecx
// 没有参数需要传递,直接跳转到parameters_done即可
je 0x00007fdf4500079a
// -- loop --
// 汇编执行到这里,说明paramter size不为0,需要传递参数
mov -0x8(%rbp),%rdx
mov %ecx,%esi
mov (%rdx),%rax
add $0x8,%rdx
dec %esi
push %rax
// 跳转到loop
jne 0x00007fdf4500078e

因为要调用Java方法,所以会为Java方法压入实际的参数,也就是压入parameter size个从parameters开始取的参数。压入参数后的栈如下图所示。

当把需要调用Java方法的参数准备就绪后,接下来就会调用Java方法。这里需要重点提示一下Java解释执行时的方法调用约定,不像C/C++在x86下的调用约定一样,不需要通过寄存器来传递参数,而是通过栈来传递参数的,说的更直白一些,是通过局部变量表来传递参数的,所以上图CallStub()函数栈帧中的argument word1 … argument word n其实是​被调用的Java方法局部变量表的一部分。

下面接着看调用Java方法的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scss复制代码// 调用Java方法
// -- parameters_done --

__ BIND(parameters_done);
// 将method地址包含的数据接Method*拷贝到rbx中
__ movptr(rbx, method);
// 将解释器的入口地址拷贝到c_rarg1寄存器中
__ movptr(c_rarg1, entry_point);
// 将rsp寄存器的数据拷贝到r13寄存器中
__ mov(r13, rsp);

// 调用解释器的解释函数,从而调用Java方法
// 调用的时候传递c_rarg1,也就是解释器的入口地址
__ call(c_rarg1);

生成的汇编代码如下:

1
2
3
4
5
6
7
8
perl复制代码// 将Method*送到%rbx中
mov -0x18(%rbp),%rbx
// 将entry_point送到%rsi中
mov -0x10(%rbp),%rsi
// 将调用者的栈顶指针保存到%r13中
mov %rsp,%r13
// 调用Java方法
callq *%rsi

注意调用callq指令后,会将callq指令的下一条指令的地址压栈,再跳转到第1操作数指定的地址,也就是*%rsi表示的地址。压入下一条指令的地址是为了让函数能通过跳转到栈上的地址从子函数返回。 

callq指令调用的是entry_point。entry_point在后面会详细介绍。

公众号 深入剖析Java虚拟机HotSpot 已经更新虚拟机源代码剖析相关文章到60+,欢迎关注,如果有任何问题,可加作者微信mazhimazh,拉你入虚拟机群交流

本文转载自: 掘金

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

1…413414415…956

开发者博客

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