Việc sử dụng cuối cùng cho các biến trong Java có cải thiện việc thu gom rác không?


86

Hôm nay tôi và các đồng nghiệp thảo luận về việc sử dụng finaltừ khóa trong Java để cải thiện việc thu gom rác.

Ví dụ: nếu bạn viết một phương thức như:

public Double doCalc(final Double value)
{
   final Double maxWeight = 1000.0;
   final Double totalWeight = maxWeight * value;
   return totalWeight;  
}

Khai báo các biến trong phương thức finalsẽ giúp bộ sưu tập dọn dẹp bộ nhớ khỏi các biến không sử dụng trong phương thức sau khi phương thức thoát.

Điều này có đúng không?


thực sự có hai điều ở đây. 1) khi bạn ghi vào một trường cục bộ của phương thức một cá thể. Khi bạn viết thư cho một phiên bản có thể có một lợi ích.
Eugene

Câu trả lời:


86

Đây là một ví dụ hơi khác, một ví dụ có các trường kiểu tham chiếu cuối cùng thay vì các biến cục bộ kiểu giá trị cuối cùng:

public class MyClass {

   public final MyOtherObject obj;

}

Mỗi khi bạn tạo một phiên bản MyClass, bạn sẽ tạo một tham chiếu gửi đi đến một phiên bản MyOtherObject và GC sẽ phải theo liên kết đó để tìm kiếm các đối tượng trực tiếp.

JVM sử dụng thuật toán GC quét đánh dấu, thuật toán này phải kiểm tra tất cả các tham chiếu trực tiếp trong các vị trí "gốc" của GC (giống như tất cả các đối tượng trong ngăn xếp cuộc gọi hiện tại). Mỗi đối tượng sống được "đánh dấu" là còn sống và bất kỳ đối tượng nào được đề cập đến bởi đối tượng sống cũng được đánh dấu là còn sống.

Sau khi hoàn thành giai đoạn đánh dấu, GC quét qua heap, giải phóng bộ nhớ cho tất cả các đối tượng không được đánh dấu (và nén bộ nhớ cho các đối tượng sống còn lại).

Ngoài ra, điều quan trọng là phải nhận ra rằng bộ nhớ heap Java được phân chia thành "thế hệ trẻ" và "thế hệ cũ". Tất cả các đối tượng ban đầu được phân bổ trong thế hệ trẻ (đôi khi được gọi là "vườn ươm"). Vì hầu hết các đối tượng đều tồn tại trong thời gian ngắn, GC tích cực hơn trong việc giải phóng rác thải gần đây khỏi thế hệ trẻ. Nếu một đối tượng tồn tại trong chu kỳ thu thập của thế hệ trẻ, nó sẽ được chuyển sang thế hệ cũ (đôi khi được gọi là "thế hệ có hiệu lực"), được xử lý ít thường xuyên hơn.

Vì vậy, tôi sẽ nói "không, modifer 'cuối cùng' không giúp GC giảm khối lượng công việc của nó".

Theo tôi, chiến lược tốt nhất để tối ưu hóa việc quản lý bộ nhớ của bạn trong Java là loại bỏ các tham chiếu giả càng nhanh càng tốt. Bạn có thể làm điều đó bằng cách gán "null" cho một tham chiếu đối tượng ngay sau khi bạn sử dụng xong.

Hoặc tốt hơn là giảm thiểu kích thước của mỗi phạm vi khai báo. Ví dụ: nếu bạn khai báo một đối tượng ở đầu phương thức 1000 dòng và nếu đối tượng vẫn tồn tại cho đến khi đóng phạm vi của phương thức đó (dấu ngoặc nhọn đóng cuối cùng), thì đối tượng có thể tồn tại lâu hơn nữa. cần thiết.

Nếu bạn sử dụng các phương thức nhỏ, chỉ với một tá dòng mã, thì các đối tượng được khai báo trong phương thức đó sẽ nằm ngoài phạm vi nhanh hơn và GC sẽ có thể thực hiện hầu hết công việc của nó trong phạm vi hiệu quả hơn nhiều thế hệ trẻ. Bạn không muốn các đối tượng được chuyển sang thế hệ cũ trừ khi thực sự cần thiết.


Thức ăn cho suy nghĩ. Tôi luôn nghĩ rằng mã nội tuyến nhanh hơn, nhưng nếu jvm thiếu bộ nhớ thì nó cũng sẽ chậm. hmmmmm ...
WolfmanDragon

