JavaRush /จาวาบล็อก /Random-TH /รวบรวมและรันแอปพลิเคชัน Java ภายใต้ประทุน
Павел Голов
ระดับ
Москва

รวบรวมและรันแอปพลิเคชัน Java ภายใต้ประทุน

เผยแพร่ในกลุ่ม

เนื้อหา:

  1. การแนะนำ
  2. รวบรวมเป็น bytecode
  3. ตัวอย่างการคอมไพล์และการทำงานของโปรแกรม
  4. การรันโปรแกรมบนเครื่องเสมือน
  5. การรวบรวม Just-in-time (JIT)
  6. บทสรุป
การคอมไพล์และรันแอปพลิเคชัน Java ภายใต้ประทุน - 1

1. บทนำ

สวัสดีทุกคน! วันนี้ผมอยากจะแบ่งปันความรู้เกี่ยวกับสิ่งที่เกิดขึ้นภายใต้การทำงานของ JVM (Java Virtual Machine) หลังจากที่เรารันแอปพลิเคชันที่เขียนด้วย Java ทุกวันนี้ มีสภาพแวดล้อมการพัฒนาที่ทันสมัยที่ช่วยให้คุณหลีกเลี่ยงการคิดถึงระบบภายในของ JVM การคอมไพล์และรันโค้ด Java ซึ่งอาจทำให้ Developer ใหม่พลาดประเด็นสำคัญเหล่านี้ ในขณะเดียวกัน คำถามเกี่ยวกับหัวข้อนี้มักจะถูกถามระหว่างการสัมภาษณ์ ซึ่งเป็นสาเหตุที่ฉันตัดสินใจเขียนบทความ

2. รวบรวมเป็น bytecode

รวบรวมและรันแอปพลิเคชัน Java ภายใต้ประทุน - 2
เริ่มจากทฤษฎีกันก่อน เมื่อเราเขียนแอปพลิเคชันใด ๆ เราจะสร้างไฟล์ที่มีนามสกุล.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 ซับซ้อนที่สุดและเร็วที่สุด
รวบรวมและรันแอปพลิเคชัน Java ภายใต้ประทุน - 3
จากที่กล่าวมาข้างต้น เราสามารถสรุปได้ว่าสำหรับแอปพลิเคชันไคลเอนต์แบบธรรมดา การใช้คอมไพเลอร์ C1 จะให้ผลกำไรมากกว่า เนื่องจากในกรณีนี้ สิ่งสำคัญสำหรับเราคือความรวดเร็วในการเริ่มต้นแอปพลิเคชัน แอปพลิเคชันฝั่งเซิร์ฟเวอร์ที่มีอายุการใช้งานยาวนานอาจใช้เวลาเริ่มต้นนานกว่า แต่ในอนาคต แอปพลิเคชันเหล่านั้นจะต้องทำงานและทำหน้าที่ได้อย่างรวดเร็ว - ที่นี่คอมไพเลอร์ C2 เหมาะสำหรับเรา เมื่อรันโปรแกรม Java บน JVM เวอร์ชัน x32 เราสามารถระบุโหมดที่เราต้องการใช้ด้วยตนเองโดยใช้-clientและ แฟล็ -serverก เมื่อระบุแฟล็กนี้-clientJVM จะไม่ทำการเพิ่มประสิทธิภาพโค้ดไบต์ที่ซับซ้อน ซึ่งจะเร่งเวลาการเริ่มต้นแอปพลิเคชันให้เร็วขึ้นและลดจำนวนหน่วยความจำที่ใช้ เมื่อระบุแฟล็ก แอปพลิเคชัน-serverจะใช้เวลาเริ่มต้นนานขึ้นเนื่องจากการเพิ่มประสิทธิภาพโค้ดไบต์ที่ซับซ้อน และจะใช้หน่วยความจำมากขึ้นในการจัดเก็บโค้ดเครื่อง แต่โปรแกรมจะทำงานเร็วขึ้นในอนาคต ใน JVM เวอร์ชัน x64 ค่าสถานะ-clientจะถูกละเว้นและการกำหนดค่าแอปพลิเคชันเซิร์ฟเวอร์จะถูกใช้ตามค่าเริ่มต้น

6. บทสรุป

นี่เป็นการสรุปภาพรวมโดยย่อของฉันเกี่ยวกับวิธีการทำงานของการคอมไพล์และรันแอปพลิเคชัน Java ประเด็นหลัก:
  1. คอมไพเลอร์javacแปลงซอร์สโค้ดของโปรแกรมเป็นไบต์โค้ดที่สามารถดำเนินการบนแพลตฟอร์มใด ๆ ที่ติดตั้งเครื่องเสมือน Java
  2. หลังจากการคอมไพล์ JVM จะตีความรหัสไบต์ผลลัพธ์
  3. เพื่อเร่งความเร็วแอปพลิเคชัน Java JVM ใช้กลไกการคอมไพล์ Just-In-Time ที่แปลงส่วนที่ดำเนินการบ่อยที่สุดของโปรแกรมเป็นรหัสเครื่องและจัดเก็บไว้ในหน่วยความจำ
ฉันหวังว่าบทความนี้จะช่วยให้คุณมีความเข้าใจที่ลึกซึ้งยิ่งขึ้นว่าภาษาการเขียนโปรแกรมที่เราชื่นชอบทำงานอย่างไร ขอบคุณที่อ่าน ยินดีรับคำวิจารณ์!
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION