การเริ่มต้นวัตถุใน Java“ Foo f = new Foo ()” เป็นหลักเหมือนกับการใช้ malloc สำหรับตัวชี้ใน C หรือไม่?


9

ฉันพยายามที่จะเข้าใจกระบวนการจริงของการสร้างวัตถุใน Java และฉันคิดว่าภาษาการเขียนโปรแกรมอื่น ๆ

จะผิดไหมถ้าสมมติว่าการเริ่มต้นวัตถุใน Java นั้นเหมือนกับเมื่อคุณใช้ malloc สำหรับโครงสร้างใน C หรือไม่?

ตัวอย่าง:

Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));

นี่คือสาเหตุที่วัตถุถูกกล่าวว่าอยู่บนกองมากกว่ากองซ้อนหรือไม่? เพราะพวกมันเป็นเพียงตัวชี้ไปยังข้อมูล?


วัตถุถูกสร้างขึ้นบนฮีปสำหรับภาษาที่มีการจัดการเช่น c # / java ใน cpp คุณสามารถสร้างวัตถุบนสแต็กได้เช่นกัน
bas

ทำไมผู้สร้างของ Java / C # ตัดสินใจที่จะเก็บวัตถุเฉพาะในกอง?
จูลส์

ฉันคิดว่าเพื่อความเรียบง่าย การจัดเก็บวัตถุบนสแต็กและส่งต่อระดับที่ลึกกว่านั้นเกี่ยวข้องกับการคัดลอกวัตถุบนสแต็กซึ่งเกี่ยวข้องกับตัวสร้างสำเนา ฉันไม่ได้ google สำหรับคำตอบที่ถูกต้อง แต่ฉันแน่ใจว่าคุณสามารถหาคำตอบที่น่าพอใจมากขึ้นด้วยตัวคุณเอง (มิฉะนั้นคนอื่นจะอธิบายรายละเอียดเกี่ยวกับคำถามด้านนี้)
bas

@Jules วัตถุใน java อาจยัง "decomponsed" ณ รันไทม์ (เรียกว่าscalar-replacement) ลงในเขตข้อมูลธรรมดาที่อยู่บนกองซ้อนเท่านั้น แต่นั่นคือสิ่งที่ไม่ได้JIT javac
ยูจีน

“ heap” เป็นเพียงชื่อของชุดคุณสมบัติที่เกี่ยวข้องกับการจัดสรรวัตถุ / หน่วยความจำ ใน C / C ++ คุณสามารถเลือกจากชุดคุณสมบัติที่แตกต่างกันสองชุดเรียกว่า“ stack” และ“ heap” ใน C # และ Java การจัดสรรวัตถุทั้งหมดมีลักษณะการทำงานที่ระบุเหมือนกันซึ่งอยู่ภายใต้ชื่อ“ heap” ซึ่งไม่ได้ หมายความว่าคุณสมบัติเหล่านี้เหมือนกับ“ ฮีป” ของ C / C ++ จริงๆแล้วมันไม่ใช่ นี่ไม่ได้หมายความว่าการใช้งานไม่สามารถมีกลยุทธ์ที่แตกต่างกันสำหรับการจัดการวัตถุมันก็หมายความว่ากลยุทธ์เหล่านั้นไม่เกี่ยวข้องกับตรรกะแอปพลิเคชัน
Holger

คำตอบ:


5

ใน C malloc()จัดสรรพื้นที่หน่วยความจำในฮีปและส่งคืนพอยน์เตอร์ให้กับมัน นั่นคือทั้งหมดที่คุณได้รับ หน่วยความจำนั้นไม่ได้กำหนดค่าเริ่มต้นและคุณไม่สามารถรับประกันได้ว่าจะเป็นศูนย์ทั้งหมดหรืออย่างอื่น

