티스토리 뷰

deep_dive_into_java_object_creation

자바(버전 8 기준) 오브젝트 생성 과정 톺아보기 (+ JVM Runtime Data Area 영역)

Intro

이번에 java 및 JVM의 Runtime Data Area 영역에 대해 공부를 하였는데,
자바에서 오브젝트가 생성 시 어떤 과정을 거치며 jvm 메모리 영역에 올라가는지에 대해 조금 더 자세히 알게되어 정리하고자 합니다.
(사실 지금부터 제가하는 말은 거짓말입니다.; 가려서 들어주시고, 틀린점은 바로잡아주시면 업데이트해놓도록 하겠습니다.)

아래와 같은 Product 모델 클래스가 있다고 가정을 하고, 이 클래스의 오브젝트를 생성해보도록 한다.

public class Product {
  public static final int PAYMENT_WINDOW_TITLE_MAX_BYTE = 4000;
  public static final String ERROR_MESSAGE = "ERROR";

  private GlobalProduct globalProduct = new GlobalProduct();
  private String productId;
  private ProductMeta productMeta;

  public String getProductName() { return globalProduct.getProductName(); }

  public static String getTitleUsingPaymentWindow(Product product) {
    String title = product.getProductName();
    if (title.getBytes().length > PAYMENT_WINDOW_TITLE_MAX_BYTE) {
      System.out.println(ERROR_MESSAGE);
    }
    return title;
  }

}

public class Main {
    public static void main(String[] args) {
      Product p = new Product();
    }
}

우선 어떤 자바 책에 나와있는 오브젝트 생성과정을 요약해보면 다음과 같습니다.(위의 main() method에서의 Product 오브젝트를 생성하는 코드를 예로 들겠습니다.)

  1. Product 클래스의 static 멤버(field/method)에 처음으로 접근하거나, 생성자를 처음으로 호출하는 경우 java interpreter는 Product.class를 JVM 상에 올려야한다.(classpath상에서 Product.class를 뒤짐.)
  2. Product.class가 loaded된 후에(= Product.class에 대한 Class 오브젝트가 생성된 후), Product의 모든 static initializer가 실행이 된다.(JVM이 기동된 후 딱 한번만 실행됨.)
  3. new Product()를 수행할 때, JVM의 heap 영역에 Product 오브젝트를 위한 메모리를 할당한다.
  4. 이 오브젝트에 메모리 영역은 primitive type은 0, reference type은 null로 초기 세팅된다.
  5. 멤버 field의 선언문에 초기화를 같이 하고 있다면 이 초기화가 수행된다.(위에선 globalProduct 필드가 이런 케이스)
  6. 그 후에 생성자가 호출된다.

이번 블로깅에서 오브젝트 생성 과정을 설명한다고 되어있지만,
오브젝트 생성 뿐만 아니라, 오브젝트가 JVM의 Runtime Data Area영역의 어디에 쌓이는지도 개략적으로 알아볼 생각입니다.
(그 오브젝트 생성을 위한 클래스 파일을 로딩하는 과정은 다음 포스트를 위해 간단히만 설명할 것 입니다.)





JVM Start up

위의 Product() 호출하는 main 메소드를 수행하기 위해 JVM을 기동시켜보자.

java -classpath target/classes org.eminentstar.Main

우선 JVM은 bootstrap class loader라는 최상위 클래스 로더가 먼저 맨 처음에 지정한 클래스(임의의 initial class)를 로딩한다.(여기서는 Main 클래스.)
그 후에 JVM은 이 Main.class를 link/initialize하고, 메소드 access specifier가 public, return type void, main(String[])인 메소드를 찾아 invoke하면서 JVM이 기동된다. (해당 클래스를 사용하기 위해서는 load/link/initialize 총 3가지 과정이 필요하다. 추후 설명하겠음.)

  • 여기서 언급한 public static main() method를 가지는 initial class는 특정한 기능을 수행하는 class라기보다는, 그냥 JVM 기동을 위한 entry point 역할이라고 보면 됨. 이 main을 실행하면서 코드의 흐름이 타고 타면서 프로그램이 진행되는 느낌. 예시로 톰캣(현재 사용하는 7버전대에서의 코드)의 서블릿 컨테이너인 catalina engine을 실행시킬때는 Tomcat에서 별도로 JVM 기동을 위한 entry class Bootstrap를 만들어놓았음.)
