当前位置: 首页 > news >正文

Golang升级到1.7后,之前正确的函数出现错误,分析原因及解决办法

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

最近尝试把开发环境,升级到Golang1.7.1后,程序会偶发性的宕掉,查看日志后,发现总是在一个计算切片的哈希值的地方,错误信息是:

unexpected fault address 0xc043df4000,
fatal error: fault

在1.7之前程序持续运行2年了,从来没有出现这个问题,怀疑是Golang编译器升级到SSA后导致的。将程序的代码精简为以下函数:

//本代码的主要作用是,把一个字符串的Assii的值累加起来。
func SimpleCrc(ptr uintptr, size int) int {
	ret := 0
	maxPtr := ptr + uintptr(size)
	for ptr < maxPtr {
		b := *(*byte)(unsafe.Pointer(ptr)) //出错的地方
		ret += int(b)
		ptr++
	}
	return ret
}

注:实际的代码比这个复杂很多。采用类似这种写法后,相比常规写法性能提升高达8倍。

分析错误直接表现是“非法内存地址访问”导致的,只有一种原因是“字符串使用的内存被SSA编译释放了”,被GC提前回收了并且归还给了windows操作系统。因此查阅了SSA编译器的原理。发现SSA编译器变得聪明很多,它能根据(既定规则)快速判断出,内存不再被使用,所以内存回收非常迅速。由此思考的着眼点变为:有没有什么办法告知SSA编译器,特定的内存在指定的代码区不要回收?,记得之前看过Golang1.7在runtime包中,增加一个函数func KeepAlive(interface{}) {},查看注释后发现“使用该函数可以设定内存在指定的代码区保持有效”,而不被GC回收。

为了重现上述推断,因此编写以下示例:

// memTest
package main

import (
    "fmt"
    "reflect"
    "runtime"
    "unsafe"
)

func SimpleCrc(ptr uintptr, size int) int {
    ret := 0
    maxPtr := ptr + uintptr(size)
    for ptr < maxPtr {
        b := *(*byte)(unsafe.Pointer(ptr))
        ret += int(b)
        ptr++
    }
    return ret
}

//模拟申请内存,触发Gc回收内存
func Allocation(size int) {
    var free []byte
    free = make([]byte, size)
    if len(free) == 0 {
        panic("Allocation Error")
    }
}

func SliceCrcTest(slice []byte, N int) (ret int) {
    newSlice := []byte(string(slice))                       //获取独立内存
    sh := (*reflect.SliceHeader)(unsafe.Pointer(&newSlice)) //反射切片结构
    ptr, size := uintptr(sh.Data), sh.Len                   //获取地址尺寸
    runtime.GC()                                            //强制内存回收
    for i := 0; i < N; i++ {
        ret = SimpleCrc(ptr, size) //计算crc校验码
        Allocation(size)           //模拟申请内存,触发Gc回收内存
    }

    //runtime.KeepAlive(newSlice) //本行一旦注释后结果不再是1665,取消注释节正确
    return
}

func StringCrcTest(str string, N int) (ret int) {
    newStr := string([]byte(str))                          //获取独立内存
    runtime.SetFinalizer(&newStr, func(x *string) {})      //设置回收事件
    sh := (*reflect.StringHeader)(unsafe.Pointer(&newStr)) //反射字符串结构
    ptr, size := uintptr(sh.Data), sh.Len                  //获取地址尺寸
    runtime.GC()                                           //强制内存回收
    for i := 0; i < N; i++ {
        ret = SimpleCrc(ptr, size) //计算crc校验码
        Allocation(size)           //模拟申请内存,触发Gc回收内存
    }

    //runtime.KeepAlive(newStr) //本行一旦注释后结果不再是1665,取消注释节正确
    return
}

func main() {
    var B = []byte("1234567890-1234567890-1234567890") //Crc的值为:1665
    var S = string(B)                                  //生成字符串
    N := 1000000                                       //循环执行1,000,000次
    fmt.Printf("SimpleCrc(\"%s\") = %v\n", B, SliceCrcTest(B, N))
    fmt.Printf("SimpleCrc(\"%s\") = %v\n", B, StringCrcTest(S, N))
}

上述代码重现的思路是,首先申请内存,具体是new一个切片或字符串(其值是"1234567890-1234567890-1234567890",它的正确CRC结果是1665),分别传入函数SliceCrcTest和StringCrcTest查看运行结果;这里只介绍SliceCrcTest函数的内部实现思路,StringCrcTest和SliceCrcTest非常一致,请自己分析理解。

在SliceCrcTest函数内部,首先是代码

newSlice := []byte(string(slice))                       //获取独立内存

本行代码重复申请了两次内存,其目的是,产生一个局部变量,加快重现GC回收newSlice。

sh := (*reflect.SliceHeader)(unsafe.Pointer(&newSlice)) //反射切片结构

本行代码是通过反射,获取到切片newSlice的数据结构,目的是读取“1234567890-1234567890-1234567890”的首地址和长度。

ptr, size := uintptr(sh.Data), sh.Len                   //获取地址尺寸

