自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

程序運(yùn)行后性能總會下降?你應(yīng)該先了解編程語言的內(nèi)存布局與管理

新聞 前端
當(dāng)今流行的編程語言,大多具備垃圾回收(Garbage Collection,以下簡稱GC)功能。它能夠?qū)⒉辉偈褂玫膬?nèi)存區(qū)域收回并重新分配。

引言

當(dāng)今流行的編程語言,大多具備垃圾回收(Garbage Collection,以下簡稱GC)功能。它能夠?qū)⒉辉偈褂玫膬?nèi)存區(qū)域收回并重新分配。

這一功能可以說,將程序員的注意力從內(nèi)存的分配/釋放工作中解放了出來,可以專注于業(yè)務(wù)邏輯的實(shí)現(xiàn)。但這并不意味著說,程序員在寫代碼的時(shí)候就可以無所顧忌了。

因?yàn)樗麄兠鎸Φ沫h(huán)境里,資源畢竟是有限的,而GC也不能包辦一切工作。尤其是程序需要運(yùn)行時(shí)性能的時(shí)候,對代碼的編寫就有更高的要求了。

而在優(yōu)化程序性能時(shí),也不能憑著猜想去實(shí)施,這就需要對編程語言的內(nèi)存布局與管理有清楚的了解。這樣才能做到有的放矢,事半而功倍。

下面我們先從編譯技術(shù)的基本概念說起。

編譯技術(shù)

編譯器方式,這種方式是將代碼經(jīng)過預(yù)處理、編譯、匯編、鏈接之后,得到一個(gè)可執(zhí)行文件。這個(gè)文件里面包含的都是二進(jìn)制的機(jī)器指令,它的優(yōu)點(diǎn)是程序執(zhí)行速度快,能將硬件性能充分發(fā)揮出來。

它的缺點(diǎn)則是編譯過程需要耗費(fèi)時(shí)間,程序修改之后必須重新編譯才能使用。在早些年硬件性能不高的時(shí)候,編譯一個(gè)大型的程序需要一兩個(gè)小時(shí)是很平常的事。

此類語言的典型代表是C/C++,以及現(xiàn)在十分流行的Go語言。

解釋器方式,程序代碼直接運(yùn)行在一個(gè)解釋器中,沒有編譯的過程。優(yōu)點(diǎn)則是可以立即運(yùn)行,且可移植性好,代碼編寫一次即可在任何平臺上運(yùn)行,而且預(yù)期效果也一樣。而編譯器方式則要麻煩的多,它需要為每一個(gè)平臺單獨(dú)編譯一次。

不過解釋器方式的缺點(diǎn)也同樣明顯,就是它的性能受限。畢竟是隔著一層解釋器去執(zhí)行,遠(yuǎn)遠(yuǎn)比不了翻譯成機(jī)器指令的二進(jìn)制可執(zhí)行文件。

此類語言的代表則有python、ruby、php、javascript等??梢哉J(rèn)為,腳本類語言都屬于解釋器方式執(zhí)行。

中間代碼方式,這是一種折衷式的方案,它會先對代碼有一次編譯過程,但不是編譯成可執(zhí)行文件,而是一份中間代碼。然后這份中間代碼會放到一個(gè)虛擬機(jī)里去執(zhí)行。以這樣的方式既獲得了良好的可移植性,也能夠擁有高于解釋器的速度。

java語言即是最佳代表。它會先編譯出一個(gè)字節(jié)碼文件,然后Java Virtual Machine(JVM)通過讀取字節(jié)碼來運(yùn)行程序。

微軟的.NET也是類似的結(jié)構(gòu),它使用的是Common Language Runtime(CLR),以此支持多種語言。例如C#、VB.net等。

[[343918]]

基礎(chǔ)知識

不論一個(gè)程序用何種語言編寫,它的運(yùn)行時(shí)內(nèi)存布局都是一致的。我們先從一個(gè)程序的三種基本內(nèi)存區(qū)域說起。

靜態(tài)區(qū):這個(gè)區(qū)域主要存放的是程序的全局變量、常量數(shù)據(jù),以及編譯成二進(jìn)制指令的代碼??梢钥吹?,這個(gè)區(qū)域存放的,主要是貫穿于程序整個(gè)生命周期所要使用到的數(shù)據(jù)與指令。

棧區(qū):熟悉數(shù)據(jù)結(jié)構(gòu)的朋友們都知道,棧(stack)是一個(gè)后入先出(LIFO)的隊(duì)列。在程序運(yùn)行中,它用來實(shí)現(xiàn)函數(shù)的調(diào)用。程序執(zhí)行函數(shù)調(diào)用時(shí),會在棧上依次壓入?yún)?shù),局部變量、返回位置等,執(zhí)行完成后再依次將數(shù)據(jù)出棧。所以,棧上的數(shù)據(jù)都是臨時(shí)性的,只在調(diào)用時(shí)可用。