1
Tôi chỉ đoán ở đây ... nhưng tôi giả định rằng trình biên dịch JIT có thể nội tuyến các giá trị nguyên thủy cuối cùng (không phải đối tượng), để tăng hiệu suất khiêm tốn. Mặt khác, nội tuyến mã có khả năng tạo ra các tối ưu hóa đáng kể, nhưng không liên quan gì đến các biến cuối cùng.
benjismith

2
Nó sẽ không thể gán null để một đối tượng cuối cùng đã tạo ra, thì có lẽ thức thay vì giúp đỡ, có thể làm cho mọi việc khó khăn hơn
Hernán Eche

1
Người ta cũng có thể sử dụng {} để giới hạn phạm vi trong một phương thức lớn, thay vì chia nhỏ nó thành một số phương thức riêng tư có thể không liên quan đến các phương thức lớp khác.
mmm

Bạn cũng có thể sử dụng các biến cục bộ thay vì các trường khi có thể để tăng cường khả năng thu gom rác bộ nhớ và giảm các ràng buộc tham chiếu.
sivi

37

Khai báo một biến cục bộ finalsẽ không ảnh hưởng đến việc thu gom rác, nó chỉ có nghĩa là bạn không thể sửa đổi biến. Ví dụ của bạn ở trên không nên biên dịch vì bạn đang sửa đổi biến totalWeightđã được đánh dấu final. Mặt khác, khai báo một biến nguyên thủy ( doublethay vì Double) finalsẽ cho phép biến đó được đưa vào mã gọi, do đó có thể gây ra một số cải thiện về bộ nhớ và hiệu suất. Điều này được sử dụng khi bạn có một số public static final Stringstrong một lớp.

Nói chung, trình biên dịch và thời gian chạy sẽ tối ưu hóa ở những nơi nó có thể. Tốt nhất là viết mã phù hợp và không cố gắng quá phức tạp. Sử dụng finalkhi bạn không muốn biến được sửa đổi. Giả sử rằng bất kỳ tối ưu hóa dễ dàng nào sẽ được trình biên dịch thực hiện và nếu bạn lo lắng về hiệu suất hoặc việc sử dụng bộ nhớ, hãy sử dụng trình biên dịch để xác định vấn đề thực sự.


26

Không, nó hoàn toàn không đúng.

Hãy nhớ rằng điều finalđó không có nghĩa là không đổi, nó chỉ có nghĩa là bạn không thể thay đổi tham chiếu.

final MyObject o = new MyObject();
o.setValue("foo"); // Works just fine
o = new MyObject(); // Doesn't work.

Có thể có một số tối ưu hóa nhỏ dựa trên kiến ​​thức mà JVM sẽ không bao giờ phải sửa đổi tham chiếu (chẳng hạn như không phải kiểm tra xem nó đã thay đổi chưa) nhưng nó sẽ rất nhỏ để không phải lo lắng.

Final nên được coi là siêu dữ liệu hữu ích đối với nhà phát triển chứ không phải là tối ưu hóa trình biên dịch.


17

Một số điểm cần làm rõ:

  • Việc bỏ tham chiếu không giúp ích gì cho GC. Nếu đúng như vậy, nó sẽ chỉ ra rằng các biến của bạn đã vượt quá phạm vi. Một ngoại lệ là trường hợp của chủ nghĩa tân quyền đối tượng.

  • Chưa có phân bổ on-stack trong Java.

  • Khai báo một biến cuối cùng có nghĩa là bạn không thể (trong điều kiện bình thường) gán một giá trị mới cho biến đó. Vì cuối cùng không nói gì về phạm vi, nó không nói bất cứ điều gì về ảnh hưởng của nó đối với GC.


Có phân bổ on-stack (các nguyên thủy và tham chiếu đến các đối tượng trong heap) trong java: stackoverflow.com/a/8061692/32453 Java yêu cầu các đối tượng trong cùng một bao đóng như một lớp ẩn danh / lambda cũng phải là cuối cùng, nhưng lần lượt ra đó chỉ là để giảm "bộ nhớ yêu cầu / khung" / giảm sự nhầm lẫn như vậy là không liên quan đến nó được thu thập ...
rogerdpack

11

Tôi không biết về việc sử dụng công cụ sửa đổi "cuối cùng" trong trường hợp này, hoặc ảnh hưởng của nó đối với GC.