ใน Java การโทรnewจะทำการจัดสรรโดยใช้ฮีปmalloc()แต่คุณจะได้รับความสะดวกสบายเพิ่มขึ้นเช่นกัน (หรือค่าใช้จ่ายหากคุณต้องการ) ตัวอย่างเช่นคุณไม่จำเป็นต้องระบุจำนวนไบต์ที่จะจัดสรรอย่างชัดเจน คอมไพเลอร์คิดให้คุณตามประเภทของวัตถุที่คุณพยายามจัดสรร นอกจากนี้ตัวสร้างวัตถุจะถูกเรียก (ซึ่งคุณสามารถส่งผ่านข้อโต้แย้งไปยังหากคุณต้องการควบคุมวิธีการเริ่มต้นเกิดขึ้น) เมื่อnewส่งคืนคุณรับประกันว่าจะมีวัตถุที่เริ่มต้นได้

แต่ใช่ในตอนท้ายของการโทรทั้งผลลัพธ์malloc()และnewเป็นเพียงตัวชี้ไปยังข้อมูลบางส่วนของกอง

ส่วนที่สองของคำถามของคุณถามเกี่ยวกับความแตกต่างระหว่างกองและกอง คำตอบที่ครอบคลุมมากขึ้นสามารถพบได้โดยการออกแบบหลักสูตร (หรืออ่านหนังสือเกี่ยวกับ) คอมไพเลอร์ หลักสูตรเกี่ยวกับระบบปฏิบัติการก็มีประโยชน์เช่นกัน นอกจากนี้ยังมีคำถามและคำตอบมากมายเกี่ยวกับ SO เกี่ยวกับกองและกอง

ต้องบอกว่าฉันจะให้ภาพรวมทั่วไปฉันหวังว่าจะไม่ verbose มากเกินไปและมีวัตถุประสงค์เพื่ออธิบายความแตกต่างในระดับที่ค่อนข้างสูง

พื้นฐานเหตุผลหลักที่จะมีสองระบบการจัดการหน่วยความจำคือกองและกองสำหรับประสิทธิภาพ เหตุผลที่สองคือปัญหาแต่ละประเภทนั้นดีกว่าปัญหาประเภทอื่น

สแต็คค่อนข้างง่ายสำหรับฉันที่จะเข้าใจในแนวคิดดังนั้นฉันเริ่มต้นด้วยสแต็ค ลองพิจารณาฟังก์ชั่นนี้ใน C ...

int add(int lhs, int rhs) {
    int result = lhs + rhs;
    return result;
}

ดูเหมือนว่าข้างต้นค่อนข้างตรงไปตรง เรากำหนดฟังก์ชั่นชื่อadd()และผ่านในการเพิ่มด้านซ้ายและขวา ฟังก์ชั่นเพิ่มพวกเขาและส่งกลับผลลัพธ์ โปรดเพิกเฉยสิ่งที่เป็นขอบทั้งหมดเช่นโอเวอร์โฟลว์ที่อาจเกิดขึ้น ณ จุดนี้มันไม่ได้มีประโยชน์ต่อการอภิปราย

add()วัตถุประสงค์ของฟังก์ชันดูเหมือนตรงไปตรงสวย แต่สิ่งที่เราสามารถบอกเกี่ยวกับวงจรชีวิตของตนหรือไม่ โดยเฉพาะอย่างยิ่งการใช้งานหน่วยความจำความต้องการ?

สิ่งสำคัญที่สุดคือคอมไพเลอร์รู้เบื้องต้น (เช่นในเวลารวบรวม) ชนิดข้อมูลที่มีขนาดใหญ่และจำนวนที่จะใช้ lhsและrhsข้อโต้แย้งที่มีsizeof(int)4 ไบต์แต่ละ ตัวแปรresultก็เช่นsizeof(int)กัน คอมไพเลอร์สามารถบอกได้ว่าadd()ฟังก์ชั่นใช้4 bytes * 3 intsหรือหน่วยความจำรวม 12 ไบต์