# tomcat/bin/catalina.sh의 일부
...
    eval exec "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
      -D$ENDORSED_PROP="\"$JAVA_ENDORSED_DIRS\"" \
      -classpath "\"$CLASSPATH\"" \
      -Djava.security.manager \
      -Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \
      -Dcatalina.base="\"$CATALINA_BASE\"" \
      -Dcatalina.home="\"$CATALINA_HOME\"" \
      -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
      org.apache.catalina.startup.Bootstrap "$@" start 
...

// tomcat의 Bootstrap 클래스의 main 메소드
// https://github.com/apache/tomcat70/blob/trunk/java/org/apache/catalina/startup/Bootstrap.java
/**
     * Main method and entry point when starting Tomcat via the provided
     * scripts.
     *
     * @param args Command line arguments to be processed
     */
    public static void main(String args[]) {

        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            try {
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            // When running as a service the call to stop will be on a new
            // thread so make sure the correct class loader is used to prevent
            // a range of class not found exceptions.
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }

        try {
            String command = "start";
            if (args.length > 0) {
                command = args[args.length - 1];
            }

            if (command.equals("startd")) {
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
                args[args.length - 1] = "stop";
                daemon.stop();
            } else if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
            } else if (command.equals("stop")) {
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
                daemon.load(args);
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }

    }

main 메소드 관련 Java Language Specification 내용

12.1 Java Virtual Machine Startup
The Java Virtual Machine starts execution by invoking the method main of some specified class, passing it a single argument, which is an array of strings.

main 메소드가 호출됬을 때의 Runtime Data Area 영역을 보면 다음과 같다.(추상적)

Heap에 대해서는 이 포스트에서 다루지 않을꺼라, 세부 영역은 표시하지 않았다.

jvm_runtime_data_area

Runtime Data Area in JVM

  • Java Performance Fundamental 책의 내용을 참조해보면, Runtime Data Area는 Process로서의 JVM이 프로그램을 수행하기 위해 OS로부터 할당 받는 메모리 영역이라고 나와있다.

    • 내 느낌으로 받아들인 Runtime Data Area를 말해보면, JVM이 기동되면서 initial class의 main 메소드가 실행되고 그에 따라 애플리케이션이 연계적으로 수행될 때 이 실행을 위한, 말 그대로 Runtime에서의 모든 Data를 저장하는 곳(?)이지 싶다. 특정 method를 수행하려면 그 method가 속한 class에 대한 metadata(class 및 method에 대한 정보, method code 등)도 필요하고, 이 metadata를 활용해서 class에 대한 object를 만들면, 그 오브젝트들 또한 저장이되고, 이 오브젝트를 가리키는 reference 변수들도 저장이 되어야하니 말이다.

JVM Specification 문서를 참고했을 때의 Runtime Data Area 영역을 구분해보면 다음과 같다. (data area 영역 중 몇몇은 JVM 시작/종료시 같이 생성/소멸되고, 그외 몇몇은 JVM Thread의 생성/소멸시에 같이 생성/소멸된다.)

1. PC(program counter) register (per JVM Thread)

thread에서 지금 실행중인 메서드(즉 JVM Stack의 top에 있는 stack frame)내에서 현재 실행중인 instruction(= bytecode)의 address를 포함함.(method가 native가 아닌 경우.)

  • 멀티스레드 환경에서 특정 JVM Thread가 RUNNABLE 상태에서 밀려났을 때, 다시 RUNNABLE 상태가 되었을 때 이전까지 수행하던 흐름을 기억해야함.

2. JVM Stack (per JVM Thread)

stack frame을 저장하는 jvm thread의 스택.

  • Stack Frame

    • stack frame은 method 실행시마다 생김. 실행중인 메서드의 상태를 저장하는 단위이다.
    • JVM stack의 top에 위치하는 frame이 현재 실행중인 메서드의 frame.
    • stack frame을 구성하는 영역

      1. Local Variable(이하 lv라고 칭하겠음.):

        • 메소드의 파라미터 및 지역변수가 저장되는 영역.(method invocation시에 파라미터들이 lv에 먼저 세팅되고, 지역변수는 그 뒤의 index에 저장된다.)

          • instance method의 invocation의 경우, 해당 instance의 reference(this)가 lv의 0번째에 저장되고, 그뒤에 paramter들이 뒤를 따름.

          • class(static) method의 invocation의 경우, invocation시의 parameter들(오브젝트의 경우 reference가)이 lv의 0번째 위치부터 저장됨.

            • method를 invoke하는 instruction들 중에서 static method를 invoke하는 invokestatic instruction는 다른 instruction들(invokevirtual/invokespecial/invokeinterface)와는 달리 호출당시 operand stack에 object의 reference를 필요로 하지 않음.ㅇ
      2. Operand Stack: JVM이 해당 메서드내에서 연산을 할 때 쓰는 스택 영역..(?)(뭔가 한마디로 표현하기 애매하니 어떤 경우에 operand stack이 사용되는지 예시를 들겠음.)

        • (예시를 보니 뭔가 jvm이(execution engine이) instruction을 수행할 때 필요한 데이터를 operand stack에 놔두고 소모하는 느낌이라고 해야되나.?)

          1. primitive type 연산의 피연산자 push 및 연산 후의 결과 push(연산시에 피연산자들은 pop됨.)

          2. method invocation시에 해당 method가 속한 object의 reference 및 그 method 수행을 위한 parameter들 push

            • method invocation instruction 수행과 동시에 objectref와 parameter들은 pop됨.
          3. invoked된 method에서 값을 return하면, 그 invoked method의 caller의 operand stack에 return 값이 push됨.

          4. method에 값을 return하는 instruction 수행 시 operand stack의 top에 있는 녀석이 return 됨.

      3. method가 속한 클래스의 run-time constant pool에 대한 reference(구현체에 따라 또 다른듯하다.)

        • 속한 클래스의 runtime constant pool reference를 알고 있으면, 해당 클래스의 runtime constant pool에서 symbolic하게 참조하는 자신/다른 클래스의 정보(method라던가, variable이라던가)를 참조할 수 있음.(단순히 이름으로 참조하는 symbolic한 reference는 딱 처음 이 symbolic reference에 접근할 때 resolution을 통해서 실제 jvm상의 memory address로 변환이 필요함.)

3. Native Method Stack (per JVM Thread)

JVM에서 Java 이외의(C와 같은) 언어로 작성된 method를 수행할 때 필요한 stack

  • (Native Method에 대해서 아직 들어본게 익숙하지 않아 간단히 넘어가도록 하겠습니다.(뭐 JNI, Native Heap 이런걸 들어본 거 같긴한데..))

4. Heap (shared b/t Threads)

class의 instance와 array를 위한 메모리가 할당되는 영역

  • object reference의 scope이 method던, instance던, class던 간에 상관없이 new 혹은 newarray와 같은(anewarray) instruction에 의해 수행되면 heap(java heap)에 저장됨.

5. Method Area (shared b/t Threads)

(스펙이 좀 추상적임) 각 클래스별 데이터들(run-time constant pool, class fields/methods, method code, 그외 class metadata(정확히 어떤 metadata인지는 확인을 못했음.))가 저장되는 곳.

6. Run-Time Constant Pool (shared b/t Threads)

class 파일에 포함되있는 constant pool이 runtime 환경에 맞게 converted됬다고 보면 됨.(각 class 마다 별도의 constant pool이 존재함.)

