Tải bản đầy đủ (.pdf) (29 trang)

Phát triển với Java thời gian thực, Phần 2: Cải thiện chất lượng dịch vụ Sử dụng Java thời gian thực để giảm bớt độ đa dạng trong các ứng dụng Java pptx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (257.25 KB, 29 trang )

Phát triển với Java thời gian thực, Phần 2: Cải thiện chất lượng dịch vụ
Sử dụng Java thời gian thực để giảm bớt độ đa dạng trong các ứng dụng Java
Mark Stoodley, Lãnh đạo Kỹ thuật Thời gian Thực WebSphere, IBM Toronto Lab
Charlie Gracie, Lãnh đạo nhóm Máy ảo J9, IBM
Tóm tắt: Một số ứng dụng Java™ không cung cấp được chất lượng hợp lý của
dịch vụ mặc dù đạt được các mục tiêu hiệu năng khác, chẳng hạn như thời gian trễ
trung bình hoặc thông lượng tổng thể. Bằng cách đưa ra các đoạn dừng hoặc ngắt
không chịu kiểm soát của ứng dụng, ngôn ngữ Java và hệ thống thời gian chạy đôi
khi có thể chịu trách nhiệm về không đáp ứng các độ đo hiệu năng của ứng dụng.
Bài viết này là bài thứ hai trong loạt bài ba phần, giải thích nguồn gốc căn nguyên
của trễ và ngắt trong một JVM và mô tả các kỹ thuật cho phép bạn có thể dùng để
giảm thiểu các căn nguyên, nhằm ứng dụng của bạn cung cấp chất lượng dịch vụ
ổn định hơn.
Tính đa dạng trong một ứng dụng Java — thường gây ra do các đoạn dừng, hoặc
trễ, xảy ra vào những lúc không thể đoán trước được — có thể xảy ra qua ngăn
xếp phần mềm. Các trễ có thể xuất hiện do:
 Phần cứng (trong các quá trình xử lý chẳng hạn như nhớ nhanh).
 Phần đệm (xử lý của các ngắt quản lý hệ thống chẳng hạn như dữ liệu về
nhiệt độ bộ xử lý trung tâm).
 Hệ điều hành (trả lời một ngắt hoặc khai thác một hoạt động thông minh đã
lên lịch thường kì).
 Các chương trình khác chạy trên cùng hệ thống.
 JVM (gom rác, biên dịch Đúng lúc, và tải lớp).
 Chính ứng dụng Java.
Bạn có thể hiếm khi bù lại ở một mức cao cho các trễ do mức thấp hơn gây nên,
vậy nếu bạn cố gắng giải quyết độ đa dạng chỉ ở mức ứng dụng, có thể bạn chỉ cần
chuyển đổi JVM hoặc các trễ của hệ điều hành ở đâu đó mà không giải quyết vấn
đề thực. May mắn là các thời gian chờ đối với các mức thấp hơn có xu hướng
tương đối ngắn hơn các thời gian chờ ở các mức cao, như vậy chỉ khi nào yêu cầu
của bạn đối với việc giảm độ đa dạng là vô cùng nhiều thì bạn mới cần xem xét
thấp hơn JVM hoặc hệ điều hành. Nếu các yêu cầu của bạn không nhiều đến như


vậy, thì bạn có thể gần như chắc chắn tập trung đủ các cố gắng của bạn ở mức
JVM và trong ứng dụng của bạn.
Java thời gian thực cho bạn các công cụ mà bạn cần phải vật lộn với các tài
nguyên biến đổi trong một JVM và trong các ứng dụng của bạn để cung cấp chất
lượng dịch vụ mà những người sử dụng của bạn đòi hỏi. Bài viết này đề cập đến
nguồn gốc của sự đa dạng ở các mức JVM và ứng dụng một cách chi tiết và mô tả
các công cụ và kỹ thuật mà bạn có thể sử dụng để giảm nhẹ tác động của chúng.
Sau đó nó đưa ra một ứng dụng máy chủ Java đơn giản trình bày một số khái niệm
này.
Nhằm vào các nguồn biến đổi
Các nguồn biến đổi ban đầu trong một JVM xuất phát từ tính chất động của ngôn
ngữ Java:
 Bộ nhớ không giải phóng hiển hiện do ứng dụng mà thay vào đó được phục
