본문 바로가기

Programing/JVM(Java, Kotlin)

[Java] String: literal vs new

자바 기초중에 하나이다.

자바에는 문자열을 생성하는 방법이 크게 두가지가 있다.

리터럴(literal) 방식이고 다른 하나는 new 생성자를 이용한 방법이다.

코드로 설명하는 것이 이해가 빠를 것 같다.

String str1 = "a";
String str2 = new String("a");

str1 이 리터럴 방식이고, str2 가 생성자를 이용한 방식이다.

퀴즈. 둘 중에 안티패턴은?

답부터 이야기하면 바로 생성자를 이용한 방식이다.

왜 그런지는 아래에서 살펴 볼 수 있다.

String Internal

위의 자바코드를 읽을 수 있는 바이트 코드로 나타내보면 아래와 같다.

ldc "a"
astore_1

new java/lang/String
dup
ldc "a"
invokespecial java/lang/String.<init>(Ljava/lang/String;)V
astore_2

일부러 간격을 두었는데, 앞의 두줄이 자바코드의 1라인이고,

나머지 5줄이 2라인의 자바코드이다.

case1. 리터럴 형태로 문자열 생성

ldc "a"
astore_1

LDC (18; 0x12)

LDC라는 바이트코드 명령이 등장했는데 런타임 상수 풀(run-time constant pool)에 아이템을 넣어라(push)라는 명령이다.

위에서 문자열이 아닌 아이템이라고 이야기 한 이유는 문자열 뿐만 아니라 long 이나 double을 위한 수치형 상수도 넣을 수 있기 때문이다.

 

바이트 코드 인터프리터의 코드를 보면 LDC 부분의 case 문이 여러가지 레이블로 분기를 하고 있음을 알 수 있다.

CASE(_ldc):
{
  u2 index;
  bool wide = false;
  int incr = 2; // frequent case
  if (opcode == Bytecodes::_ldc) {
    index = pc[1];
  } else {
    index = Bytes::get_Java_u2(pc+1);
    incr = 3;
    wide = true;
  }

  ConstantPool* constants = METHOD->constants();
  switch (constants->tag_at(index).value()) {
  case JVM_CONSTANT_Integer:
    SET_STACK_INT(constants->int_at(index), 0);
    break;

  case JVM_CONSTANT_Float:
    SET_STACK_FLOAT(constants->float_at(index), 0);
    break;

  case JVM_CONSTANT_String:
    {
      oop result = constants->resolved_references()->obj_at(index);
      if (result == NULL) {
        CALL_VM(InterpreterRuntime::resolve_ldc(THREAD, (Bytecodes::Code) opcode), handle_exception);
        SET_STACK_OBJECT(THREAD->vm_result(), 0);
        THREAD->set_vm_result(NULL);
      } else {
        VERIFY_OOP(result);
        SET_STACK_OBJECT(result, 0);
      }
    break;
    }

  case JVM_CONSTANT_Class:
    VERIFY_OOP(constants->resolved_klass_at(index)->java_mirror());
    SET_STACK_OBJECT(constants->resolved_klass_at(index)->java_mirror(), 0);
    break;

  case JVM_CONSTANT_UnresolvedClass:
  case JVM_CONSTANT_UnresolvedClassInError:
    CALL_VM(InterpreterRuntime::ldc(THREAD, wide), handle_exception);
    SET_STACK_OBJECT(THREAD->vm_result(), 0);
    THREAD->set_vm_result(NULL);
    break;

  default:  ShouldNotReachHere();
  }
  UPDATE_PC_AND_TOS_AND_CONTINUE(incr, 1);
}
  1. JVM_CONSTANT_Integer
  2. JVM_CONSTANT_Float
  3. JVM_CONSTANT_String
  4. JVM_CONSTANT_Class

우리는 이중 JVM_CONSTANT_String 부분에 수행을 하게 된다.

코드는 딱히 없다. 해당 상수가 상수 풀에 있는지 확인하고, 없으면(result == NULL) 넣는 것(resolve_ldc)이다.

 

ldc, ldc_w, ldc2_w를 추상화하기 위한 Bytecode_loadconstant 클래스는 bytecode.hpp / cpp 에 존재한다.

Bytecode를 상속받고 있다.

// header (hpp)
class Bytecode_loadconstant: public Bytecode {
 private:
  const methodHandle _method;

  int raw_index() const;

