Stealthpool, an Off Heap Golang Memory Pool
Go 语言和 Java,C# 一样,都在运行时通过垃圾收集器(Garbage Collector)管理内存。这虽然方便了程序编写,但也带来了额外开销,垃圾回收有时会导致应用响应时间过长,这也催生了 GC 调优概念。本文将介绍 Github 上的一个 Golang 非堆内存池,它借助系统调用,能获得不受 GC 控制的内存。如果你需要长期使用一块内存,这能够降低 GC 负担。
Project Structure
➜ git clone https://github.com/Link512/stealthpool.git
➜ cd stealthpool
➜ tree -P '*.go'
.
├── alloc_unix.go
├── alloc_windows.go
├── doc.go
├── errors.go
├── example_test.go
├── multierr.go
├── multierr_test.go
├── pool.go
└── pool_test.go
0 directories, 9 files
项目结构非常简单,只有一级目录,共 9 个源文件。最上面两个 alloc_
文件封装了真正的内存分配系统调用,由于不同操作系统的 API 有所区别,所有使用两个文件。得益于 Golang 的条件编译,这些文件在编译到目标平台时会自动选择。xxx_test.go
命名的文件是对 xxx.go
的单元测试,这些文件可以通过 go test
命令编译执行。doc.go
中包含整个包的整体注解,go doc
命令会读取它生成项目文档的 Overview
。errors.go
定义了包下的所有自定义错误。multierr.go
则是多个错误的包装。pool.go
提供了项目的核心接口和实现。
Bussiness Logic
首先是物理内存申请,*nix
系统逻辑位于 alloc_unix.go
,Windows
位于 alloc_windows.go
,两个文件内部包含的函数签名是一样的,编译器会根据目标系统任选其一。
第一个函数是 alloc
,它接收一个 int
型参数,代表要分配的字节大小。返回 byte
切片,代表分配到的内存,还有一个 error
代表是否分配成功。
func alloc(size int) ([]byte, error)
第二个函数是 dealloc
,它接收一个 byte
切片代表要释放的内存,返回一个 error
代表操作是否成功。它的参数应该是 alloc
的返回值。
func dealloc(b []byte) error
下面是 *nix
系统对应的源码内容。
// +build !windows !appengine
package stealthpool
import "golang.org/x/sys/unix"
func alloc(size int) ([]byte, error) {
return unix.Mmap(
-1, // required by MAP_ANONYMOUS
0, // offset from file descriptor start, required by MAP_ANONYMOUS
size, // how much memory
unix.PROT_READ|unix.PROT_WRITE, // protection on memory
unix.MAP_ANONYMOUS|unix.MAP_PRIVATE, // private so other processes don't see the changes, anonymous so that nothing gets synced to the file
)
}
func dealloc(b []byte) error {
return unix.Munmap(b)
}
alloc
函数体直接通过 Golang 封装的 Mmap
执行 mmap
系统调用,它实际上是内存文件映射,签名如下
func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error)
实际参数的含义如下
参数 | 含义 |
---|---|
-1 | 映射的文件描述符。-1 用于兼容匿名映射 |
0 | 映射对象内容的起点。0 代表从起始位置映射 |
size | 映射区的长度。单位字节 |
unix.PROT_READ / unix.PROT_WRITE | 期望的内存保护标志。此处表示页可读可写 |
unix.MAP_ANONYMOUS / unix.MAP_PRIVATE | 映射对象的类型。此处为匿名私有映射,映射区不与任何文件关联,内存区域的写入不会影响到原文件。不与其它进程共享映射 |
dealloc
函数体直接通过 Golang 封装的 Munmap
执行 munmap
系统调用,它用于取消内存文件映射,签名如下
func Munmap(b []byte) (err error)
再看 Windows
下对应的源代码内容。
[collapse title="Windows 下的内存申请与释放"]
package stealthpool
import (
"reflect"
"runtime"
"syscall"
"unsafe"
)
const (
memCommit = 0x1000
memReserve = 0x2000
memRelease = 0x8000
pageRW = 0x04
kernelDll = "kernel32.dll"
allocFunc = "VirtualAlloc"
deallocFunc = "VirtualFree"
errOK = 0
)
var (
kernel *syscall.DLL
virtualAlloc *syscall.Proc
virtualDealloc *syscall.Proc
)
func init() {
runtime.LockOSThread()
kernel = syscall.MustLoadDLL(kernelDll)
virtualAlloc = kernel.MustFindProc(allocFunc)
virtualDealloc = kernel.MustFindProc(deallocFunc)
runtime.UnlockOSThread()
}
func alloc(size int) ([]byte, error) {
addr, _, err := virtualAlloc.Call(uintptr(0), uintptr(size), memCommit|memReserve, pageRW)
errNo := err.(syscall.Errno)
if errNo != errOK {
return nil, err
}
var result []byte
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&result))
hdr.Data = addr
hdr.Cap = size
hdr.Len = size
return result, nil
}
func dealloc(b []byte) error {
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&b))
_, _, err := virtualDealloc.Call(hdr.Data, 0, memRelease)
errNo := err.(syscall.Errno)
if errNo != errOK {
return err
}
return nil
}
[/collapse]
Golang 并未封装 Windows 下的内存分配专用系统调用,但提供了加载动态链接库和过程函数的通用调用,使用者需要加载动态链接库和其中的过程函数,随后执行过程函数完成所需功能。下面是涉及到的类型和函数签名:
[collapse title="相关类型和函数签名"]
package runtime
/*
LockOSThread wires the calling goroutine to its current operating system thread. The calling goroutine will always execute in that thread, and no other goroutine will execute in it, until the calling goroutine has made as many calls to UnlockOSThread as to LockOSThread. If the calling goroutine exits without unlocking the thread, the thread will be terminated.
All init functions are run on the startup thread. Calling LockOSThread from an init function will cause the main function to be invoked on that thread.
A goroutine should call LockOSThread before calling OS services or non-Go library functions that depend on per-thread state.
*/
func LockOSThread()
/*
UnlockOSThread undoes an earlier call to LockOSThread. If this drops the number of active LockOSThread calls on the calling goroutine to zero, it unwires the calling goroutine from its fixed operating system thread. If there are no active LockOSThread calls, this is a no-op.
Before calling UnlockOSThread, the caller must ensure that the OS thread is suitable for running other goroutines. If the caller made any permanent changes to the state of the thread that would affect other goroutines, it should not call this function and thus leave the goroutine locked to the OS thread until the goroutine (and hence the thread) exits.
*/
func UnlockOSThread()
package syscall
/*
An Errno is an unsigned number describing an error condition. It implements the error interface. The zero Errno is by convention a non-error, so code to convert from Errno to error should use:
err = nil
if errno != 0 {
err = errno
}
Errno values can be tested against error values from the os package using errors.Is. For example:
_, _, err := syscall.Syscall(...)
if errors.Is(err, fs.ErrNotExist) ...
*/
type Errno uintptr
// A DLL implements access to a single DLL.
type DLL struct {
Name string
Handle Handle
}
// A Proc implements access to a procedure inside a DLL.
type Proc struct {
Dll *DLL
Name string
// contains filtered or unexported fields
}
/*
LoadDLL loads the named DLL file into memory.
If name is not an absolute path and is not a known system DLL used by Go, Windows will search for the named DLL in many locations, causing potential DLL preloading attacks.
Use LazyDLL in golang.org/x/sys/windows for a secure way to load system DLLs.
*/
func LoadDLL(name string) (*DLL, error)
// MustLoadDLL is like LoadDLL but panics if load operation fails.
func MustLoadDLL(name string) *DLL
// FindProc searches DLL d for procedure named name and returns *Proc if found. It returns an error if search fails.
func (d *DLL) FindProc(name string) (proc *Proc, err error)
// MustFindProc is like FindProc but panics if search fails.
func (d *DLL) MustFindProc(name string) *Proc
/*
Call executes procedure p with arguments a. It will panic if more than 18 arguments are supplied.
The returned error is always non-nil, constructed from the result of GetLastError. Callers must inspect the primary return value to decide whether an error occurred (according to the semantics of the specific function being called) before consulting the error. The error always has type syscall.Errno.
On amd64, Call can pass and return floating-point values. To pass an argument x with C type "float", use uintptr(math.Float32bits(x)). To pass an argument with C type "double", use uintptr(math.Float64bits(x)). Floating-point return values are returned in r2. The return value for C type "float" is math.Float32frombits(uint32(r2)). For C type "double", it is math.Float64frombits(uint64(r2)).
*/
func (p *Proc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error)
package builtin
// uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
type uintptr uintptr
package unsafe
// ArbitraryType is here for the purposes of documentation only and is not actually part of the unsafe package. It represents the type of an arbitrary Go expression.
type ArbitraryType int
/*
Pointer represents a pointer to an arbitrary type. There are four special operations available for type Pointer that are not available for other types:
- A pointer value of any type can be converted to a Pointer.
- A Pointer can be converted to a pointer value of any type.
- A uintptr can be converted to a Pointer.
- A Pointer can be converted to a uintptr.
Pointer therefore allows a program to defeat the type system and read and write arbitrary memory. It should be used with extreme care.
*/
type Pointer *ArbitraryType
package reflect
// SliceHeader is the runtime representation of a slice. It cannot be used safely or portably and its representation may change in a later release. Moreover, the Data field is not sufficient to guarantee the data it references will not be garbage collected, so programs must keep a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
[/collapse]
文件首先定义了一组常量,它们是系统调用要用到的动态链接库和过程名称,以及过程参数的值。它们可以在 Win32 API 文档中找到。
const (
memCommit = 0x1000 // 物理存储提交给保留区域。作为 VirtualAlloc 的参数
memReserve = 0x2000 // 页面被保留以供将来使用。作为 VirtualAlloc 的参数
memRelease = 0x8000 // 释放特定区域的页面。作为 VirtualFree 的参数
pageRW = 0x04 // 页面可读可写
kernelDll = "kernel32.dll" // 内核动态链接库名
allocFunc = "VirtualAlloc" // 页面分配过程名
deallocFunc = "VirtualFree" // 页面回收过程名
errOK = 0 // 系统调用成功的返回值
)
紧接着定义了一组变量:
var (
kernel *syscall.DLL // 内核系统调用
virtualAlloc *syscall.Proc // 页面分配过程
virtualDealloc *syscall.Proc // 页面释放过程
)
下面是初始化函数,它通过通用系统调用加载上面三个变量。该函数使用 LockOSThread()
和 UnlockOSThread()
包裹函数体,确保 goroutine
在同一线程执行,这样才能保证系统调用成功。
func init() {
runtime.LockOSThread()
kernel = syscall.MustLoadDLL(kernelDll)
virtualAlloc = kernel.MustFindProc(allocFunc)
virtualDealloc = kernel.MustFindProc(deallocFunc)
runtime.UnlockOSThread()
}
再往下是内存分配函数,它通过 VirtualAlloc
内核调用向操作系统申请内存,这个 Win32 API 签名如下:
// Reserves, commits, or changes the state of a region of pages in the virtual address space of the calling process. Memory allocated by this function is automatically initialized to zero.
LPVOID VirtualAlloc(
LPVOID lpAddress, // The starting address of the region to allocate.
SIZE_T dwSize, // The size of the region, in bytes.
DWORD flAllocationType, // The type of memory allocation.
DWORD flProtect // The memory protection for the region of pages to be allocated.
);
VirtualAlloc.Call
的第一个参数是起始地址,uintptr(0)
代表 C
中的 NULL
,NULL
让系统决定起始地址。第二个参数是申请的内存大小,单位字节。第三个参数是分配类型,此处为保留并映射物理内存。最后一个参数是内存保护类型,此处为可读可写。申请成功后,会把地址 addr
绑定到一个字节切片返回,借助 unsafe.Pointer
可以实现任意类型指针的转换。
func alloc(size int) ([]byte, error) {
addr, _, err := virtualAlloc.Call(uintptr(0), uintptr(size), memCommit|memReserve, pageRW)
errNo := err.(syscall.Errno)
if errNo != errOK {
return nil, err
}
var result []byte
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&result))
hdr.Data = addr
hdr.Cap = size
hdr.Len = size
return result, nil
}
该文件的最后一个函数是 dealloc
,它通过 VirtualFree
让操作系统释放内存,这个 Win32 API 签名如下:
// Releases, decommits, or releases and decommits a region of pages within the virtual address space of the calling process.
BOOL VirtualFree(
LPVOID lpAddress, // A pointer to the base address of the region of pages to be freed.
SIZE_T dwSize, // The size of the region of memory to be freed, in bytes.
DWORD dwFreeType // The type of free operation.
);
func dealloc(b []byte) error {
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&b))
_, _, err := virtualDealloc.Call(hdr.Data, 0, memRelease)
errNo := err.(syscall.Errno)
if errNo != errOK {
return err
}
return nil
}
VirtualDealloc.Call
的第一个参数是要释放页的基地址。第二个参数是要释放的字节数,和 memRelease
搭配使用时,必须是 0
。第三个参数是释放类型,此处是直接释放。
以上是涉及操作系统的全部内容,下面全是 Go 语言的自身特性。首先看 errors.go
,它定义了三个自定义错误,分别是 池满
,预分配越界
和 块非法
。此处主要关注错误集中定义的思想。
package stealthpool
import "errors"
var (
// ErrPoolFull is returned when the maximum number of allocated blocks has been reached
ErrPoolFull = errors.New("pool is full")
// ErrPreallocOutOfBounds is returned when whe number of preallocated blocks requested is either negative or above maxBlocks
ErrPreallocOutOfBounds = errors.New("prealloc value out of bounds")
// ErrInvalidBlock is returned when an invalid slice is passed to Return()
ErrInvalidBlock = errors.New("trying to return invalid block")
)
接下来是 multierr.go
,它定义了一个 multiErr
结构,内部是 error
切片。之后定义了一个构造方法 newMultiErr
,它返回一个类型指针。之后在 *multiErr
上定义了三个函数,其中的 Error()
使得 *multiErr
也成为 error
类型。
package stealthpool
import "strings"
type multiErr struct {
errs []error
}
func newMultiErr() *multiErr {
return &multiErr{}
}
func (e *multiErr) Add(err error) {
if err != nil {
e.errs = append(e.errs, err)
}
}
func (e *multiErr) Return() error {
if len(e.errs) == 0 {
return nil
}
return e
}
func (e *multiErr) Error() string {
result := strings.Builder{}
for _, e := range e.errs {
result.WriteString(e.Error() + "\n")
}
return strings.TrimRight(result.String(), "\n")
}
接下来是一个包含核心业务逻辑和接口设计的 pool.go
文件,它实现了一个内存池。
[collapse title="pool.go 源码"]
package stealthpool
import (
"reflect"
"runtime"
"sync"
"unsafe"
)
type poolOpts struct {
blockSize int
preAlloc int
}
var (
defaultPoolOpts = poolOpts{
blockSize: 4 * 1024,
}
)
// PoolOpt is a configuration option for a stealthpool
type PoolOpt func(*poolOpts)
// WithPreAlloc specifies how many blocks the pool should preallocate on initialization. Default is 0.
func WithPreAlloc(prealloc int) PoolOpt {
return func(opts *poolOpts) {
opts.preAlloc = prealloc
}
}
// WithBlockSize specifies the block size that will be returned. It is highly advised that this block size be a multiple of 4KB or whatever value
// `os.Getpagesize()`, since the mmap syscall returns page aligned memory
func WithBlockSize(blockSize int) PoolOpt {
return func(opts *poolOpts) {
opts.blockSize = blockSize
}
}
// Pool is the off heap memory pool. It it safe to be used concurrently
type Pool struct {
sync.RWMutex
free [][]byte
allocated map[*byte]struct{}
initOpts poolOpts
maxBlocks int
}
// New returns a new stealthpool with the given capacity. The configuration options can be used to change how many blocks are preallocated or block size.
// If preallocation fails (out of memory, etc), a cleanup of all previously preallocated will be attempted
func New(maxBlocks int, opts ...PoolOpt) (*Pool, error) {
o := defaultPoolOpts
for _, opt := range opts {
opt(&o)
}
p := &Pool{
initOpts: o,
free: make([][]byte, 0, maxBlocks),
allocated: make(map[*byte]struct{}, maxBlocks),
maxBlocks: maxBlocks,
}
if o.preAlloc > 0 {
if err := p.prealloc(o.preAlloc); err != nil {
return nil, err
}
}
runtime.SetFinalizer(p, func(pool *Pool) {
pool.Close()
})
return p, nil
}
// Get returns a memory block. It will first try and retrieve a previously allocated block and if that's not possible, will allocate a new block.
// If there were maxBlocks blocks already allocated, returns ErrPoolFull
func (p *Pool) Get() ([]byte, error) {
if b, ok := p.tryPop(); ok {
return b, nil
}
p.Lock()
defer p.Unlock()
if len(p.allocated) == p.maxBlocks {
return nil, ErrPoolFull
}
result, err := alloc(p.initOpts.blockSize)
if err != nil {
return nil, err
}
k := &result[0]
p.allocated[k] = struct{}{}
return result, nil
}
// Return gives back a block retrieved from Get and stores it for future re-use.
// The block has to be exactly the same slice object returned from Get(), otherwise ErrInvalidBlock will be returned.
func (p *Pool) Return(b []byte) error {
if err := p.checkValidBlock(b); err != nil {
return err
}
p.Lock()
defer p.Unlock()
p.free = append(p.free, b)
return nil
}
// FreeCount returns the number of free blocks that can be reused
func (p *Pool) FreeCount() int {
p.RLock()
defer p.RUnlock()
return len(p.free)
}
// AllocCount returns the total number of allocated blocks so far
func (p *Pool) AllocCount() int {
p.RLock()
defer p.RUnlock()
return len(p.allocated)
}
// Close will cleanup the memory pool and deallocate ALL previously allocated blocks.
// Using any of the blocks returned from Get() after a call to Close() will result in a panic
func (p *Pool) Close() error {
return p.cleanup()
}
func (p *Pool) tryPop() ([]byte, bool) {
p.Lock()
defer p.Unlock()
if len(p.free) == 0 {
return nil, false
}
n := len(p.free) - 1
result := p.free[n]
p.free[n] = nil
p.free = p.free[:n]
return result, true
}
func (p *Pool) checkValidBlock(block []byte) error {
if len(block) == 0 || len(block) != cap(block) {
return ErrInvalidBlock
}
k := &block[0]
p.RLock()
_, found := p.allocated[k]
p.RUnlock()
if !found || len(block) != p.initOpts.blockSize {
return ErrInvalidBlock
}
return nil
}
func (p *Pool) prealloc(n int) error {
if n < 0 || n > p.maxBlocks {
return ErrPreallocOutOfBounds
}
for i := 0; i < n; i++ {
block, err := alloc(p.initOpts.blockSize)
if err != nil {
_ = p.cleanup()
return err
}
k := &block[0]
p.allocated[k] = struct{}{}
p.free = append(p.free, block)
}
return nil
}
func (p *Pool) cleanup() error {
p.Lock()
defer p.Unlock()
multiErr := newMultiErr()
for arrayPtr := range p.allocated {
var block []byte
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&block))
hdr.Cap = p.initOpts.blockSize
hdr.Len = p.initOpts.blockSize
hdr.Data = uintptr(unsafe.Pointer(arrayPtr))
if err := dealloc(block); err != nil {
multiErr.Add(err)
}
}
p.allocated = nil
p.free = nil
return multiErr.Return()
}
[/collapse]
文件首先定义了一个 poolOpts
结构,表示内存池选项,包含块大小和预分配块数两个属性。紧接着定义了一个 defaultPoolOpts
变量,代表默认选项,指定了块大小为 4K
。
type poolOpts struct {
blockSize int
preAlloc int
}
var (
defaultPoolOpts = poolOpts{
blockSize: 4 * 1024,
}
)
接着,定义了一个 PoolOpt
类型,它是一个函数,接收 *poolOpts
参数,暴露为公共接口。紧接着定义了两个公共函数,它们分别接收 poolOpts
的一个属性,返回一个 PoolOpt
函数,返回的函数接收 *poolOpts
,会在参数上设置相应属性,值是外层函数参数。这是一种 函数式选项设计模式
,它使用了闭包,可以使下文提到的构造器参数非常灵活。
// PoolOpt is a configuration option for a stealthpool
type PoolOpt func(*poolOpts)
// WithPreAlloc specifies how many blocks the pool should preallocate on initialization. Default is 0.
func WithPreAlloc(prealloc int) PoolOpt {
return func(opts *poolOpts) {
opts.preAlloc = prealloc
}
}
// WithBlockSize specifies the block size that will be returned. It is highly advised that this block size be a multiple of 4KB or whatever value
// `os.Getpagesize()`, since the mmap syscall returns page aligned memory
func WithBlockSize(blockSize int) PoolOpt {
return func(opts *poolOpts) {
opts.blockSize = blockSize
}
}
再往下,定义了内存池结构和它的构造函数。该结构持有一个读写锁,确保对池的访问是线程安全的。free
属性是一个二维字节切片,第一维对应一块真正的内存空间,第二维表明有很多块。allocated
属性是一个字节指针到空结构的映射,用来记录申请到的各个物理内存块地址。maxBlocks
代表内存池中有多少块内存。
构造函数的第一个参数是 maxBlocks
,指定内存池内存块个数。后面是任意多个 PoolOpt
参数,用来设定内存池属性,上文提到这种设计叫 函数式选项设计模式
,它让调用者可以非常方便地构建对象。
如果指定了预分配内存,则调用 p.prealloc()
处理。函数最后通过 runtime.SetFinalizer
为内存池指定了不可达回调,这可避免使用者未关闭内存池导致的内存泄露。
// Pool is the off heap memory pool. It it safe to be used concurrently
type Pool struct {
sync.RWMutex
free [][]byte
allocated map[*byte]struct{}
initOpts poolOpts
maxBlocks int
}
// New returns a new stealthpool with the given capacity. The configuration options can be used to change how many blocks are preallocated or block size.
// If preallocation fails (out of memory, etc), a cleanup of all previously preallocated will be attempted
func New(maxBlocks int, opts ...PoolOpt) (*Pool, error) {
o := defaultPoolOpts
for _, opt := range opts {
opt(&o)
}
p := &Pool{
initOpts: o,
free: make([][]byte, 0, maxBlocks),
allocated: make(map[*byte]struct{}, maxBlocks),
maxBlocks: maxBlocks,
}
if o.preAlloc > 0 {
if err := p.prealloc(o.preAlloc); err != nil {
return nil, err
}
}
runtime.SetFinalizer(p, func(pool *Pool) {
pool.Close()
})
return p, nil
}
下面的函数都是切片、映射等结构和互斥锁的基本操作,就不一一介绍了。项目还包含编译配置和持续集成/持续交互文件,交由读者自行了解。
➜ tree -a -I '.git|*.go|go.*'
.
├── .drone.yml
├── .golangci.yml
├── LICENSE
├── Makefile
└── README.md
0 directories, 5 files
API
首先是内存池的创建,需要指定内存块个数。不要忘记使用完毕后关闭内存池,否则会造成内存泄漏,虽然构造器预留了预防措施,但那要等待一次垃圾收集。
// initialize a pool which will allocate a maximum of 100 blocks
pool, err := stealthpool.New(100)
defer pool.Close() // ALWAYS close the pool unless you're very fond of memory leaks
得益于 选项设计模式
,还可以方便地指定块大小和预申请块数。
// initialize a pool with custom block size and preallocated blocks
poolCustom, err := stealthpool.New(100, stealthpool.WithBlockSize(8*1024), stealthpool.WithPreAlloc(100))
defer poolCustom.Close() // ALWAYS close the pool unless you're very fond of memory leaks
内存池创建完毕后,通过 Get
方法获取一块内存,通过 Return
方法将内存块还给内存池。
block, err := poolCustom.Get()
// do some work with block
// then return it exactly as-is to the pool
err = poolCustom.Return(block)
References
- Golang 内存管理
- 内存分配器 - Go 语言设计与实现
- 用户态系统调用 - Go 语言原本
- Golang 与系统调用
- 系统调用在 Golang 中的实践
- Go 的线程何时会阻塞
- Golang 中的 runtime.LockOSThread 和 runtime.UnlockOSThread
- Linux 内存分配小结 - malloc、brk、mmap
- 认真分析 mmap:是什么 为什么 怎么用
- 内存映射 - Linux/UNIX 系统编程手册
- MMAP - Linux 手册页
- C 语言 malloc 和 free 实现原理
- DUMP 文件分析:Windows 中 malloc 和 free 的实现
- 内存分配详解 - malloc, new, HeapAlloc, VirtualAlloc,GlobalAlloc
- Undefined: syscall.Mmap on Windows 10
- Win32 内存结构
- Windows API 之 VirtualAlloc
- VirtualAlloc function - Win32 API
- VirtualFree function - Win32 API
- 动态链接库 - 维基百科
- 内存泄漏 - 维基百科
- 既然每个程序占用的内存都是操作系统管理的,为什么内存泄漏还是个问题?
- Rust 垃圾回收机制
- Rust 所有权:内存管理新流派
- 使用堆外内存优化 JVM GC 问题小记
- 从实际案例聊聊 Java 应用的 GC 优化
- GC 原理及调优
- It's all about buffers: zero-copy, mmap and Java NIO
- Java 直接内存 DirectMemory 详解
- Java 堆外内存、零拷贝、直接内存以及针对于 NIO 中的 FileChannel 的思考
- Java: 未来已来
- 指针函数和函数指针
- void 指针 - void * 的用法
- C 语言条件编译详解
- Go 语言如何使用条件编译
- 你一直在寻找的 Go 语言条件编译
- go doc 文档生成查看
- go test 命令完全攻略
- Go 语言设计模式之函数式选项模式
- SliceHeader:slice 高效处理数据
- unsafe.Pointer - 指针类型转换
当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »