본문 바로가기

Programing/JVM(Java, Kotlin)

[Java] ArithmeticException 는 누가 던지는 것인가?

#ifndef SHARE_VM_CLASSFILE_VMSYMBOLS_HPP
#define SHARE_VM_CLASSFILE_VMSYMBOLS_HPP

#include "oops/symbol.hpp"
#include "memory/iterator.hpp"
#include "trace/traceMacros.hpp"

// ...

#define VM_SYMBOLS_DO(template, do_alias)                                                         \
  // ...                                                                                         \
  /* exception klasses: at least all exceptions thrown by the VM have entries here */             \
  template(java_lang_ArithmeticException,             "java/lang/ArithmeticException")            \

#endif // SHARE_VM_CLASSFILE_VMSYMBOLS_HPP

투수(投手) 이야기는 아니다.

간단한 코드

public class Dbz {
    public static void main(String[] args) {
        System.out.println(10/ 0);
    }
}

실행결과는 다음과 같다.

Exception in thread "main" java.lang.ArithmeticException: / by zero at com.example.springboot.sandbox.naver.goharrm.dividedbyzero.Dbz.main(Dbz.java:5)

0으로 나누어서 수학연산 예외 - ArithmeticException - 가 발생한다.

그렇다면 어떤 주체가 이 예외를 만들어서 throw를 할까?

JVM이 던지는 것은 확실하다. 그럼 어디서 저런 동작이 발생할까?

 

나눗셈 연산의 바이트 코드

정수 타입의 경우 자바 코드의 나눗셈은 int 인지, long 타입인지에 따라서 IDIV 혹은 LDIV 명령으로 나뉜다.

위의 샘플 코드를 디버깅 해보면 알겠지만 별도의 내부 콜 스택이 없다.

단지 바이트 코드를 수행(Runtime)하는 동안에 예외가 발생(raise)된다.

HotSpot 코드

우선 HotSpot 코드에서는 예외를 던지는 곳은 다음 코드로 추측된다. (hotspot/src/share/vm/c1/c1_Runtime1.cpp)

추측이라고 하는 이유는 자바의 컴파일러는크게 C1과 C2로 나뉘는데 C2 runtime에서는 아래와 같은 코드를 못찾았기 때문이다.
(혹시 아시는 분은 댓글을 남겨주세요...)

JRT_ENTRY(void, Runtime1::throw_div0_exception(JavaThread* thread))
  NOT_PRODUCT(_throw_div0_exception_count++;)
  SharedRuntime::throw_and_post_jvmti_exception(thread, vmSymbols::java_lang_ArithmeticException(), "/ by zero");
JRT_END

참고로 위의 코드는 자바가 아닌 C++  코드이다.

JRT_ENTRY 나 NOT_PRODUCT는 전처리에의해 치환되는 매크로이다.

interfaceSupport.hpp