本行代码是获取“1234567890-1234567890-1234567890”的首地址和长度,到变量ptr, size。

runtime.GC()                                           //强制内存回收

本行代码是强制启动内存回收扫描,然后for循环一百万次,这样做的目的是留出足够的时间让GC取回收内存,循环体类执行代码如下。

ret = SimpleCrc(ptr, size) //计算crc校验码
Allocation(size)           //模拟申请内存,触发Gc回收内存

调用SimpleCrc计算“1234567890-1234567890-1234567890”的校验码,并把最后一次的结果保存到ret返回变量(正确值是1665)。Allocation函数是模拟申请一次内存,函数返回后就内存会被GC回收。

//runtime.KeepAlive(newSlice) //本行一旦注释后结果不再是1665,取消注释节正确

这条语句最为关键,本语句被注释了,那么SliceCrcTest的结果应该是0,这代表着,newSlice 内存被GC回收了,并且同一块内存被再次分配给Allocation函数中的free变量,由于free的初始化为由32个‘0’组成的切片,因此SliceCrcTest计算结果变成了“0”。这样就问题重现了,被SSA编译器误认为,内存不在有效,因此GC就会回收。

注:在实际的重现过程中,因为这是一个随机的过程,不同的操作系统可能不会重现,但是只要知道思路和原理,稍微调整一下N的数值,把它加大就会重现。

 N := 1000000                                       //循环执行1,000,000次

总结: 由于Golang的SSA的编译器,变得非常聪明了,因此会把使用反射reflect.StringHeader,reflect.SliceHeader返回值中的uintptr指向的内存块,当成了没有被使用的内存块回收了。

解决办法有两个:

一是尽量不要过分追求性能,使用反射reflect和unsafe包内的函数。这样能避免一些诡异的、很难分析的bug出现。 如果非要使用反射reflect和unsafe包内的函数,请注意一定要使用runtime.KeepAlive告诉SSA编译器,在指定的代码段内,不要回收内存块。

转载于:https://my.oschina.net/henrylee2cn/blog/832533

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 更新image的方法
  • Docker学习笔记 - Docker容器与外部网络的连接
  • proxy是什么
  • RIP路由配置实例V2
  • 前端开源项目周报0207
  • Codeforces Round #396 (Div. 2) D. Mahmoud and a Dictionary 并查集
  • 为你的网络传输加把锁(OpenSSL)
  • Java之戳中痛点 - (2)取余用偶判断,不要用奇判断
  • 如何才能弥补实际工作经验不足,而获得一份好工作?
  • CentOS 7 网卡命名修改为eth0格式
  • 案例分享〡三拾众筹持续交付开发流程支撑创新业务
  • python学习笔记 - ThreadLocal
  • ettercap dns_spoof
  • TYVJ1860 后缀数组
  • 【Spark Summit EU 2016】Spark与Couchbase——使用Spark扩展数据库操作
  • @angular/forms 源码解析之双向绑定
  • Android 架构优化~MVP 架构改造
  • Android优雅地处理按钮重复点击
  • ECMAScript入门(七)--Module语法
  • HTML-表单
  • httpie使用详解
  • Java 内存分配及垃圾回收机制初探
  • MYSQL 的 IF 函数
  • Python中eval与exec的使用及区别
  • React-redux的原理以及使用
  • Travix是如何部署应用程序到Kubernetes上的
  • vue 个人积累(使用工具,组件)
  • Web设计流程优化:网页效果图设计新思路
  • 对话:中国为什么有前途/ 写给中国的经济学
  • 基于 Ueditor 的现代化编辑器 Neditor 1.5.4 发布
  • 基于组件的设计工作流与界面抽象
  • 码农张的Bug人生 - 见面之礼
  • 面试题:给你个id,去拿到name,多叉树遍历
  • 前端学习笔记之观察者模式
  • 前端之Sass/Scss实战笔记
  • 使用 QuickBI 搭建酷炫可视化分析
  • 原生Ajax
  • # 透过事物看本质的能力怎么培养?
  • #ifdef 的技巧用法
  • (1)Nginx简介和安装教程
  • (LeetCode) T14. Longest Common Prefix
  • (Spark3.2.0)Spark SQL 初探: 使用大数据分析2000万KF数据
  • (附源码)基于ssm的模具配件账单管理系统 毕业设计 081848
  • (附源码)计算机毕业设计SSM基于健身房管理系统
  • (接口封装)
  • (深度全面解析)ChatGPT的重大更新给创业者带来了哪些红利机会
  • (十三)Maven插件解析运行机制
  • (详细文档!)javaswing图书管理系统+mysql数据库
  • (原創) 如何將struct塞進vector? (C/C++) (STL)
  • (转)chrome浏览器收藏夹(书签)的导出与导入
  • (转载)VS2010/MFC编程入门之三十四(菜单:VS2010菜单资源详解)
  • ***微信公众号支付+微信H5支付+微信扫码支付+小程序支付+APP微信支付解决方案总结...
  • .“空心村”成因分析及解决对策122344
  • .net core控制台应用程序初识
  • .net framework4与其client profile版本的区别