text:
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.
Đá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 %
Việc sử dụng thời gian thực bộ gom đã giảm bớt thời gian phép toán tối đa một cách đáng kể, nhưng nó cũng tăng thêm thời gian phép toán kéo dài. Tồi hơn nữa, tốc độ truyền thông giảm đáng kể.
Bước cuối cùng là sử dụng các
RealtimeThread— chứ không phải là các xử lí Java thông thường
— đối với các xử lí làm việc. Chúng ta đã tạo ra một
lớp
RealtimeThreadFactory cho phép chúng ta có
thể cung cấp cho dịch vụ
Executors, như trong
Liệt kê 10:
Liệt kê 10. Lớp
RealtimeThreadFactory
import java.util.concurrent.ThreadFactory; import javax.realtime.PriorityScheduler; import javax.realtime.RealtimeThread; import javax.realtime.Scheduler; import javax.realtime.PriorityParameters; class RealtimeThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { RealtimeThread rtThread = new RealtimeThread(null, null, null, null, null, r); // adjust parameters as needed PriorityParameters pp = (PriorityParameters) rtThread.getSchedulingParameters(); PriorityScheduler scheduler = PriorityScheduler.instance(); pp.setPriority(scheduler.getMaxPriority()); return rtThread; } }
Việc vượt qua một cá thể của lớp
RealtimeThreadFactory đến dịch vụ
Executors.newFixedThreadPool() làm cho các xử lí làm việc là
các
RealtimeThread bằng cách sử dụng việc lập
lịch biểu FIFO với quyền ưu tiên cao nhất sẵn có. Bộ gom rác sẽ vẫn còn
ngắt các xử lí này khi nó cần thực hiện công việc, nhưng không có tác vụ
ưu-tiên-thấp-hơn nào khác sẽ can thiệp với các xử lí làm việc:
$ java -Xms700m -Xmx700m -Xgcpolicy:metronome -Xgcthreads8 Server 6 Handled 10000 operations in 27975 ms Throughput is 357 operations / second Histogram of operation times: 11ms - 12ms 159 1 % 12ms - 13ms 61 0 % 13ms - 14ms 17 0 % 14ms - 15ms 63 0 % 15ms - 16ms 1613 16 % 16ms - 17ms 4249 42 % 17ms - 18ms 2862 28 % 18ms - 19ms 975 9 % 19ms - 20ms 1 0 %
Với thay đổi cuối cùng này, chúng ta cải thiện đáng kể cả thời gian phép toán xấu nhất (xuống chỉ còn 19 mili-giây) cũng như là thông lượng tổng thể (lên đến 357 phép toán mỗi giây). Như vậy chúng ta đã cải thiện đáng kể theo độ đa dạng của các thời gian phép toán, nhưng chúng ta đã trả một giá khá đắt về hiệu năng thông lượng. Phép toán của bộ gom rác, sử dụng đến 3 mili-giây cứ mỗi 10 mili-giây, giải thích tại sao một phép toán mà thường mất khoảng 12 mili-giây có thể được kéo dài đến 4 - 5 mili-giây, đó là lý do số lượng lớn các phép toán bây giờ lại mất khoảng 16 - 17 mili-giây. Sự cắt giảm thông lượng gần như chắc chắn nhiều hơn bạn mong đợi vì JVM thời gian thực, ngoài việc sử dụng bộ gom rác thời gian thực Metronome, cũng đã sửa đổi việc khoá các sơ khởi (primitives) cho phép chống được việc đảo ngược quyền ưu tiên, một vấn đề quan trọng khi việc lập lịch biểu FIFO được dùng (xem "Java thời gian thực, Phần 1: Sử dụng mã Java để lập trình các hệ thống thời gian thực"). Đáng tiếc là, việc đồng bộ hoá giữa xử lí chủ (master thread) và các xử lí làm việc tạo nên nhiều gia tăng ảnh hưởng về thông lượng, mặc dù nó không được đo đạc như là một bộ phận của bất kỳ thời gian phép toán (nên nó không thể hiện trong biểu đồ).
Như vậy khi máy chủ của chúng ta được lợi từ các sửa đổi được thực hiện để
cải thiện khả năng dự đoán, chắc chắn nó trải qua một sự giảm thông lượng
khá lớn. Dù sao, nếu vài phép toán chạy lâu khó tin nổi, ứng với mức độ
chất lượng không thể chấp nhận được, thì sử dụng các
RealtimeThread với một JVM thời gian thực có thể chính là giải
pháp đúng.
Tóm lược
Trong thế giới của các ứng dụng Java, thông lượng và thời gian chờ theo truyền thống đã trở thành các độ đo được ứng dụng chọn và các nhà thiết kế băng chuẩn để báo cáo và tối ưu hoá. Lựa chọn này đã có một tác động rộng khắp lên sự tiến hoá của các thời gian chạy Java được xây dựng nên để cải thiện hiệu năng. Mặc dù các thời gian chạy Java được khởi động như là các phiên dịch viên với thời gian chờ chạy và thông lượng vô cùng chậm, các JVM hiện đại có thể cạnh tranh tốt với các ngôn ngữ khác về các thước đo này đối với nhiều ứng dụng. Mặc dù cho đến thời kỳ tương đối gần đây cũng không thể được nói giống như vậy về một số thước đo khác mà có thể có một ảnh hưởng lớn về một hiệu năng nhận biết của ứng dụng — nhất là độ đa dạng, mà ảnh hưởng đến chất lượng dịch vụ.
Việc đưa ra Java thời gian thực đã cho các nhà thiết kế ứng dụng các công cụ mà họ cần phải nhắm đến nguồn thay đổi trong một JVM và trong các ứng dụng của họ để cung cấp chất lượng dịch vụ mà các khách hàng và tác nhân tiêu thụ của họ chờ đợi. Bài viết này đã giới thiệu một số kỹ thuật mà bạn có thể sử dụng để sửa đổi một ứng dụng Java để giảm bớt các đoạn dừng và sự thay đổi mà bắt nguồn từ JVM và từ việc lập lịch biểu xử lí. Việc giảm bớt sự thay đổi thường gây ra việc cắt bớt về thời gian chờ và hiệu năng thông lượng. Mức độ theo đó việc cắt bỏ có thể chấp nhận được xác định các công cụ nào là thích hợp cho một ứng dụng đặc thù.
Tài nguyên
Học tập
- Phát triển với Java thời gian thực (developerworks, 9.2009): Đọc đầy đủ loạt bài.
- Real-time Java: Đọc loạt bài developerWorks sáu phần này về Java thời gian thực.
- Đặc tả Thời gian thực dùng cho Java: Bạn sẽ tìm thấy RTSJ tại trang web Quy trình Cộng đồng Java (Java Community Process).
- Sử dụng công cụ admincache: Tìm hiểu nhiều hơn về Thời gian Thực
WebSphere của IBM dùng cho công cụ
admincachecủa sản phẩm Linux RT.
- Hiệu sách công nghệ: Duyệt tìm các sách về chủ đề này và các chủ đề kỹ thuật khác.
- Vùng công nghệ Java developerWorks: Tìm hàng trăm bài viết về các khía cạnh về lập trình Java.
Lấy sản phẩm và công nghệ
- Thời gian Thực WebSphere của IBM: Tải về một sản phẩm dùng thử.
- Bộ Tối ưu hoá Khai thác Ứng dụng Thời gian Thực của IBM dùng cho Java (IBM Real Time Application Execution Optimizer for Java): Tối ưu hóa và xác thực ứng dụng Java thời gian thực của bạn đối với các môi trường chuyên môn hoá bằng công cụ này.
- Công cụ Phân tích Lớp Thời gian Thực của IBM dùng cho Java: Công cụ này cho phép ứng dụng của bạn nạp trước các lớp mà không đòi hỏi bạn phải định nghĩa danh sách lớp theo cách thủ công.
Thảo luận
- Tham gia vào cộng đồng My developerWorks.
Bình luận
-
Vùng dữ liệu trên developerWorks
Khám phá các bài viết xoay quanh lĩnh vực dữ liệu, Quản lý hệ thống thông tin
-
Vùng mã nguồn mở trên developerWorks
Xem các bài viết về HTML, CSS, JavaScript,...
-
Điện toán đám mây
Tìm hiểu về điện toán đám mây

markdown:
*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](http://www.ibm.com/developerworks/java/library/j-rtj4/index.html?S_TACT=105AGY75)" để đượ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](#listing2), 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](#resources)).
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.
Đá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](#listing2) để đ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](#listing2) 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](#resources) để 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á](http://www.ibm.com/developerworks/java/library/j-rtj3/index.html?S_TACT=105AGY75).")
## 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 %
Việc sử dụng thời gian thực bộ gom đã giảm bớt thời gian phép toán tối đa một cách đáng kể, nhưng nó cũng tăng thêm thời gian phép toán kéo dài. Tồi hơn nữa, tốc độ truyền thông giảm đáng kể.
Bước cuối cùng là sử dụng các
RealtimeThread— chứ không phải là các xử lí Java thông thường
— đối với các xử lí làm việc. Chúng ta đã tạo ra một
lớp
RealtimeThreadFactory cho phép chúng ta có
thể cung cấp cho dịch vụ
Executors, như trong
Liệt kê 10:
##### Liệt kê 10. Lớp
RealtimeThreadFactory
import java.util.concurrent.ThreadFactory; import javax.realtime.PriorityScheduler; import javax.realtime.RealtimeThread; import javax.realtime.Scheduler; import javax.realtime.PriorityParameters; class RealtimeThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { RealtimeThread rtThread = new RealtimeThread(null, null, null, null, null, r); // adjust parameters as needed PriorityParameters pp = (PriorityParameters) rtThread.getSchedulingParameters(); PriorityScheduler scheduler = PriorityScheduler.instance(); pp.setPriority(scheduler.getMaxPriority()); return rtThread; } }
Việc vượt qua một cá thể của lớp
RealtimeThreadFactory đến dịch vụ
Executors.newFixedThreadPool() làm cho các xử lí làm việc là
các
RealtimeThread bằng cách sử dụng việc lập
lịch biểu FIFO với quyền ưu tiên cao nhất sẵn có. Bộ gom rác sẽ vẫn còn
ngắt các xử lí này khi nó cần thực hiện công việc, nhưng không có tác vụ
ưu-tiên-thấp-hơn nào khác sẽ can thiệp với các xử lí làm việc:
$ java -Xms700m -Xmx700m -Xgcpolicy:metronome -Xgcthreads8 Server 6 Handled 10000 operations in 27975 ms Throughput is 357 operations / second Histogram of operation times: 11ms - 12ms 159 1 % 12ms - 13ms 61 0 % 13ms - 14ms 17 0 % 14ms - 15ms 63 0 % 15ms - 16ms 1613 16 % 16ms - 17ms 4249 42 % 17ms - 18ms 2862 28 % 18ms - 19ms 975 9 % 19ms - 20ms 1 0 %
Với thay đổi cuối cùng này, chúng ta cải thiện đáng kể cả thời gian phép
toán xấu nhất (xuống chỉ còn 19 mili-giây) cũng như là thông lượng tổng
thể (lên đến 357 phép toán mỗi giây). Như vậy chúng ta đã cải thiện đáng
kể theo độ đa dạng của các thời gian phép toán, nhưng chúng ta đã trả một
giá khá đắt về hiệu năng thông lượng. Phép toán của bộ gom rác, sử dụng
đến 3 mili-giây cứ mỗi 10 mili-giây, giải thích tại sao một phép toán mà
thường mất khoảng 12 mili-giây có thể được kéo dài đến 4 - 5 mili-giây, đó
là lý do số lượng lớn các phép toán bây giờ lại mất khoảng 16 - 17
mili-giây. Sự cắt giảm thông lượng gần như chắc chắn nhiều hơn bạn mong
đợi vì JVM thời gian thực, ngoài việc sử dụng bộ gom rác thời gian thực
Metronome, cũng đã sửa đổi việc khoá các sơ khởi (primitives) cho phép
chống được việc đảo ngược quyền ưu tiên, một vấn đề quan trọng khi việc
lập lịch biểu FIFO được dùng (xem "
[Java thời gian thực, Phần 1: Sử dụng mã Java để lập trình các hệ
thống thời gian thực](http://www.ibm.com/developerworks/java/library/j-rtj1/index.html?S_TACT=105AGY75)"). Đáng tiếc là, việc đồng bộ hoá giữa xử lí
chủ (master thread) và các xử lí làm việc tạo nên nhiều gia tăng ảnh hưởng
về thông lượng, mặc dù nó không được đo đạc như là một bộ phận của bất kỳ
thời gian phép toán (nên nó không thể hiện trong biểu đồ).
Như vậy khi máy chủ của chúng ta được lợi từ các sửa đổi được thực hiện để
cải thiện khả năng dự đoán, chắc chắn nó trải qua một sự giảm thông lượng
khá lớn. Dù sao, nếu vài phép toán chạy lâu khó tin nổi, ứng với mức độ
chất lượng không thể chấp nhận được, thì sử dụng các
RealtimeThread với một JVM thời gian thực có thể chính là giải
pháp đúng.
## Tóm lược
Trong thế giới của các ứng dụng Java, thông lượng và thời gian chờ theo truyền thống đã trở thành các độ đo được ứng dụng chọn và các nhà thiết kế băng chuẩn để báo cáo và tối ưu hoá. Lựa chọn này đã có một tác động rộng khắp lên sự tiến hoá của các thời gian chạy Java được xây dựng nên để cải thiện hiệu năng. Mặc dù các thời gian chạy Java được khởi động như là các phiên dịch viên với thời gian chờ chạy và thông lượng vô cùng chậm, các JVM hiện đại có thể cạnh tranh tốt với các ngôn ngữ khác về các thước đo này đối với nhiều ứng dụng. Mặc dù cho đến thời kỳ tương đối gần đây cũng không thể được nói giống như vậy về một số thước đo khác mà có thể có một ảnh hưởng lớn về một hiệu năng nhận biết của ứng dụng — nhất là độ đa dạng, mà ảnh hưởng đến chất lượng dịch vụ.
Việc đưa ra Java thời gian thực đã cho các nhà thiết kế ứng dụng các công cụ mà họ cần phải nhắm đến nguồn thay đổi trong một JVM và trong các ứng dụng của họ để cung cấp chất lượng dịch vụ mà các khách hàng và tác nhân tiêu thụ của họ chờ đợi. Bài viết này đã giới thiệu một số kỹ thuật mà bạn có thể sử dụng để sửa đổi một ứng dụng Java để giảm bớt các đoạn dừng và sự thay đổi mà bắt nguồn từ JVM và từ việc lập lịch biểu xử lí. Việc giảm bớt sự thay đổi thường gây ra việc cắt bớt về thời gian chờ và hiệu năng thông lượng. Mức độ theo đó việc cắt bỏ có thể chấp nhận được xác định các công cụ nào là thích hợp cho một ứng dụng đặc thù.
## Tài nguyên
### Học tập
[(developerworks, 9.2009): Đọc đầy đủ loạt bài.](http://www.ibm.com/developerworks/vn/views/java/libraryview.jsp?search_by=Phát+triển+với+Java+thời+gian+thực) *Phát triển với Java thời gian thực* [: Đọc loạt bài developerWorks sáu phần này về Java thời gian thực.](http://www.ibm.com/developerworks/views/java/libraryview.jsp?search_by=Real+time+Java+Part&S_TACT=105AGY75) *Real-time Java* [Đặc tả Thời gian thực dùng cho Java](http://jcp.org/en/jsr/detail?id=1): Bạn sẽ tìm thấy RTSJ tại trang web Quy trình Cộng đồng Java (Java Community Process). [Sử dụng công cụ admincache](http://publib.boulder.ibm.com/infocenter/realtime/v2r0/index.jsp?topic=/com.ibm.rt.doc.20/realtime/admincache_using.html): Tìm hiểu nhiều hơn về Thời gian Thực WebSphere của IBM dùng cho công cụ
admincachecủa sản phẩm Linux RT.
[Hiệu sách công nghệ](http://www.ibm.com/developerworks/apps/SendTo?bookstore=safari&S_TACT=105AGY75): Duyệt tìm các sách về chủ đề này và các chủ đề kỹ thuật khác. [Vùng công nghệ Java developerWorks](http://www.ibm.com/developerworks/java?S_TACT=105AGY75): Tìm hàng trăm bài viết về các khía cạnh về lập trình Java.
### Lấy sản phẩm và công nghệ
[Thời gian Thực WebSphere của IBM](http://www-01.ibm.com/software/webservers/realtime/): Tải về [một sản phẩm dùng thử](https://www14.software.ibm.com/webapp/iwm/web/preLogin.do?source=swg-wsrt). [Bộ Tối ưu hoá Khai thác Ứng dụng Thời gian Thực của IBM dùng cho Java (IBM Real Time Application Execution Optimizer for Java)](http://alphaworks.ibm.com/tech/javaoptimizer): Tối ưu hóa và xác thực ứng dụng Java thời gian thực của bạn đối với các môi trường chuyên môn hoá bằng công cụ này. [Công cụ Phân tích Lớp Thời gian Thực của IBM dùng cho Java](http://alphaworks.ibm.com/tech/ratcat): Công cụ này cho phép ứng dụng của bạn nạp trước các lớp mà không đòi hỏi bạn phải định nghĩa danh sách lớp theo cách thủ công.
### Thảo luận
- Tham gia vào
[cộng đồng My developerWorks](http://www.ibm.com/developerworks/mydeveloperworks?S_TACT=105AGY75).
## Bình luận
[Vùng dữ liệu trên developerWorks](http://www.ibm.com/developerworks/vn/views/data/libraryview.jsp)
Khám phá các bài viết xoay quanh lĩnh vực dữ liệu, Quản lý hệ thống thông tin
[Vùng mã nguồn mở trên developerWorks](http://www.ibm.com/developerworks/vn/views/opensource/libraryview.jsp)
Xem các bài viết về HTML, CSS, JavaScript,...
[Điện toán đám mây](http://www.ibm.com/developerworks/vn/views/cloud/libraryview.jsp)
Tìm hiểu về điện toán đám mây

Content-Language: en-US

Kp-eeAlive: timeout=10, max=90


text:
Phim Online
Phim Bộ
Hồng Kông
Phim Đang Chiếu HOT
Hàn Quốc
Hoạt Hình
Đài Loan
HongKong English Subtitle
Phim Lẻ
Tình Cảm
Võ Thuật Kiếm Hiệp
Ma - Kinh Di
̣
Phim Hài
Hành Động - XHĐ
Phim Âu - Mỹ
Việt Nam
Phim Việt Nam
Hài Kịch
Nhạc Việt Nam
Phóng Sự
Cả Lương
Thể Thao - Sport
New Release
Phim Rất Hay
Mới Cập Nhật
Phim Mới
Đánh Giá Cao
Xem Nhiều
Danh Sach Phim
Forum
Facebook
Neu ban khong vao duoc, xin vui long thu lai sau, chung toi dang co gang khac phuc su co.
Home
-
hai kich
A-Đ
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z
Xếp theo
Last Update
Date - Ngày gửi
Name - Tên Phim
Views - Lượt Xem
Rated - Đánh Giá
thứ tự
Ascending - Tăng Dần
Descending - Giảm Dần
Hiện chưa có phim nào trong category này.
Bạn vui lòng quay lại sau!
Copyright © 2007 - 2012
Kenh88.Com
Phim Online Entertainment Network
Phim88.com
is a portal that made for
Phim
,
Phim Online
,
Xem Phim
, Online Phim and
Xem Phim
Online.
Phim88.Com main focus on phim online,
online phim
,
xem phim online
,
download phim
. Phim tai phim88.com chat luong cao va download truc tiep. Ban co the xem
phim online
hoac download phim toc do cao. Phim88.Com covers
phim viet nam
,
phim hong kong
,
phim han quoc
, phim trung quoc, phim dai loan, phim le,
phim bo
, phim tap, phim tvb.
Phim88.Com
is absolutely legal and contain only links to other sites on the Internet : (youtube.com, dailymotion.com,.. ).Phim88.Com is not responsible for the accuracy, compliance, copyright, legality, decency, or any other aspect of the content of other linked sites.
If you have any legal issues please contact appropriate media file owners / hosters.
DMCA Notification
|
Privacy Policy

WARC-Type: response

Content-Type: application/http; msgtype=response

WARC-Truncated: length

Content-Encoding: gzip

Vary: User-Agent

Connection: close

Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

Pragma: no-cache

Content-Type: text/html