เมื่อadd()ฟังก์ชั่นถูกเรียกลงทะเบียนฮาร์ดแวร์ที่เรียกว่าตัวชี้สแต็กจะมีที่อยู่ในนั้นที่ชี้ไปที่ด้านบนของสแต็ค ในการจัดสรรหน่วยความจำที่add()จำเป็นต้องใช้ฟังก์ชั่นรหัสฟังก์ชั่นทั้งหมดที่ต้องทำคือการออกคำสั่งภาษาแอสเซมบลีหนึ่งเดียวเพื่อลดค่าตัวชี้สแต็กตัวชี้สแต็ค 12 โดยการทำเช่นนั้นintsแต่ละคนสำหรับlhs, และrhs resultการหาพื้นที่หน่วยความจำที่คุณต้องการโดยการดำเนินการคำสั่งเดียวนั้นเป็นชัยชนะที่ยิ่งใหญ่ในแง่ของความเร็วเพราะคำสั่งเดียวมักจะดำเนินการในหนึ่งนาฬิกาติ๊ก (1 พันล้านของวินาทีต่อวินาทีเป็น 1 GHz CPU)

นอกจากนี้จากมุมมองของคอมไพเลอร์ก็สามารถสร้างแผนที่ไปยังตัวแปรที่มีลักษณะที่น่ากลัวมากเช่นการจัดทำดัชนีอาร์เรย์:

lhs:     ((int *)stack_pointer_register)[0]
rhs:     ((int *)stack_pointer_register)[1]
result:  ((int *)stack_pointer_register)[2]

ทั้งหมดนี้เร็วมาก

เมื่อadd()ฟังก์ชั่นออกมามันจะต้องทำความสะอาด มันทำได้โดยการลบ 12 ไบต์จากการลงทะเบียนตัวชี้สแต็ก มันคล้ายกับการเรียกfree()แต่ใช้เพียงหนึ่งคำสั่ง CPU และใช้เพียงหนึ่งเห็บ มันเร็วมาก ๆ


พิจารณาการจัดสรรแบบอิงฮีป สิ่งนี้เข้ามาเล่นเมื่อเราไม่ทราบว่าจะต้องใช้หน่วยความจำก่อนหน้าเท่าใด (เช่นเราจะเรียนรู้เฉพาะตอนรันไทม์)

พิจารณาฟังก์ชั่นนี้:

int addRandom(int count) {
    int numberOfBytesToAllocate = sizeof(int) * count;
    int *array = malloc(numberOfBytesToAllocate);
    int result = 0;

    if array != NULL {
        for (i = 0; i < count; ++i) {
            array[i] = (int) random();
            result += array[i];
        }

        free(array);
    }

    return result;
}

ขอให้สังเกตว่าaddRandom()ฟังก์ชั่นไม่ทราบว่าในเวลารวบรวมสิ่งที่ค่าของการcountโต้แย้งจะเป็น ด้วยเหตุนี้มันจึงไม่สมเหตุสมผลที่จะพยายามกำหนดarrayอย่างที่เราต้องการหากเราวางมันลงบนสแต็กดังนี้:

int array[count];

หากcountมีขนาดใหญ่อาจทำให้สแต็กของเรามีขนาดใหญ่เกินไปและเขียนทับส่วนโปรแกรมอื่น ๆ เมื่อเกิดการล้นสแต็กนี้เกิดขึ้นโปรแกรมของคุณขัดข้อง (หรือแย่กว่า)

malloc()ดังนั้นในกรณีที่เราไม่ทราบว่าหน่วยความจำมากที่เราจะต้องจนรันไทม์ที่เราใช้ จากนั้นเราสามารถขอจำนวนไบต์ที่เราต้องการเมื่อเราต้องการและmalloc()จะไปตรวจสอบว่าสามารถขายจำนวนไบต์ที่ ถ้าทำได้ดีเราได้รับมันคืนถ้าไม่เราจะได้ตัวชี้ NULL ที่บอกให้เราโทรmalloc()ไม่สำเร็จ สะดุดตาแม้ว่าโปรแกรมจะไม่ผิดพลาด! แน่นอนว่าคุณในฐานะโปรแกรมเมอร์สามารถตัดสินใจว่าโปรแกรมของคุณไม่ได้รับอนุญาตให้ทำงานหากการจัดสรรทรัพยากรล้มเหลว แต่การเลิกจ้างที่เริ่มโดยโปรแกรมเมอร์นั้นแตกต่างจากความผิดพลาดปลอม

