အခန်း ၂ - Random Access Memory
Algorithm တွေဟာ data structure ပေါ်မှာ အခြေခံပြီး အလုပ်လုပ်ပါတယ်။ Data Structure တွေဟာ Random Access Memory (RAM) ပေါ်မှာ နေရာယူထားပါတယ်။ ဒါကြောင့် RAM သဘောတရား အခြေခံ ကို အနည်းငယ် နားလည် ထားဖို့ လိုပါတယ်။
RAM Architecture
RAM ကို ကြီးမားသည့် စာတိုက်ပုံး (P.O. BOX) လို့ မြင်ယောင်ကြည့်ပါ။ နောက်ပိုင်း မြန်မာနိုင်ငံက condo တွေမှာ ရှိသလို စာတိုက်ပုံး အကွက်လေး တွေ အများကြီး ရှိပါတယ်။ အကွက် တစ်ကွက် စီ က သူ့ရဲ့ သီးသန့် အိမ်လိပ်စာ အတွက် ပါပဲ။ Computer မှာလည်း Data ကို သိမ်းချင်သည့် အခါမှာ RAM ပေါ်မှာ သွားသိမ်းဖို့ လိုပါတယ်။ တနည်းပြောရင် အခန်းလွတ်နေသည့် address တစ်ခု မှာ သွားသိမ်းရတာပေါ့။ အခန်း တစ်ခန်း စီ အတွက် Address တစ်ခု ဆီ ထားရှိသည့် သဘောပါ။
ဥပမာ သင့်သူငယ်ချင်း ဆီကို စာပို့ချင် သူငယ်ချင်းနာမည် (variable name) သိရုံ နဲ့ မရပါဘူး။ သူ့ အိမ်လိပ်စာ (memory address) သိမှ ပို့လို့ရမှာပါ (memory ပေါ်မှာ တန်ဖိုးသိမ်း)။ Computer မှာ variable တစ်ခု ကြေငြာလိုက်သည် နှင့် RAM မှာ variable နဲ့ memory address ကို ချိတ်ပေးလိုက်သည့် သဘောပါ။
ဥပမာ - Address 100 မှာ တန်ဖိုး 50 ကို သိမ်းမယ် ဆိုပါစို့။ ပုံမှန်အားဖြင့် အောက်ပါဇယားအတိုင်း မြင်ယောင်နိုင်ပါတယ်။
| Memory Address (လိပ်စာ) | Value (သိမ်းဆည်းထားသော တန်ဖိုး) | Description (ရှင်းလင်းချက်) |
|---|---|---|
0x0099 |
0 | အလွတ် |
0x0100 |
50 | Integer တန်ဖိုး 50 သိမ်းထားသည့်နေရာ |
0x0101 |
'A' | Character 'A' သိမ်းထားသည့်နေရာ |
0x0102 |
0 | အလွတ် |
(မှတ်ချက် - Memory Address များကို များသောအားဖြင့် Hexadecimal (16 လီစနစ်) ဥပမာ
0x0100ဖြင့် ပြသလေ့ရှိပါတယ်။)
Random Access ဆိုတာ ဘာလဲ
Random Access ဆိုတာကတော့ လိပ်စာ သိရင် ဘယ်နားမှာ ပဲ ရှိရှိ Access လုပ်လို့ရသည့် သဘောပါ။ ဥပမာ အားဖြင့် ကျွန်တော်တို့ ငယ်ငယ်က ကက်ဆက် ခွေ တွေမှာ သီချင်း နားထောင်ရင် နောက် တစ်ခုကို ကျော်ချင်သည့် အခါမှာ ရှေ့ပိုင်းကို Fast Forward လုပ်ရပါတယ်။ တနည်းပြောရင် တိတ်ခွေက အရှေ့ ပိုင်းကို လိပ်ပြီး ကျော်သည့် သဘောပေါ့။ ဒါကို Sequential Access လို့ ခေါ်ပါတယ်။ ဒါပေမယ့် Youtube , Spotify နဲ့ computer မှာ သီချင်း နားထောင်ရင် ကြိုက်သည့် သီချင်းကို ကျော်လို့ ရသလို ကြိုက်သည့် အချိန်ကို ကျော်လို့ ရပါတယ်။ တနည်းပြောရင် တိကျသည့် နေရာကို တန်းသွားလို့ရတယ်ပေါ့။ ဒါကို Random Access လို့ ခေါ်ပါတယ်။
RAM မှာလည်း Address 0 ကို သွားတာ နဲ့ Address 1000 ကို သွားတာ ကြာချိန် အတူတူပါပဲ။ တနည်းပြောရင် Memory Access ရဲ့ Time Complexity က ဖြစ်ပါတယ်။
Stack နှင့် Heap
Stack နဲ့ Heap က အမြဲတန်း ရောထွေးနေတတ်တယ်။ memory အပိုင်း Stack and Heap နဲ့ Data Structure Stack and Heap က မတူပါဘူး။ တစ်ခါတစ်လေ interview တွေမှာ မင်း Stack နဲ့ Heap ကို သိလားလို့ မေးရင် သေချာအောင် ပြန်မေးရတယ်။ Memory က Stack နဲ့ Heap ကို မေးတာလား။ Data Structure က Stack နဲ့ Heap ကို မေးတာလားပေါ့။ အခုပြောမည့် အကြောင်းကတော့ Memory မှာ Data သိမ်းသည့် Stack နှင့် Heap ပါ။
Java လို Programming ဘာသာစကား အများစုမှာ Program တစ်ခု အလုပ်လုပ်ချိန် (Runtime) မှာ Memory ကို Stack/Heap ဆိုပြီး ရှင်းပြလေ့ရှိပါတယ်။ ဒီ chapter ထဲမှာလည်း နားလည်လွယ်အောင် အဲ့ဒီ model နဲ့ ဆက်ရှင်းပါမယ်။ ဒါပေမယ့် JVM specification က memory layout အတိအကျကို guarantee မပေးပါဘူး။
Stack Memory
Stack ဆိုတာက မြန်မြန် အလုပ်လုပ်နိုင်ပြီး စနစ်တကျ စီစဥ်ထားသည့် သေးငယ် သည့် memory နေရာပါ။
Stack Memory က ဘယ်လို အလုပ်လုပ်သလဲ ဆိုတော့ LIFO , Last In , First Out (နောက်ဆုံး ဝင်တာက အရင်ဆုံးပြန်ထွက်သည်) စနစ် နဲ့ အလုပ်လုပ်ပါတယ်။ စာအုပ် အဆင့်ဆင့် ထပ်ထားသလိုပေါ့။ အသစ်ထပ်တင်ရင် အပေါ်ဆုံးမှာ တင်ရတယ်။ ပြန်ယူသည့် အခါမှာလည်း အပေါ်ဆုံးက စာအုပ်ကို အရင် ယူရတယ်။
Function (Method) တစ်ခုခု ကို ခေါ်လိုက်သည့် အခါ အဲဒီ Function အတွက် temporary နေရာ ဖန်တီးပေးရတယ်လို့ စဉ်းစားနိုင်ပါတယ်။ Conceptually ဒီနေရာထဲမှာ local variable ဥပမာ int x = 10;, double y = 5.5 စသည်တို့နဲ့ object ကို ညွှန်ပြတဲ့ reference value တွေ (int[] arr, String s) ကို သိမ်းထားတယ်လို့ နားလည်နိုင်ပါတယ်။
Function တစ်ခု အလုပ်လုပ် ပြီးသွားတာ သို့မဟုတ် Return ပြန်လိုက်သည့် အခါမှာ Stack Frame တစ်ခုလုံး Memory ပေါ်က အလိုအလျောက် ချက်ချင်း ဖျက်ပစ်လိုက်ပါတယ်။
Stack Memory က နေရာ သေးငယ်တယ်။ သိမ်းထားတာတွေကလည်း Primitive Data နဲ့ reference value တွေပဲ ဖြစ်သည့် အတွက်ကြောင့် အများကြီး မလိုဘူး။ Base case မရှိတဲ့ recursion တွေလိုမျိုး ခေါ်မိသည့် အခါမှာ StackOverflowError ဆိုသည့် ပြဿနာ တက်တတ်ပါတယ်။ တနည်းပြောရင် function တွေကို ဆင့်ကာ ဆင့် ကာ ခေါ်ယူပြီး ပြည့်သွားတာမျိုးပေါ့။
Heap
Heap ဆိုတာ runtime မှာ dynamically allocate လုပ်ထားတဲ့ objects တွေကို သိမ်းထားတယ်လို့ ရှင်းပြလေ့ရှိတဲ့ memory area ဖြစ်ပါတယ်။
Stack လိုမျိုး အစဥ်လိုက် စီနေဖို့ မလိုပါဘူး။ အလွတ်ရှိတဲ့ memory နေရာတွေမှာ allocate လုပ်ပြီး သိမ်းနိုင်ပါတယ်။
Java ကို conceptually ရှင်းမယ်ဆိုရင် Object တွေကို Heap ပေါ်မှာ သိမ်းတယ်လို့ သဘောထားနိုင်ပါတယ်။
ဥပမာ -
String name = "Mg Mg";
Scanner input = new Scanner(System.in);
int[] arr = new int[100];
Custom Class Object များ စသည်တို့ ဖြစ်ပါတယ်။
ဒီနေရာမှာ ရိုးရိုး သင်ကြားရေး model အရ local reference variables တွေက Stack ဘက်မှာ ရှိပြီး Object တွေက Heap ဘက်မှာ ရှိတယ်လို့ မြင်နိုင်ပါတယ်။
ဥပမာ -
String name = "Mg Mg";
ဒီ statement ကို run လိုက်တဲ့အခါ memory ထဲမှာ အောက်ပါအတိုင်း ဖြစ်ပါတယ်။
1. Stack
name ဆိုတဲ့ variable ကို reference variable အနေနဲ့ Stack side မှာ ရှိတယ်လို့ conceptual model နဲ့ မြင်နိုင်ပါတယ်။
ဒီ variable ထဲမှာ Object ကို မသိမ်းပါဘူး။ Object ကို ညွှန်တဲ့ reference value ကိုသာ သိမ်းထားပါတယ်။ Java က ဒီ reference ကို raw memory address အဖြစ် တိုက်ရိုက် မပြပေးပါဘူး။
2. Heap
"Mg Mg" ဆိုတဲ့ String Object ကတော့ Heap side မှာ ရှိတယ်လို့ နားလည်နိုင်ပါတယ်။
Stack ထဲက name variable က Heap ပေါ်က ဒီ Object ကို လှမ်းညွှန် (reference) လုပ်ထားတာ ဖြစ်ပါတယ်။
Conceptually memory ကို အောက်လိုမြင်နိုင်ပါတယ်။
flowchart LR
subgraph Stack
A["name<br/>(reference)"]
end
subgraph Heap
B["'Mg Mg'<br/>(String Object)"]
end
A --> B
အကယ်၍ reference variable မရှိတော့တဲ့အခါ (ဥပမာ function ပြီးသွားတဲ့အခါ) Stack ပေါ်က variable က ပျက်သွားနိုင်ပါတယ်။ ဒါပေမယ့် Heap ပေါ်က Object ကတော့ ချက်ချင်း မဖျက်ပါဘူး။
Java မှာတော့ အသုံးမပြုတော့တဲ့ Object တွေကို Garbage Collector ဆိုတဲ့ အလိုအလျောက် စနစ်က နောက်ကွယ်ကနေ ရှင်းလင်းပေးပါတယ်။
C/C++ တို့မှာဆိုရင်တော့ programmer က ကိုယ်တိုင် free() သို့မဟုတ် delete ကို ခေါ်ပြီး memory ကို ဖျက်ပေးရပါတယ်။
ဘာကြောင့် Object ကို function ပြီးတာနဲ့ အလိုအလျောက် မဖျက်တာလဲ ဆိုတော့ Heap ပေါ်က Object တစ်ခုကို နေရာအများကြီးကနေ တစ်ပြိုင်တည်း reference လုပ်ထားနိုင်လို့ ဖြစ်ပါတယ်။
Function တစ်ခု ပြီးသွားတာနဲ့ Object ကို ဖျက်လိုက်မယ်ဆိုရင် ကျန်နေတဲ့ အခြား reference တွေက အသုံးပြုတဲ့အခါ crash ဖြစ်နိုင်ပါတယ်။
ဒါကြောင့် Object ကို ဘယ်သူမှ reference မလုပ်တော့တဲ့အခါ (Unreferenced ဖြစ်တဲ့အခါ) မှသာ ဖျက်ပေးရပါတယ်။
Java မှာတော့ Garbage Collector က ဒီအလုပ်ကို အလိုအလျောက် လုပ်ပေးပါတယ်။
Stack နှင့် Heap အတူတကွ လုပ်ဆောင်ခြင်း
ကျွန်တော်တို့ အပေါ်မှာ ပြထားသည့် String ဥပမာ ကို ပြန်ကြည့်ရအောင်။
String name1 = "Mg Mg";
String name2 = "Mg Mg";
String name3 = "Mg Mg";
ဒီ code ကို run လိုက်တဲ့အခါ Java က memory ကို optimize လုပ်တဲ့အတွက် "Mg Mg" ဆိုတဲ့ String literal Object ကို တစ်ခါပဲ ဖန်တီးပါတယ်။ ဒါကို String Pool လို့ခေါ်ပါတယ်။ (မှတ်ချက် - String Pool သည် Heap တစ်ခုလုံးကို ဆိုလိုခြင်းမဟုတ်ဘဲ Heap အတွင်းရှိ သီးခြား subset တစ်ခုသာ ဖြစ်ပါသည်။)
Stack ပေါ်မှာတော့ name1, name2, name3 ဆိုတဲ့ variables တွေ ရှိပါတယ်။ ဒီ variables တွေက reference variables ဖြစ်ပြီး Object ကို မသိမ်းပါဘူး။ Object ကို ညွှန်တဲ့ reference value ကိုသာ သိမ်းထားပါတယ်။
Heap ထဲမှာတော့ "Mg Mg" ဆိုတဲ့ String Object တစ်ခုတည်းပဲ ရှိပါတယ်။
Conceptually memory ကို အောက်လိုမြင်နိုင်ပါတယ်။
flowchart LR
subgraph Stack
A["name1<br/>(reference)"]
B["name2<br/>(reference)"]
C["name3<br/>(reference)"]
end
subgraph Heap
D["'Mg Mg'<br/>(String Object)"]
end
A --> D
B --> D
C --> D
ဆိုလိုတာက "Mg Mg" ဆိုတဲ့ Object တစ်ခုတည်းကို reference variable အများကြီးကနေ share လုပ်ပြီး အသုံးပြုနိုင်ပါတယ်။
ဒီလို design လုပ်ထားတာက memory usage ကို လျော့ချစေပြီး performance ကိုလည်း ပိုကောင်းစေပါတယ်။
အကယ်၍ Stack ပေါ်က variable တစ်ခုခု ပျက်သွားတယ်ဆိုရင် (ဥပမာ function ပြီးသွားတဲ့အခါ) ကျန်နေတဲ့ reference တွေ ရှိနေသေးတဲ့အတွက် Heap ပေါ်က Object ကို ချက်ချင်း မဖျက်ပါဘူး။
Java မှာတော့ GC roots တွေကနေ မရောက်နိုင်တော့တဲ့ Object (unreachable object) ဖြစ်သွားတဲ့အခါမှာ GC က ရှင်းလင်းနိုင်တဲ့ candidate ဖြစ်လာပါတယ်။
ဒါပေမယ့် unreachable ဖြစ်သွားတာနဲ့ ချက်ချင်း ဖျက်မယ်လို့ မဆိုလိုပါဘူး။ Garbage Collector က ဘယ်အချိန် run မလဲဆိုတာကို collector ရဲ့ heuristics နဲ့ runtime အခြေအနေပေါ် မူတည်ပြီး ဆုံးဖြတ်တာ ဖြစ်ပါတယ်။
တနည်းဆိုလျှင် Heap နဲ့ Stack ကလည်း အတူ တကွ တွဲပြီး အလုပ်လုပ်တာကို မြင်နိုင်ပါတယ်။
Function Argument Passing: String vs Object
Stack နဲ့ Heap ကို နားလည်ပြီးရင် Function ထဲကို variable တွေ ပို့လိုက်တဲ့အခါ ဘာကြောင့် တချို့ case တွေမှာ value မပြောင်းသလို မြင်ရပြီး တချို့ case တွေမှာ ပြောင်းသွားသလဲ ဆိုတာကို ဆက်ကြည့်လို့ ရပါတယ်။
Java မှာ pass-by-value ပဲ ရှိပါတယ်။ ဒါက အရေးကြီးဆုံး rule ပါ။
- Primitive type (
int,double,boolean) တွေဆိုရင် value ကို တိုက်ရိုက် copy လုပ်ပြီး ပို့ပါတယ်။ - Object type (
String,User,Scanner,int[]) တွေဆိုရင် reference value ကို copy လုပ်ပြီး ပို့ပါတယ်။
အဓိက မှတ်ထားရမယ့် အချက်က Java က original variable ကို မပို့ပါဘူး။ copy တစ်ခုကိုပဲ ပို့တာ ဖြစ်ပါတယ်။
String ကို Function ထဲပို့တဲ့အခါ
ဥပမာ -
void updateName(String name) {
name = "HELLO";
}
String name = "good";
updateName(name);
System.out.println(name);
Output က good ပဲ ဖြစ်ပါတယ်။
ဘာကြောင့်လဲဆိုတော့ Function ထဲက name ဟာ caller ထဲက name မဟုတ်ဘဲ reference copy အသစ် ဖြစ်ပါတယ်။ အစပိုင်းမှာ နှစ်ခုလုံးက "good" ကိုပဲ ညွှန်နေပါတယ်။
flowchart LR
subgraph Caller Stack
A[name]
end
subgraph Callee Stack
B[param name]
end
subgraph Heap
C["String: good"]
end
A --> C
B --> C
Function ထဲမှာ
name = "HELLO";
လို့ ရေးလိုက်တဲ့အခါ "good" String object ကို မပြင်ပါဘူး။ Function ထဲက local reference copy ကို "HELLO" ဆိုတဲ့ String object အသစ်ကို ညွှန်အောင် ပြောင်းလိုက်တာပါ။
flowchart LR
subgraph Caller Stack
A[name]
end
subgraph Callee Stack
B[param name]
end
subgraph Heap
C["String: good"]
D["String: HELLO"]
end
A --> C
B --> D
Function ပြီးသွားတာနဲ့ Callee Stack ပေါ်က param name ပျက်သွားပြီး Caller ထဲက name ကတော့ "good" ကိုပဲ ဆက်ညွှန်နေပါတယ်။ ဒါကြောင့် output က good ဖြစ်တာပါ။
Object ကို Function ထဲပို့တဲ့အခါ
ဥပမာ -
class User {
String name;
}
void update(User user) {
user.name = "HELLO";
}
User myUser = new User();
myUser.name = "good";
update(myUser);
System.out.println(myUser.name);
ဒီ code ရဲ့ output က HELLO ဖြစ်ပါတယ်။
ဘာကြောင့်လဲဆိုတော့ Function ထဲကို ဝင်လာတဲ့ user ဟာ reference copy ဖြစ်ပေမယ့် Caller ထဲက myUser နဲ့ Heap ပေါ်က object တစ်ခုတည်းကိုပဲ ညွှန်နေပါတယ်။
flowchart LR
subgraph Caller Stack
A[myUser]
end
subgraph Callee Stack
B[param user]
end
subgraph Heap
C["User Object<br/>name = good"]
end
A --> C
B --> C
Function ထဲမှာ
user.name = "HELLO";
လို့ ရေးလိုက်တဲ့အခါ reference ကို မပြောင်းဘဲ Heap ပေါ်က User object ထဲက name field ကို တိုက်ရိုက်ပြောင်းလိုက်တာ ဖြစ်ပါတယ်။
flowchart LR
subgraph Caller Stack
A[myUser]
end
subgraph Callee Stack
B[param user]
end
subgraph Heap
C["User Object<br/>name = HELLO"]
end
A --> C
B --> C
ဒါကြောင့် Function ပြီးသွားပြီးနောက် Caller ထဲက myUser.name ကို print လုပ်ရင် HELLO ထွက်လာတာပါ။
Mutable နှင့် Immutable
ဒီနေရာမှာ လူအများစု ရှုပ်သွားတာက String က Object မဟုတ်ဘူးလား ဆိုတာပါ။ အဖြေက String ကလည်း Object ပဲ ဖြစ်ပါတယ်။ ဒါပေမယ့် String နဲ့ User object က behavior မတူတာက mutable / immutable ကွာလို့ ဖြစ်ပါတယ်။
Immutable ဆိုတာ ဘာလဲ
Immutable object ဆိုတာ create လုပ်ပြီးနောက် internal value ကို မပြင်နိုင်တဲ့ object ဖြစ်ပါတယ်။ Value ပြောင်းချင်ရင် object အသစ်ဖန်တီးရပါတယ်။
Java မှာ String က immutable ဖြစ်ပါတယ်။
String s = "good";
s = "HELLO";
ဒီ code မှာ "good" String object ကို မပြင်ပါဘူး။ "HELLO" String object အသစ်ကို ယူညွှန်လိုက်တာ ဖြစ်ပါတယ်။
flowchart LR
A[s] --> B["String: good"]
A -.reassign.-> C["String: HELLO"]
တကယ့် memory semantics အရ ပြောရရင် variable s က အရင် "good" ကို ညွှန်နေရာမှ "HELLO" ကို ပြန်ညွှန်သွားတာ ဖြစ်ပါတယ်။ "good" object ကို ကိုယ်တိုင် ပြင်လိုက်တာ မဟုတ်ပါဘူး။
Mutable ဆိုတာ ဘာလဲ
Mutable object ဆိုတာ create လုပ်ပြီးနောက် internal state ကို ပြင်နိုင်တဲ့ object ဖြစ်ပါတယ်။
ဥပမာ custom class တစ်ခု -
class User {
String name;
}
User u = new User();
u.name = "good";
u.name = "HELLO";
ဒီနေရာမှာ User object အသစ် မဖန်တီးပါဘူး။ Heap ပေါ်က object တစ်ခုတည်းထဲက field value ကို update လုပ်တာပါ။
flowchart LR
A[u] --> B["User Object<br/>name: 'good' → 'HELLO'"]
Reassign လုပ်တာနှင့် Object ကိုပြင်တာ မတူဘူး
ဥပမာ ဒီ code ကို ကြည့်ပါ -
void update(User user) {
user = new User();
user.name = "HELLO";
}
ဒီ code မှာ output မပြောင်းပါဘူး။ ဘာကြောင့်လဲဆိုတော့ user = new User(); က Function ထဲက local reference copy ကို object အသစ်ဆီ ပြောင်းလိုက်တာပဲ ဖြစ်ပါတယ်။ Caller ထဲက myUser reference ကို မထိခိုက်ပါဘူး။
ဒါကြောင့် Java Function parameter တွေကို ရှင်းလင်းစွာ နားလည်ဖို့အတွက် အောက်ပါ rule နှစ်ခုကို မှတ်ထားရင် လုံလောက်ပါတယ်။
- Java က pass-by-value ပဲ ရှိတယ်။
- reference ကိုပြောင်းတာ နဲ့ reference ညွှန်ထားတဲ့ object ကိုပြောင်းတာ မတူဘူး။
အခြားသော Programming Language များရှိ Memory Management
Stack နဲ့ Heap ကို Java တစ်ခုတည်းကပဲ သုံးတာ မဟုတ်ပါဘူး။ Programming Language အများစုဟာ ဒီ Memory ခွဲဝေမှုစနစ် (Stack & Heap) ကိုပဲ အခြေခံပြီး အလုပ်လုပ်ကြပါတယ်။ ဒါပေမယ့် နောက်ကွယ်ကနေ Memory ကို ဘယ်သူက ဘယ်လို ရှင်းလင်းပေးလဲ (Memory Management Strategy) ဆိုတာပေါ် မူတည်ပြီး ကွာခြားသွားပါတယ်-
Tracing GC ကို အဓိက အသုံးပြုသော Runtime များ (Java, C#, JavaScript):
- ဒီ runtime တွေမှာ Developer က
free()/deleteလို memory free code ကို ကိုယ်တိုင် မရေးရပါဘူး။ - အသုံးမပြုတော့တဲ့ object တွေကို runtime ရဲ့ Garbage Collector (GC) က နောက်ကွယ်ကနေ ရှင်းလင်းပေးပါတယ်။
- ဒီ runtime တွေမှာ Developer က
TypeScript:
- TypeScript က JavaScript ပေါ်မှာ syntax/type system တင်ထားတာဖြစ်ပြီး ကိုယ်ပိုင် runtime သို့မဟုတ် GC မရှိပါဘူး။
- ဒါကြောင့် TypeScript program တွေရဲ့ memory behavior က JavaScript runtime (ဥပမာ V8) ပေါ် မူတည်ပါတယ်။
Reference Counting + GC ပေါင်းစပ်အသုံးပြုသော Runtime များ (Python, PHP):
- Python နဲ့ PHP တို့မှာ object lifetime ကို reference counting နဲ့အဓိက စောင့်ကြည့်ပြီး cycle ဖြစ်နေတဲ့ object တွေအတွက် GC ကို ထပ်မံသုံးပါတယ်။
- အဲ့ဒါကြောင့် Java/Go လို tracing GC-only model နဲ့ တစ်ပုံစံတည်း မဟုတ်ပါဘူး။
GC + Escape Analysis (Go):
- Go (Golang) ဟာ GC ကို သုံးတဲ့ ဘာသာစကားဖြစ်ပေမယ့် Escape Analysis ကိုလည်း ပေါင်းစပ်အသုံးပြုပါတယ်။
- Compile လုပ်တဲ့အချိန်မှာ "ဒီ value က function scope အပြင်ကို ထွက်သွားမလား" ဆိုတာကို ဆုံးဖြတ်ပါတယ်။
- Escape မလုပ်တဲ့ value တချို့ကို GC ကို မပေးဘဲ stack-like storage ပေါ်မှာပဲ ထားနိုင်တာကြောင့် performance ပိုကောင်းစေပါတယ်။
Manual Memory Management (C, C++):
- Stack ကို Function ပြီးရင် အလိုအလျောက် ရှင်းပေးတာက အတူတူပါပဲ။
- ဒါပေမယ့် Heap ပေါ်မှာ နေရာယူထားတဲ့ Memory ကိုတော့ GC က အလိုအလျောက် မရှင်းပေးပါဘူး။ Developer ကိုယ်တိုင် Code ရေးပြီး (
free(),delete) မဖြစ်မနေ ပြန်လွတ်ပေးရပါတယ်။ မလွတ်ပေးမိရင် Memory Leak (မှတ်ဉာဏ်မလွတ်ဘဲ ပိတ်ဆို့နေမှု) ဆိုတဲ့ ပြဿနာကြီး ဖြစ်ပါတယ်။
Ownership/Borrowing Model (Rust):
- Rust ဟာ GC လည်း မသုံးပါဘူး၊ အဲ့ဒီအတွက် မြန်ဆန်ပါတယ်။ ဒါပေမယ့် C/C++ လို ကိုယ်တိုင်လိုက်ဖျက်ပေးစရာလည်း မလိုပါဘူး။
- Compiler ကနေ Ownership စည်းမျဉ်းတွေ (ဒီ Memory ကို ဘယ်သူက ပိုင်တယ်ဆိုတာ) ကို တင်းတင်းကျပ်ကျပ် သတ်မှတ်ပေးထားပါတယ်။ အဲ့ဒီ ပိုင်ရှင် Variable သက်တမ်းကုန်သွားတာနဲ့ (Out of Scope ဖြစ်တာနဲ့) Heap ပေါ်က Data ကို Compiler ကနေ အလိုအလျောက် Code (Drop) ပြန်ထည့်ပေးပြီး ဖျက်ပစ်ပါတယ်။
- ဒီစနစ်ကြောင့် dangling pointer လို ပြဿနာတွေကို များစွာ လျော့ချပေးနိုင်ပေမယ့် Memory Leak လုံးဝမဖြစ်ဘူးလို့တော့ မဆိုနိုင်ပါဘူး။ ဥပမာ reference cycle တချို့ကြောင့် leak ဖြစ်နိုင်သေးပါတယ်။
Reference နှင့် Pointer
C/C++ လို ဘာသာစကားတွေမှာ "Pointer" ဆိုတဲ့ စကားလုံးကို ကြားဖူးပါလိမ့်မယ်။ Pointer ဆိုတာ raw memory address ကို ကိုင်ထားပြီး dereference လုပ်ကာ data ကို တိုက်ရိုက် သွားဖတ်/သွားပြင်နိုင်တဲ့ အရာပါ။ Java မှာတော့ Pointer တွေကို တိုက်ရိုက် ကိုင်တွယ်ခွင့် မပေးထားပါဘူး။ အဲဒီအစား Reference ဆိုတဲ့ abstraction နဲ့ object ကို ညွှန်ခိုင်းထားတာ ဖြစ်ပါတယ်။
Pointer ကို တိုက်ရိုက်မြင်ရအောင် Objective-C code နဲ့ အရင်ကြည့်ရအောင်။
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 20;
int *ptr = &age; // ptr က age ရဲ့ memory address ကို သိမ်းထားတဲ့ pointer
NSLog(@"age value = %d", age);
NSLog(@"age address = %p", &age);
NSLog(@"ptr value = %p", ptr);
NSLog(@"*ptr value = %d", *ptr);
*ptr = 25; // pointer ကနေတစ်ဆင့် age ရဲ့ value ကို ပြင်လိုက်တာ
NSLog(@"updated age = %d", age);
}
return 0;
}
ဒီ code ထဲမှာ-
&ageကageရဲ့ memory address ကို ယူပေးတာပါ။int *ptrက address ကို သိမ်းမယ့် pointer variable ဖြစ်ပါတယ်။*ptrက pointer ညွှန်နေတဲ့ နေရာထဲက တန်ဖိုးအစစ် ကို သွားဖတ်တာ၊ သွားပြင်တာပါ။
ဒါကြောင့် *ptr = 25; လို့ ရေးလိုက်တာနဲ့ age ရဲ့ value ကပါ 25 ဖြစ်သွားပါတယ်။ Pointer က address ကို သိမ်းထားပြီး၊ dereference (*ptr) လုပ်တဲ့အခါ အဲ့ဒီ address ထဲက data ကို တိုက်ရိုက် သွားထိတာပါ။
Java မှာတော့ ဒီလို *ptr, &age လို syntax တွေကို မသုံးရပါဘူး။ ဒါပေမယ့် Object တွေကို ကိုင်တွယ်တဲ့အခါ Reference ဆိုတဲ့ အလားတူ သဘောတရားနဲ့ အလုပ်လုပ်နေပါတယ်။ အရေးကြီးတာက Java reference ကို raw address လို့ မမြင်သင့်ဘဲ JVM က စီမံထားတဲ့ opaque handle တစ်ခုလိုပဲ နားလည်သင့်ပါတယ်။
Java reference ကိုလည်း အောက်က code နဲ့ တိုက်ရိုက် နှိုင်းယှဉ်ကြည့်နိုင်ပါတယ်။
class User {
String name;
}
public class Main {
public static void main(String[] args) {
User user1 = new User();
user1.name = "Mg Mg";
User user2 = user1; // object ကို copy ကူးတာ မဟုတ်ဘဲ reference ကိုပဲ copy လုပ်တာပါ။
user2.name = "Aung Aung";
System.out.println(user1.name); // Output: Aung Aung
System.out.println(user2.name); // Output: Aung Aung
}
}
ဒီနေရာမှာ user1 နဲ့ user2 က variable ၂ ခု ဖြစ်ပေမယ့် conceptual model အရ User object တစ်ခုတည်းကိုပဲ ညွှန်နေပါတယ်။ ဒါကြောင့် user2 ကနေ name ကို ပြင်လိုက်တာနဲ့ user1 ကနေလည်း အဲ့ဒီ ပြောင်းလဲမှုကို မြင်ရတာပါ။
Primitives (ရိုးရိုး Data တန်ဖိုးများ)
Primitive ဆိုတာ Programming Language ကနေ အခြေခံအကျဆုံး ကြိုတင်သတ်မှတ်ထားတဲ့ ရိုးရှင်းတဲ့ Data အမျိုးအစားတွေပါ။ Java မှာ Primitive Types (၈) မျိုး ရှိပါတယ်။ အဲ့ဒါတွေကတော့-
- ဂဏန်းပြည့်များ:
byte,short,int,long - ဒသမကိန်းများ:
float,double - အက္ခရာတစ်လုံး:
char - အမှန်/အမှား:
boolean
ဒီ Primitive variables တွေဟာ အရွယ်အစား ပုံသေ သတ်မှတ်ထားပြီးသားဖြစ်လို့ local variable အနေနဲ့ သင်ကြားရေး model ထဲမှာ တန်ဖိုးအစစ်ကို တိုက်ရိုက် ကိုင်ထားတယ် လို့ နားလည်နိုင်ပါတယ်။ ဒါပေမယ့် Java implementation အမှန်တကယ်က အမြဲ stack ပေါ်မှာပဲ သိမ်းရမယ်လို့ မဆိုလိုပါဘူး။ ဥပမာ object field သို့မဟုတ် array element အဖြစ် ရှိနေတဲ့ primitive data က heap object အတွင်းမှာလည်း ရှိနိုင်ပါတယ်။
int a = 10;
int b = a; // 'a' ရဲ့ တန်ဖိုး (10) ကို 'b' ထဲ လုံးဝ "မိတ္တူ" ကူးထည့်လိုက်ပါတယ်။
b = 20; // 'b' ကို ပြင်လိုက်ပေမယ့် 'a' ကတော့ 10 အတိုင်းပဲ ဆက်ရှိနေပါတယ်။ (အချင်းချင်း မသက်ဆိုင်တော့ပါ)
System.out.println(a); // Output: 10
flowchart LR
subgraph Stack["Stack Memory"]
direction TB
v_a["a = 10"]
v_b["b = 20 <br>(မိတ္တူကူးပြီး သီးခြားပြင်ထား)"]
style v_a fill:#f9f6e3,stroke:#333
style v_b fill:#f9f6e3,stroke:#333
end
Reference Types (Objects/Arrays)
Primitive အမျိုးအစား ၈ မျိုးကလွဲရင် ကျန်တဲ့ Data တွေ အားလုံးဟာ Reference Types (ရည်ညွှန်းလိပ်စာများ) ဖြစ်ပါတယ်။
- Arrays အားလုံး (Primitive type data လေးတွေ စုထားတဲ့ Array ဖြစ်နေရင်တောင်မှ)
- Objects တွေ အားလုံး (ဥပမာ
String, တခြား ကိုယ်တိုင်ရေးထားတဲ့ Classes တွေnew Scanner(),new CustomClass())
ဒီအမျိုးအစားတွေကို Java teaching model ထဲမှာတော့ object အစစ်ကို heap ဘက်မှာရှိတယ်, variable ကတော့ အဲ့ဒီ object ကို ညွှန်တဲ့ reference value ကို ကိုင်ထားတယ်လို့ ရှင်းပြလေ့ရှိပါတယ်။ ဒါပေမယ့် ဒီ reference value ကို raw memory address အတိအကျလို့ မမြင်သင့်ပါဘူး။ Runtime optimization ပေါ် မူတည်ပြီး representation ပြောင်းနိုင်ပါတယ်။
int[] arr1 = {1, 2, 3}; // (Heap ပေါ်မှာ Array အစစ်ကြီး သွားဆောက်ပြီး၊ arr1 ထဲမှာ အဲ့ဒီ array ကို ညွှန်တဲ့ reference value ရှိပါတယ်။)
int[] arr2 = arr1; // (arr1 ထဲက reference value ကို မိတ္တူကူးပေးလိုက်တာပါ။ ဒါကြောင့် Remote Control ၂ ခုက Array အစစ် တစ်ခုတည်းကို ညွှန်ပြနေပါတယ်။)
arr2[0] = 99; // Remote 'arr2' ကိုသုံးပြီး Heap ပေါ်က အစစ်ကြီးရဲ့ ပထမနေရာကို ပြင်လိုက်ပါတယ်။
// arr1 ကလည်း အတူတူပဲ ညွှန်ပြနေတဲ့ Remote ဖြစ်နေတဲ့အတွက် arr1 ရဲ့ တန်ဖိုးပါ ပြောင်းသွားပါတယ်။
System.out.println(arr1[0]); // Output: 99
flowchart LR
subgraph Stack["Stack Memory"]
direction TB
v_arr1["arr1 = (same reference)"]
v_arr2["arr2 = (same reference)"]
style v_arr1 fill:#e3f2fd,stroke:#333
style v_arr2 fill:#e3f2fd,stroke:#333
end
subgraph Heap["Heap Memory"]
direction TB
obj_array["Array Object <br> [ 99, 2, 3 ]"]
style obj_array fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
end
v_arr1 -.->|Reference 1| obj_array
v_arr2 -.->|Reference 2| obj_array
(မှတ်ချက် - အပေါ်က diagram က conceptual model သာ ဖြစ်ပါတယ်။ Java program ထဲက reference value ကို raw hex memory address အနေနဲ့ တိုက်ရိုက် မြင်ရတာ မဟုတ်ပါဘူး။)
ဥပမာ
- Primitives: ခဲတံတစ်ချောင်း ဝယ်တယ်။ သူငယ်ချင်းကို တစ်ချောင်း ထပ်ဝယ်ပေးလိုက်တယ် (မိတ္တူကူးလိုက်တယ်)။ သူငယ်ချင်းက သူ့ခဲတံကို ချိုးပစ်လိုက်ပေမယ့် ကိုယ့်ခဲတံက အကောင်းအတိုင်းပဲ ရှိနေပါတယ်။
- References: အိမ်ဆောက်ပြီး သော့ တစ်ချောင်း ထုတ်တယ်။ အဲ့ဒီ သော့ကို ပွားပြီး သူငယ်ချင်းကို ပေးလိုက်တယ်။ သူငယ်ချင်းက အဲ့ဒီသော့နဲ့ အိမ်ထဲဝင်ပြီး တီဗွီကို ဖျက်ဆီးလိုက်ရင်၊ ကိုယ်ဝင်ကြည့်တဲ့အခါမှာလည်း တီဗွီက ပျက်စီးနေမှာပါပဲ။ (ဘာလို့လဲဆိုတော့ အိမ်က တစ်လုံးတည်း ဖြစ်နေပြီး နှစ်ယောက်စလုံးက တစ်အိမ်တည်းကို ညွှန်ပြနေလို့ပါ။)
ဒီ Memory Address ညွှန်ပြတဲ့ သဘောတရားကို သေချာနားလည်ရင် ရှေ့ဆက်လေ့လာမယ့် Array တွေ၊ Linked List တွေ ဘာကြောင့် အလုပ်လုပ်ပုံ ကွာခြားရသလဲဆိုတာကို ရှင်းရှင်းလင်းလင်း မြင်လာမှာ ဖြစ်ပါတယ်။