hồi theo định kỳ bởi bộ gom rác.
 Các lớp được giải quyết khi ứng dụng sử dụng chúng đầu tiên.
 Mã riêng được biên dịch (và có thể được biên dịch lại bằng một Bộ biên
dịch Đúng lúc (Just-in-time (JIT) compiler) khi ứng dụng đang chạy, dựa
trên các lớp và phương thức nào được gọi ra thường xuyên.
Ở mức ứng dụng Java, việc quản lý các xử lí là lĩnh vực chủ yếu liên quan đến độ
đa dạng.
Đoạn dừng gom rác
Khi bộ gom rác chạy để phục hồi bộ nhớ mà chương trình không còn sử dụng nữa,
nó có thể dừng tất cả các xử lí ứng dụng. (Kiểu bộ gom này gọi là một bộ gom
Stop-the-world (Dừng lại tất cả), hay bộ gom STW.) Hoặc nó có thể thực hiện một
số công việc của mình đồng thời với ứng dụng. Trong trường hợp ấy, các tài
nguyên mà bộ gom rác cần có cũng không đủ dùng cho ứng dụng, cho nên việc
gom rác (GC) là nguyên nhân gây dừng và biến đổi đối với hiệu năng ứng dụng
Java, như thường được biết đến. Mặc dù nhiều hình mẫu GC có các ưu điểm và
nhược điểm của nó, khi mục tiêu đối với một ứng dụng là các đoạn dừng GC ngắn,
hai lựa chọn chính là bộ gom sản sinh (generational) và và bộ gom thời gian thực

(real-time).
Các bộ gom sản sinh tổ chức đống thành ít nhất 2 phần thường gọi là không gian
mới và không gian cũ (đôi khi gọi là theo nhiệm kỳ tenured). Các đối tượng mới
luôn luôn được phân bổ trong không gian mới. Khi không gian mới hết bộ nhớ tự
do, rác chỉ được thu gom trong không gian đó. Việc sử dụng một không gian mới
tương đối nhỏ có thể giữ được thời gian chu trình GC khá ngắn. Các đối tượng qua
được một số thu gom không gian-mới được thúc đẩy trở thành không gian cũ. Các
thu gom không gian cũ thường ít xuất hiện hơn nhiều so với các thu gom không
gian mới, nhưng do không gian cũ lớn hơn nhiều so với không gian mới, các chu
trình GC này có thể mất nhiều thời gian hơn. Các bộ gom rác sản sinh đưa ra các
đoạn dừng GC trung bình tương đối ngắn, nhưng chi phí của các thu gom không
gian cũ có thể gây ra sai lệch chuẩn sau các lần đoạn dừng này sẽ là khá lớn. Các
bộ gom sản sinh là hiệu quả nhất trong các ứng dụng mà tập hợp các dữ liệu sống
không thay đổi nhiều theo thời gian nhưng nhiều rác được tạo ra. Trong kịch bản
này, các thu gom không gian cũ là vô cùng hiếm, và như vậy các lần tạm ngừng
GC là do các thu gom không gian cũ ngắn.
Ngược lại với các bộ gom sản sinh, các bộ gom rác thời gian thực điều khiển hành
vi của chúng để thu ngắn rất nhiều độ dài của chu trình GC (bằng cách khai thác
các chu trình khi ứng dụng không dùng đến) hoặc để giảm bớt ảnh hưởng của các
chu trình này về hiệu năng ứng dụng (bằng cách thực hiện công việc bằng các gia
số nhỏ phù hợp với một “mức độ rút ngắn” với ứng dụng). Việc sử dụng một trong
những bộ gom này cho phép bạn lường trước được trường hợp xấu nhất để hoàn
tất một tác vụ riêng. Thí dụ, bộ gom rác trong các JVM thời gian thực IBM®
WebSphere® chia các chu trình GC thành các việc nhỏ — gọi là các lượng tử GC
(GC quanta ) — mà có thể được hoàn tất gia tăng. Việc lên lịch các lượng tử có
một tác động vô cùng thấp về hiệu năng ứng dụng, với các trễ thấp đến phần trăm
micro-giây nhưng thường nhỏ hơn 1 milli-giây. Để đạt được mức trễ này, bộ gom
rác phải có khả năng lập kế hoạch công việc của nó bằng cách đưa ra khái niệm về
một mức độ rút ngắn sử dụng ứng dụng. Mức độ rút ngắn này điều khiển mức độ
thường xuyên mà GC được phép ngắt ứng dụng để thực hiện công việc của nó. Thí

dụ, mức độ rút ngắn sử dụng mặc định là 70% mà chỉ cho phép GC sử dụng đến 3
mili-giây trong mỗi 10 mili-giây, với các đoạn dừng điển hình khoảng 500 micro-
giây, khi chạy trên một hệ điều hành thời gian thực. (xem "Java thời gian thực,
Phần 4: Gom rác Thời gian thực" để được mô tả chi tiết về phép gom rác Thời
gian Thực WebSphere của IBM).
Kích thước của đống và mức sử dụng ứng dụng là các tuỳ chọn điều chỉnh quan
trọng cần cân nhắc khi chạy một ứng dụng trên một bộ gom rác thời gian thực. Khi
mức sử dụng ứng dụng tăng lên, bộ gom rác nhận được ít thời gian hơn để hoàn tất
công việc của nó, như vậy cần có đống lớn hơn để đảm bảo chu trình GC có thể
được hoàn tất gia tăng. Nếu bộ gom rác không thể theo kịp với tốc độ phân bố, GC
quay trở lại một thu gom đồng bộ.
Thí dụ, một ứng dụng chạy trên các JVM thời gian thực WebSphere của IBM, với
mức độ sử dụng ứng dụng mặc định 70% của chúng, đòi hỏi nhiều đống theo mặc
định hơn nếu nó được chạy trên một JVM bằng cách sử dụng một bộ gom rác sản
sinh (mà không đưa ra mức độ rút ngắn việc sử dụng. Do các bộ gom rác thời gian
thực điều khiển thời lượng dừng GC, việc gia tăng kích thước của đống làm hạ tần
suất GC mà không kéo dài thời gian tạm ngừng. Trong các bộ gom rác không phải
thời gian thực, về mặt khác, việc gia tăng kích thước đống thường làm giảm bớt
tần số của các chu trình GC, nó giảm bớt tổng thể tác động của bộ gom rác; khi
một chu trình GC xuất hiện, thời gian đoạn dừng nói chung là lớn hơn (do có
nhiều đống hơn cần kiểm tra).
Trong các JVM thời gian thực WebSphere của IBM, bạn có thể điều chỉnh kích
thước đống bằng tuỳ chọn -Xmx<size>. Thí dụ, -Xmx512m quy định một đống
512MB. Bạn cũng có thể điều chỉnh việc sử dụng ứng dụng. Ví dụ, -
Xgc:targetUtilization=80 đặt nó ở mức 80%.
Các đoạn dừng nạp lớp Java
Đặc tả ngôn ngữ Java đòi hỏi các lớp phải được giải quyết, nạp, xác thực, và khởi
tạo khi có ứng dụng đầu tiên tham chiếu đến chúng. Nếu tham chiếu đầu tiên đến
một lớp C xuất hiện trong thời gian có một phép toán chiếm nhiều thời gian, thì
thời gian để giải quyết, xác thực, nạp, và khởi tạo C có thể làm cho phép toán đó

mất nhiều thời gian hơn được chờ đợi. Do việc nạp C gồm cả việc xác thực lớp đó
— mà có thể yêu cầu phải nạp các lớp khác — trễ toàn bộ ứng dụng Java xảy ra để
có thể sử dụng một lớp cụ thể đối với lần đầu tiên với thời gian có nghĩa lâu hơn
dự kiến.
Tại sao một lớp chỉ có thể được tham chiếu đến lần đầu tiên sau đó trong một khai
thác ứng dụng? Các đường dẫn được khai thác rất hiếm là nguyên nhân phổ biến
của một việc nạp lớp mới. Ví dụ, bộ mã trong Liệt kê 1 chứa một điều kiện if mà
rất hiếm được phép khai thác. (để cho ngắn gọn, việc xử lý ngoại lệ và sai sót phần
lớn được bỏ qua, từ tất cả các liệt kê trong bài viết này.)

Liệt kê 1. Thí dụ về một điều kiện rất hiếm được thực hiện để nạp một lớp
mới

Iterator<MyClass> cursor = list.iterator();
while (cursor.hasNext()) {
MyClass o = cursor.next();
if (o.getID() == 17) {
NeverBeforeLoadedClass o2 = new NeverBeforeLoadedClass(o);
// do something with o2
}
else {
// do something with o
}
}

Các lớp ngoại lệ là các ví dụ khác của các lớp mà không được phép nạp cho đến
khi chuyển sang hẳn một sự khai thác của ứng dụng, do các ngoại lệ hiếm khi xảy
ra (mặc dù không phải luôn thế). Vì các ngoại lệ hiếm khi được xử lý nhanh
chóng, việc tăng các quá tải lớp phụ có thể đẩy thời gian chờ phép toán đến
ngưỡng gay cấn. Nói chung, các ngoại lệ bị loại bỏ khi có các phép toán chiếm

nhiều thời gian phải được tránh đi bất cứ khi nào có thể.
Các lớp mới cũng có thể được nạp khi các dịch vụ nào đó, chẳng hạn như phản
chiếu, được sử dụng trong thư viện lớp Java. Việc cài đặt ẩn của các lớp phản
chiếu tạo ra các lớp mới đang chạy sẽ được nạp trong JVM. Việc sử dụng lặp lại
các lớp phản chiếu trong mã nhạy thời gian có thể gây ra hoạt động nạp-lớp liên
tục mà đưa ra các trở ngại. Sử dụng tuỳ chọn -verbose:class là cách tốt nhất để
phát hiện ra các lớp này đang được tạo ra. Có lẽ cách tốt nhất để tránh việc tạo ra
chúng trong thời gian chạy chương trình là tránh sử dụng các dịch vụ phản chiếu
để ánh xạ lớp, trường, hoặc các phương thức từ các chuỗi khi chạy các bộ phận
tiêu tốn thời gian của ứng dụng của bạn. Thay vào đó, gọi trước các dịch vụ này
trong ứng dụng của bạn và lưu các kết quả để sau này sử dụng nhằm ngăn chặn
hầu hết các loại lớp này được tạo ra khi đang chạy mà bạn không muốn tạo ra
chúng.
Một kỹ thuật chung để tránh các trễ khi nạp lớp khi chạy các bộ phận nhạy thời
gian của ứng dụng của bạn là nạp trước các lớp khi khởi động hoặc khởi tạo ứng
dụng. Mặc dù bước nạp trước này đưa ra một trở ngại khởi động bổ sung nào đó
(thật đáng tiếc là, việc cải tiến một độ đo thường gây hậu quả tiêu cực đối với độ
đo khác), nếu được sử dụng cẩn thận, có thể loại bỏ việc nạp lớp không mong
muốn về sau. Quy trình khởi động này rất dễ thực hiện, như trong Liệt kê 2:

Liệt kê 2. Nạp lớp được điều khiển từ một danh sách lớp

Iterator<String> classIt = listOfClassNamesToLoad.iterator();
while (classIt.hasNext()) {
String className = classIt.next();
try {
Class clazz = Class.forName(className);
String n=clazz.getName();
} catch (Exception e) {
System.err.println("Could not load class: " + className);

System.err.println(e);
}

Hãy chú ý đến lần gọi clazz.getName() nó buộc lớp này phải được khởi tạo. Việc
xây dựng danh sách các lớp đòi hỏi phải thu thập thông tin từ ứng dụng của bạn
khi nó chạy, hoặc phải sử dụng một tiện ích mà có thể xác định được các lớp nào
mà ứng dụng của bạn sẽ nạp. Thí dụ, bạn có thể nắm bắt được kết quả đầu ra của
chương trình của bạn khi chạy với tuỳ chọn -verbose:class. Liệt kê 3 trình bày kết
quả của lệnh này sẽ trông như thế nào nếu bạn sử dụng một sản phẩm thời gian
thực WebSphere của IBM:

Liệt kê 3. Trích đoạn kết quả từ việc chạy java với -verbose:class


class load: java/util/zip/ZipConstants
class load: java/util/zip/ZipFile
class load: java/util/jar/JarFile
class load: sun/misc/JavaUtilJarAccess
class load: java/util/jar/JavaUtilJarAccessImpl
class load: java/util/zip/ZipEntry
class load: java/util/jar/JarEntry
class load: java/util/jar/JarFile$JarFileEntry
class load: java/net/URLConnection
class load: java/net/JarURLConnection
class load: sun/net/www/protocol/jar/JarURLConnection


Bằng cách lưu lại danh sách các lớp do ứng dụng của bạn nạp khi khai thác và sử
dụng danh sách đó để đưa vào danh sách các tên lớp cho vòng lặp như hiển thị
trong Liệt kê 2, bạn có thể chắc chắn rằng các lớp đó nạp trước khi ứng dụng của

bạn bắt đầu chạy. Dĩ nhiên, các khai thác khác nhau của ứng dụng của bạn có thể
dùng các đường dẫn khác nhau, nên danh sách từ một khai thác có thể không đầy
đủ. Chuẩn bị cho điều đó, nếu ứng dụng của bạn đang được phát triển, bộ mã vừa
mới viết ra hoặc sửa đổi có thể dựa vào các lớp mới mà không phải là bộ phận của
danh sách (hoặc lớp mà nằm trong danh sách có thể không lâu hơn yêu cầu). Đáng
tiếc là, việc bảo trì danh sách lớp lại là một phần vô cùng rắc rối khi theo tiếp cận
này đối với việc nạp trước lớp. Nếu bạn theo tiếp cận này, hãy nhớ rằng tên của
sản phẩm đầu ra lớp của -verbose:class không phù hợp với định dạng mà
Class.forName() đòi hỏi: đầu ra rườm rà (verbose output) tách riêng các gói lớp
bằng các dấu gạch chéo tiến, khi Class.forName() chờ chúng được tách riêng bằng
các dấu chấm câu.
Đối với các ứng dụng mà việc nạp lớp là công việc, một số công cụ có thể giúp
bạn quản lý việc nạp trước, gồm có Công cụ Phân tích Lớp Thời gian Thực (Real
Time Class Analysis Tool - RATCAT) và Bộ Tối ưu hoá Khai thác Ứng dụng
Thời gian Thực dùng cho Java của IBM (IBM Real Time Application Execution
Optimizer for Java) (xem Tài nguyên). Các công cụ này cung cấp một số kỹ thuật
tự động để định danh danh sách các lớp để nạp trước và kết hợp bộ mã nạp trước
vào ứng dụng của bạn.
Các đoạn dừng biên dịch-mã JIT
Vẫn còn một nguồn thứ ba của các trở ngại trong chính JVM là bộ biên dịch JIT.
Nó hoạt động khi ứng dụng của bạn chạy để dịch các phương thức của chương
trình từ các từ ngôn ngữ máy được bộ biên dịch javac tạo ra thành các chỉ thị riêng
của bộ xử lý trung tâm mà ứng dụng chạy trên nó. Bộ biên dịch JIT là cốt yếu đối
với sự thành công của nền Java vì nó tạo ra hiệu năng ứng dụng cao mà không
phải hi sinh tính trung lập nền của các ngôn ngữ máy Java. Trong thập niên vừa
qua và sau này, các kỹ sư biên dịch JIT đã có các bước tiến dài trong việc cải thiện
thông lượng và thời gian chờ đối với ứng dụng Java.
Một ví dụ về sự tối ưu hoá JIT
Một ví dụ tốt về tối ưu hoá JIT là sự chuyên môn hoá về các arraycopy (sao chép
mảng/dãy). Đối với một phương thức thường xuyên được thực hiện, bộ biên dịch

JIT có thể vẽ biên dạng độ dài của một lần gọi arraycopy riêng để xem liệu các độ
dài nhất định là phổ biến nhất hay không. Sau tạo hình lần gọi một lúc, bộ biên
dịch JIT có thể thấy rằng chiều dài gần như lúc nào cũng là 12 byte. Với tri thức
này, JIT có thể tạo ra một đường dẫn vô cùng nhanh cho lần gọi arraycopy mà sao
chép trực tiếp số 12 byte đòi hỏi này theo cách hiệu quả nhất đối với bộ xử lý đích.
JIT chèn vào một vận cản có điều kiện để xem độ dài có phải là 12 hay không, và
nếu như vậy thì việc sao chép đường dẫn-nhanh siêu hiệu quả sẽ được thực hiện.
Nếu độ dài không phải là 12, thì một đường dẫn khác xuất hiện, thực hiện việc sao
chép theo kiểu mặc định, nó có thể liên quan đến việc bổ sung lâu hơn nhiều vì nó
có thể xử lý bất kỳ độ dài mảng nào. Nếu phần lớn các phép toán trong ứng dụng
sử dụng đường dẫn nhanh, thì thời gian chờ hoạt động chung sẽ dựa trên thời gian
mà nó mất để sao chép trực tiếp 12 byte đó. Tuy nhiên bất kỳ phép toán nào mà
đòi hỏi một sự sao chép của một độ dài khác sẽ xuất hiện để được làm chậm lại
liên quan đến việc định thời hoạt động chung.
Đáng tiếc là, các cải tiến như vậy đi kèm các đoạn dừng trong hiệu năng ứng dụng
Java, do bộ biên dịch JIT “ăn cắp” các chu trình từ chương trình ứng dụng để tạo
ra bộ mã được biên dịch (hoặc thậm chí biên dịch lại) cho một phương thức riêng.
Tuỳ thuộc vào kích thước của phương thức mà được biên dịch và mức độ tích cực
mà JIT chọn để biên dịch nó, thời gian biên dịch có thể có biên độ từ dưới 1 milli-
giây đến hơn một giây đối với các phương thức đặc biệt lớn mà được bộ biên dịch
JIT quan sát đang góp phần đáng kể vào thời gian thực hiện của ứng dụng. Tuy
nhiên hoạt động của bộ biên dịch JIT tự nó không phải là nguồn duy nhất của các
biến đổi bất ngờ trong các việc định thời mức ứng dụng. Do các kỹ sư biên dịch
JIT đã hầu như chỉ tập trung vào hiệu năng ca trung bình (average-case
performance) để cải thiện hiệu năng thông lượng và thời gian chờ một cách hiệu
quả nhất, các bộ biên dịch JIT thường thực hiện một loạt các tối ưu hoá mà
“thường” là đúng hoặc “chủ yếu” là hiệu năng cao. Trong trường hợp chung, các
tối ưu hoá này vô cùng hiệu quả, và kinh nghiệm được phát triển, thực hiện một
công việc ráp nối sự tối ưu hoá khá tốt với các tình huống phổ biến nhất khi một
ứng dụng đang chạy. Tuy nhiên, trong một số trường hợp thì các tối ưu hoá như

vậy có thể đưa ra nhiều mức thay đổi hiệu năng.
Ngoài việc nạp trước tất cả các lớp, bạn cũng có thể yêu cầu bộ biên dịch JIT biên
dịch hiện các phương thức của các lớp đó khi khởi tạo ứng dụng. Liệt kê 4 mở
rộng bộ mã nạp trước lớp trong Liệt kê 2 để điều khiển việc biên dịch phương
thức:

Liệt kê 4. Biên dịch phương thức được điều khiển

Iterator<String> classIt = listOfClassNamesToLoad.iterator();
while (classIt.hasNext()) {
String className = classIt.next();
try {
Class clazz = Class.forName(className);
String n = clazz.name();
java.lang.Compiler.compileClass(clazz);
} catch (Exception e) {
System.err.println("Could not load class: " + className);
System.err.println(e);
}
}
java.lang.Compiler.disable(); // optional


Bộ mã này làm cho một tập hợp các lớp được nạp và các phương thức của tất cả
các lớp được biên dịch bởi bộ biên dịch JIT. Dòng cuối cùng vô hiệu hoá bộ biên
dịch JIT đối với phần còn lại của khai thác ứng dụng.
Cách tiếp cận này nói chung làm cho thông lượng tổng thể hoặc hiệu năng thời
gian chờ thấp hơn so với việc bộ biên dịch JIT hoàn toàn tự do chọn ra các phương
thức sẽ được biên dịch. Do các phương thức không được gọi trước khi bộ biên
dịch JIT chạy — bộ biên dịch JIT có ít thông tin hơn nhiều về mức độ tối ưu hóa

các phương thức mà nó biên dịch; nên mong chờ các phương thức này thực hiện
chậm hơn. Ngoài ra, vì bộ biên dịch bị vô hiệu hóa, không phương thức nào sẽ
được biên dịch lại ngay cả khi chúng chịu trách nhiệm về phần lớn thời gian khai
thác của chương trình, nên các khung làm việc biên dịch JIT có khả năng thích
nghi như các khung được sử dụng trong hầu hết các JVM hiện đại nhất sẽ không
hoạt động. Lệnh Compiler.disable() không hoàn toàn cần thiết để giảm bớt một số
lớn các đoạn dừng gây-ra-bởi-bộ-biên-dịch-JIT, nhưng các đoạn dừng vẫn còn sẽ
vẫn cần dịch trên các phương thức nóng của ứng dụng, thường đòi hỏi các thời
gian biên dịch lâu hơn với tác động tiềm tàng cao hơn lên các định thời ứng dụng.
Bộ biên dịch JIT trong một JVM riêng không thể được bỏ nạp khi phương thức
disable() được gọi, nên có thể vẫn còn bộ nhớ được tiêu thụ, các thư viện được
chia sẻ được nạp, và các tạo tác khác của bộ biên dịch JIT có mặt trong giai đoạn
thời gian chạy của chương trình ứng dụng.
Mức độ tác động của biên dịch mã riêng đến hiệu năng của ứng dụng thay đổi theo
ứng dụng. Cách tiếp cận tốt nhất của bạn để xem việc biên dịch có thể là vấn đề
hay không là bật đầu ra rườm rà lên, cho biết khi nào các biên dịch xảy ra để xem
chúng có thể ảnh hưởng đến các định thời ứng dụng của bạn hay không. Ví dụ, với
JVM Thời gian Thực WebSphere của IBM, bạn có thể bật ghi nhật ký rườm rà JIT
với dòng lệnh tuỳ chọn -Xjit:verbose.
Ngoài cách tiếp cận nạp trước và biên tập sớm này, không có cách nào một người
viết ứng dụng có thể tránh các đoạn dừng do bộ biên dịch JIT tạo nên, làm ngắn
việc dùng tuỳ chọn dòng lệnh bộ biên dịch JIT dành riêng cho nhà cung cấp bên
ngoài — một cách tiếp cận rủi ro. Các nhà cung cấp JVM hiếm khi hỗ trợ các tuỳ
chọn này trong các kịch bản sản xuất. Vì chúng không phải là các cấu hình mặc
định, chúng được kiểm thử kém cẩn thận hơn bởi các nhà cung cấp, và chúng có
thể thay đổi về cả tên và ý nghĩa từ bản phát hành này đến bản tiếp theo.
Tuy nhiên, một số JVM thay thế có thể cung cấp một vài tuỳ chọn cho bạn, tuỳ
thuộc vào mức độ quan trọng các đoạn dừng gây-ra-bởi-bộ-biên-dịch-JIT cho bạn.
Các JVM thời gian thực được thiết kế để sử dụng trong các hệ thống Java thời
gian thực cứng nhìn chung cung cấp nhiều tuỳ chọn hơn. Ví dụ thời gian Thực

WebSphere của IBM dùng cho JVM Linux® Thời gian Thực có 5 chiến lược biên
dịch-mã sẵn có để dùng với việc thay đổi khả năng giảm bớt các đoạn dừng bộ
biên dịch JIT:
 Biên dịch JIT mặc định, nhờ đó xử lí bộ biên dịch JIT chạy ở mức ưu tiên
thấp.
 Biên dịch JIT mặc định ở mức ưu tiên thấp với bộ mã được biên dịch Đi-
trước-thời-gian (Ahead-of-time - AOT) được dùng khởi đầu.
 Dịch điều khiển theo chương trình (Program-controlled compilation) vào
lúc khởi động với việc biên tập lại được kích hoạt.
 Dịch điều khiển theo chương trình vào lúc khởi động với việc biên dịch lại
bị vô hiệu hoá.
 Chỉ bộ mã được-AOT-biên dịch.
Các tuỳ chọn này được lên danh sách nói chung theo thứ tự giảm dần của mức
được mong đợi về hiệu năng thông lượng/thời gian chờ và số lần dừng dự kiến.
Như vậy tuỳ chọn biên dịch JIT mặc định, sử dụng một xử lí biên dịch JIT chạy ở
mức ưu tiên thấp nhất (mà có thể thấp hơn các xử lí ứng dụng), cung cấp hiệu
năng thông lượng mong đợi cao nhất nhưng cũng được mong đợi thể hiện các
đoạn dừng lớn nhất do biên dịch JIT (của 5 tuỳ chọn này.) Hai tuỳ chọn đầu tiên
sử dụng biên dịch không đồng bộ, nghĩa là một xử lí ứng dụng mà cố gắng gọi một
phương thức mà đã được chọn để biên dịch (lại) không cần đợi đến khi việc biên
dịch hoàn tất. Tuỳ chọn cuối cùng có hiệu năng thông lượng/thời gian chờ mong
đợi nhưng không có đoạn dừng nào từ bộ biên dịch JIT vì bộ biên dịch JIT bị vô
hiệu hoá hoàn toàn theo kịch bản này.
Thời gian Thực WebSphere của IBM dùng cho JVM Linux® Thời gian Thực cung
cấp một công cụ với tên admincache cho phép bạn tạo ra bộ nhớ nhanh cho lớp
dùng chung chứa các tệp lớp từ một tập hợp các tệp JAR và, theo tùy chọn, lưu giữ
bộ mã được-AOT-biên dịch đối với các lớp đó trong cùng một bộ nhớ nhanh. Bạn
có thể thiết đặt một tuỳ chọn trong dòng lệnh java của bạn mà làm cho các lớp
được lưu lại trong bộ nhớ nhanh lớp được chia sẻ sẽ được nạp từ bộ nhớ nhanh và
mã AOT sẽ tự động được nạp vào JVM khi lớp được nạp. Một vòng lặp nạp trước

lớp như vòng lặp trong Liệt kê 2 là tất cả các thứ được yêu cầu để đảm bảo cho
bạn nhận được lợi ích đầy đủ của bộ mã được-AOT-biên dịch. Xem phần Tài
nguyên để có một liên kết đến tư liệu admincache.
Quản lý các xử lí
Việc điều khiển khai thác của các xử lí trong một ứng dụng đa xử lí chẳng hạn
máy chủ giao dịch là cốt yếu đối với việc loại bỏ độ đa dạng trong các lần giao
dịch. Mặc dù ngôn ngữ lập trình Java định nghĩa một mô hình xử lí, có ý niệm về
các ưu tiên xử lí, hành vi của các xử lí trong một JVM thực phần lớn được định
nghĩa bằng sự cài đặt với một số nguyên tắc mà một chương trình Java có thể dựa
vào. Thí dụ, mặc dù các xử lí Java có thể được chỉ định 1 trong số 10 ưu tiên xử lí,
việc ánh xạ của các quyền ưu tiên mức-ứng-dụng đó cho các giá trị ưu tiên hệ điều
hành được định nghĩa bằng cài đặt. (Hoàn toàn hợp lệ đối với một JVM khi ánh xạ
tất cả các ưu tiên xử lí Java lên cùng một giá trị ưu tiên hệ điều hành.) Chuẩn bị
cho điều đó, chính sách lên lịch cho các xử lí Java cũng được định nghĩa bằng cài
đặt nhưng thường kết thúc việc bị chia cắt thời gian để ngay cả các xử lí ưu-tiên-
cao kết thúc việc chia sẻ các tài nguyên bộ xử lý trung tâm với các xử lí ưu-tiên-
thấp-hơn. Việc chia sẻ các tài nguyên với các xử lí ưu-tiên-thấp-hơn có thể làm
cho các xử lí ưu-tiên-cao phải trễ khi chúng được lên lịch sao cho các tác vụ khác
có thể nhận được một lát cắt thời gian. Hãy nhớ rằng khối lượng bộ xử lý trung
tâm mà một xử lí trở nên phụ thuộc không những về quyền ưu tiên mà còn về tổng
số các xử lí mà cần được lên lịch. Nếu bạn không thể kiểm soát nghiêm ngặt có
bao nhiêu xử lí là hoạt động vào bất kỳ thời gian nào cho trước, thì thời gian nó sử
dụng ngay cả các xử lí ưu-tiên-cao-nhất của bạn để thực hiện một phép toán có lẽ
rơi vào một phạm vi tương đối lớn.
Như vậy ngay cả khi bạn quy định quyền ưu tiên xử lí Java cao nhất
(java.lang.Thread.MAX_PRIORITY) cho các xử lí lao động của bạn, nó có lẽ
không đảm bảo nhiều cô lập với các tác vụ ưu-tiên–thấp-hơn trên hệ thống. Đáng
tiếc là, trừ việc sử dụng một tập hợp cố định các xử lí làm việc (đừng tiếp tục phân
bổ các xử lí mới khi dựa vào GC để thu thập các xử lí chưa dùng đến, hoặc phát
triển lên và thu nhỏ lại bộ trữ xử lí của bạn) và cố gắng giảm thiểu số lượng các

hoạt động ưu-tiên-thấp-hơn trên hệ thống trong lúc ứng dụng của bạn chạy, có lẽ
bạn không thể làm được gì nhiều hơn vì mô hình xử lí Java chuẩn không cung cấp
các công cụ cần thiết để điều khiển hành vi xử lí. Ngay cả một JVM thời gian thực
mềm, nếu nó dựa trên mô hình xử lí Java chuẩn, thường không thể cung cấp được
nhiều trợ giúp ở đây.
Một JVM thời gian thực cứng mà hỗ trợ Đặc tả Thời gian Thực cho Java (RTSJ),
tuy nhiên — chẳng hạn như Thời gian Thực WebSphere của IBM dùng cho
Linux® Thời gian Thực V2.0 hoặc RTS 2 của Sun — có thể cung cấp một hành vi
xử lí được cải thiện một cách rõ rệt trên Java chuẩn. Trong số các cải tiến của nó
về ngôn ngữ Java chuẩn và các đặc tả VM, RTSJ đưa ra hai kiểu xử lí mới,
RealtimeThread và NoHeapRealtimeThread, nó được định nghĩa nghiêm túc hơn
nhiều so với mô hình xử lí Java chuẩn. Các loại xử lí này đưa ra việc lên lịch dựa
trên quyền ưu tiên trước: Nếu một tác vụ ưu-tiên-cao cần thực hiện và một tác vụ
ưu-tiên-thấp-hơn hiện thời được lên lịch trên một lõi bộ xử lý, thì tác vụ ưu-tiên-
thấp-hơn có quyền để tác vụ ưu-tiên-cao có thể thực hiện.
Phần lớn các hệ điều hành thời gian thực có thể thực hiện quyền ưu tiên trước này
trên thứ tự hàng chục micro-giây, nó chỉ ảnh hưởng đến các ứng dụng với các yêu
cầu định thời vô cùng nhạy. Cả hai kiểu xử lí mới cũng thường sử dụng một chính
sách lập lịch biểu FIFO (vào trước, ra trước) chứ không phải kiểu lập lịch biểu
luân chuyển quen thuộc (round-robin scheduling) do các JVM sử dụng chạy trên
hầu hết các hệ điều hành. Sự khác biệt rõ nhất giữa chính sách lập lịch biểu luân
chuyển và chính sách lập lịch biểu FIFO là ở chỗ, trong số các xử lí của cùng
quyền ưu tiên, một khi được lên lịch biểu một xử lí tiếp tục thực hiện cho đến khi
nó chặn lại hoặc tự nguyện giải phóng bộ xử lý. Ưu điểm của mô hình này là thời
gian để thực hiện một tác vụ riêng có thể đoán trước được nhiều hơn do bộ xử lý
không bị chia sẻ, ngay cả khi có một số tác vụ với cùng quyền ưu tiên. Chuẩn bị
cho điều đó, nếu bạn giữ xử lí đó tránh bị khóa nhờ xóa bỏ đồng bộ và hoạt động
nhập/xuất, hệ điều hành sẽ không can thiệp với tác vụ khi nó khởi động. Tuy
nhiên, Trên thực tế, việc loại bỏ tất cả các đồng bộ hoá là vô cùng khó khăn, nên
có thể khó đạt được lý tưởng này đối với các tác vụ thực tế. Dù sao thì việc lên

lịch biểu FIFO cũng đưa ra một sự trợ giúp quan trọng cho một người thiết kế ứng
dụng cố gắng để khắc phục trễ.
Bạn có thể nghĩ tới RTSJ như một chiếc hộp lớn đựng các công cụ mà có thể giúp
bạn thiết kế các ứng dụng với hành vi thời gian thực; có thể chỉ sử dụng một vài
công cụ hoặc có thể viết lại hoàn toàn ứng dụng của bạn để đảm bảo hiệu năng dự
tính trước. Thường không khó sửa đổi ứng dụng của bạn để sử dụng các
RealtimeThread (xử lí Thời gian thực), và bạn có thể thực hiện điều đó thậm chí
không cần có quyền truy cập đến một JVM thời gian thực để biên dịch mã Java
của bạn, thông qua việc sử dụng cẩn thận các dịch vụ phản chiếu Java.
Việc tận dụng các lợi ích thay đổi của việc lập lịch biểu FIFO, tất nhiên có thể đòi
hỏi thay đổi nhiều đối với ứng dụng của bạn. Việc lập lịch biểu FIFO đối xử khác
nhau từ việc lập lịch biểu luân chuyển, và các sự khác biệt có thể làm treo máy
trong một số chương trình Java. Thí dụ, nếu ứng dụng của bạn dựa vào
Thread.yield() để cho phép các xử lí khác chạy trên một lõi — một kỹ thuật
thường sử dụng để thăm dò đối với một số điều kiện mà không cần sử dụng một
lõi đầy đủ để làm điều nó — thì hiệu quả mong đợi sẽ không xảy ra, vì với việc
lập lịch biểu, Thread.yield() không ngăn chặn xử lí hiện tại. Vì xử lí hiện tại bảo
trì được tính có thể lập lịch biểu và nó đã là xử lí từ trước tại phía trước của hàng
đợi lập lịch biểu trong lõi hệ điều hành, nó sẽ chỉ cần tiếp tục thực hiện. Như vậy
một mô hình mã hoá dự định để cung cấp quyền truy cập công bằng đến các tài
nguyên bộ xử lý trung tâm khi đợi một điều kiện để trở thành hiện thực trên thực
tế tiêu thụ 100% bất kỳ lõi bộ xử lý trung tâm nào mà nó ngẫu nhiên bắt đầu chạy
trên đó. Và đó là kết quả khả dĩ tốt nhất. Nếu xử lí cần thiết lập điều kiện với
quyền ưu tiên thấp hơn, thì nó không bao giờ có thể nhận được quyền truy cập đến
một lõi để thiết lập điều kiện. Với các bộ xử lý nhiều lõi ngày nay, vấn đề này có
lẽ ít khả năng xảy ra, nhưng nó nhấn mạnh rằng bạn cần phải suy nghĩ cẩn thận về
việc bạn sử dụng quyền ưu tiên nào nếu bạn sử dụng các RealtimeThread. Cách
tiếp cận an toàn nhất là làm cho tất cả các xử lí sử dụng một giá trị ưu tiên đơn lẻ
và loại bỏ việc sử dụng Thread.yield() và các loại vòng lặp quay vòng khác mà sẽ
tiêu thụ hết một bộ xử lý trung tâm vì chúng không bao giờ ngăn chặn. Dĩ nhiên,

việc tận dụng ưu điểm của các giá trị ưu tiên sẵn có cho các RealtimeThread sẽ
cho bạn cơ hội tốt nhất đáp ứng các mục tiêu chất lượng dịch vụ của bạn. (Để có
nhiều mách nước hơn về việc sử dụng các RealtimeThread trong ứng dụng của
bạn, xin xem "Java thời gian thực, Phần 3: Xử lí và đồng bộ hoá.")


Một thí dụ về máy chủ Java
Trong phần còn lại của bài này, chúng ta sẽ áp dụng một số ý tưởng đưa ra trong
các mục trước đây vào một ứng dụng máy chủ Java tương đối đơn giản được xây
dựng nên bằng cách sử dụng dịch vụ Executors (Các bộ khai thác) trong Thư viện
Lớp Java. Chỉ với một lượng nhỏ bé của mã ứng dụng, dịch vụ Executors cho
phép bạn tạo ra một máy chủ quản lý vùng đệm cho các xử lí làm việc, như trong
Liệt kê 5:

Liệt kê 5. Các lớp Server và TaskHandler sử dụng dịch vụ Executors

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadFactory;

class Server {
private ExecutorService threadPool;
Server(int numThreads) {
ThreadFactory theFactory = new ThreadFactory();
this.threadPool = Executors.newFixedThreadPool(numThreads, theFactory);
}

public void start() {
while (true) {
// main server handling loop, find a task to do

// create a "TaskHandler" object to complete this operation
TaskHandler task = new TaskHandler();
this.threadPool.execute(task);
}
this.threadPool.shutdown();
}

public static void main(String[] args) {
int serverThreads = Integer.parseInt(args[0]);
Server theServer = new Server(serverThreads);
theServer.start();
}
}

class TaskHandler extends Runnable {
public void run() {
// code to handle a "task"
}
}

Máy chủ này tạo ra nhiều nhất các xử lí làm việc cần có, đến mức quy định tối đa
khi máy chủ được tạo ra (được giải mã từ dòng lệnh trong thí dụ đặc biệt này).
Mỗi xử lí làm việc thực hiện một ít công việc bằng cách sử dụng lớp TaskHandler
(Bộ xử lý Tác vụ). Theo mục đích của chúng ta, sẽ tạo ra một phương thức
TaskHandler.run(), cần đến một khoảng thời gian tương tự mỗi khi nó chạy. Vậy
nên bất kỳ sự biến đổi nào vào lúc đo để thực hiện TaskHandler.run(), phụ thuộc
vào các đoạn ngừng hoặc biến đổi trong JVM ẩn, một vấn đề xử lí nào đó, hoặc
đoạn ngừng do mức thấp hơn của vùng nhớ dự trữ yêu cầu. Liệt kê 6 hiển thị lớp
TaskHandler:


Liệt kê 6. Lớp TaskHandler với hiệu năng có thể đoán trước được

import java.lang.Runnable;
class TaskHandler implements Runnable {
static public int N=50000;
static public int M=100;
static long result=0L;

// constant work per transaction
public void run() {
long dispatchTime = System.nanoTime();
long x=0L;
for (int j=0;j < M;j++) {
for (int i=0;i < N;i++) {
x = x + i;
}
}
result = x;
long endTime = System.nanoTime();
Server.reportTiming(dispatchTime, endTime);
}
}

Các vòng lặp trong phương thức run() này tính toán M (100) lần tổng của các số
nguyên N đầu tiên (50,000). Các giá trị của M và N được chọn sao cho số lần giao
dịch trên máy mà chúng ta chạy nó trên khoảng 10 mili-giây đo được để một phép
toán đơn lẻ có thể được ngắt bởi một lượng tử lập lịch biểu (mà thường kéo dài
khoảng 10 mili-giây). Chúng ta xây dựng nên các vòng lặp trong tính toán này sao
cho một bộ biên dịch JIT có thể tạo ra bộ mã tuyệt hảo, thực hiện trong một
khoảng thời gian có thể dự đoán được: Phương thức run() không ngăn chặn rõ rệt

giữa hai lần gọi đến System.nanoTime() dùng để tính thời gian cho các vòng lặp
chạy. Do bộ mã được đo là rất dễ đoán trước được, chúng ta có thể sử dụng nó để
hiển thị các tài nguyên quan trọng của các trễ và thay đổi không nhất thiết phải bắt
nguồn từ mã bạn đang đo.
Chúng ta hãy làm cho ứng dụng này một chút hiện thực hơn bằng cách buộc hệ
thống con bộ thu gom rác được hoạt động khi mã TaskHandler đang chạy. Liệt kê
7 hiển thị lớp GCStressThread này:

Liệt kê 7. Lớp GCStressThread để tạo ra rác liên tục

class GCStressThread extends Thread {
HashMap<Integer,BinaryTree> map;
volatile boolean stop = false;

class BinaryTree {
public BinaryTree left;
public BinaryTree right;
public Long value;
}
private void allocateSomeData(boolean useSleep) {
try {
for (int i=0;i < 125;i++) {
if (useSleep)
Thread.sleep(100);
BinaryTree newTree = createNewTree(15); // create full 15-level
BinaryTree
this.map.put(new Integer(i), newTree);
}
} catch (InterruptedException e) {
stop = true;

}
}

public void initialize() {
this.map = new HashMap<Integer,BinaryTree>();
allocateSomeData(false);
System.out.println("\nFinished initializing\n");
}

public void run() {
while (!stop) {
allocateSomeData(true);
}
}
}

GCStressThread bảo trì một tập hợp các BinaryTree qua một HashMap. Nó lặp lại
trên cùng một tập hợp các khoá Integer (số nguyên) để HashMap lưu lại các cấu
trúc mới BinaryTree, mà chỉ cần được đưa vào đủ BinaryTrees 15-mức. (Như vậy
có 215 = 32.768 nút trong mỗi BinaryTree được lưu lại vào HashMap.) HashMap
giữ 125 BinaryTree vào bất cứ một lần nào (dữ liệu sống), và cứ 100 mili-giây nó
thay thế một trong số chúng với một BinaryTree mới. Với cách này, cấu trúc dữ
liệu bảo trì một tập hợp khá phức tạp các đối tượng sống cũng như tạo ra rác với
một tốc độ riêng. HashMap đầu tiên được khởi tạo với một tập hợp 125
BinaryTree bằng cách sử dụng chương trình con initialize(), nó không làm phiền
đến việc đoạn dừng giữa các cấp phát của từng cây. Khi GCStressThread đã được
khởi động (ngay trước khi máy chủ được khởi động) nó hoạt động xuyên qua việc
xử lý của các phép toán TaskHandler của các xử lí làm việc của máy chủ.
Chúng ta sẽ không sử dụng một máy khách để điều khiển máy chủ này. Đơn giản
là ta sẽ tạo NUM_OPERATIONS == 10000 phép toán trực tiếp bên trong vòng lặp

chính của máy chủ (trong phương thức Server.start()). Liệt kê 8 trình bày phương
thức Server.start():

Liệt kê 8. Gửi các phép toán vào trong máy chủ

public void start() {
for (int m=0; m < NUM_OPERATIONS;m++) {
TaskHandler task = new TaskHandler();
threadPool.execute(task);
}
try {
while (!serverShutdown) { // boolean set to true when done
Thread.sleep(1000);
}
}
catch (InterruptedException e) {
}
}

Nếu chúng ta thu thập các thống kê về số lần hoàn thành từng lần dẫn ra
TaskHandler.run()chúng ta có thể thấy mức độ biến đổi được JVM và thiết kế của
ứng dụng đưa vào. Chúng ta đã sử dụng một máy chủ IBM xServer e5440 với 8
lõi vật lý với hệ điều hành thời gian thực Red Hat RHEL MRG (Siêu xử lí bị vô
hiệu hóa). Chú ý rằng mặc dù việc siêu phân luồng có thể cung cấp một số cải tiến
về thông lượng trong một điểm định chuẩn, vì các lõi ảo của nó là không đầy, hiệu
năng lõi vật lý của các phép toán trên các bộ xử lý với siêu phân luồng được kích
hoạt có thể có các định thời khác nhau rõ rệt. Khi chúng ta chạy máy chủ này với 6
xử lí trên máy 8-lõi (chúng ta sẽ thoải mái để lại 1 lõi để xử lí chính Server và 1 lõi
để GCStressorThread sử dụng) với JVM IBM Java6 SR3, chúng ta nhận được các
kết quả (đại diện) sau đây:

$ java -Xms700m -Xmx700m -Xgcpolicy:optthruput Server 6
10000 operations in 16582 ms
Throughput is 603 operations / second
Histogram of operation times:
9ms - 10ms 9942 99 %
10ms - 11ms 2 0 %
11ms - 12ms 32 0 %
30ms - 40ms 4 0 %
70ms - 80ms 1 0 %
200ms - 300ms 6 0 %
400ms - 500ms 6 0 %
500ms - 542ms 6 0 %

Bạn có thể thấy rằng hầu như tất cả các phép toán hoàn tất trong 10 mili-giây,
nhưng một số phép toán mất hơn một nửa giây (chậm hơn 50 lần.) Đó đúng là một
sự khác nhau! Chúng ta hãy xem cách chúng ta có thể loại bỏ một số thay đổi này
bằng cách loại bỏ các trễ xuất hiện do việc nạp lớp Java, Biên dịch mã riêng JIT,
GC, và xử lí.
Đầu tiên chúng ta đã thu thập danh sách các lớp được nạp bởi ứng dụng qua việc
chạy đầy đủ với -verbose:class. Chúng ta đã lưu lại kết quả vào một tệp và sau đó
sửa đổi nó để có một tên được định dạng thích hợp trên mỗi dòng của tệp đó.
Chúng ta đã gộp một phương thức preload() vào lớp Server để nạp các lớp, JIT
biên dịch tất cả các phương thức của các lớp đó, và sau đó vô hiệu hóa bộ biên
dịch JIT, như trình bày trong Liệt kê 9:

Liệt kê 9. Nạp trước các lớp và các phương thức cho máy chủ

private void preload(String classesFileName) {
try {
FileReader fReader = new FileReader(classesFileName);

BufferedReader reader = new BufferedReader(fReader);
String className = reader.readLine();
while (className != null) {
try {
Class clazz = Class.forName(className);
String n = clazz.getName();
Compiler.compileClass(clazz);
} catch (Exception e) {
}
className = reader.readLine();
}
} catch (Exception e) {
}
Compiler.disable();
}

Việc nạp lớp không phải là một vấn đề quan trọng trong máy chủ đơn giản của
chúng ta vì phương thức TaskHandler.run() của chúng ta rất đơn giản: một khi lớp
nào đó được nạp, việc nạp lớp xảy ra không nhiều sau đó trong khai thác của
Server, nó có thể được xác thực bằng cách chạy với -verbose:class. Lợi ích chính
thu được từ việc biên dịch các phương thức trước khi chạy bất kỳ phép toán
TaskHandler được đo. Mặc dù chúng ta đã có thể sử dụng được một vòng lặp khởi
động (warm-up loop), cách tiếp cận này có xu hướng riêng cho JVM vì sự suy
nghiệm mà bộ biên dịch JIT sử dụng để chọn ra các phương thức để biên dịch
khác với các cài đặt JVM. Việc sử dụng dịch vụ Compiler.compile() đưa vào hoạt
động biên dịch có thể điều khiển được nhiều hơn, nhưng như chúng ta đã đề cập
trước đó trong bài viết, chúng ta sẽ chờ đợi sự giảm thông lượng khi dùng tiếp cận
này. Các kết quả từ việc chạy ứng dụng với các tuỳ chọn này là:
$ java -Xms700m -Xmx700m -Xgcpolicy:optthruput Server 6
10000 operations in 20936 ms

Throughput is 477 operations / second
Histogram of operation times:
11ms - 12ms 9509 95 %
12ms - 13ms 478 4 %
13ms - 14ms 1 0 %
400ms - 500ms 6 0 %
500ms - 527ms 6 0 %

Chú ý rằng mặc dù các trễ dài nhất không thay đổi nhiều, biểu đồ này ngắn hơn
nhiều so với ban đầu. Nhiều trễ ngắn hơn thấy được ngay từ bộ biên dịch JIT nên
việc thực hiện các biên dịch trước đó và sau đó vô hiệu hoá bộ biên dịch JIT rõ
ràng là một bước tiến. Một sự nhận xét thú vị khác là số thời gian hoạt động chung
đã chiếm thời gian lâu hơn một chút (từ khoảng 9 đến 10 mili-giây, đến 11-12
mili-giây). Các phép toán đã bị chậm lại do chất lượng của mã được tạo ra bởi một
biên dịch JIT bị áp đặt trước khi gọi các phương thức đó thường thấp hơn các
phương thức của bộ mã được áp dụng đầy đủ. Đó không phải là một kết quả bất
ngờ, vì một trong những lợi thế to lớn của bộ biên dịch JIT là khai thác các đặc
tính động của ứng dụng đang chạy để làm cho nó chạy hiệu quả hơn.
Chúng ta sẽ tiếp tục sử dụng bộ mã nạp trước lớp này và biên dịch trước phương
thức này trong phần còn lại của bài viết.
Vì GCStressThread của chúng ta gây nên thay đổi đều đặn tập hợp dữ liệu sống,
việc sử dụng chính sách GC sản sinh là không mong muốn, để đảm bảo lợi ích
nhiều thời gian đoạn dừng hơn. Thay vào đó, chúng ta đã thử bộ gom rác thời gian
thực trong sản phẩm Thời gian Thực WebSphere của IBM dùng cho Linux Thời
gian Thực V2.0 SR1. Các kết quả ban đầu thật thất vọng, thậm chí sau khi chúng
ta đã bổ sung tuỳ chọn -Xgcthreads8, cho phép bộ gom sử dụng 8 xử lí GC chứ
không phải xử lí đơn lẻ mặc định. (Bộ gom không thể theo kịp một cách tin cậy
tốc độ phân bổ của ứng dụng này với chỉ một xử lí GC đơn lẻ.)
$ java -Xms700m -Xmx700m -Xgcpolicy:metronome -Xgcthreads8 Server 6
10000 operations in 72024 ms

Throughput is 138 operations / second
Histogram of operation times:
11ms - 12ms 82 0 %
12ms - 13ms 250 2 %
13ms - 14ms 19 0 %
14ms - 15ms 50 0 %
15ms - 16ms 339 3 %
16ms - 17ms 889 8 %
17ms - 18ms 730 7 %
18ms - 19ms 411 4 %
19ms - 20ms 287 2 %
20ms - 30ms 1051 10 %
30ms - 40ms 504 5 %
40ms - 50ms 846 8 %
50ms - 60ms 1168 11 %
60ms - 70ms 1434 14 %
70ms - 80ms 980 9 %
80ms - 90ms 349 3 %
90ms - 100ms 28 0 %
100ms - 112ms 7 0 %

×