JVM 스펙상의 Method Area/Run-Time Constant Pool(5,6번 영역)에 대한 명세는 좀 추상적이고 구현 벤더에 따라 다른 것 같다.
참고 article: HotSpot JVM의 경우 java 8 기준으로

  • clss metadata, method codes -> Metaspace(in native heap)
  • rum-time constant pool(article상의 symbol이 constant pool이 아닐까?) -> native heap
  • static variables -> java heap
    에 저장된다고 한다. (자세한 내용은 permgen to metaspace라는 키워드등으로 구글을 찾아보면 될 것 같다.)




Object Creation

JVM이 기동되고 main이 invoked된 시점은 다음과 같다.
when_jvm_just_started

이제 JVM이 기동되면서 Main.main() 메소드를 invoke했고, main() 메소드의 instruction들이 수행이 되어야 할 것이다. (instruction의 흐름만 보도록 하겠다. Execution Engine 쪽은 다음에 다루도록.)

public class Main {
    public static void main(String[] args) {
      Product p = new Product();
    }
}

아래의 command를 통해서 Main clsss 파일의 constant pool 및 main() method의 instruction을 파악해보자.

javap -v -p -s target.classes.org.eminentstar.Main

(class 파일의 구조는 jvm spec 혹은 here을 참조해보자.)

Classfile /Users/user/Documents/NCSDrive/work/eminentstar/share/java_object_creation/deep-dive-into-object-creation/target/classes/org/eminentstar/Main.class
  Last modified Dec 9, 2018; size 507 bytes
  MD5 checksum b4a17089102c321a99811c34d191db73
  Compiled from "Main.java"
public class org.eminentstar.Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V
   #2 = Class              #22            // org/eminentstar/model/simple/Product
   #3 = Methodref          #2.#21         // org/eminentstar/model/simple/Product."<init>":()V
   #4 = Class              #23            // org/eminentstar/Main
   #5 = Class              #24            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lorg/eminentstar/Main;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               product
  #18 = Utf8               Lorg/eminentstar/model/simple/Product;
  #19 = Utf8               SourceFile
  #20 = Utf8               Main.java
  #21 = NameAndType        #6:#7          // "<init>":()V
  #22 = Utf8               org/eminentstar/model/simple/Product
  #23 = Utf8               org/eminentstar/Main
  #24 = Utf8               java/lang/Object
{
  public org.eminentstar.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lorg/eminentstar/Main;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class org/eminentstar/model/simple/Product
         3: dup
         4: invokespecial #3                  // Method org/eminentstar/model/simple/Product."<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 8: 0
        line 9: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
            8       1     1 product   Lorg/eminentstar/model/simple/Product;
}
SourceFile: "Main.java"

java 코드로 1줄로 표현된 아래 코드는 Product object에 대한 reference 변수 p에 new 키워드 및 생성자를 호출해서 생성된 Product object의 address를 초기화한다는 의미이다.

Product p = new Product();

이 한줄이 instruction으로는 아래와 같이 변환된다.

0: new           #2                  // class org/eminentstar/model/simple/Product
3: dup
4: invokespecial #3                  // Method org/eminentstar/model/simple/Product."<init>":()V
7: astore_1

new instruction

  • new는 object를 생성하는 instruction이다.

  • 이 instruction은 3byte로 구성되는데, 첫번째 1byte는 new에 대한 opcode, 나머지 2byte는 오브젝트를 생성할 클래스에 대한 Main 클래스내의 constant pool entry의 index를 구성한다.

  • Main의 Runtime Constant Pool내에서 Product 클래스에 대한 entry가 resolved된 상태라면 entry에서 참조한 Product 클래스 정보를 통해 Product 오브젝트를 위한 메모리 영역을 heap(java heap)에 할당한다. 이때 오브젝트는 초기화되지 않은 상태이다.

    • (해당 클래스에 대한 첫번째 접근이라면 단순히 symbolic reference로서의 클래스의 이름을 가지는 utf8 entry의 index를 가리킬 것이고, 이 symbolic reference로부터 Product의 Class오브젝트의 reference를 얻는 resolution 과정을 거쳐야함.)