 public:
  Bytecode_loadconstant(const methodHandle& method, int bci): Bytecode(method(), method->bcp_from(bci)), _method(method) { verify(); }

  void verify() const {
    assert(_method.not_null(), "must supply method");
    Bytecodes::Code stdc = Bytecodes::java_code(code());
    assert(stdc == Bytecodes::_ldc ||
           stdc == Bytecodes::_ldc_w ||
           stdc == Bytecodes::_ldc2_w, "load constant");
  }

  // Only non-standard bytecodes (fast_aldc) have reference cache indexes.
  bool has_cache_index() const { return code() >= Bytecodes::number_of_java_codes; }

  int pool_index() const;               // index into constant pool
  int cache_index() const {             // index into reference cache (or -1 if none)
    return has_cache_index() ? raw_index() : -1;
  }

  BasicType result_type() const;        // returns the result type of the ldc

  oop resolve_constant(TRAPS) const;
};

// Implementation (cpp)
int Bytecode_loadconstant::raw_index() const {
  Bytecodes::Code rawc = code();
  assert(rawc != Bytecodes::_wide, "verifier prevents this");
  if (Bytecodes::java_code(rawc) == Bytecodes::_ldc)
    return get_index_u1(rawc);
  else
    return get_index_u2(rawc, false);
}

int Bytecode_loadconstant::pool_index() const {
  int index = raw_index();
  if (has_cache_index()) {
    return _method->constants()->object_to_cp_index(index);
  }
  return index;
}

BasicType Bytecode_loadconstant::result_type() const {
  int index = pool_index();
  constantTag tag = _method->constants()->tag_at(index);
  return tag.basic_type();
}

oop Bytecode_loadconstant::resolve_constant(TRAPS) const {
  assert(_method.not_null(), "must supply method to resolve constant");
  int index = raw_index();
  ConstantPool* constants = _method->constants();
  if (has_cache_index()) {
    return constants->resolve_cached_constant_at(index, THREAD);
  } else {
    return constants->resolve_constant_at(index, THREAD);
  }
}

LDC 명령은 상수를 상수 풀에 넣을 뿐만 아니라 해당 값 혹은 참조를 오퍼랜드 스택(operand stack)에 넣는다(push).

수치(numeric)형의 경우 값을 넣고, String 같은 참조 타입은 레퍼런스를 넣는다.

ASTORE

atore_<n> 명령은 지역 변수에 오퍼렌드 스택에 있는 객체 참조를 넣는다.

n은 숫자를 의미하는데 0부터 3까지는 아예 상수로 정해져 있고(astore_0이 75(0x4b) ~ astore_3이 78 (0x4e)) 그 이상은 숫자와 같이 쓰는 형태로 사용이 된다. 58 (0x3a)

case2. 생성자 형태로 문자열 생성

new java/lang/String
dup
ldc "a"
invokespecial java/lang/String.<init>(Ljava/lang/String;)V
astore_2

NEW (187; 0xbb)

반면에 new 연산자로 만든 코드는 바이트 코드에도 new 명령이 이루어 진다.

새로운 객체를 만드는 연산이다. 자바 코드와는 달리 new 명령은 인스턴스 초기화 메서드가 완료되기 이전에는 새로운 인스턴스의 생성을 완료까지는 하지 않는다. 연산의 결과가 오퍼랜드 스택에 들어간다.

자바코드와는 달리 new + indexbyte1 + indexbyte2 와 같은 형태로 구성이 된다.

DUP (89; 0x59)

dup은 duplicate의 줄인말이다. 오퍼랜드 스택 중 제일 위에 있는 값을 복제하여 다시 오퍼랜드 스택으로 넣는 것이다.

이렇게 하는 이유는 이후에 String의 생성자를 호출하기 위함이다.

 

(LDC는 위에서 보았기 때문에 생략한다.)

INVOKESPECIAL (183; 0xb7)

invokespecial 명령은 인스턴스 메서드를 호출(invoke)하는 명령이다.

어떤 메서드를 호출하는지는 명령 뒤에 나오는 상수 풀에 따른다.

형식은 new랑 유사하다. invokespecial + indexbyte1 + indexbyte2

위의 예시에서는 생성자가 호출된다. (새로운 프레임이 생긴다)

 

생성자가 호출 될 때 LDC의 결과인 "a"에 레퍼런스가 오퍼랜드 스택에 있기에 그 값을 생성자의 인자로 사용이 된다.

 

이후 astore_2 는 그 결과를 지역변수 인덱스 2에 담는 것이다.