堆區(qū):所有動態(tài)申請的內(nèi)存都從堆區(qū)分配。在使用C/C++語言時(shí),程序員對待內(nèi)存的申請與釋放就必須特別小心,一個(gè)疏忽就會造成內(nèi)存泄漏。而后來的java、C#等,語言內(nèi)置了GC技術(shù),情況相對改善,但也要養(yǎng)成良好的編程習(xí)慣。

對于程序來說,靜態(tài)區(qū)和堆區(qū)都是全局存在的,即所有線程共享這二者。而棧區(qū)則是為每個(gè)線程單獨(dú)準(zhǔn)備一個(gè),這一點(diǎn)程序員要記住。因?yàn)闂^(qū)的數(shù)據(jù)在函數(shù)調(diào)用之后就會失效,如果還引用棧區(qū)的數(shù)據(jù),則會產(chǎn)生不可預(yù)料的問題。

程序運(yùn)行后性能總會下降?你應(yīng)該先了解編程語言的內(nèi)存布局與管理

程序運(yùn)行時(shí)內(nèi)存布局

OOP語言的內(nèi)存結(jié)構(gòu)

因?yàn)楝F(xiàn)在市場上面向?qū)ο缶幊陶Z言(OOP)占據(jù)主流地位,所以接下來的討論也將以O(shè)OP語言的典型內(nèi)存結(jié)構(gòu)進(jìn)行講解。我們了解清楚對象的存儲區(qū)域,方法的調(diào)用之后,就會更加明白編程時(shí)應(yīng)當(dāng)注意哪些方面。

我們以使用較為廣泛的Java語言進(jìn)行說明,先要厘清一個(gè)總是爭論不休的問題。就是Java語言中究竟有沒有指針?

Java中的一系列邏輯功能,都是通過對象的間的消息傳遞和方法調(diào)用來實(shí)現(xiàn)的。對象是實(shí)現(xiàn)功能的最小單元,而一個(gè)對象是怎么來的,它存放在哪里?

先看一段派生對象的代碼:

  1. MyCar one = new MyCar() 

Java語言中的new的實(shí)質(zhì)是動態(tài)創(chuàng)建內(nèi)存,用以存放對象實(shí)例。根據(jù)上節(jié)的知識,我們知道new操作的結(jié)果是從堆區(qū)申請了一塊內(nèi)存,它將這塊內(nèi)存的地址返回,變量one就可以通過這個(gè)地址實(shí)現(xiàn)對象的操作了。

所以,變量one中存儲的不是對象本身,而是指向?qū)ο笏趦?nèi)存的地址。好吧,簡單說就兩個(gè)字:指針。在Java的術(shù)語體系里,它也叫引用。不過不管怎么稱呼,這種內(nèi)存結(jié)構(gòu)就是典型的指針式操作。

既然我們知道Java語言中所有的對象都生成在堆區(qū),那么需要注意之處就來了:堆區(qū)的存儲空間是有限的,不能將運(yùn)行時(shí)環(huán)境想象成內(nèi)存無限的場景,要對自己使用的對象所占空間做到心中有數(shù)。

接下來還要注意的,就是對象復(fù)制的操作,示例代碼:

  1. MyCar one = new MyCar() 
  2. MyCar two = one; one.SetSpeed(100); 
  3. two.SetSpeed(0); 

有了上面的知識,我們清楚地知道,MyCar two = one;這條語句并沒有復(fù)制一個(gè)對象給two變量,它和one指向的都是同一個(gè)對象實(shí)例。所以代碼執(zhí)行的結(jié)果,就是這輛車以百公里時(shí)速狂奔的下一秒就減速到零,想想都挺嚇人的吧。

方法表與屬性

那么,對象的方法代碼是存放在哪里呢?答案是在靜態(tài)區(qū)。因?yàn)榉椒ㄊ强梢栽诰幾g時(shí)就形成二進(jìn)制指令的,因此編譯后放在靜態(tài)區(qū)就可以了。

類的信息是存放在靜態(tài)區(qū)的,它會包含一張方法表(有的語言中也稱為虛函數(shù)表)。方法表中的方法名實(shí)際上是一個(gè)函數(shù)指針,它在運(yùn)行時(shí)是指向靜態(tài)區(qū)的方法代碼的。有了方法表,OOP語言就可以實(shí)現(xiàn)多態(tài)機(jī)制了。

這種方式可以節(jié)省程序存儲空間,所以從本質(zhì)上說,所有的對象實(shí)例都是在共用同一段方法代碼。只是在調(diào)用時(shí)通過壓入不同的參數(shù)以實(shí)現(xiàn)對象個(gè)性化的操作。

