Eu queria saber o que acontece quando você tenta capturar um StackOverflowError e veio com o seguinte método:
class RandomNumberGenerator {
static int cnt = 0;
public static void main(String[] args) {
try {
main(args);
} catch (StackOverflowError ignore) {
System.out.println(cnt++);
}
}
}
Agora minha pergunta:
Por que este método imprime '4'?
Pensei que talvez fosse porque System.out.println()
precisa de 3 segmentos na pilha de chamadas, mas não sei de onde vem o número 3. Quando você olha o código-fonte (e bytecode) de System.out.println()
, normalmente levaria a muito mais invocações de método do que 3 (portanto, 3 segmentos na pilha de chamadas não seriam suficientes). Se for por causa das otimizações que o Hotspot VM aplica (método inlining), eu me pergunto se o resultado seria diferente em outra VM.
Editar :
Como a saída parece ser altamente específica da JVM, obtenho o resultado 4 usando
Java (TM) SE Runtime Environment (versão 1.6.0_41-b02)
Java HotSpot (TM) Servidor VM de 64 bits (versão 20.14-b01, modo misto)
Explicação por que acho que essa pergunta é diferente de Noções básicas sobre a pilha Java :
Minha pergunta não é sobre por que há um cnt> 0 (obviamente porque System.out.println()
requer tamanho de pilha e joga outro StackOverflowError
antes de algo ser impresso), mas por que ele tem o valor particular de 4, respectivamente 0,3,8,55 ou outra coisa em outro sistemas.
fonte
5
,6
e38
com Java 1.7.0_10Respostas:
Acho que os outros fizeram um bom trabalho explicando por que cnt> 0, mas não há detalhes suficientes sobre por que cnt = 4 e por que cnt varia tão amplamente entre as diferentes configurações. Vou tentar preencher esse vazio aqui.
Deixei
System.out.println
Quando entramos pela primeira vez em main, o espaço restante é XM. Cada chamada recursiva ocupa R mais memória. Portanto, para 1 chamada recursiva (1 a mais que o original), o uso de memória é M + R. Suponha que StackOverflowError seja lançado após C chamadas recursivas bem-sucedidas, ou seja, M + C * R <= X e M + C * (R + 1)> X. No momento do primeiro StackOverflowError, há X - M - C * R de memória restante.
Para poder funcionar
System.out.prinln
, precisamos de P espaço restante na pilha. Se acontecer de X - M - C * R> = P, então 0 será impresso. Se P requer mais espaço, então removemos os quadros da pilha, ganhando memória R ao custo de cnt ++.Quando
println
finalmente puder ser executado, X - M - (C - cnt) * R> = P. Portanto, se P for grande para um sistema particular, então cnt será grande.Vejamos isso com alguns exemplos.
Exemplo 1: Suponha
Então C = piso ((XM) / R) = 49, e cnt = teto ((P - (X - M - C * R)) / R) = 0.
Exemplo 2: suponha que
Então, C = 19 e cnt = 2.
Exemplo 3: suponha que
Então C = 20 e cnt = 3.
Exemplo 4: suponha que
Então, C = 19 e cnt = 2.
Assim, vemos que tanto o sistema (M, R e P) e o tamanho da pilha (X) afetam o cnt.
Como observação lateral, não importa quanto espaço seja
catch
necessário para começar. Enquanto não houver espaço suficiente paracatch
, o cnt não aumentará, portanto, não há efeitos externos.EDITAR
Retiro o que disse sobre
catch
. Ele desempenha um papel. Suponha que seja necessária uma quantidade T de espaço para começar. cnt começa a aumentar quando o espaço restante é maior do que T eprintln
é executado quando o espaço restante é maior do que T + P. Isso adiciona uma etapa extra aos cálculos e confunde ainda mais a análise já confusa.EDITAR
Finalmente encontrei tempo para fazer alguns experimentos para apoiar minha teoria. Infelizmente, a teoria não parece corresponder aos experimentos. O que realmente acontece é muito diferente.
Configuração da experiência: servidor Ubuntu 12.04 com java e jdk padrão. Xss começando em 70.000 em incrementos de 1 byte até 460.000.
Os resultados estão disponíveis em: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM Criei outra versão em que todos os pontos de dados repetidos são removidos. Em outras palavras, apenas os pontos diferentes dos anteriores são mostrados. Isso torna mais fácil ver as anomalias. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA
fonte
StackOverflowError
é lançada e como isso afeta a saída. Se contiver apenas uma referência a um quadro de pilha no heap (como Jay sugeriu), a saída deve ser bastante previsível para um determinado sistema.Esta é a vítima de uma chamada recursiva incorreta. Como você está se perguntando por que o valor de cnt varia, é porque o tamanho da pilha depende da plataforma. Java SE 6 no Windows tem um tamanho de pilha padrão de 320k na VM de 32 bits e 1024k na VM de 64 bits. Você pode ler mais aqui .
Você pode executar usando tamanhos de pilha diferentes e você verá valores diferentes de cnt antes de a pilha estourar-
Você não vê o valor de cnt sendo impresso várias vezes, embora o valor seja maior que 1 às vezes, porque sua instrução de impressão também está gerando um erro que você pode depurar com certeza por meio do Eclipse ou outros IDEs.
Você pode alterar o código a seguir para depurar por execução de instrução, se preferir
ATUALIZAR:
Como está recebendo muito mais atenção, vamos ter outro exemplo para tornar as coisas mais claras-
Criamos outro método chamado overflow para fazer uma recursão incorreta e removemos a instrução println do bloco catch para que não comece a lançar outro conjunto de erros ao tentar imprimir. Isso funciona conforme o esperado. Você pode tentar colocar System.out.println (cnt); declaração após cnt ++ acima e compilar. Em seguida, execute várias vezes. Dependendo da sua plataforma, você pode obter valores diferentes de cnt .
É por isso que geralmente não detectamos erros, porque mistério no código não é fantasia.
fonte
O comportamento depende do tamanho da pilha (que pode ser definido manualmente usando
Xss
. O tamanho da pilha é específico da arquitetura. Do código-fonte do JDK 7 :Portanto, quando o
StackOverflowError
é lançado, o erro é capturado no bloco catch. Aquiprintln()
está outra chamada de pilha que lança exceção novamente. Isso se repete.Quantas vezes ele se repete? - Bem, depende de quando a JVM pensa que não é mais stackoverflow. E isso depende do tamanho da pilha de cada chamada de função (difícil de encontrar) e do
Xss
. Conforme mencionado acima, o tamanho total padrão e o tamanho de cada chamada de função (depende do tamanho da página de memória, etc.) é específico da plataforma. Daí um comportamento diferente.Ligar
java
para-Xss 4M
me dá41
. Daí a correlação.fonte
catch
é um bloco e que ocupa memória na pilha. Quanta memória cada chamada de método leva não pode ser conhecida. Quando a pilha for limpa, você estará adicionando mais um bloco decatch
e assim por diante. Esse pode ser o comportamento. Isso é apenas especulação.Acho que o número exibido é o número de vezes que a
System.out.println
chamada gera aStackoverflow
exceção.Provavelmente depende da implementação do
println
e do número de chamadas de empilhamento feitas nele.Como uma ilustração:
A
main()
chamada dispara aStackoverflow
exceção na chamada i. A chamada i-1 de main captura a exceção e a chamadaprintln
que dispara um segundoStackoverflow
.cnt
obtém incremento para 1. A chamada i-2 de captura principal agora é a exceção e a chamadaprintln
. Emprintln
um método é chamado de desencadear uma terceira exceção.cnt
obtenha incremento para 2. isso continua até queprintln
possa fazer todas as chamadas necessárias e, finalmente, exibir o valor decnt
.Isso depende da implementação real de
println
.Para o JDK7, ele detecta a chamada cíclica e lança a exceção mais cedo ou mantém algum recurso da pilha e lança a exceção antes de atingir o limite para dar algum espaço para a lógica de correção ou a
println
implementação não faz chamadas ou a operação ++ é feita depois aprintln
chamada, portanto, é ignorada pela exceção.fonte
main
recorre sobre si mesmo até que estourou a pilha na profundidade de recursãoR
.R-1
é executado.R-1
avaliadocnt++
.R-1
chamadas de profundidadeprintln
, colocandocnt
o valor antigo de na pilha.println
irá chamar internamente outros métodos e usar variáveis locais e coisas assim. Todos esses processos requerem espaço de pilha.println
requer espaço de pilha, um novo estouro de pilha é disparado na profundidade emR-1
vez de na profundidadeR
.R-2
.R-3
.R-4
.R-5
.println
ser concluído (observe que este é um detalhe de implementação, pode variar).cnt
foi pós-incrementado em profundidadesR-1
,R-2
,R-3
,R-4
, e, finalmente,R-5
. O quinto pós-incremento retornou quatro, que é o que foi impresso.main
conclusão bem-sucedida em profundidadeR-5
, toda a pilha se desenrola sem que mais blocos sejam executados e o programa seja concluído.fonte
Depois de cavar um pouco, não posso dizer que encontrei a resposta, mas acho que está bem perto agora.
Primeiro, precisamos saber quando um
StackOverflowError
será lançado. Na verdade, a pilha de um thread java armazena quadros, que contêm todos os dados necessários para invocar um método e continuar. De acordo com as especificações da linguagem Java para JAVA 6 , ao invocar um método,Em segundo lugar, devemos deixar claro o que é " não há memória suficiente disponível para criar esse quadro de ativação ". De acordo com as especificações de máquina virtual Java para JAVA 6 ,
Portanto, quando um quadro é criado, deve haver espaço de heap suficiente para criar um quadro de pilha e espaço de pilha suficiente para armazenar a nova referência que aponta para o novo quadro de pilha se o quadro estiver alocado em heap.
Agora vamos voltar à questão. Do exposto, podemos saber que quando um método é executado, ele pode custar apenas a mesma quantidade de espaço de pilha. E a invocação
System.out.println
(pode) precisa de 5 níveis de invocação de método, portanto, 5 quadros precisam ser criados. Então, quandoStackOverflowError
é descartado, tem que voltar 5 vezes para obter espaço de pilha suficiente para armazenar referências de 5 quadros. Portanto, 4 é impresso. Por que não 5? Porque você usacnt++
. Altere para++cnt
e você obterá 5.E você notará que quando o tamanho da pilha atinge um nível alto, às vezes você obtém 50. Isso ocorre porque a quantidade de espaço de heap disponível precisa ser levada em consideração. Quando o tamanho da pilha é muito grande, talvez o espaço da pilha acabe antes da pilha. E (talvez) o tamanho real dos frames de pilha de
System.out.println
é cerca de 51 vezes demain
, portanto, volta 51 vezes e imprime 50.fonte
System.out.print
ou a estratégia de execução do método. Conforme descrito acima, a implementação da VM também afeta onde o quadro de pilha será armazenado.Esta não é exatamente uma resposta à pergunta, mas eu só queria acrescentar algo à pergunta original que encontrei e como entendi o problema:
No problema original, a exceção é detectada onde era possível:
Por exemplo, com o jdk 1.7 ele é capturado no primeiro lugar de ocorrência.
mas em versões anteriores do jdk, parece que a exceção não está sendo capturada no primeiro lugar de ocorrência, portanto, 4, 50 etc.
Agora, se você remover o bloco try catch como segue
Então você verá todos os valores das
cnt
exceções lançadas (no jdk 1.7).Usei o netbeans para ver a saída, pois o cmd não mostrará todas as saídas e exceções lançadas.
fonte