เนื้อหา:
การแนะนำ
รวบรวมเป็น bytecode
ตัวอย่างการคอมไพล์และการทำงานของโปรแกรม
การรันโปรแกรมบนเครื่องเสมือน
การรวบรวม Just-in-time (JIT)
บทสรุป
1. บทนำ สวัสดีทุกคน! วันนี้ผมอยากจะแบ่งปันความรู้เกี่ยวกับสิ่งที่เกิดขึ้นภายใต้การทำงานของ JVM (Java Virtual Machine) หลังจากที่เรารันแอปพลิเคชันที่เขียนด้วย Java ทุกวันนี้ มีสภาพแวดล้อมการพัฒนาที่ทันสมัยที่ช่วยให้คุณหลีกเลี่ยงการคิดถึงระบบภายในของ JVM การคอมไพล์และรันโค้ด Java ซึ่งอาจทำให้ Developer ใหม่พลาดประเด็นสำคัญเหล่านี้ ในขณะเดียวกัน คำถามเกี่ยวกับหัวข้อนี้มักจะถูกถามระหว่างการสัมภาษณ์ ซึ่งเป็นสาเหตุที่ฉันตัดสินใจเขียนบทความ
2. รวบรวมเป็น bytecode
เริ่มจากทฤษฎีกันก่อน เมื่อเราเขียนแอปพลิเคชันใด ๆ เราจะสร้างไฟล์ที่มีนามสกุล
.java
และวางโค้ดไว้ในนั้นในภาษาการเขียนโปรแกรม Java ไฟล์ดังกล่าวที่มีโค้ดที่มนุษย์สามารถอ่านได้เรียกว่าไฟล์
ซอร์สโค้ด เมื่อไฟล์ซอร์สโค้ดพร้อมแล้ว คุณจะต้องดำเนินการมัน! แต่ในขั้นตอนนี้มีข้อมูลที่มนุษย์เท่านั้นที่เข้าใจได้ Java เป็นภาษาโปรแกรมหลายแพลตฟอร์ม ซึ่งหมายความว่าโปรแกรมที่เขียนด้วย Java สามารถดำเนินการบนแพลตฟอร์มใดก็ได้ที่ติดตั้งระบบรันไทม์ Java เฉพาะไว้ ระบบนี้เรียกว่า Java Virtual Machine (JVM) ในการแปลโปรแกรมจากซอร์สโค้ดเป็นโค้ดที่ JVM สามารถเข้าใจได้ คุณจำเป็นต้องคอมไพล์มัน โค้ดที่ JVM เข้าใจเรียกว่า bytecode และมีชุดคำสั่งที่เครื่องเสมือนจะดำเนินการในภายหลัง ในการรวบรวมซอร์สโค้ดเป็นไบต์โค้ด จะมีคอมไพเลอร์
javac
รวมอยู่ใน JDK (Java Development Kit) ในฐานะอินพุต คอมไพเลอร์ยอมรับไฟล์ที่มีนามสกุล
.java
ซึ่งมีซอร์สโค้ดของโปรแกรม และเป็นเอาต์พุต คอมไพเลอร์จะสร้างไฟล์ที่มีนามสกุล ซึ่ง
.class
มีไบต์โค้ดที่จำเป็นสำหรับโปรแกรมที่จะรันโดยเครื่องเสมือน เมื่อโปรแกรมได้รับการคอมไพล์เป็น bytecode แล้ว ก็สามารถดำเนินการได้โดยใช้เครื่องเสมือน
3. ตัวอย่างการคอมไพล์และการทำงานของโปรแกรม สมมติว่าเรามีโปรแกรมง่ายๆ ที่มีอยู่ในไฟล์
Calculator.java
ซึ่งรับอาร์กิวเมนต์บรรทัดคำสั่งตัวเลข 2 อาร์กิวเมนต์และพิมพ์ผลลัพธ์ของการบวก:
class Calculator {
public static void main ( String [ ] args) {
int a = Integer . valueOf ( args[ 0 ] ) ;
int b = Integer . valueOf ( args[ 1 ] ) ;
System . out. println ( a + b) ;
}
}
ในการคอมไพล์โปรแกรมนี้เป็น bytecode เราจะใช้คอมไพเลอร์
javac
บนบรรทัดคำสั่ง:
javac Calculator . java
หลังจากการคอมไพล์ เราได้รับไฟล์ที่มี bytecode เป็นเอาต์พุต
Calculator.class
ซึ่งเราสามารถดำเนินการโดยใช้เครื่อง java ที่ติดตั้งบนคอมพิวเตอร์ของเราโดยใช้คำสั่ง java บนบรรทัดคำสั่ง:
java Calculator 1 2
โปรดทราบว่าหลังจากชื่อไฟล์จะมีการระบุอาร์กิวเมนต์บรรทัดคำสั่ง 2 รายการ - หมายเลข 1 และ 2 หลังจากรันโปรแกรมหมายเลข 3 จะปรากฏบนบรรทัดคำสั่ง ในตัวอย่างด้านบน เรามีคลาสง่าย ๆ ที่ทำงานด้วยตัวมันเอง . แต่ถ้าคลาสอยู่ในแพ็คเกจล่ะ? มาจำลองสถานการณ์ต่อไปนี้: สร้างไดเรกทอรี
src/ru/javarush
และวางชั้นเรียนของเราไว้ที่นั่น ตอนนี้หน้าตาเป็นแบบนี้ (เราเพิ่มชื่อแพ็คเกจไว้ที่ตอนต้นของไฟล์):
package ru. javarush ;
class Calculator {
public static void main ( String [ ] args) {
int a = Integer . valueOf ( args[ 0 ] ) ;
int b = Integer . valueOf ( args[ 1 ] ) ;
System . out. println ( a + b) ;
}
}
มารวบรวมคลาสดังกล่าวด้วยคำสั่งต่อไปนี้:
javac - d bin src/ ru/ javarush/ Calculator . java
ในตัวอย่างนี้ เราใช้ตัวเลือกคอมไพเลอร์เพิ่มเติม
-d bin
ที่ใส่ไฟล์ที่คอมไพล์แล้วลงในไดเร็กทอรี
bin
ที่มีโครงสร้างคล้ายกับไดเร็กทอรี
src
แต่ต้องสร้างไดเร็กทอรี
bin
ไว้ล่วงหน้า เทคนิคนี้ใช้เพื่อหลีกเลี่ยงไม่ให้ไฟล์ซอร์สโค้ดสับสนกับไฟล์ bytecode ก่อนที่จะรันโปรแกรมที่คอมไพล์ ควรอธิบายแนวคิดนี้
classpath
ก่อน
Classpath
เป็นเส้นทางที่สัมพันธ์กับเครื่องเสมือนที่จะค้นหาแพ็คเกจและคลาสที่คอมไพล์แล้ว นั่นคือ ด้วยวิธีนี้ เราจะบอกเครื่องเสมือนว่าไดเร็กทอรีใดในระบบไฟล์เป็นรูทของลำดับชั้นแพ็คเกจ Java
Classpath
สามารถระบุได้เมื่อเริ่มโปรแกรมโดยใช้แฟล็
-classpath
ก เราเปิดโปรแกรมโดยใช้คำสั่ง:
java - classpath . /bin ru. javarush. Calculator 1 2
ในตัวอย่างนี้ เราต้องการชื่อเต็มของคลาส รวมถึงชื่อของแพ็คเกจที่คลาสนั้นอยู่ด้วย แผนผังไฟล์สุดท้ายมีลักษณะดังนี้:
├── src
│ └── ru
│ └── javarush
│ └── Calculator . java
└── bin
└── ru
└── javarush
└── Calculator . class
4. การรันโปรแกรมโดยเครื่องเสมือน ดังนั้นเราจึงเปิดตัวโปรแกรมเขียน แต่จะเกิดอะไรขึ้นเมื่อโปรแกรมที่คอมไพล์เปิดตัวโดยเครื่องเสมือน? ก่อนอื่น เรามาดูกันว่าแนวคิดของการคอมไพล์และการตีความโค้ดหมายถึงอะไร
การคอมไพล์ คือการแปลโปรแกรมที่เขียนด้วยภาษาต้นทางระดับสูงไปเป็นโปรแกรมที่เทียบเท่ากันในภาษาระดับต่ำคล้ายกับรหัสเครื่อง
การตีความ คือการวิเคราะห์แบบโอเปอเรเตอร์ต่อคำสั่ง (คำสั่งต่อบรรทัด บรรทัดต่อบรรทัด) การประมวลผล และการดำเนินการทันทีของโปรแกรมต้นทางหรือคำขอ (ตรงข้ามกับการคอมไพล์ ซึ่งโปรแกรมถูกแปลโดยไม่ต้องดำเนินการ) ภาษา Java มีทั้งคอมไพลเลอร์ (
javac
) และล่าม ซึ่งเป็นเครื่องเสมือนที่แปลงรหัสไบต์เป็นรหัสเครื่องทีละบรรทัดและดำเนินการทันที ดังนั้นเมื่อเรารันโปรแกรมที่คอมไพล์เครื่องเสมือนจะเริ่มตีความมันนั่นคือการแปลงไบต์โค้ดเป็นโค้ดเครื่องทีละบรรทัดรวมถึงการดำเนินการด้วย น่าเสียดายที่การตีความโค้ดไบต์ล้วนๆ เป็นกระบวนการที่ค่อนข้างยาวและทำให้จาวาช้าเมื่อเทียบกับคู่แข่ง เพื่อหลีกเลี่ยงปัญหานี้ จึงมีการใช้กลไกเพื่อเพิ่มความเร็วในการตีความรหัสไบต์โดยเครื่องเสมือน กลไกนี้เรียกว่าการคอมไพล์แบบทันเวลา (JITC)
5. การรวบรวม Just-in-time (JIT) พูดง่ายๆ ก็คือ กลไกของการคอมไพล์ Just-In-Time คือ หากมีบางส่วนของโค้ดในโปรแกรมที่ถูกเรียกใช้งานหลายครั้ง ก็สามารถคอมไพล์โค้ดเหล่านั้นเป็นโค้ดเครื่องได้หนึ่งครั้งเพื่อเร่งความเร็วในการดำเนินการในอนาคต หลังจากคอมไพล์ส่วนหนึ่งของโปรแกรมดังกล่าวเป็นโค้ดเครื่อง แล้วทุกครั้งที่เรียกส่วนนี้ของโปรแกรมตามมา เครื่องเสมือนจะรันโค้ดเครื่องที่คอมไพล์แล้วทันที แทนที่จะตีความ ซึ่งจะเร่งการทำงานของโปรแกรมโดยธรรมชาติ การเร่งความเร็วของโปรแกรมทำได้โดยการเพิ่มการใช้หน่วยความจำ (เราต้องเก็บรหัสเครื่องที่คอมไพล์ไว้ที่ใดที่หนึ่ง!) และโดยการเพิ่มเวลาที่ใช้ในการคอมไพล์ระหว่างการทำงานของโปรแกรม การคอมไพล์ JIT เป็นกลไกที่ค่อนข้างซับซ้อน ดังนั้นเรามาดูด้านบนกันดีกว่า การคอมไพล์ JIT ของรหัสไบต์เป็นรหัสเครื่องมี 4 ระดับ ยิ่งระดับการคอมไพล์สูงเท่าไรก็ยิ่งซับซ้อนมากขึ้นเท่านั้น แต่ในขณะเดียวกันการดำเนินการของส่วนดังกล่าวจะเร็วกว่าส่วนที่มีระดับต่ำกว่า JIT - คอมไพเลอร์ตัดสินใจว่าจะตั้งค่าระดับการคอมไพล์สำหรับแต่ละแฟรกเมนต์ของโปรแกรมตามความถี่ในการดำเนินการแฟรกเมนต์นั้น ภายใต้ประทุน JVM ใช้คอมไพเลอร์ JIT 2 ตัว - C1 และ C2 คอมไพเลอร์ C1 เรียกอีกอย่างว่าไคลเอนต์คอมไพเลอร์และสามารถคอมไพล์โค้ดได้จนถึงระดับที่ 3 เท่านั้น คอมไพเลอร์ C2 มีหน้าที่รับผิดชอบในระดับการคอมไพล์ที่ 4 ซับซ้อนที่สุดและเร็วที่สุด
จากที่กล่าวมาข้างต้น เราสามารถสรุปได้ว่าสำหรับแอปพลิเคชันไคลเอนต์แบบธรรมดา การใช้คอมไพเลอร์ C1 จะให้ผลกำไรมากกว่า เนื่องจากในกรณีนี้ สิ่งสำคัญสำหรับเราคือความรวดเร็วในการเริ่มต้นแอปพลิเคชัน แอปพลิเคชันฝั่งเซิร์ฟเวอร์ที่มีอายุการใช้งานยาวนานอาจใช้เวลาเริ่มต้นนานกว่า แต่ในอนาคต แอปพลิเคชันเหล่านั้นจะต้องทำงานและทำหน้าที่ได้อย่างรวดเร็ว - ที่นี่คอมไพเลอร์ C2 เหมาะสำหรับเรา
เมื่อรันโปรแกรม Java บน JVM เวอร์ชัน x32 เราสามารถระบุโหมดที่เราต้องการใช้ด้วยตนเองโดยใช้
-client
และ แฟล็
-server
ก เมื่อระบุแฟล็กนี้
-client
JVM จะไม่ทำการเพิ่มประสิทธิภาพโค้ดไบต์ที่ซับซ้อน ซึ่งจะเร่งเวลาการเริ่มต้นแอปพลิเคชันให้เร็วขึ้นและลดจำนวนหน่วยความจำที่ใช้ เมื่อระบุแฟล็ก แอปพลิเคชัน
-server
จะใช้เวลาเริ่มต้นนานขึ้นเนื่องจากการเพิ่มประสิทธิภาพโค้ดไบต์ที่ซับซ้อน และจะใช้หน่วยความจำมากขึ้นในการจัดเก็บโค้ดเครื่อง แต่โปรแกรมจะทำงานเร็วขึ้นในอนาคต ใน JVM เวอร์ชัน x64 ค่าสถานะ
-client
จะถูกละเว้นและการกำหนดค่าแอปพลิเคชันเซิร์ฟเวอร์จะถูกใช้ตามค่าเริ่มต้น
6. บทสรุป นี่เป็นการสรุปภาพรวมโดยย่อของฉันเกี่ยวกับวิธีการทำงานของการคอมไพล์และรันแอปพลิเคชัน Java ประเด็นหลัก:
คอมไพเลอร์javac แปลงซอร์สโค้ดของโปรแกรมเป็นไบต์โค้ดที่สามารถดำเนินการบนแพลตฟอร์มใด ๆ ที่ติดตั้งเครื่องเสมือน Java
หลังจากการคอมไพล์ JVM จะตีความรหัสไบต์ผลลัพธ์
เพื่อเร่งความเร็วแอปพลิเคชัน Java JVM ใช้กลไกการคอมไพล์ Just-In-Time ที่แปลงส่วนที่ดำเนินการบ่อยที่สุดของโปรแกรมเป็นรหัสเครื่องและจัดเก็บไว้ในหน่วยความจำ
ฉันหวังว่าบทความนี้จะช่วยให้คุณมีความเข้าใจที่ลึกซึ้งยิ่งขึ้นว่าภาษาการเขียนโปรแกรมที่เราชื่นชอบทำงานอย่างไร ขอบคุณที่อ่าน ยินดีรับคำวิจารณ์!
GO TO FULL VERSION