Nhưng tôi có thể cho bạn biết điều này: việc bạn sử dụng các giá trị Boxed thay vì nguyên thủy (ví dụ: Double thay vì double) sẽ phân bổ các đối tượng đó trên heap thay vì ngăn xếp và sẽ tạo ra rác không cần thiết mà GC sẽ phải dọn dẹp.

Tôi chỉ sử dụng các bản gốc đóng hộp khi được yêu cầu bởi một API hiện có hoặc khi tôi cần các bản gốc có thể đóng hộp.


1
Bạn đúng rồi. Tôi chỉ cần một ví dụ nhanh để giải thích câu hỏi của tôi.
Goran Martinic

5

Không thể thay đổi các biến cuối cùng sau khi gán ban đầu (được thực thi bởi trình biên dịch).

Điều này không thay đổi hành vi của bộ sưu tập rác như vậy. Chỉ có điều là các biến này không thể bị vô hiệu hóa khi không được sử dụng nữa (điều này có thể giúp ích cho việc gom rác trong các tình huống hạn chế bộ nhớ).

Bạn nên biết rằng cuối cùng cho phép trình biên dịch đưa ra các giả định về những gì cần tối ưu hóa. Mã nội tuyến và không bao gồm mã được biết là không thể truy cập được.

final boolean debug = false;

......

if (debug) {
  System.out.println("DEBUG INFO!");
}

Println sẽ không được bao gồm trong mã byte.


@Eugene Phụ thuộc vào trình quản lý bảo mật của bạn và trình biên dịch có nội tuyến biến hay không.
Thorbjørn Ravn Andersen

đúng, tôi chỉ đang bị lãng phí; chỉ có bấy nhiêu thôi; cũng đã cung cấp một câu trả lời
Eugene

4

Có một trường hợp góc khuất không mấy nổi tiếng với những người thu gom rác nhiều thế hệ. (Để có một mô tả ngắn gọn, hãy đọc câu trả lời của benjismith để có cái nhìn sâu sắc hơn, hãy đọc các bài viết ở cuối).

Ý tưởng trong các Tổng công ty theo thế hệ là phần lớn thời gian chỉ cần xem xét các thế hệ trẻ. Vị trí gốc được quét để tìm các tham chiếu, và sau đó các đối tượng thế hệ trẻ được quét. Trong quá trình quét thường xuyên hơn này, không có đối tượng nào trong thế hệ cũ được kiểm tra.

Bây giờ, vấn đề xuất phát từ thực tế là một đối tượng không được phép có tham chiếu đến các đối tượng trẻ hơn. Khi một đối tượng đã tồn tại lâu (thế hệ cũ) nhận được một tham chiếu đến một đối tượng mới, thì tham chiếu đó phải được bộ thu gom rác theo dõi rõ ràng (xem bài viết của IBM về bộ thu thập JVM điểm phát sóng ), thực sự ảnh hưởng đến hiệu suất GC.

Lý do tại sao một đối tượng cũ không thể tham chiếu đến đối tượng trẻ hơn là vì đối tượng cũ không được kiểm tra trong các tập hợp nhỏ, nếu tham chiếu duy nhất đến đối tượng được giữ trong đối tượng cũ, nó sẽ không được đánh dấu và sẽ bị sai. phân bổ trong giai đoạn quét.

Tất nhiên, như nhiều người đã chỉ ra, từ khóa cuối cùng không thực sự ảnh hưởng đến trình thu gom rác, nhưng nó đảm bảo rằng tham chiếu sẽ không bao giờ bị thay đổi thành đối tượng trẻ hơn nếu đối tượng này tồn tại trong các bộ sưu tập nhỏ và đưa nó vào đống cũ hơn.

Bài viết:

IBM về thu thập rác: lịch sử , trong JVM điểm phát sónghiệu suất . Những điều này có thể không còn hoàn toàn hợp lệ, vì nó có từ năm 2003/04, nhưng chúng cung cấp một số thông tin chi tiết dễ đọc về GC.

Sun on Tuning thu gom rác


3

GC hoạt động trên các ref không thể truy cập. Điều này không liên quan gì đến "cuối cùng", mà chỉ đơn thuần là một xác nhận của việc chỉ định một lần. Có thể là một số GC của VM có thể sử dụng "cuối cùng"? Tôi không hiểu bằng cách nào hoặc tại sao.