#define JRT_ENTRY(result_type, header)                               \
  result_type header {                                               \
    ThreadInVMfromJava __tiv(thread);                                \
    VM_ENTRY_BASE(result_type, header, thread)                       \
    debug_only(VMEntryWrapper __vew;)
    
#define JRT_END }

따라서 아래와 같아진다.

void Runtime1::throw_div0_exception(JavaThread* thread) {
  ThreadInVMfromJava __tiv(thread);                                \
  VM_ENTRY_BASE(result_type, header, thread)                       \
  debug_only(VMEntryWrapper __vew;)
  NOT_PRODUCT(_throw_div0_exception_count++;)
  SharedRuntime::throw_and_post_jvmti_exception(thread, vmSymbols::java_lang_ArithmeticException(), "/ by zero");
}


java.lang.ArithmeticException을 java_lang_ArithmeticException 라는 심볼로 매핑을 해놓았다.

vmSymbolhotspot/src/share/vm/classfile/vmSymbols.hpp 에서 템플릿으로 등록하는 부분이 있다.

#ifndef SHARE_VM_CLASSFILE_VMSYMBOLS_HPP
#define SHARE_VM_CLASSFILE_VMSYMBOLS_HPP

#include "oops/symbol.hpp"
#include "memory/iterator.hpp"
#include "trace/traceMacros.hpp"

// ...

#define VM_SYMBOLS_DO(template, do_alias)                                                         \
  // ...                                                                                        \
  /* exception klasses: at least all exceptions thrown by the VM have entries here */             \
  template(java_lang_ArithmeticException,             "java/lang/ArithmeticException")            \

#endif // SHARE_VM_CLASSFILE_VMSYMBOLS_HPP

따라서 JVM내에서는 java_lang_ArithmeticException 라는 이름으로 사용이 가능하다.

 

그렇다면 이 Runtime은 누가 수행할까?

 

c1_Runtime1_x86.cpp

소스코드 참고

OopMapSet* Runtime1::generate_code_for(StubID id, StubAssembler* sasm) {

  // for better readability
  const bool must_gc_arguments = true;
  const bool dont_gc_arguments = false;

  // default value; overwritten for some optimized stubs that are called from methods that do not use the fpu
  bool save_fpu_registers = true;

  // stub code & info for the different stubs
  OopMapSet* oop_maps = NULL;
  switch (id) {
    // ...
    case throw_div0_exception_id:
      { StubFrame f(sasm, "throw_div0_exception", dont_gc_arguments);
        oop_maps = generate_exception_throw(sasm, CAST_FROM_FN_PTR(address, throw_div0_exception), false);
      }
      break;

id가 throw_div0_exception_id 일 경우 위에서 봤던 throw_div0_exception 스텁을 던지는 것으로 보인다.

c1_CodeStubs.hpp

C1 코드 조각에는 DivByZeroStub 이라는 클래스가 있다.

class DivByZeroStub: public CodeStub {
 private:
  CodeEmitInfo* _info;
  int           _offset;

 public:
  DivByZeroStub(CodeEmitInfo* info)
    : _info(info), _offset(-1) {
  }
  DivByZeroStub(int offset, CodeEmitInfo* info)
    : _info(info), _offset(offset) {
  }
  virtual void emit_code(LIR_Assembler* e);
  virtual CodeEmitInfo* info() const             { return _info; }
  virtual bool is_exception_throw_stub() const   { return true; }
  virtual bool is_divbyzero_stub() const         { return true; }
  virtual void visit(LIR_OpVisitState* visitor) {
    visitor->do_slow_case(_info);
  }
#ifndef PRODUCT
  virtual void print_name(outputStream* out) const { out->print("DivByZeroStub"); }
#endif // PRODUCT
};

c1_CodeStubs_x86.cpp

throw_div0_exception_id 를 사용하는 코드조각은 아래와 같다. (코드)

void DivByZeroStub::emit_code(LIR_Assembler* ce) {
  if (_offset != -1) {
    ce->compilation()->implicit_exception_table()->append(_offset, __ offset());
  }
  __ bind(_entry);
  __ call(RuntimeAddress(Runtime1::entry_for(Runtime1::throw_div0_exception_id)));
  ce->add_call_info_here(_info);
  debug_only(__ should_not_reach_here());
}

결국 DivByZeroStub 클래스의 emit_code 메서드 호출에 의해 예외가 발생함을 알 수 있다.

 

c1_LIRGenerator_x86.cpp

이제 거의 끝이 보인다.

위에서 정수의 나눗셈 연산은 바이트코드에서 IDIV 혹은 LDIV 으로 동작한다고 언급했다.

LIRGenerator 클래스에는 long 타입과 int 타입의 연산의 수행하는 코드가 있다.

주석에도 잘 써있지만, do_ArithmeticOp_Long 이 LDIV 명령이 수행되는 경우 동작한다.

중간에 보면 피제수(나눠지는 값)가 0인지 비교(cmp)하여 0이면 DivByZeroStub 객체를 만들어서 branch 함을 알 수 있다.

// for  _ladd, _lmul, _lsub, _ldiv, _lrem
void LIRGenerator::do_ArithmeticOp_Long(ArithmeticOp* x) {
  if (x->op() == Bytecodes::_ldiv || x->op() == Bytecodes::_lrem ) {
    // long division is implemented as a direct call into the runtime
    LIRItem left(x->x(), this);
    LIRItem right(x->y(), this);

    // the check for division by zero destroys the right operand
    right.set_destroys_register();

    BasicTypeList signature(2);
    signature.append(T_LONG);
    signature.append(T_LONG);
    CallingConvention* cc = frame_map()->c_calling_convention(&signature);

    // check for division by zero (destroys registers of right operand!)
    CodeEmitInfo* info = state_for(x);

    const LIR_Opr result_reg = result_register_for(x->type());
    left.load_item_force(cc->at(1));
    right.load_item();

    __ move(right.result(), cc->at(0));

    __ cmp(lir_cond_equal, right.result(), LIR_OprFact::longConst(0));
    __ branch(lir_cond_equal, T_LONG, new DivByZeroStub(info));

    // ...
  } else if (x->op() == Bytecodes::_lmul) {
    // ...
}
// ...
// for: _iadd, _imul, _isub, _idiv, _irem
void LIRGenerator::do_ArithmeticOp_Int(ArithmeticOp* x) {
  if (x->op() == Bytecodes::_idiv || x->op() == Bytecodes::_irem) {
    // ...
    if (!ImplicitDiv0Checks) {
      __ cmp(lir_cond_equal, right.result(), LIR_OprFact::intConst(0));
      __ branch(lir_cond_equal, T_INT, new DivByZeroStub(info));
    }
    // ...
    __ move(result_reg, result);
  } else {
    // ...
  }
}

다만 의아했던 것이 do_ArithmeticOp_Int 메서드에서는 ImplicitDiv0Checks 값에 따라 스위칭하게 되어 있다.

이 값은 c1_globals.hpp 에서 정의한다.

#ifndef SHARE_VM_C1_C1_GLOBALS_HPP
#define SHARE_VM_C1_C1_GLOBALS_HPP
// ...
//
// Defines all global flags used by the client compiler.
//
#define C1_FLAGS(develop, develop_pd, product, product_pd, diagnostic, notproduct) \
// ...
  develop(bool, ImplicitDiv0Checks, true,                                   \
          "Use implicit division by zero checks")                           \
// ...

// Read default values for c1 globals

C1_FLAGS(DECLARE_DEVELOPER_FLAG, DECLARE_PD_DEVELOPER_FLAG, DECLARE_PRODUCT_FLAG, DECLARE_PD_PRODUCT_FLAG, DECLARE_DIAGNOSTIC_FLAG, DECLARE_NOTPRODUCT_FLAG)

#endif // SHARE_VM_C1_C1_GLOBALS_HPP

globals.hpp는 다음과 같다.

#ifdef PRODUCT
#define DECLARE_DEVELOPER_FLAG(type, name, value, doc)    extern "C" type CONST_##name; const type name = value;
#define DECLARE_PD_DEVELOPER_FLAG(type, name, doc)        extern "C" type CONST_##name; const type name = pd_##name;
#define DECLARE_NOTPRODUCT_FLAG(type, name, value, doc)   extern "C" type CONST_##name;
#else
#define DECLARE_DEVELOPER_FLAG(type, name, value, doc)    extern "C" type name;
#define DECLARE_PD_DEVELOPER_FLAG(type, name, doc)        extern "C" type name;
#define DECLARE_NOTPRODUCT_FLAG(type, name, value, doc)   extern "C" type name;
#endif

보통은 PRODUCT 코드를 사용하게 될 테니 아래와 같이 평가(evaluation)된다.

extern "C" bool CONST_ImplicitDiv0Checks;
const bool ImplicitDiv0Checks = true;

따라서 true의 invert(!)는 false가 될 테니 "if (!ImplicitDiv0Checks)" 문 블럭은 수행이 안되어야 할 텐데 동작이 되는 것에 의문이 들었다.

VM Options : ImplicitDiv0Checks 켜면?

스위치: -XX:+ImplicitDiv0Checks

개발용으로 컴파일된 런타임이 아니라 아래와 같이 에러가 난다.

Error: VM option 'ImplicitDiv0Checks' is develop and is available only in debug version of VM.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.