Tại sao phương pháp này in 4?


111

Tôi đã tự hỏi điều gì sẽ xảy ra khi bạn cố gắng bắt lỗi StackOverflowError và tìm ra phương pháp sau:

class RandomNumberGenerator {

    static int cnt = 0;

    public static void main(String[] args) {
        try {
            main(args);
        } catch (StackOverflowError ignore) {
            System.out.println(cnt++);
        }
    }
}

Bây giờ câu hỏi của tôi:

Tại sao phương thức này in ra '4'?

Tôi nghĩ có lẽ là do System.out.println()cần 3 phân đoạn trên ngăn xếp cuộc gọi, nhưng tôi không biết số 3 đến từ đâu. Khi bạn nhìn vào mã nguồn (và bytecode) của System.out.println(), nó thường dẫn đến nhiều lần gọi phương thức hơn 3 (vì vậy 3 phân đoạn trên ngăn xếp cuộc gọi sẽ không đủ). Nếu đó là do tối ưu hóa mà Hotspot VM áp dụng (nội tuyến phương pháp), tôi tự hỏi liệu kết quả có khác trên VM khác không.

Chỉnh sửa :

Vì đầu ra có vẻ là JVM cụ thể cao, tôi nhận được kết quả 4 bằng cách sử dụng
Java (TM) SE Runtime Environment (build 1.6.0_41-b02)
Java HotSpot (TM) 64-Bit Server VM (build 20.14-b01, hỗn hợp)


Giải thích lý do tại sao tôi nghĩ câu hỏi này khác với Hiểu ngăn xếp Java :

Câu hỏi của tôi không phải về lý do tại sao có cnt> 0 (rõ ràng là vì System.out.println()yêu cầu kích thước ngăn xếp và ném một StackOverflowErrorcái khác trước khi thứ gì đó được in), mà tại sao nó có giá trị cụ thể là 4, tương ứng là 0,3,8,55 hoặc một cái gì đó khác các hệ thống.


4
Ở địa phương của tôi, tôi nhận được "0" như mong đợi.
Reddy

2
Điều này có thể liên quan đến rất nhiều công cụ kiến ​​trúc. Vì vậy, tốt hơn gửi đầu ra của bạn với phiên bản jdk Đối với tôi đầu ra là 0 trên jdk 1.7
Lokesh

3
Tôi đã nhận được 5, 638với Java 1.7.0_10
Kon

8
@Elist Sẽ không cùng một đầu ra khi bạn đang làm thủ thuật liên quan đến kiến trúc cơ bản;)
m0skit0

3
@flrnb Đó chỉ là một phong cách tôi sử dụng để xếp hàng niềng răng. Nó giúp tôi dễ dàng biết điều kiện và chức năng bắt đầu và kết thúc ở đâu. Bạn có thể thay đổi nó nếu bạn muốn, nhưng theo tôi nó dễ đọc hơn theo cách này.
syb0rg,

Câu trả lời:


41

Tôi nghĩ những người khác đã làm rất tốt trong việc giải thích tại sao cnt> 0, nhưng không có đủ chi tiết liên quan đến lý do tại sao cnt = 4 và tại sao cnt lại khác nhau rất nhiều giữa các cài đặt khác nhau. Tôi sẽ cố gắng lấp đầy khoảng trống đó ở đây.

Để cho

  • X là tổng kích thước ngăn xếp
  • M là không gian ngăn xếp được sử dụng khi chúng ta nhập chính lần đầu tiên
  • R là không gian ngăn xếp tăng lên mỗi khi chúng ta vào chính
  • P là không gian ngăn xếp cần thiết để chạy System.out.println

Khi chúng ta lần đầu tiên vào main, khoảng trống còn lại là XM. Mỗi cuộc gọi đệ quy chiếm thêm R bộ nhớ. Vì vậy, đối với 1 cuộc gọi đệ quy (nhiều hơn 1 lần so với ban đầu), bộ nhớ sử dụng là M + R. Giả sử rằng StackOverflowError được ném ra sau khi C cuộc gọi đệ quy thành công, nghĩa là M + C * R <= X và M + C * (R + 1)> X. Tại thời điểm xảy ra lỗi StackOverflowError đầu tiên, bộ nhớ X - M - C * R còn lại.

Để có thể chạy System.out.prinln, chúng ta cần P khoảng trống trên ngăn xếp. Nếu điều đó xảy ra mà X - M - C * R> = P, thì 0 sẽ được in ra. Nếu P yêu cầu thêm không gian, thì chúng ta loại bỏ các khung khỏi ngăn xếp, thu được bộ nhớ R với chi phí là cnt ++.

Khi printlncuối cùng có thể chạy, X - M - (C - cnt) * R> = P. Vì vậy, nếu P lớn đối với một hệ thống cụ thể, thì cnt sẽ lớn.

Hãy xem xét điều này với một số ví dụ.

Ví dụ 1: Giả sử

  • X = 100
  • M = 1
  • R = 2
  • P = 1

Khi đó C = sàn ((XM) / R) = 49, và cnt = trần ((P - (X - M - C * R)) / R) = 0.

Ví dụ 2: Giả sử rằng

  • X = 100
  • M = 1
  • R = 5
  • P = 12

Khi đó C = 19 và cnt = 2.

Ví dụ 3: Giả sử rằng

  • X = 101
  • M = 1
  • R = 5
  • P = 12

Khi đó C = 20 và cnt = 3.

Ví dụ 4: Giả sử rằng

  • X = 101
  • M = 2
  • R = 5
  • P = 12

Khi đó C = 19 và cnt = 2.

Do đó, chúng ta thấy rằng cả hệ thống (M, R và P) và kích thước ngăn xếp (X) đều ảnh hưởng đến cnt.

Một lưu ý phụ, không quan trọng cần bao nhiêu dung lượng catchđể bắt đầu. Miễn là không có đủ không gian cho catch, sau đó cnt sẽ không tăng, do đó, không có tác động bên ngoài.

BIÊN TẬP

Tôi rút lại những gì tôi đã nói về catch. Nó đóng một vai trò. Giả sử nó yêu cầu T lượng không gian để bắt đầu. cnt bắt đầu tăng lên khi khoảng trống còn lại lớn hơn T, vàprintln chạy khi không gian còn lại lớn hơn T + P. Điều này bổ sung thêm một bước cho các phép tính và làm xáo trộn thêm phân tích đã có nhiều bùn.

BIÊN TẬP

Cuối cùng tôi đã tìm thấy thời gian để chạy một số thử nghiệm để sao lưu lý thuyết của mình. Thật không may, lý thuyết dường như không phù hợp với các thí nghiệm. Những gì thực sự xảy ra là rất khác nhau.

Thiết lập thử nghiệm: Máy chủ Ubuntu 12.04 với java mặc định và jdk mặc định. Xss bắt đầu từ 70.000 ở mức tăng 1 byte đến 460.000.

Kết quả có sẵn tại: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM Tôi đã tạo một phiên bản khác, nơi mọi điểm dữ liệu lặp lại đều bị xóa. Nói cách khác, chỉ những điểm khác với điểm trước đó mới được hiển thị. Điều này giúp bạn dễ dàng nhìn thấy sự bất thường hơn. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA


Cảm ơn bạn vì bản tóm tắt tốt, tôi đoán tất cả chỉ tập trung vào câu hỏi: điều gì ảnh hưởng đến M, R và P (vì X có thể được đặt bởi VM-option -Xss)?
flrnb

@flrnb M, R và P là các hệ thống cụ thể. Bạn không thể dễ dàng thay đổi chúng. Tôi hy vọng chúng cũng khác nhau giữa một số bản phát hành.
John Tseng

Sau đó, tại sao tôi nhận được các kết quả khác nhau bằng cách thay đổi Xss (hay còn gọi là X)? Thay đổi X từ 100 thành 10000, với điều kiện M, R và P giữ nguyên, sẽ không ảnh hưởng đến cnt theo công thức của bạn, hay tôi nhầm lẫn?
flrnb,

Chỉ riêng @flrnb X thay đổi cnt do bản chất rời rạc của các biến này. Ví dụ 2 và 3 chỉ khác nhau về X, nhưng cnt thì khác.
John Tseng

1
@JohnTseng Tôi cũng coi câu trả lời của bạn là dễ hiểu và đầy đủ nhất cho đến lúc này - dù sao, tôi thực sự quan tâm đến việc ngăn xếp thực sự trông như thế nào vào thời điểmStackOverflowError được ném và điều này ảnh hưởng đến kết quả như thế nào. Nếu nó chỉ chứa một tham chiếu đến một khung ngăn xếp trên heap (như Jay đã đề xuất), thì đầu ra sẽ khá dễ đoán đối với một hệ thống nhất định.
flrnb

20