symbolic 상태의 constant pool entries
#2 = Class              #22            // org/eminentstar/model/simple/Product
#22 = Utf8               org/eminentstar/model/simple/Product

-resolved되지 않은 constant pool entry-
unresolved_entry

-resolved된 pool entry-
resolved_entry

-new instruction 수행 직후-
after_new

 

HotSpot JVM에서의 pool entry의 resolution에 대해 좀 더 파악하려면 here를 참조해보도록 한다.

  • new 수행 결과로, heap에 할당된 메모리에 대한 reference를 operand stack에 push한다.


논외. HotSpot JVM으로 봤을때, 오브젝트들은 자신의 클래스의 Class 오브젝트를 klass라는 포인터로 참조한다고 함. klass_pointer

 

dup instruction

  • dup은 operand stack의 top을 duplicate해서 operand stack에 push하는 instruction이다.

  • dup을 통해 operand stack에 있는 objectref의 copy를 만드는 이유를 생각해보았다. new를 통해 메모리영역을 할당하고, instance initialization method(<init>)를 invoke하는 invokespecial instruction을 수행할 때 stack의 objectref를 pop한다. 생성된 오브젝트는 분명히 로직을 수행하기 위해서 reference 변수에 의해서 참조되야한다. 근데 invokespecial을 수행할 시점에 objectref가 1개 밖에 없다면 수행 후에는 reference 변수에 전혀 넣을 수가 없다. 그래서 dup을 통해 reference를 복사하나보다.

    • 사실 정확히 어떠한 문서의 형태로도 확인해보지는 못했다. reference 변수에 담지않고 단순히 new 생성자()를 한다고 해도 dup을 하고, 그 후에 남아있는 ref를 pop하는 형식으로 instruction들이 생성된다. 거의 모든 경우를 위해 dup을 한다고 봐야겠다.

-dup 실행 이후-
after_dup

 

invokespecial instruction

  • invokespecial은 instance method를 inovke하는 instruction이긴 한데, <init>(생성자)나 superclass/private 메소드를 호출할 때 사용한다.

  • <init>(instance intialization method)

    • JVM 레벨에서의 생성자는 <init>이란 이름을 가짐.
  • stacktrace상에서의 생성자 로그를 보면 확인가능하다.
    init_stacktrace

 