對象的屬性變量又是存放在哪里?答案是在堆區(qū),所以我們現(xiàn)在知道,一個(gè)對象實(shí)例里,屬性變量的大小決定了它實(shí)際占用的存儲空間。

需要注意的事項(xiàng)又來了:不要在類的聲明中,將屬性變量定義的過大。例如為了圖方便,定義個(gè)超大的數(shù)組。這樣帶來的問題,一是會影響對象生成的效率,因?yàn)閯討B(tài)分配一段大內(nèi)存是很耗時(shí)的;二是會導(dǎo)致內(nèi)存空間急劇減少。

GC的運(yùn)行并不是實(shí)時(shí)清理的,它會有延時(shí)判斷策略,那么大量閑置的內(nèi)存還來不及回收,新的對象又得不到可用空間,這只會降低程序的運(yùn)行時(shí)性能了。

通過方法表,繼承結(jié)構(gòu)也得以實(shí)現(xiàn)。對于超類中的方法,子類中無需再存儲相同的副本,它只要在自己的方法表中增加一條指向超類的方法引用即可。

程序運(yùn)行后性能總會下降?你應(yīng)該先了解編程語言的內(nèi)存布局與管理

對象通過方法表調(diào)用方法

GC會回收哪些對象實(shí)例?

通過上述幾節(jié)的知識,我們知道GC要處理的肯定是在堆區(qū)上動態(tài)分配的對象實(shí)例。那是不是有了這個(gè)原則,我們就可以高枕無憂了呢?并不是,這要從GC的回收原理上說起。

GC的實(shí)現(xiàn)基礎(chǔ),必定是通過引用計(jì)數(shù)來判定對象是否被使用,未被使用的對象則會進(jìn)入回收工作中。但是如果對象變量是在靜態(tài)區(qū)或者棧區(qū),那么這個(gè)對象永遠(yuǎn)都不會被回收。

靜態(tài)區(qū)的對象,在Java中就是以static定義的類變量。程序員對此一定要心中有數(shù),一定要記住類變量生成的對象,它的生命周期是和程序本身一樣的。

而棧上所引用的對象,它的存活周期則和方法調(diào)用一致。也就是說如果方法退出,那么期間所產(chǎn)生的對象不再使用了,是會被回收的。

在多線程環(huán)境中,程序員要注意,如果一個(gè)方法是長期后臺運(yùn)行的,則不要進(jìn)行頻繁地創(chuàng)建對象的工作,以避免內(nèi)存無法回收。

程序運(yùn)行后性能總會下降?你應(yīng)該先了解編程語言的內(nèi)存布局與管理

被棧區(qū)和靜態(tài)區(qū)引用的對象是不會被回收的

總結(jié)

經(jīng)過了解編程語言的內(nèi)存布局與管理,我們發(fā)現(xiàn)還是有很多細(xì)節(jié)處不注意的話,很容易掉到坑里去的。那時(shí)候,代碼功能看著都正常,但程序運(yùn)行一段時(shí)間后性能就下降。不得不來一次萬能的重啟以解決問題,這顯然不是最佳解決辦法。

所以,我將文中涉及到的注意事項(xiàng),整理出來再列舉如下。希望可以幫助遇到性能問題的程序員們。

  • 堆區(qū)的存儲空間是有限的,創(chuàng)建對象時(shí)要心中有數(shù);
  • 對象變量存儲的不是實(shí)例本身,而是指向堆區(qū)實(shí)例的指針;
  • 類中屬性變量不要定義過大,避免出現(xiàn)超大數(shù)組;
  • 堆區(qū)和棧區(qū)所引用的對象,是不會被GC所回收的。

 

 

責(zé)任編輯:張燕妮 來源: 今日頭條
相關(guān)推薦

2019-07-11 15:24:23

CPU芯片元器

2020-07-30 08:09:47

硬件軟件電腦

2019-04-24 08:34:46

編程語言PythonJava

2020-01-12 19:48:13

編程語言RustPython

2021-04-21 13:29:42

內(nèi)存安全Java

2025-03-27 10:30:51

2022-09-21 18:06:10

Python內(nèi)存管理

2022-11-02 07:23:06

2024-12-05 15:33:50

Python列表元組

2019-09-25 10:37:16

SpringBeanUtils接口

2023-09-02 21:31:16

Java內(nèi)存泄漏

2016-06-13 14:13:27

開發(fā)者全新編程語言

2020-03-23 09:17:32

內(nèi)存操作系統(tǒng)Windows

2024-09-02 14:24:13

2019-06-28 08:56:35

編程語言框架工具

2018-08-20 08:29:18

2015-12-23 10:00:04

多種編程語言

2019-11-12 14:40:43

CPU緩存內(nèi)存

2021-10-26 16:25:25

編程語言JavaPython

2013-03-20 17:58:41

虛擬內(nèi)存程序員
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號