Đây là nạn nhân của cuộc gọi đệ quy xấu. Khi bạn đang thắc mắc tại sao giá trị của cnt lại khác nhau, đó là vì kích thước ngăn xếp phụ thuộc vào nền tảng. Java SE 6 trên Windows có kích thước ngăn xếp mặc định là 320k trong máy ảo 32 bit và 1024k trong máy ảo 64 bit. Bạn có thể đọc thêm ở đây .

Bạn có thể chạy bằng các kích thước ngăn xếp khác nhau và bạn sẽ thấy các giá trị khác nhau của cnt trước khi ngăn xếp tràn-

java -Xss1024k RandomNumberGenerator

Bạn không thấy giá trị của cnt được in nhiều lần mặc dù đôi khi giá trị lớn hơn 1 vì câu lệnh in của bạn cũng gặp lỗi mà bạn có thể gỡ lỗi để chắc chắn thông qua Eclipse hoặc các IDE khác.

Bạn có thể thay đổi mã thành mã sau để gỡ lỗi cho mỗi lần thực thi câu lệnh nếu bạn muốn-

static int cnt = 0;

public static void main(String[] args) {                  

    try {     

        main(args);   

    } catch (Throwable ignore) {

        cnt++;

        try { 

            System.out.println(cnt);

        } catch (Throwable t) {   

        }        
    }        
}

CẬP NHẬT:

Vì điều này đang được chú ý nhiều hơn, hãy lấy một ví dụ khác để làm rõ ràng hơn mọi thứ-

static int cnt = 0;

public static void overflow(){

    try {     

      overflow();     

    } catch (Throwable t) {

      cnt++;                      

    }

}

public static void main(String[] args) {

    overflow();
    System.out.println(cnt);

}

Chúng tôi đã tạo một phương thức khác có tên là tràn để thực hiện đệ quy không hợp lệ và xóa câu lệnh println khỏi khối catch để nó không bắt đầu tạo ra một tập hợp lỗi khác trong khi cố gắng in. Điều này hoạt động như mong đợi. Bạn có thể thử đặt System.out.println (cnt);câu lệnh sau cnt ++ ở trên và biên dịch. Sau đó chạy nhiều lần. Tùy thuộc vào nền tảng của bạn, bạn có thể nhận được các giá trị cnt khác nhau .

Đây là lý do tại sao nói chung chúng tôi không bắt lỗi vì bí ẩn trong mã không phải là giả tưởng.


13