Constant pool:
   #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V
   #2 = Class              #22            // org/eminentstar/model/simple/Product
   #3 = Methodref          #2.#21         // org/eminentstar/model/simple/Product."<init>":()V
   #4 = Class              #23            // org/eminentstar/Main
   #5 = Class              #24            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
  #21 = NameAndType        #6:#7          // "<init>":()V
  #24 = Utf8               java/lang/Object
{
  public org.eminentstar.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lorg/eminentstar/Main;
  • Main()의 instruction들을 보면 알겠지만, Product.<init>을 호출할 때 넘어온 Product object의 ref를 다시 operand stack에 올려서 superclass(여기선 Object 클래스)의 <init>을 다시 호출한다. 이 recursive한 superclass의 init 호출은 base class(Object 클래스)에 다다를때까지 반복된다.

 

-Product.<init>이 수행된 직후-
after_product_init_invoked

-Object.<init>이 수행된 직후-
after_object_init_invoked

-<init>수행 후 오브젝트가 초기화가 됨.-
after_init

 

astore instruction

마지막으로 astore instruction을 통해서 operand stack의 top(objectref)를 pop해서 local variable에 저장한다.

astore을 통해서 local variable에 저장된 object reference
after_astore





 

Class loading/Linking/Initializing

자세히 다루면 너무 길어질 것 같아, 이건 간략히 정리 후에 별도의 포스트로 정리하려 한다.(또한, 좀 더 자세히 공부를 하고 난 다음에..)

책에서의 instance 생성과정을 보면서도 제일 처음 생소했던 부분은 class loading이었다. class loader라는 것을 블로그나 책등에서 간혹 보긴 했지만 감이 안왔었는데, 간단하게 보면,

  • class loader는 JVM내의 일종의 서브 시스템엔데, 임의의 클래스의 Class 오브젝트를 생성하는 주체이다.(JVM의 Runtime Data Area의 Method Area에 클래스 파일을 로드함.)
  • 이 Class 오브젝트는 .class 파일 한개당 하나씩 만들어지고, 특정 클래스에 대한 타입 정보를 가지고 있는 runtime의 오브젝트라고 보면 된다.(오브젝트 생성을 위해 필요하다.)

어떤 클래스의 오브젝트를 생성하려면 그 클래스의 정보가 JVM에 배치되어야한다.(어떤 책에선 해당 클래스의 Class 오브젝트가 JVM상에 생성어야한다고 표현하는데, 이는 좀 좁은 의미를 나타내는 것 같다.)
이는 Class Loader가 담당을 하고, 크게 다음과 같이 3가지의 step으로 구성된다.

Class Loader의 클래스 배치 과정

1. loading : 클래스패스상에서 binary 형태의 .class 파일을 찾아 JVM에 올리고, 이 .class 파일을 이용해서 Class를 생성하는 과정
  • load하고자하는 클래스의 superclass가 없다면 이 superclass부터 먼저 load한다.
2. linking : (책 내용 인용) loading된 생성된 Class 타입을 Runtime 상태의 Binrary Type Data로 구성하는 과정
  • linking은 verification, preparation, resolution 단계를 거친다.(JVM 구현체에 따라 resolution은 optional할 수도 있음.)
  1. verification : .class 파일의 구조가 제대로 됬는지 검증하는 단계.

    • 이미 Java Compiler가 class파일을 생성할 때 제대로 생성하겠지만서도, JVM입장에서는 어떤 class파일이 들어올지를 모르기 때문에 verification단계를 거침.

Q: 여기서 내가 의문인 것은, 이미 loading 단계를 통해서 .class 파일을 Class 오브젝트로 parsing을 완료했는데, 왜 linking단계에서 이 .class 파일을 검증하느냐? 이다. 이미 structurally incorrect 상태이면 parse하는 단계가 불필요 하지 않나?

  1. preparation : 클래스의 static fields를 위한 메모리를 할당하는 단계

  2. resolution : runtime constant pool의 entry들(클래스/메소드/필드에 대한) symbolic reference를 실제 메모리상의 address로 변환하는 과정

3. initialization : <clinit>(class intialization method)를 수행하는 과정
  • 클래스의 모든 static initializers 및 static fields의 초기화를 수행한다.

  • <clinit> 호출은 superclass, superinterfaces의 initialization을 진행한 후에 일어난다.

  • 클래스 initialization은 동시에 여러 스레드에서 시도를 할 수 있기 때문에 어떤 클래스에 대해 initialization 진행 시 lock을 잡고 진행함.

  • 클래스의 Initialization이 일어나는 시점

    • 다음의 JVM instruction을 수행할 때(new, getstatic, putstatic, invokestatic)

      • 위 instruction들은 직간접적으로 해당 class를 참조함.

        • 클래스의 instance를 생성한다던가
        • static method를 호출한다거나
        • static field를 참조한다거나
    • java.lang.reflect 패키지에서 해당 클래스에 대한 접근 시도를 할 때 (예를 들면 Class.forName("org.eminentstar.model.simple.Product")와 같이)

    • subclass의 initialization시

    • default method를 가지는 인터페이스인 경우, 이 인터페이스를 구현한(같은 hierarchy안에 있는) 클래스의 initialization시에 인터페이스가 initialization이 일어남.

    • JVM 기동시의 initial class인 경우

 





정리

위 정리된 내용을 토대로 오브젝트 생성과정을 다시 한번 보도록 하겠다.

public class Product {
  public static final int PAYMENT_WINDOW_TITLE_MAX_BYTE = 4000;
  public static final String ERROR_MESSAGE = "ERROR";

  private GlobalProduct globalProduct = new GlobalProduct();
  private String productId;
  private ProductMeta productMeta;

  public String getProductName() { return globalProduct.getProductName(); }

  public static String getTitleUsingPaymentWindow(Product product) {
    String title = product.getProductName();
    if (title.getBytes().length > PAYMENT_WINDOW_TITLE_MAX_BYTE) {
      System.out.println(ERROR_MESSAGE);
    }
    return title;
  }

}

public class Main {
    public static void main(String[] args) {
      Product p = new Product();
    }
}

우선 어떤 자바 책에 나와있는 오브젝트 생성과정

  1. Product 클래스의 static 멤버(field/method)에 처음으로 접근하거나, 생성자를 처음으로 호출하는 경우 java interpreter는 Product.class를 JVM 상에 올려야한다.(classpath상에서 Product.class를 뒤짐.)

    • Main에서 new Product()를 통해서 Product에 대한 오브젝트를 생성하려고 한다. 이때 JVM이 기동된 후에 Product 클래스에 대해 한번도 참조가 되지 않았기에 class loader는 클래스패스내에서 Product.class를 찾아야한다.
  2. Product.class가 loaded된 후에(= Product.class에 대한 Class 오브젝트가 생성된 후), Product의 모든 static initializer가 실행이 된다.(JVM이 기동된 후 딱 한번만 실행됨.)

    • class loader가 Product.class 파일을 찾아서 loadind/linking 과정을 거친 후에, initialization을 수행한다. 이 initialization을 통해서 <clinit>이 inovke되고, Product내의 모든 static 필드 초기화 / static initializer가 수행이 된다.

      • linking에서 preparation단계에서 PAYMENT_WINDOW_TITLE_MAX_BYTE와 ERROR_MESSAGE를 위한 메모리가 할당된다.
      • initialization 단계에서 PAYMENT_WINDOW_TITLE_MAX_BYTE와 ERROR_MESSAGE 필드가 초기화된다.
  3. new Product()를 수행할 때, JVM의 heap 영역에 Product 오브젝트를 위한 메모리를 할당한다.

    • new instruction을 수행하면서 Product 오브젝트에 대한 메모리가 Java Heap에 할당된다.
  4. 이 오브젝트에 메모리 영역은 primitive type은 0, reference type은 null로 초기 세팅된다.

    • new 를 통해 heap에다 메모리를 할당만 했을 뿐이지 어떤 초기화도 일어나지 않음.
  5. 멤버 field의 선언문에 초기화를 같이 하고 있다면 이 초기화가 수행된다.(위에선 globalProduct 필드가 이런 케이스)

    • 추가로 확인해본 결과, 이렇게 필드 선언에 초기화가 있다거나 initialization block을 통해 초기화를 하는 라인은 bytecode 변환시에 <init> 메서드에서 superclass의 <init>을 invokespecial하는 bytecode의 바로 아래에 copy되어서 들어간다.
    • 즉, 이런 애들은 <init> 메소드 호출을 하면 superclass의 <init> 호출 직후, 생성자 자바 코드에 있는 초기화보다 먼저 초기화가 일어난다.
  6. 그 후에 생성자가 호출된다.

    • parameter가 없는 <init>이 invoke된다.


여기까지 Java Virtual Machine의 Runtime Data Area 및 Java 오브젝트 생성과정에 대해 알아보았다. 다음엔 class loader의 링킹에 대해 좀 더 자세히 알아보도록 하려한다.






이상하거나 궁금한 점은 아래 코멘트 달아주시면 감사하겠습니다. ( _ _)

 

댓글
댓글쓰기 폼
공지사항
Total
94,212
Today
0
Yesterday
17
«   2019/08   »
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함