คำนำ
คลาสที่ไม่ปลอดภัยใช้ในหลายคลาสของซอร์สโค้ด JDK คลาสนี้มีฟังก์ชั่นพื้นฐานบางอย่างเพื่อข้าม JVM และการใช้งานสามารถปรับปรุงประสิทธิภาพได้ อย่างไรก็ตามมันเป็นดาบสองคม: ตามชื่อของมันที่คาดการณ์ไว้มันไม่ปลอดภัยและหน่วยความจำที่จัดสรรจะต้องฟรีด้วยตนเอง (ไม่ได้รีไซเคิลโดย GC) คลาสที่ไม่ปลอดภัยเป็นทางเลือกง่าย ๆ สำหรับคุณสมบัติบางอย่างของ JNI: สร้างความมั่นใจในประสิทธิภาพในขณะที่ทำให้สิ่งต่าง ๆ ง่ายขึ้น
ชั้นเรียนนี้เป็นของชั้นเรียนในดวงอาทิตย์* API และไม่ใช่ส่วนที่แท้จริงของ J2SE ดังนั้นคุณอาจไม่พบเอกสารทางการใด ๆ และน่าเศร้าที่มันไม่มีเอกสารรหัสที่ดีกว่าเช่นกัน
บทความนี้ส่วนใหญ่เกี่ยวกับการรวบรวมและการแปลของบทความต่อไปนี้
http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
1. วิธีการส่วนใหญ่ของ API ที่ไม่ปลอดภัยคือการใช้งานดั้งเดิมซึ่งประกอบด้วย 105 วิธีส่วนใหญ่รวมถึงหมวดหมู่ต่อไปนี้:
(1) ข้อมูลที่เกี่ยวข้อง ส่วนใหญ่ส่งคืนข้อมูลหน่วยความจำระดับต่ำบางส่วน: addressSize (), pageSize ()
(2) วัตถุที่เกี่ยวข้อง ส่วนใหญ่จัดเตรียมวัตถุและวิธีการจัดการโดเมน: allocateInstance (), ObjectFieldOffset ()
(3) ชั้นเรียนที่เกี่ยวข้อง ส่วนใหญ่ให้บริการชั้นเรียนและวิธีการจัดการโดเมนแบบคงที่: StaticFieldOffset (), defineclass (), defineanonymousclass (), ensureclassInitialized ()
(4) อาร์เรย์ที่เกี่ยวข้อง วิธีการจัดการอาร์เรย์: arraybaseoffset (), arrayIndexscale ()
(5) เกี่ยวข้องกับการซิงโครไนซ์ ส่วนใหญ่ให้การซิงโครไนซ์ระดับต่ำ (เช่น CAS-based CAS (เปรียบเทียบและ swap) ดั้งเดิม): MonitorEnter (), trymonitorenter (), monitorexit (), ()
(6) หน่วยความจำที่เกี่ยวข้อง วิธีการเข้าถึงหน่วยความจำโดยตรง (บายพาส jvm heap และจัดการหน่วยความจำท้องถิ่นโดยตรง): allocatememory (), copymemory (), freememory (), getAddress (), getint (), putint ()
2. รับอินสแตนซ์ชั้นเรียนที่ไม่ปลอดภัย
การออกแบบคลาสที่ไม่ปลอดภัยนั้นมีให้เฉพาะกับตัวโหลดคลาสเริ่มต้น JVM ที่เชื่อถือได้และเป็นคลาสรูปแบบซิงเกิลทั่วไป วิธีการซื้ออินสแตนซ์ของมันมีดังนี้:
สาธารณะคงที่ไม่ปลอดภัย getunsafe () {class cc = sun.reflect.reflection.getCallerClass (2); if (cc.getClassLoader ()! = null) โยนความปลอดภัยใหม่ ("unsafe"); ส่งคืน Theunsafe;}ตัวโหลดคลาสที่ไม่เริ่มต้นจะเรียกวิธี unsafe.getunsafe () โดยตรงและจะโยน securityException (เหตุผลเฉพาะเกี่ยวข้องกับกลไกการโหลดหลักของคลาส JVM)
มีสองวิธี หนึ่งคือการระบุคลาสที่จะใช้เป็นคลาสเริ่มต้นผ่านพารามิเตอร์ JVM - XbootClassPath อีกวิธีหนึ่งคือการสะท้อน Java
ฟิลด์ f = unsafe.class.getDeclaredField ("theunsafe"); f.setAccessible (จริง); unsafe unsafe = (ไม่ปลอดภัย) f.get (null);โดยการตั้งค่าอย่างไร้ความปราณีที่สามารถเข้าถึงได้จริงสำหรับอินสแตนซ์ซิงเกิลส่วนตัวจากนั้นจึงได้รับการคัดเลือกวัตถุโดยตรงเพื่อไม่ปลอดภัยผ่านวิธีการรับของฟิลด์ ใน IDE วิธีการเหล่านี้จะถูกทำเครื่องหมายว่าเป็นข้อผิดพลาดและสามารถแก้ไขได้โดยการตั้งค่าต่อไปนี้:
การตั้งค่า -> java -> คอมไพเลอร์ -> ข้อผิดพลาด/คำเตือน -> API ที่เลิกใช้แล้วและ จำกัด -> การอ้างอิงต้องห้าม -> คำเตือน
3. สถานการณ์แอปพลิเคชัน "น่าสนใจ" ของคลาสที่ไม่ปลอดภัย
(1) บายพาสวิธีการเริ่มต้นคลาส เมธอด allocateInstance () มีประโยชน์มากเมื่อคุณต้องการหลีกเลี่ยงตัวสร้างวัตถุตัวตรวจสอบความปลอดภัยหรือตัวสร้างที่ไม่มีสาธารณะ
คลาส A {ส่วนตัวยาว A; // ไม่ใช่ค่าเริ่มต้นสาธารณะ A () {this.a = 1; // การเริ่มต้น} สาธารณะยาว A () {ส่งคืนสิ่งนี้; -ต่อไปนี้คือการเปรียบเทียบวิธีการก่อสร้างวิธีการสะท้อนและ allocateInstance ()
A O1 = ใหม่ A (); // constructoro1.a (); // พิมพ์ 1 a o2 = a.class.newinstance (); // reflectiono2.a (); // พิมพ์ 1 a o3 = (a) unsafe.allocateInstance (a.class); // unsafeo3.a (); // พิมพ์ 0
allocateInstance () ไม่ได้ป้อนวิธีการสร้างเลยและในโหมดซิงเกิลเราดูเหมือนจะเห็นวิกฤต
(2) การปรับเปลี่ยนหน่วยความจำ
การปรับเปลี่ยนหน่วยความจำค่อนข้างธรรมดาในภาษา C ใน Java สามารถใช้เพื่อข้ามตัวตรวจสอบความปลอดภัย
พิจารณากฎการตรวจสอบการเข้าถึงอย่างง่ายต่อไปนี้:
ตัวป้องกันคลาส {ส่วนตัว int access_allowed = 1; บูลีนสาธารณะ giveaccess () {return 42 == access_allowed; -ภายใต้สถานการณ์ปกติ GiveAccess จะกลับมาเป็นเท็จเสมอ แต่ก็ไม่ได้เกิดขึ้นเสมอไป
Guard Guard = New Guard (); Guard.GiveAccess (); // false, ไม่มีการเข้าถึง // bypassunsafe unsafe = getUnsafe (); ฟิลด์ f = guard.getClass (). getDeclaredField ("access_allowed"); unsafe.putint (guard, unsafe.objectfieldoffset (f), 42); // หน่วยความจำคอร์รัปชั่น Guard.GiveAccess (); // จริงการเข้าถึงที่ได้รับโดยการคำนวณการชดเชยหน่วยความจำและการใช้เมธอด putint () จะมีการแก้ไข access_allowed ของคลาส เมื่อรู้โครงสร้างคลาสข้อมูลชดเชยข้อมูลสามารถคำนวณได้เสมอ (สอดคล้องกับการคำนวณการชดเชยข้อมูลในคลาสใน C ++)
(3) ใช้ฟังก์ชัน sizeof () คล้ายกับภาษา c
ใช้ฟังก์ชั่นขนาด C-like () โดยการรวมการสะท้อน Java และฟังก์ชั่น ObjectFieldOffset ()
ขนาดยาวคงที่สาธารณะ (วัตถุ o) {unsafe u = getUnsafe (); ฟิลด์ hashset = new hashset (); คลาส C = o.getClass (); ในขณะที่ (c! = object.class) {สำหรับ (ฟิลด์ f: c.getDeclaredFields ()) {ถ้า ((f.getModifiers () & modifier.static) == 0) {fields.add (f); }} c = c.getSuperClass (); } // รับ Offset Long Long MaxSize = 0; สำหรับ (ฟิลด์ F: ฟิลด์) {Long Offset = U.ObjectFieldOffset (F); if (Offset> maxSize) {maxSize = Offset; }} return ((maxsize/8) + 1) * 8; // padding}ความคิดของอัลกอริทึมนั้นชัดเจนมาก: เริ่มต้นจากคลาสย่อยพื้นฐานนำโดเมนที่ไม่คงที่ของตัวเองและ superclasses ทั้งหมดของมันในทางกลับกันวางไว้ใน hashset (การคำนวณซ้ำ ๆ เพียงครั้งเดียว Java เป็นมรดกเดียว) จากนั้นใช้ ObjectFieldOffset ()
ใน JVM ขนาด 32 บิตสามารถรับขนาดได้โดยการอ่านความยาวด้วยไฟล์คลาสชดเชย 12
ขนาดยาวคงที่สาธารณะ (วัตถุวัตถุ) {return getUnsafe (). getAddress (ปกติ (getUnsafe (). getInt (วัตถุ, 4L)) + 12L);}ฟังก์ชั่น Normalize () เป็นวิธีที่แปลง Int ที่ลงนามเป็นความยาวที่ไม่ได้ลงนาม
ส่วนตัวคงที่แบบคงที่ยาว (ค่า int) {ถ้า (ค่า> = 0) ค่าส่งคืน; return (0l >>> 32) & value;}ขนาดของสอง sizeof () ที่คำนวณเหมือนกัน การใช้งานขนาดมาตรฐานมากที่สุด () คือการใช้ java.lang.instrument อย่างไรก็ตามมันต้องการการระบุพารามิเตอร์บรรทัดคำสั่ง -javaagent
(4) การดำเนินการจำลองแบบ Java ตื้น
รูปแบบการจำลองแบบตื้นแบบมาตรฐานคือการใช้อินเตอร์เฟส cloneable หรือฟังก์ชั่นการจำลองแบบที่นำมาใช้ด้วยตัวเองและไม่ใช่ฟังก์ชั่นอเนกประสงค์ โดยการรวมวิธีการขนาด () สามารถคัดลอกตื้น ๆ ได้
วัตถุคงที่ตื้นเขิน (วัตถุ obj) {ขนาดยาว = sizeof (obj); เริ่มต้นยาว = toaddress (obj); ที่อยู่ยาว = getUnsafe (). ollocatememory (ขนาด); getunsafe (). copymemory (เริ่มต้นที่อยู่ขนาด); กลับมาจากที่อยู่ (ที่อยู่);}toaddress () และ fromaddress () ต่อไปนี้แปลงวัตถุเป็นที่อยู่และการดำเนินการย้อนกลับตามลำดับ
toaddress ยาวคงที่ (Object obj) {Object [] array = วัตถุใหม่ [] {obj}; long baseoffset = getUnsafe (). arraybaseoffset (วัตถุ []. class); ส่งคืนปกติ (getunsafe (). getint (อาร์เรย์, baseoffset));} วัตถุคงที่จากที่อยู่ (ที่อยู่ยาว) {object [] array = วัตถุใหม่ [] {null}; long baseoffset = getUnsafe (). arraybaseoffset (วัตถุ []. class); getunsafe (). Putlong (อาร์เรย์, baseoffset, ที่อยู่); return array [0];}ฟังก์ชั่นการคัดลอกตื้นข้างต้นสามารถนำไปใช้กับวัตถุ Java ใด ๆ และขนาดของมันจะถูกคำนวณแบบไดนามิก
(5) กำจัดรหัสผ่านในหน่วยความจำ
ฟิลด์รหัสผ่านจะถูกเก็บไว้ในสตริงอย่างไรก็ตามการรีไซเคิลสตริงได้รับการจัดการโดย JVM วิธีที่ปลอดภัยที่สุดคือเขียนทับฟิลด์รหัสผ่านหลังจากใช้งานแล้ว
Field StringValue = string.class.getDeclaredField ("value"); stringValue.setAccessible (จริง); char [] mem = (char []) stringValue.get (รหัสผ่าน); สำหรับ (int i = 0; i <mem.length; i ++) {mem [i] = '?';(6) การโหลดแบบไดนามิกของคลาส
วิธีมาตรฐานของคลาสโหลดแบบไดนามิกคือ class.forname () (เมื่อเขียนโปรแกรม JDBC ฉันจำได้อย่างลึกซึ้ง) Unsafe ยังสามารถโหลดไฟล์คลาส Java แบบไดนามิกได้
Byte [] classContents = getClassContent (); คลาส C = getUnSafe (). defineclass (null, classContents, 0, classContents.length); C.GetMethod ("A"). เรียกใช้ (C.NewInstance (), null); // 1GetClassContent () วิธีการอ่านไฟล์คลาสเป็นอาร์เรย์ไบต์ BYTE แบบคงที่ส่วนตัว [] getClassContent () โยนข้อยกเว้น {ไฟล์ f = ไฟล์ใหม่ ("/home/mishadoff/tmp/a.class"); fileInputStream input = new FileInputStream (F); ไบต์ [] content = byte ใหม่ [(int) f.length ()]; input.read (เนื้อหา); input.close (); ส่งคืนเนื้อหา;}มันสามารถนำไปใช้ในการโหลดแบบไดนามิกพร็อกซ์การหั่นและฟังก์ชั่นอื่น ๆ
(7) ข้อยกเว้นการตรวจจับแพ็คเกจเป็นข้อยกเว้นรันไทม์
getUnsafe (). throlexception (ใหม่ ioexception ());
สามารถทำได้เมื่อคุณไม่ต้องการจับข้อยกเว้นที่ตรวจสอบ (ไม่แนะนำ)
(8) การทำให้เป็นอนุกรมอย่างรวดเร็ว
java serializable มาตรฐานนั้นช้ามากและยัง จำกัด ว่าชั้นเรียนจะต้องมีตัวสร้างพารามิเตอร์ที่ไม่มีพารามิเตอร์ Externalizable นั้นดีกว่าจำเป็นต้องระบุสคีมาสำหรับคลาสที่จะได้รับการจัดลำดับ ห้องสมุดอนุกรมที่มีประสิทธิภาพที่ได้รับความนิยมเช่น Kryo ซึ่งอาศัยห้องสมุดบุคคลที่สามจะเพิ่มการใช้หน่วยความจำ คุณสามารถรับค่าที่แท้จริงของโดเมนในชั้นเรียนผ่าน getint (), getLong (), getObject () และวิธีการอื่น ๆ และคงข้อมูลข้อมูลเช่นชื่อคลาสไปยังไฟล์ด้วยกัน Kryo พยายามใช้ไม่ปลอดภัย แต่ไม่มีข้อมูลการปรับปรุงประสิทธิภาพเฉพาะ (http://code.google.com/p/kryo/issues/detail?id=75)
(9) จัดสรรหน่วยความจำในกองที่ไม่ใช่ Java
ใหม่โดยใช้ Java จะจัดสรรหน่วยความจำสำหรับวัตถุในกองและวงจรชีวิตของวัตถุจะได้รับการจัดการโดย JVM GC
ชั้น superarray {ส่วนตัวสุดท้ายคงที่ int byte = 1; ขนาดส่วนตัว ที่อยู่ยาวส่วนตัว superarray สาธารณะ (ขนาดยาว) {this.size = size; ที่อยู่ = getUnsafe (). allocatememory (ขนาด * ไบต์); } ชุดโมฆะสาธารณะ (Long I, ค่าไบต์) {getUnsafe (). putbyte (ที่อยู่ + i * byte, ค่า); } public int get (Long idx) {return getUnsafe (). getByte (ที่อยู่ + IDX * ไบต์); } ขนาดยาวสาธารณะ () {ขนาดคืน; -หน่วยความจำที่จัดสรรโดย UNSAFE ไม่ได้ถูก จำกัด โดย Integer.max_value และจัดสรรในหน่วยความจำที่ไม่ใช่กอง เมื่อใช้มันคุณจะต้องระมัดระวังมาก: ถ้าคุณลืมรีไซเคิลด้วยตนเองการรั่วไหลของหน่วยความจำจะเกิดขึ้น หากคุณเข้าถึงที่อยู่ผิดกฎหมายมันจะทำให้ JVM ผิดพลาด สามารถใช้งานได้เมื่อคุณต้องการจัดสรรพื้นที่ต่อเนื่องขนาดใหญ่การเขียนโปรแกรมแบบเรียลไทม์ (ไม่ทนต่อเวลาแฝง JVM) java.nio ใช้เทคโนโลยีนี้
(10) แอปพลิเคชันใน Java พร้อมกัน
ด้วยการใช้ unsafe.compareandswap () สามารถใช้ในการใช้โครงสร้างข้อมูลที่ปราศจากล็อคที่มีประสิทธิภาพ
Class Cascounter ใช้เคาน์เตอร์ {ตัวนับความยาวส่วนตัว = 0; ไม่ปลอดภัยส่วนตัวไม่ปลอดภัย ชดเชยส่วนตัว Public Cascounter () โยนข้อยกเว้น {unsafe = getUnsafe (); Offset = unsafe.ObjectFieldOffset (cascounter.class.getDeclaredField ("counter")); } @Override โมฆะสาธารณะเพิ่มขึ้น () {ยาวก่อน = ตัวนับ; ในขณะที่ (! unsafe.compareandswaplong (นี่, ชดเชย, ก่อนหน้า, ก่อน + 1)) {ก่อน = counter; }} @Override สาธารณะ Long GetCounter () {Counter return; -ผ่านการทดสอบโครงสร้างข้อมูลข้างต้นนั้นเหมือนกับประสิทธิภาพของตัวแปรอะตอมจาวา ตัวแปรอะตอม Java ยังใช้วิธีการเปรียบเทียบและ SAFE ของ UNSAFE และวิธีนี้ในที่สุดจะสอดคล้องกับต้นฉบับที่สอดคล้องกันของ CPU ดังนั้นจึงมีประสิทธิภาพมาก นี่คือวิธีแก้ปัญหาในการใช้ HashMap ฟรีล็อค (http://www.azulsystems.com/about_us/presentations/lock-free-hash ความคิดของโซลูชันนี้คือ: วิเคราะห์แต่ละรัฐสร้างสำเนาดัดแปลงสำเนาใช้ CAS ดั้งเดิม ในเครื่องเซิร์ฟเวอร์ทั่วไป (Core <32) โดยใช้ ConcurrentHashMap (ก่อน JDK8 การล็อคการแยก 16 ช่องเริ่มต้นได้ถูกนำมาใช้และใช้งานพร้อมกันโดยใช้การล็อคฟรี) นั้นเพียงพอแล้ว
สรุป
ข้างต้นเป็นเนื้อหาทั้งหมดของบทความนี้ ฉันหวังว่าเนื้อหาของบทความนี้จะมีค่าอ้างอิงบางอย่างสำหรับการศึกษาหรือที่ทำงานของทุกคน หากคุณมีคำถามใด ๆ คุณสามารถฝากข้อความไว้เพื่อสื่อสาร ขอบคุณสำหรับการสนับสนุน Wulin.com