Hành vi phụ thuộc vào kích thước ngăn xếp (có thể được đặt thủ công bằng cách sử dụng Xss. Kích thước ngăn xếp là kiến ​​trúc cụ thể. Từ mã nguồn JDK 7 :

// Kích thước ngăn xếp mặc định trên Windows được xác định bởi tệp thực thi (java.exe
// có giá trị mặc định là 320K / 1MB [32bit / 64bit]). Tùy thuộc vào phiên bản Windows, việc thay đổi
// ThreadStackSize thành khác 0 có thể có tác động đáng kể đến việc sử dụng bộ nhớ.
// Xem nhận xét trong os_windows.cpp.

Vì vậy, khi StackOverflowErrorném, lỗi sẽ bị mắc vào khối bắt. Đây println()là một lệnh gọi ngăn xếp khác lại ném ngoại lệ. Điều này được lặp lại.

Nó lặp lại bao nhiêu lần? - Nó phụ thuộc vào thời điểm JVM nghĩ rằng nó không còn là stackoverflow nữa. Và điều đó phụ thuộc vào kích thước ngăn xếp của mỗi lệnh gọi hàm (khó tìm) vàXss . Như đã đề cập ở trên tổng kích thước và kích thước mặc định của mỗi lệnh gọi hàm (phụ thuộc vào kích thước trang bộ nhớ, v.v.) là nền tảng cụ thể. Do đó hành vi khác nhau.

Gọi javacuộc gọi với -Xss 4Mcho tôi 41. Do đó, tương quan.


4
Tôi không hiểu tại sao kích thước ngăn xếp lại ảnh hưởng đến kết quả, vì nó đã bị vượt quá khi chúng tôi cố gắng in giá trị của cnt. Do đó, sự khác biệt duy nhất có thể đến từ "kích thước ngăn xếp của mỗi lệnh gọi hàm". Và tôi không hiểu tại sao điều này lại khác nhau giữa 2 máy chạy cùng một phiên bản JVM.
flrnb

Hành vi chính xác chỉ có thể được lấy từ nguồn JVM. Nhưng lý do có thể là điều này. Nhớ chẵn catchlà một khối và chiếm bộ nhớ trên ngăn xếp. Không thể biết được mỗi lần gọi phương thức chiếm bao nhiêu bộ nhớ. Khi ngăn xếp được xóa, bạn đang thêm một khối nữa catchvà như vậy. Đây có thể là hành vi. Đây chỉ là suy đoán.
Jatin

Và kích thước ngăn xếp có thể khác nhau ở hai máy khác nhau. Kích cỡ đống phụ thuộc vào rất nhiều yếu tố os-based cụ thể là bộ nhớ trang có kích thước vv
Jatin

6

Tôi nghĩ rằng số được hiển thị là số lần System.out.printlncuộc gọi ném ra Stackoverflowngoại lệ.

Nó có thể phụ thuộc vào việc thực hiện printlnvà số lượng lệnh gọi xếp chồng mà nó được thực hiện trong đó.

Như một minh họa:

Cuộc main()gọi kích hoạt Stackoverflowngoại lệ ở cuộc gọi i. Lệnh gọi i-1 của main bắt ngoại lệ và gọi lệnh printlnnào kích hoạt giây Stackoverflow. cnttăng lên 1. Cuộc gọi i-2 của main catch bây giờ là ngoại lệ và cuộc gọi println. Trong printlnmột phương thức được gọi là kích hoạt một ngoại lệ thứ 3. cnttăng dần lên 2. điều này tiếp tục cho đến khi printlncó thể thực hiện tất cả các lệnh gọi cần thiết và cuối cùng hiển thị giá trị củacnt .

Sau đó, điều này phụ thuộc vào việc triển khai thực tế println .

Đối với JDK7 hoặc nó phát hiện cuộc gọi quay vòng và ném ngoại lệ sớm hơn hoặc nó giữ một số tài nguyên ngăn xếp và ném ngoại lệ trước khi đạt đến giới hạn để cung cấp một số chỗ cho logic khắc phục hoặc việc printlntriển khai không thực hiện cuộc gọi hoặc hoạt động ++ được thực hiện sau các printlncuộc gọi như vậy là bởi vượt qua bởi các ngoại lệ.


Đó là ý của tôi khi "Tôi nghĩ có thể là do System.out.println cần 3 phân đoạn trên ngăn xếp cuộc gọi" - nhưng tôi đã không hiểu tại sao lại chính xác là con số này và bây giờ tôi càng khó hiểu tại sao con số lại khác nhau phần lớn (ảo) máy
flrnb

Tôi đồng ý một phần với nó nhưng điều tôi không đồng ý là trên tuyên bố `` phụ thuộc vào việc triển khai thực tế của println. 'Nó liên quan đến kích thước ngăn xếp trong mỗi jvm hơn là việc triển khai.
Jatin

6
  1. main tự đệ quy cho đến khi nó tràn ngăn xếp ở độ sâu đệ quy R .
  2. Khối bắt ở độ sâu đệ quy R-1 được chạy.
  3. Khối bắt ở độ sâu đệ quy R-1đánh giá cnt++.
  4. Khối bắt ở độ sâu R-1gọi println, đặt cntgiá trị cũ của ngăn xếp. printlnsẽ gọi nội bộ các phương thức khác và sử dụng các biến cục bộ và các thứ. Tất cả các quy trình này đều yêu cầu không gian ngăn xếp.
  5. Bởi vì ngăn xếp đã vượt qua giới hạn và việc gọi / thực thi printlnyêu cầu không gian ngăn xếp, nên một tràn ngăn xếp mới được kích hoạt ở độ sâu R-1thay vì độ sâu R.
  6. Các bước 2-5 xảy ra một lần nữa, nhưng ở độ sâu đệ quy R-2.
  7. Các bước 2-5 xảy ra một lần nữa, nhưng ở độ sâu đệ quy R-3.
  8. Các bước 2-5 xảy ra một lần nữa, nhưng ở độ sâu đệ quy R-4.
  9. Các bước 2-4 lại xảy ra nhưng ở độ sâu đệ quy R-5.
  10. Điều đó xảy ra là bây giờ có đủ không gian ngăn xếp printlnđể hoàn thành (lưu ý rằng đây là chi tiết triển khai, nó có thể thay đổi).
  11. cntđược sau tăng lên ở độ sâu R-1, R-2, R-3, R-4, và cuối cùng tạiR-5 . Hậu tăng thứ năm trả về bốn, là những gì đã được in.
  12. Khi mainhoàn thành thành công ở độ sâu R-5, toàn bộ ngăn xếp được mở mà không có thêm khối bắt nào được chạy và chương trình hoàn tất.

1

Sau khi tìm hiểu một lúc, tôi không thể nói rằng tôi đã tìm thấy câu trả lời, nhưng tôi nghĩ nó đã khá gần.

Đầu tiên, chúng ta cần biết khi nào một StackOverflowErrorsẽ được ném. Trên thực tế, ngăn xếp cho một luồng java lưu trữ các khung chứa tất cả dữ liệu cần thiết để gọi một phương thức và tiếp tục. Theo Đặc tả ngôn ngữ Java cho JAVA 6 , khi gọi một phương thức,

Nếu không có đủ bộ nhớ để tạo khung kích hoạt như vậy, lỗi StackOverflowError sẽ xuất hiện.

Thứ hai, chúng ta nên làm rõ thế nào là " không có đủ bộ nhớ để tạo một khung kích hoạt như vậy ". Theo Thông số kỹ thuật máy ảo Java cho JAVA 6 ,

khung có thể được phân bổ đống.

Vì vậy, khi một khung được tạo, cần có đủ không gian đống để tạo khung ngăn xếp và đủ không gian ngăn xếp để lưu trữ tham chiếu mới trỏ đến khung ngăn xếp mới nếu khung được cấp phát đống.

Bây giờ chúng ta hãy quay lại câu hỏi. Từ những điều trên, chúng ta có thể biết rằng khi một phương thức được thực thi, nó có thể tốn cùng một lượng không gian ngăn xếp. Và việc gọi System.out.println(có thể) cần 5 mức của phương thức gọi, vì vậy cần tạo 5 khung. Sau đó, khi StackOverflowErrorđược ném ra ngoài, nó phải quay lại 5 lần để có đủ không gian ngăn xếp để lưu trữ các tham chiếu của 5 khung hình. Do đó 4 được in ra. Tại sao không phải là 5? Bởi vì bạn sử dụng cnt++. Thay đổi nó thành ++cnt, và sau đó bạn sẽ nhận được 5.

Và bạn sẽ nhận thấy rằng khi kích thước ngăn xếp lên mức cao, đôi khi bạn sẽ nhận được 50. Đó là bởi vì lượng không gian heap có sẵn cần được xem xét sau đó. Khi kích thước của ngăn xếp quá lớn, có thể không gian đống sẽ hết trước khi ngăn xếp. Và (có thể) kích thước thực của khung xếp chồng System.out.printlnlà khoảng 51 lần main, do đó nó quay ngược 51 lần và in 50.


Suy nghĩ đầu tiên của tôi cũng là đếm mức độ của các lệnh gọi phương thức (và bạn nói đúng, tôi không chú ý đến thực tế là tôi đăng cnt tăng dần), nhưng nếu giải pháp đơn giản như vậy, tại sao kết quả lại khác nhau nhiều như vậy trên các nền tảng và triển khai VM?
flrnb

@flrnb Đó là bởi vì các nền tảng khác nhau có thể ảnh hưởng đến kích thước của khung ngăn xếp và phiên bản jre khác nhau sẽ ảnh hưởng đến việc triển khai System.out.printhoặc chiến lược thực thi phương thức. Như đã mô tả ở trên, việc triển khai VM cũng ảnh hưởng đến nơi thực sự lưu trữ khung ngăn xếp.
Jay,

0

Đây không phải là câu trả lời chính xác cho câu hỏi nhưng tôi chỉ muốn thêm điều gì đó vào câu hỏi ban đầu mà tôi đã xem và cách tôi hiểu vấn đề:

Trong bài toán ban đầu, ngoại lệ được bắt ở những nơi có thể:

Ví dụ với jdk 1.7, nó bị bắt ở vị trí xuất hiện đầu tiên.

nhưng trong các phiên bản jdk trước đó, có vẻ như ngoại lệ không bị bắt ở vị trí đầu tiên xuất hiện, do đó 4, 50, v.v.

Bây giờ nếu bạn xóa khối try catch như sau

public static void main( String[] args ){
    System.out.println(cnt++);
    main(args);
}

Sau đó, bạn sẽ thấy tất cả các giá trị của cntant, các ngoại lệ được ném ra (trên jdk 1.7).

Tôi đã sử dụng netbeans để xem đầu ra, vì cmd sẽ không hiển thị tất cả đầu ra và ngoại lệ được đưa ra.

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.