3

finaltrên các biến cục bộ và các tham số không tạo ra sự khác biệt nào đối với các tệp lớp được tạo ra, vì vậy không thể ảnh hưởng đến hiệu suất thời gian chạy. Nếu một lớp không có lớp con, HotSpot sẽ xử lý lớp đó như thể nó là lớp cuối cùng (nó có thể hoàn tác sau nếu một lớp phá vỡ giả định đó được tải). Tôi tin rằng finalcác phương thức cũng giống như các lớp. finaltrên trường tĩnh có thể cho phép biến được hiểu là "hằng số thời gian biên dịch" và việc tối ưu hóa sẽ được thực hiện bởi javac trên cơ sở đó. finaltrên các trường cho phép JVM một số quyền tự do bỏ qua các quan hệ xảy ra trước .


2

Có vẻ như có rất nhiều câu trả lời là phỏng đoán lang thang. Sự thật là không có công cụ sửa đổi cuối cùng cho các biến cục bộ ở cấp bytecode. Máy ảo sẽ không bao giờ biết rằng các biến cục bộ của bạn đã được xác định là cuối cùng hay chưa.

Câu trả lời cho câu hỏi của bạn là một không.


Điều đó có thể đúng, nhưng trình biên dịch vẫn có thể sử dụng thông tin cuối cùng trong quá trình phân tích luồng dữ liệu.

@WernerVanBelle trình biên dịch đã biết rằng một biến chỉ được đặt một lần. Nó phải thực hiện phân tích luồng dữ liệu để biết rằng một biến có thể là null, không được khởi tạo trước khi sử dụng, v.v. Vì vậy, local final không cung cấp bất kỳ thông tin mới nào cho trình biên dịch.
Matt Quigley

Nó không. Phân tích luồng dữ liệu có thể suy ra rất nhiều thứ, nhưng có thể có một chương trình hoàn chỉnh bên trong một khối sẽ hoặc sẽ không đặt biến cục bộ. Trình biên dịch không thể biết trước liệu biến sẽ được viết và sẽ không đổi. Do đó, trình biên dịch, không có từ khóa cuối cùng, không thể đảm bảo rằng một biến là cuối cùng hay không.

@WernerVanBelle Tôi thực sự bị thu hút, bạn có thể cho một ví dụ không? Tôi không hiểu làm thế nào có thể có một phép gán cuối cùng cho một biến không phải là biến cuối cùng mà trình biên dịch không biết. Trình biên dịch biết rằng nếu bạn có một biến chưa được khởi tạo, nó sẽ không cho phép bạn sử dụng nó. Nếu bạn cố gắng gán một biến cuối cùng bên trong vòng lặp, trình biên dịch sẽ không cho phép bạn. Ví dụ trong đó biến CÓ THỂ được khai báo là cuối cùng nhưng KHÔNG ĐƯỢC, và trình biên dịch không thể đảm bảo rằng biến đó là biến cuối cùng? Tôi nghi ngờ rằng bất kỳ ví dụ nào sẽ là một biến không thể được khai báo là cuối cùng ngay từ đầu.
Matt Quigley

Ah sau khi đọc lại bình luận của bạn, tôi thấy rằng ví dụ của bạn là một biến được khởi tạo bên trong một khối điều kiện. Các biến đó không thể là biến cuối cùng ngay từ đầu, trừ khi tất cả các đường dẫn điều kiện khởi tạo biến một lần. Vì vậy, trình biên dịch biết về các khai báo như vậy - đó là cách nó cho phép bạn biên dịch một biến cuối cùng được khởi tạo ở hai nơi khác nhau (hãy nghĩ final int x; if (cond) x=1; else x=2;). Do đó, trình biên dịch không có từ khóa cuối cùng có thể đảm bảo rằng một biến là biến cuối cùng hay không.
Matt Quigley

1

Tất cả các phương thức và biến có thể bị mặc định ghi đè trong các lớp con. Nếu chúng ta muốn lưu các lớp con khỏi việc ghi đè các thành viên của lớp cha, chúng ta có thể khai báo chúng là cuối cùng bằng cách sử dụng từ khóa final. Ví dụ, final int a=10; final void display(){......} tạo một phương thức cuối cùng đảm bảo rằng chức năng được định nghĩa trong lớp cha sẽ không bao giờ bị thay đổi. Tương tự, giá trị của một biến cuối cùng không bao giờ có thể thay đổi được. Các biến cuối cùng hoạt động giống như các biến lớp.


