記憶體是在程式中打交道不可缺少的存在,或者在GC語言中記憶體概念會被刻意的屏蔽掉,但如果是棧遞歸函數調用自身這種情況,stack overflow這種情況是一樣的會碰到的,所以瞭解下堆與棧碰到問題的時候好解決問題 ...
wmproxy
wmproxy
已用Rust
實現http/https
代理, socks5
代理, 反向代理, 靜態文件伺服器,四層TCP/UDP轉發,七層負載均衡,內網穿透,後續將實現websocket
代理等,會將實現過程分享出來,感興趣的可以一起造個輪子
項目地址
國內: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
關於棧Stack
Stack
可以被認為是一堆書。當我們添加更多的書時,我們將它們添加到棧的頂部。當我們需要一本書時,我們從上面拿一本。
- 添加數據稱為壓入棧
- 移除數據稱為彈出棧
這種現象在編程中被稱為後進先出(LIFO)。
存儲在棧上的數據在編譯時必須具有固定的大小。預設情況下,Rust在棧上為原始類型分配記憶體。所有存儲在堆棧上的數據必須具有已知的固定大小。未知數據編譯時的大小或可能更改的大小必須存儲在堆中而不是棧中。
關於堆Heap
與棧相反,大多數情況下,我們需要將變數(記憶體)傳遞給不同的函數,並使它們保持比單個函數執行更長的時間。這就是我們可以使用heap的時候。
堆的組織性較差:當您將數據放在堆上時,您會請求一個一定的空間。記憶體分配器在堆中找到一個空位這是足夠大的,標志著它正在使用,並返回一個指針,就是那個地方的地址此過程稱為在堆,有時縮寫為分配(將值推到堆棧不被認為是分配的)。因為指向堆的指針是已知的,固定大小的,你可以把指針存儲在堆棧上,但是當你想要的時候,實際數據,您必須遵循指針。想象一下坐在一個餐廳當你進入時,你說明你的小組人數,主人會找到一張適合所有人的空桌子,然後把你帶到那裡。如果如果你的團隊中有人遲到了,他們可以問你坐在哪裡,找到你。
棧與堆對比
- 分配到棧比在堆上分配更快,因為分配器永遠不必搜索存儲新數據的位置;該位置總是在棧的頂部。相比之下,在堆上分配空間需要更多的工作,因為分配器必須首先找到足夠大的空間,保存數據,然後進行簿記,為下一次配置。
- 在堆中訪問數據比訪問棧上的數據慢,因為你得跟著指示牌走。因為訪問堆需要得到相應的指示牌,然後再根據相應的指示牌去尋找相應的位置,然後還要確定位置所占的大小。
statck棧 | heap堆 |
---|---|
在棧中存儲數據的速度更快。 | 在堆中存儲數據的速度較慢。 |
管理棧中的記憶體是可預測的,也是微不足道的。 | 管理堆的記憶體(任意大小)是非常重要的。 |
Rust堆棧預設分配。 | Box用於分配到堆。 |
函數的基元類型和局部變數在棧上分配。 | 大小動態的數據類型,如String 、Vector 、HashMap 、Box 等,在heap上分配。 |
棧與堆的分配示例
讓我們通過一個例子來直觀地瞭解記憶體是如何在堆棧上分配和釋放的。
fn foo() {
let y = 999;
let z = 333;
}
fn main() {
let x = 111;
foo();
}
在上面的例子中,我們首先調用函數main()
。main()
函數有一個變數綁定x
。
Address地址 | Name名稱 | Value值 |
---|---|---|
0 | x | 111 |
在表中,“地址”列指的是RAM的記憶體地址。它從0開始,並轉到您的電腦有多少RAM(位元組數)。“名稱”列是指變數,“值”列是指變數的值。
當foo()
被調用時,一個新的棧幀被分配。foo()
函數有兩個變數綁定,y
和z
。
Address地址 | Name名稱 | Value值 |
---|---|---|
2 | z | 333 |
1 | y | 999 |
0 | x | 111 |
數字0、1和2不使用電腦實際使用的地址值。實際上,地址根據值由一定數量的位元組分隔。
foo()
完成後,其棧幀被釋放。
Address地址 | Name名稱 | Value值 |
---|---|---|
0 | x | 111 |
main()
完成後,其棧幀被釋放。Rust自動在堆棧中分配和釋放記憶體。
與堆棧相反,大多數情況下,我們需要將變數(記憶體)傳遞給不同的函數,並使它們保持比單個函數執行更長的時間。這就是我們可以使用heap的時候。
我們可以使用Box<T>
類型在堆上分配記憶體。比如說,
fn main() {
let x = Box::new(100);
let y = 222;
println!("x = {}, y = {}", x, y);
}
讓我們可視化在上面的例子中調用main()
時的記憶體。
Address地址 | Name名稱 | Value值 |
---|---|---|
0 | x | ??? addr |
1 | y | 222 |
和前面一樣,我們在堆棧上分配兩個變數x和y。
然而,當調用x時,Box::new()的值被分配在堆上。因此,x的實際值是指向堆的指針。
Address地址 | Name名稱 | Value值 |
---|---|---|
578 | 100 | |
... | ... | ... |
0 | x | -> 578 |
1 | y | 222 |
這裡,變數x保存指向地址→578,這是用於演示的任意地址。堆可以以任何順序分配和釋放。因此,它可能會以不同的地址結束,併在地址之間產生漏洞。
因此,當x消失時,它首先釋放堆上分配的記憶體。
Address地址 | Name名稱 | Value值 |
---|---|---|
... | ... | ... |
1 | y | 222 |
一旦main()完成,我們釋放堆棧幀,所有東西都消失了,釋放了所有記憶體。
如何排查問題
堆記憶體的排查
關於堆記憶體的排查,堆記憶體的記憶體量比較大,因此數值相對會大很多,堆記憶體的大小通常小到幾M,大到幾個G,所以在堆記憶體排查的時候可以用巨集觀的記憶體管理器,有以下幾種方法
- 如
TOP
查看記憶體,也可以通過調用系統的api, - 如
memory-stats
實時查看進程當前占用記憶體數:
use memory_stats::memory_stats;
fn main() {
if let Some(usage) = memory_stats() {
println!("Current physical memory usage: {}", usage.physical_mem);
println!("Current virtual memory usage: {}", usage.virtual_mem);
} else {
println!("Couldn't get the current memory usage :(");
}
}
- 可以自定義
Alloc
,因為Rust提供的全局global_alloc
,我們可以通過自定義Alloc
計算當前申請的記憶體數,以及可以用這種方式檢查記憶體泄漏,典型的jemalloc
就是通過這種方式來的,我們用這種方式實現簡單的記憶體統計,我們定義了一個Trallocator
:
use std::alloc::{GlobalAlloc, Layout};
use std::sync::atomic::{AtomicU64, Ordering};
pub struct Trallocator<A: GlobalAlloc>(pub A, AtomicU64);
unsafe impl<A: GlobalAlloc> GlobalAlloc for Trallocator<A> {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
self.1.fetch_add(l.size() as u64, Ordering::SeqCst);
self.0.alloc(l)
}
unsafe fn dealloc(&self, ptr: *mut u8, l: Layout) {
self.0.dealloc(ptr, l);
self.1.fetch_sub(l.size() as u64, Ordering::SeqCst);
}
}
impl<A: GlobalAlloc> Trallocator<A> {
pub const fn new(a: A) -> Self {
Trallocator(a, AtomicU64::new(0))
}
pub fn reset(&self) {
self.1.store(0, Ordering::SeqCst);
}
pub fn get(&self) -> u64 {
self.1.load(Ordering::SeqCst)
}
}
我們通過調用該類,實現
use std::alloc::System;
// 這句使全局的的分配器變成我們自己的分配器
#[global_allocator]
static GLOBAL: Trallocator<System> = Trallocator::new(System);
fn main() {
GLOBAL.reset();
println!("memory used: {} bytes", GLOBAL.get());
GLOBAL.reset();
{
let mut vec = vec![1, 2, 3, 4];
for i in 5..20 {
vec.push(i);
println!("memory used: {} bytes", GLOBAL.get());
}
println!("{:?}", v);
}
println!("memory used: {} bytes", GLOBAL.get());
}
我們可以得到以下輸出:
memory used: 0 bytes
memory used: 32 bytes
memory used: 32 bytes
memory used: 32 bytes
memory used: 32 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 128 bytes
memory used: 128 bytes
memory used: 128 bytes
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
memory used: 0 bytes
可以看到分配完之後已經及時釋放
棧記憶體的排查
因為系統提供的棧記憶體通常只有8m左右,且Rust中的線程的預設棧記憶體只有2M,如果分配過大的棧記憶體將會導致棧溢出,比如
fn main() {
let bad = [0;10240000];
}
就會出現如下提示
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
在現在的方法中,我並未找到有合適的檢查當前進程占用的棧記憶體數。
- 測試用
alloc
看是否能測出棧記憶體:
use std::alloc::System;
#[global_allocator]
static GLOBAL: Trallocator<System> = Trallocator::new(System);
fn main() {
GLOBAL.reset();
println!("memory used: {} bytes", GLOBAL.get());
GLOBAL.reset();
let x = 0;
let bad = [0;10240];
println!("memory used: {} bytes", GLOBAL.get());
}
運行上述程式,如下輸出:
memory used: 0 bytes
memory used: 0 bytes
程式無法感知到棧記憶體的變化。
- 測試用
memory-stats
實時查看記憶體
use memory_stats::memory_stats;
fn main() {
if let Some(usage) = memory_stats() {
println!("初始記憶體 usage: {}", usage.physical_mem);
} else {
println!("Couldn't get the current memory usage :(");
}
let value1 = vec![10;102400];
std::thread::sleep(std::time::Duration::from_secs(1));
if let Some(usage) = memory_stats() {
println!("申請堆記憶體後 usage: {}", usage.physical_mem);
} else {
println!("Couldn't get the current memory usage :(");
}
let value = [10;102400];
std::thread::sleep(std::time::Duration::from_secs(1));
if let Some(usage) = memory_stats() {
println!("申請棧記憶體後 usage: {}", usage.physical_mem);
} else {
println!("Couldn't get the current memory usage :(");
}
}
以上程式會輸出:
初始記憶體 usage: 1024000
申請堆記憶體後 usage: 1478656
申請棧記憶體後 usage: 1478656
我們可以感知到堆記憶體的變化,無法感知到棧記憶體的變化。
- 目前找到的可以測量類對象的棧記憶體值。可以用
std::mem::size_of_val
來測量類對象占用的棧記憶體大小,我們可以通過該方法進行棧大小的排查,看是否存在超級大的占用棧的對象,如果存在,需將其移動到堆,也就是用Box
進行包裹。
fn main() {
let x = 0u32;
assert_eq!(4, std::mem::size_of_val(&x));
let val = vec![0u64;9999];
assert_eq!(24, std::mem::size_of_val(&val));
let mut hash = HashMap::new();
hash.insert(1, 2);
assert_eq!(48, std::mem::size_of_val(&hash));
hash.insert(2, 4);
assert_eq!(48, std::mem::size_of_val(&hash));
}
我們來分析下Vec的記憶體,為什麼其占用大小為24個位元組(64位的機器)
pub struct Vec<T, A: Allocator = Global> {
buf: RawVec<T, A>, /// 需要再進行類的分析
len: usize, /// 占用64位,也就是8個位元組
}
pub(crate) struct RawVec<T, A: Allocator = Global> {
ptr: Unique<T>, /// 指針大小,占用64位,8位元組
cap: usize, /// 容量大小,占用64位,8位元組
alloc: A, /// 分配器,不占用棧記憶體
}
綜上分析,每個Vec
的棧大小占用記憶體均為24位元組。程式測試一致。同樣HashMap
占用的棧大小均為48個位元組,不受其Map大小的影響。
註意:如果用非同步的Future的包圍,如果返回的對象也就是
Furture<Output=xxx>
的棧大小過大,很容易在遞進處理非同步的情況下直接棧溢出,而此時完全還未執行到該函數,造成一種很難排查的景象
註意!!!非同步的返回值千萬棧大小不要過大!不要過大!不要過大!
- 另外還有一種是遞歸的函數調用,也會造成棧溢出,這類問題相對好定位:
fn f(x: i32) {
f(1);
}
fn main() {
f(2);
}
直接會顯示
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
小結
所以在排查記憶體泄漏還是排查棧大小時都需要對當前的數據進行分析,需要處理的東西較多,需要有比較好的耐心去處理,一步步的去排查推進。記得非同步返回的Output
如果過大,會導致代碼還未執行,但已經棧溢出的情況。
點擊 [關註],[在看],[點贊] 是對作者最大的支持