ดังนั้นตอนนี้เราต้องกลับมาดูที่ประสิทธิภาพ ตัวจัดสรรสแต็คนั้นเร็วมาก - หนึ่งคำสั่งในการจัดสรรหนึ่งคำสั่งสำหรับการจัดสรรคืนและดำเนินการโดยคอมไพเลอร์ แต่จำได้ว่าสแต็กนั้นมีความหมายสำหรับสิ่งต่าง ๆ เช่นตัวแปรโลคอลที่มีขนาดที่รู้จัก

ตัวจัดสรรฮีปในอีกทางหนึ่งมีขนาดของคำสั่งช้ากว่า จะต้องทำการค้นหาในตารางเพื่อดูว่ามีหน่วยความจำว่างเพียงพอที่จะสามารถขายจำนวนหน่วยความจำที่ผู้ใช้ต้องการ มีการปรับปรุงตารางเหล่านั้นหลังจากที่ vends หน่วยความจำเพื่อให้แน่ใจว่าไม่มีใครสามารถใช้บล็อกนั้น (การทำบัญชีนี้อาจต้องการให้ตัวจัดสรรเพื่อจองหน่วยความจำสำหรับตัวเองนอกเหนือจากสิ่งที่วางแผนจะขาย) ตัวจัดสรรต้องใช้กลยุทธ์การล็อกเพื่อให้แน่ใจว่าหน่วยความจำจะได้รับประโยชน์อย่างปลอดภัย และเมื่อหน่วยความจำในที่สุดfree()d ซึ่งเกิดขึ้นในเวลาที่ต่างกันและโดยปกติแล้วไม่มีคำสั่งที่คาดเดาได้ผู้จัดสรรต้องค้นหาบล็อกที่ต่อเนื่องกันและต่อเข้าด้วยกันเพื่อซ่อมแซมการแยกส่วนของฮีป ถ้าฟังดูเหมือนว่าจะต้องใช้มากกว่าคำสั่ง CPU เพียงคำเดียวเพื่อให้บรรลุทั้งหมดคุณก็พูดถูก! มันซับซ้อนมากและใช้เวลาสักครู่

แต่กองใหญ่มาก มีขนาดใหญ่กว่ากองมาก เราสามารถได้รับความทรงจำมากมายจากพวกเขาและพวกเขาก็ยอดเยี่ยมเมื่อเราไม่รู้เวลารวบรวมจำนวนหน่วยความจำที่เราต้องการ ดังนั้นเราจึงแลกเปลี่ยนความเร็วสำหรับระบบหน่วยความจำที่มีการจัดการที่ปฏิเสธเราอย่างสุภาพแทนที่จะล้มเหลวเมื่อเราพยายามจัดสรรบางสิ่งที่ใหญ่เกินไป

ฉันหวังว่าจะช่วยตอบคำถามของคุณ โปรดแจ้งให้เราทราบหากคุณต้องการคำชี้แจงใด ๆ ข้างต้น


intไม่ใช่ 8 ไบต์บนแพลตฟอร์ม 64 บิต ยังคงเป็น 4 พร้อมด้วยนั้นคอมไพเลอร์มีแนวโน้มมากที่จะปรับintสแต็กที่สามจากสแต็กให้เป็นรีจิสเตอร์ย้อนกลับ ในความเป็นจริงข้อโต้แย้งทั้งสองนั้นมีแนวโน้มว่าจะลงทะเบียนกับแพลตฟอร์ม 64 บิตใด ๆ
SS Anne

ฉันได้แก้ไขคำตอบเพื่อลบข้อความเกี่ยวกับ 8 ไบต์intบนแพลตฟอร์ม 64 บิต คุณถูกต้องที่intเหลือ 4 ไบต์ใน Java ฉันออกจากคำตอบที่เหลือของฉัน แต่เพราะฉันเชื่อว่าการเพิ่มประสิทธิภาพของคอมไพเลอร์ทำให้รถเข็นก่อนม้า ใช่คุณยังถูกต้องในประเด็นเหล่านี้ แต่คำถามถามเพื่อความกระจ่างเกี่ยวกับสแต็คกับกอง RVO การทะเลาะกันผ่านการลงทะเบียนการตัดโค้ด ฯลฯ ทำให้เกิดแนวคิดพื้นฐานมากเกินไปและทำความเข้าใจกับปัจจัยพื้นฐาน
พาร์
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.