1

Nghiêm nói về dụ lĩnh vực, final có thể cải thiện hiệu suất hơi nếu một GC đặc biệt muốn khai thác đó. Khi đồng thời GCxảy ra (điều đó có nghĩa là ứng dụng của bạn vẫn đang chạy, trong khi GC đang được xử lý ), hãy xem điều này để được giải thích rộng hơn , GC phải sử dụng một số rào cản nhất định khi quá trình ghi và / hoặc đọc được thực hiện. Liên kết tôi đã cung cấp cho bạn giải thích khá nhiều về điều đó, nhưng để làm cho nó thực sự ngắn gọn: khi a GCthực hiện một số công việc đồng thời, tất cả đọc và ghi vào heap (trong khi GC đó đang được xử lý), được "chặn" và áp dụng sau đó; để pha GC đồng thời có thể kết thúc công việc của nó.

Đối với các finaltrường ví dụ, vì chúng không thể được sửa đổi (trừ khi phản chiếu), các rào cản này có thể được bỏ qua. Và đây không chỉ là lý thuyết suông.

Shenandoah GCcó chúng trong thực tế (mặc dù không lâu ), và bạn có thể làm, ví dụ:

-XX:+UnlockExperimentalVMOptions  
-XX:+UseShenandoahGC  
-XX:+ShenandoahOptimizeInstanceFinals

sẽ có những tối ưu hóa trong thuật toán GC để làm cho nó nhanh hơn một chút. Điều này là do sẽ không có rào cản nào ngăn cản final, vì không ai nên sửa đổi chúng. Thậm chí không thông qua phản chiếu hoặc JNI.


0

Điều duy nhất mà tôi có thể nghĩ đến là trình biên dịch có thể tối ưu hóa loại bỏ các biến cuối cùng và nội dòng chúng dưới dạng hằng số vào mã, do đó bạn sẽ không được cấp phát bộ nhớ.


0

hoàn toàn, miễn là làm cho tuổi thọ của đối tượng ngắn hơn, mang lại lợi ích lớn của việc quản lý bộ nhớ, gần đây chúng tôi đã kiểm tra chức năng xuất có các biến phiên bản trong một thử nghiệm và một thử nghiệm khác có biến cục bộ cấp phương thức. trong quá trình thử nghiệm tải, JVM ném ra lỗi bộ nhớ trong lần thử nghiệm đầu tiên và JVM bị tạm dừng. nhưng trong thử nghiệm thứ hai, thành công có thể nhận được báo cáo do quản lý bộ nhớ tốt hơn.


0

Lần duy nhất tôi thích khai báo các biến cục bộ là cuối cùng là khi:

  • Tôi phải làm cho chúng cuối cùng để chúng có thể được chia sẻ với một số lớp ẩn danh (ví dụ: tạo luồng daemon và cho phép nó truy cập một số giá trị từ phương thức enclosing)

  • Tôi muốn biến chúng thành cuối cùng (ví dụ: một số giá trị không nên / không bị ghi đè do nhầm lẫn)

Chúng có giúp thu gom rác nhanh chóng không?
AFAIK một đối tượng sẽ trở thành một ứng cử viên của bộ sưu tập GC nếu nó không có tham chiếu mạnh đến nó và trong trường hợp đó cũng không có gì đảm bảo rằng chúng sẽ được thu gom rác ngay lập tức. Nói chung, một tham chiếu mạnh được cho là chết khi nó vượt ra khỏi phạm vi hoặc người dùng gán lại rõ ràng nó thành tham chiếu rỗng, do đó, việc khai báo chúng cuối cùng có nghĩa là tham chiếu sẽ tiếp tục tồn tại cho đến khi phương thức tồn tại (trừ khi phạm vi của nó được thu hẹp rõ ràng xuống một khối bên trong cụ thể {}) vì bạn không thể gán lại các biến cuối cùng (tức là không thể gán lại thành null). Vì vậy, tôi nghĩ rằng wrt Garbage Collection 'cuối cùng' có thể gây ra sự chậm trễ không mong muốn có thể xảy ra, vì vậy người ta phải cẩn thận một chút trong việc xác định phạm vi đó vì điều đó kiểm soát khi nào chúng trở thành ứng cử viên cho GC.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.