문자열 클래스

Assembled by GimunLee (2019-10-28)

 

Goal

  • JAVA의 3가지 문자열 클래스에 대해 간략하게 설명할 수 있다.
  • 각 문자열 클래스의 차이점에 대해 설명할 수 있다.
  • 상황에 맞게 문자열 클래스를 사용할 수 있다.

 

Introduction

JAVA에는 문자열 클래스로 String, StringBuffer, StringBuilder 3가지가 있습니다. 사소해보이지만 상황에라 어떤 클래스를 쓰냐에 따라, 성능차이가 발생하는데요. 어떤 차이점이 있는지 알아보도록 하겠습니다.

 

String vs StringBuffer vs StringBuilder

IndexStringStringBufferStringBuilder

Storage Area Heap or Constant String Pool Heap Heap
Modifable No(immutable) Yes(mutable) Yes(mutable)
Thread-Safe YES YES NO

String과 다른 클래스(StringBuffer, StringBuilder)의 기본적인 차이는 String은 Immutable(불변), StringBuffer, StringBuilder는 Mutable(가변)에 있습니다.

 

String Class

String 객체는 한번 생성되면 할당된 메모리 공간이 변하지 않습니다. 즉, '+' 연산 또는 concat 메서드를 통해 기존에 생성된 String 객체에 다른 문자열을 붙여도 기존 문자열에 새로운 문자열을 붙이는 것이 아닙니다. 새로운 String 객체를 만든 후, 이 객체에 연결된 문자열을 저장하고, 그 객체를 참조하도록합니다.

  • 장점
    • String Class의 객체는 Immutable(불변)하기 때문에 단순하게 읽어가는 조회 연산에서는 타 클래스보다 빠르게 읽을 수 있습니다.
    • Immutable(불변)하기 때문에 멀티쓰레드 환경에서 동기화를 신경 쓸 필요가 없습니다. (Thread-Safe)
  • 단점
    • 문자열 연산('+', concat 등)을 많이 일어나는 경우, 더이상 참조되지 않는 기존 객체는 Garbage Collection(이하, GC)에 의해 제거되야하기 때문에 성능이 좋지 않습니다.
    • 또한, 문자열 연산이 많아질 때 연산 내부적으로 char 배열을 사용하고, 계속해서 객체를 만드는 오버헤드가 발생하므로 성능이 떨어질 수 밖에 없습니다.

 

StringBuffer와 StringBuilder Class

StringBuffer와 StringBuilder 클래스는 String과 다르게 mutable(변경가능)합니다. 즉, 문자열 연산에 있어서 클래스를 한번만 만들고(new), 연산이 필요할 때 크기를 변경시켜서 문자열을 변경합니다. 그러므로 문자열 연산이 자주 있을 때 사용하면 성능이 좋습니다.

StringBuffer와 StringBuilder 클래스가 제공하는 메서드는 서로 동일합니다. 그렇다면 두 클래스의 차이점을 무엇일까요? 바로 동기화 여부입니다.

StringBuffer는 각 메서드별로 Synchronized Keyword가 존재하여, Multi-Thread 환경에서도 동기화를 지원하여 Thread-Safe합니다.

반면, StringBuilder는 동기화를 보장하지 않습니다. 하지만 StringBuilder는 Single-Thread 환경에서 동기화를 고려하지 않기 때문에 StringBuffer에 비해 연산처리가 빠릅니다.

그렇기 때문에 Multi-Thread 환경이라면 값 동기화 보장을 위해 StringBuffer를 사용하고, Single-Thread 환경이라면 StringBuilder를 사용하는 것이 좋습니다.

 

Conclusion

String Class는 JDK 1.5버전 이전에 문자열연산('+', concat)을 할 때에는 조합된 문자열을 새로운 메모리에 할당하여 참조함으로 인해서 성능상의 이슈가 있었습니다. 그러나 JDK1.5 버전 이후에는 컴파일 단계에서 String 객체를 사용하더라도 StringBuilder로 컴파일 되도록 변경되었습니다. 그리하여 JDK 1.5 이후 버전에서는 String 클래스를 활용해도 StringBuilder와 성능상으로 차이가 없어졌습니다. 하지만 반복 루프를 사용해서 문자열을 더할 때에는 객체를 계속 추가한다는 사실에는 변함이 없습니다.

String Class를 쓰는 대신, Thread와 관련이 있으면 StringBuffer를, Thread 안전 여부와 상관이 없으면 StringBuilder를 사용하는 것을 권장합니다.

단순히 성능만 놓고 본다면 연산이 많은 경우, StringBuilder > StringBuffer >>> String 라고 합니다.

 

Reference & Additional Resources

'Language > JAVA' 카테고리의 다른 글

Promotion & Casting  (0) 2022.05.05
Garbage Collection  (0) 2022.05.05
Intrinsic Lock  (0) 2022.05.05
Java에서의 Thread  (0) 2022.05.05
Auto Boxing & Unboxing  (0) 2022.05.05

Java 고유 락 (Intrinsic Lock)


Intrinsic Lock / Synchronized Block / Reentrancy

Intrinsic Lock (= monitor lock = monitor) : Java의 모든 객체는 lock을 갖고 있음.

Synchronized 블록은 Intrinsic Lock을 이용해서, Thread의 접근을 제어함.

public class Counter {
    private int count;
    
    public int increase() {
        return ++count;		// Thread-safe 하지 않은 연산
    }
}

 

Q) ++count 문이 atomic 연산인가?

A) read (count 값을 읽음) -> modify (count 값 수정) -> write (count 값 저장)의 과정에서, 여러 Thread가 **공유 자원(count)으로 접근할 수 있으므로, 동시성 문제가 발생**함.

 

Synchronized 블록을 사용한 Thread-safe Case

public class Counter{
    private Object lock = new Object(); // 모든 객체가 가능 (Lock이 있음)
    private int count;
    
    public int increase() {
        // 단계 (1)
        synchronized(lock){	// lock을 이용하여, count 변수에의 접근을 막음
            return ++count;
        }
        
        /* 
        단계 (2)
        synchronized(this) { // this도 객체이므로 lock으로 사용 가능
        	return ++count;
        }
        */
    }
    /*
    단계 (3)
    public synchronized int increase() {
    	return ++count;
    }
    */
}

단계 3과 같이 lock 생성 없이 synchronized 블록 구현 가능

 

Reentrancy

재진입 : Lock을 획득한 Thread가 같은 Lock을 얻기 위해 대기할 필요가 없는 것

(Lock의 획득이 '호출 단위'가 아닌 **Thread 단위**로 일어나는 것)

public class Reentrancy {
    // b가 Synchronized로 선언되어 있더라도, a 진입시 lock을 획득하였음.
    // b를 호출할 수 있게 됨.
    public synchronized void a() {
        System.out.println("a");
        b();
    }
    
    public synchronized void b() {
        System.out.println("b");
    }
    
    public static void main (String[] args) {
        new Reentrancy().a();
    }
}

 

Structured Lock vs Reentrant Lock

Structured Lock (구조적 Lock) : 고유 lock을 이용한 동기화

(Synchronized 블록 단위로 lock의 획득 / 해제가 일어나므로)

따라서,

A획득 -> B획득 -> B해제 -> A해제는 가능하지만,

A획득 -> B획득 -> A해제 -> B해제는 불가능함.

이것을 가능하게 하기 위해서는 **Reentrant Lock (명시적 Lock) 을 사용**해야 함.

 

Visibility

  • 가시성 : 여러 Thread가 동시에 작동하였을 때, 한 Thread가 쓴 값을 다른 Thread가 볼 수 있는지, 없는지 여부
  • 문제 : 하나의 Thread가 쓴 값을 다른 Thread가 볼 수 있느냐 없느냐. (볼 수 없으면 문제가 됨)
  • Lock : Structure Lock과 Reentrant Lock은 Visibility를 보장.
  • 원인 :
  1. 최적화를 위해 Compiler나 CPU에서 발생하는 코드 재배열로 인해서.
  2. CPU core의 cache 값이 Memory에 제때 쓰이지 않아 발생하는 문제.

'Language > JAVA' 카테고리의 다른 글

Garbage Collection  (0) 2022.05.05
문자열 클래스  (0) 2022.05.05
Java에서의 Thread  (0) 2022.05.05
Auto Boxing & Unboxing  (0) 2022.05.05
Casting(업캐스팅 & 다운캐스팅)  (0) 2022.05.05

Java에서의 Thread

 

요즘 OS는 모두 멀티태스킹을 지원한다.

멀티태스킹이란?

예를 들면, 컴퓨터로 음악을 들으면서 웹서핑도 하는 것

쉽게 말해서 두 가지 이상의 작업을 동시에 하는 것을 말한다.

 

실제로 동시에 처리될 수 있는 프로세스의 개수는 CPU 코어의 개수와 동일한데, 이보다 많은 개수의 프로세스가 존재하기 때문에 모두 함께 동시에 처리할 수는 없다.

각 코어들은 아주 짧은 시간동안 여러 프로세스를 번갈아가며 처리하는 방식을 통해 동시에 동작하는 것처럼 보이게 할 뿐이다.

이와 마찬가지로, 멀티스레딩이란 하나의 프로세스 안에 여러개의 스레드가 동시에 작업을 수행하는 것을 말한다. 스레드는 하나의 작업단위라고 생각하면 편하다.

 

스레드 구현


자바에서 스레드 구현 방법은 2가지가 있다.

  1. Runnable 인터페이스 구현
  2. Thread 클래스 상속

둘다 run() 메소드를 오버라이딩 하는 방식이다.

 

public class MyThread implements Runnable {
    @Override
    public void run() {
        // 수행 코드
    }
}

 

public class MyThread extends Thread {
    @Override
    public void run() {
        // 수행 코드
    }
}

 

스레드 생성


하지만 두가지 방법은 인스턴스 생성 방법에 차이가 있다.

Runnable 인터페이스를 구현한 경우는, 해당 클래스를 인스턴스화해서 Thread 생성자에 argument로 넘겨줘야 한다.

그리고 run()을 호출하면 Runnable 인터페이스에서 구현한 run()이 호출되므로 따로 오버라이딩하지 않아도 되는 장점이 있다.

public static void main(String[] args) {
    Runnable r = new MyThread();
    Thread t = new Thread(r, "mythread");
}

 

Thread 클래스를 상속받은 경우는, 상속받은 클래스 자체를 스레드로 사용할 수 있다.

또, Thread 클래스를 상속받으면 스레드 클래스의 메소드(getName())를 바로 사용할 수 있지만, Runnable 구현의 경우 Thread 클래스의 static 메소드인 currentThread()를 호출하여 현재 스레드에 대한 참조를 얻어와야만 호출이 가능하다.

public class ThreadTest implements Runnable {
    public ThreadTest() {}
    
    public ThreadTest(String name){
        Thread t = new Thread(this, name);
        t.start();
    }
    
    @Override
    public void run() {
        for(int i = 0; i <= 50; i++) {
            System.out.print(i + ":" + Thread.currentThread().getName() + " ");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

스레드 실행

스레드의 실행은 run() 호출이 아닌 start() 호출로 해야한다.

Why?

우리는 분명 run() 메소드를 정의했는데, 실제 스레드 작업을 시키려면 start()로 작업해야 한다고 한다.

run()으로 작업 지시를 하면 스레드가 일을 안할까? 그렇지 않다. 두 메소드 모두 같은 작업을 한다. 하지만 run() 메소드를 사용한다면, 이건 스레드를 사용하는 것이 아니다.

 

Java에는 콜 스택(call stack)이 있다. 이 영역이 실질적인 명령어들을 담고 있는 메모리로, 하나씩 꺼내서 실행시키는 역할을 한다.

만약 동시에 두 가지 작업을 한다면, 두 개 이상의 콜 스택이 필요하게 된다.

스레드를 이용한다는 건, JVM이 다수의 콜 스택을 번갈아가며 일처리를 하고 사용자는 동시에 작업하는 것처럼 보여준다.

즉, run() 메소드를 이용한다는 것은 main()의 콜 스택 하나만 이용하는 것으로 스레드 활용이 아니다. (그냥 스레드 객체의 run이라는 메소드를 호출하는 것 뿐이게 되는 것..)

start() 메소드를 호출하면, JVM은 알아서 스레드를 위한 콜 스택을 새로 만들어주고 context switching을 통해 스레드답게 동작하도록 해준다.

우리는 새로운 콜 스택을 만들어 작업을 해야 스레드 일처리가 되는 것이기 때문에 start() 메소드를 써야하는 것이다!

start()는 스레드가 작업을 실행하는데 필요한 콜 스택을 생성한 다음 run()을 호출해서 그 스택 안에 run()을 저장할 수 있도록 해준다.

 

스레드의 실행제어

스레드의 상태는 5가지가 있다

  • NEW : 스레드가 생성되고 아직 start()가 호출되지 않은 상태
  • RUNNABLE : 실행 중 또는 실행 가능 상태
  • BLOCKED : 동기화 블럭에 의해 일시정지된 상태(lock이 풀릴 때까지 기다림)
  • WAITING, TIME_WAITING : 실행가능하지 않은 일시정지 상태
  • TERMINATED : 스레드 작업이 종료된 상태

 

스레드로 구현하는 것이 어려운 이유는 바로 동기화와 스케줄링 때문이다.

스케줄링과 관련된 메소드는 sleep(), join(), yield(), interrupt()와 같은 것들이 있다.

start() 이후에 join()을 해주면 main 스레드가 모두 종료될 때까지 기다려주는 일도 해준다.



동기화

멀티스레드로 구현을 하다보면, 동기화는 필수적이다.

동기화가 필요한 이유는, 여러 스레드가 같은 프로세스 내의 자원을 공유하면서 작업할 때 서로의 작업이 다른 작업에 영향을 주기 때문이다.

스레드의 동기화를 위해선, 임계 영역(critical section)과 잠금(lock)을 활용한다.

임계영역을 지정하고, 임계영역을 가지고 있는 lock을 단 하나의 스레드에게만 빌려주는 개념으로 이루어져있다.

따라서 임계구역 안에서 수행할 코드가 완료되면, lock을 반납해줘야 한다.

 

스레드 동기화 방법

  • 임계 영역(critical section) : 공유 자원에 단 하나의 스레드만 접근하도록(하나의 프로세스에 속한 스레드만 가능)
  • 뮤텍스(mutex) : 공유 자원에 단 하나의 스레드만 접근하도록(서로 다른 프로세스에 속한 스레드도 가능)
  • 이벤트(event) : 특정한 사건 발생을 다른 스레드에게 알림
  • 세마포어(semaphore) : 한정된 개수의 자원을 여러 스레드가 사용하려고 할 때 접근 제한
  • 대기 가능 타이머(waitable timer) : 특정 시간이 되면 대기 중이던 스레드 깨움

 

synchronized 활용

synchronized를 활용해 임계영역을 설정할 수 있다.

서로 다른 두 객체가 동기화를 하지 않은 메소드를 같이 오버라이딩해서 이용하면, 두 스레드가 동시에 진행되므로 원하는 출력 값을 얻지 못한다.

이때 오버라이딩되는 부모 클래스의 메소드에 synchronized 키워드로 임계영역을 설정해주면 해결할 수 있다.

//synchronized : 스레드의 동기화. 공유 자원에 lock
public synchronized void saveMoney(int save){    // 입금
    int m = money;
    try{
        Thread.sleep(2000);    // 지연시간 2초
    } catch (Exception e){

    }
    money = m + save;
    System.out.println("입금 처리");

}

public synchronized void minusMoney(int minus){    // 출금
    int m = money;
    try{
        Thread.sleep(3000);    // 지연시간 3초
    } catch (Exception e){

    }
    money = m - minus;
    System.out.println("출금 완료");
}

 

wait()과 notify() 활용

스레드가 서로 협력관계일 경우에는 무작정 대기시키는 것으로 올바르게 실행되지 않기 때문에 사용한다.

  • wait() : 스레드가 lock을 가지고 있으면, lock 권한을 반납하고 대기하게 만듬
  • notify() : 대기 상태인 스레드에게 다시 lock 권한을 부여하고 수행하게 만듬

이 두 메소드는 동기화 된 영역(임계 영역)내에서 사용되어야 한다.

동기화 처리한 메소드들이 반복문에서 활용된다면, 의도한대로 결과가 나오지 않는다. 이때 wait()과 notify()를 try-catch 문에서 적절히 활용해 해결할 수 있다.

/**
* 스레드 동기화 중 협력관계 처리작업 : wait() notify()
* 스레드 간 협력 작업 강화
*/

public synchronized void makeBread(){
    if (breadCount >= 10){
        try {
            System.out.println("빵 생산 초과");
            wait();    // Thread를 Not Runnable 상태로 전환
        } catch (Exception e) {

        }
    }
    breadCount++;    // 빵 생산
    System.out.println("빵을 만듦. 총 " + breadCount + "개");
    notify();    // Thread를 Runnable 상태로 전환
}

public synchronized void eatBread(){
    if (breadCount < 1){
        try {
            System.out.println("빵이 없어 기다림");
            wait();
        } catch (Exception e) {

        }
    }
    breadCount--;
    System.out.println("빵을 먹음. 총 " + breadCount + "개");
    notify();
}

조건 만족 안할 시 wait(), 만족 시 notify()를 받아 수행한다.

'Language > JAVA' 카테고리의 다른 글

문자열 클래스  (0) 2022.05.05
Intrinsic Lock  (0) 2022.05.05
Auto Boxing & Unboxing  (0) 2022.05.05
Casting(업캐스팅 & 다운캐스팅)  (0) 2022.05.05
자바 가상 머신(Java Virtual Machine)  (0) 2022.05.05

[Java] 오토 박싱 & 오토 언박싱

 

자바에는 기본 타입과 Wrapper 클래스가 존재한다.

  • 기본 타입 : int, long, float, double, boolean 
  • Wrapper 클래스 : Integer, Long, Float, Double, Boolean 

 

박싱과 언박싱에 대한 개념을 먼저 살펴보자

박싱 : 기본 타입 데이터에 대응하는 Wrapper 클래스로 만드는 동작

언박싱 : Wrapper 클래스에서 기본 타입으로 변환

// 박싱
int i = 10;
Integer num = new Integer(i);

// 언박싱
Integer num = new Integer(10);
int i = num.intValue();

 

 

오토 박싱 & 오토 언박싱

JDK 1.5부터는 자바 컴파일러가 박싱과 언박싱이 필요한 상황에 자동으로 처리를 해준다.

// 오토 박싱
int i = 10;
Integer num = i;

// 오토 언박싱
Integer num = new Integer(10);
int i = num;

 

성능

편의성을 위해 오토 박싱과 언박싱이 제공되고 있지만, 내부적으로 추가 연산 작업이 거치게 된다.

따라서, 오토 박싱&언박싱이 일어나지 않도록 동일한 타입 연산이 이루어지도록 구현하자.

오토 박싱 연산

public static void main(String[] args) {
    long t = System.currentTimeMillis();
    Long sum = 0L;
    for (long i = 0; i < 1000000; i++) {
        sum += i;
    }
    System.out.println("실행 시간: " + (System.currentTimeMillis() - t) + " ms");
}

// 실행 시간 : 19 ms

동일 타입 연산

public static void main(String[] args) {
    long t = System.currentTimeMillis();
    long sum = 0L;
    for (long i = 0; i < 1000000; i++) {
        sum += i;
    }
    System.out.println("실행 시간: " + (System.currentTimeMillis() - t) + " ms") ;
}

// 실행 시간 : 4 ms

 

100만건 기준으로 약 5배의 성능 차이가 난다. 따라서 서비스를 개발하면서 불필요한 오토 캐스팅이 일어나는 지 확인하는 습관을 가지자.



[참고 사항]

'Language > JAVA' 카테고리의 다른 글

Intrinsic Lock  (0) 2022.05.05
Java에서의 Thread  (0) 2022.05.05
Casting(업캐스팅 & 다운캐스팅)  (0) 2022.05.05
자바 가상 머신(Java Virtual Machine)  (0) 2022.05.05
String StringBuilder StringBuffer 차이  (0) 2022.05.05

Casting(업캐스팅 & 다운캐스팅)

캐스팅이란?

변수가 원하는 정보를 다 갖고 있는 것

int a = 0.1; // (1) 에러 발생 X
int b = (int) true; // (2) 에러 발생 O, boolean은 int로 캐스트 불가

(1)은 0.1이 double형이지만, int로 될 정보 또한 가지고 있음

(2)는 true는 int형이 될 정보를 가지고 있지 않음


캐스팅이 필요한 이유는?

  1. 다형성 : 오버라이딩된 함수를 분리해서 활용할 수 있다.
  2. 상속 : 캐스팅을 통해 범용적인 프로그래밍이 가능하다.


형변환의 종류

  1. 묵시적 형변환 : 캐스팅이 자동으로 발생 (업캐스팅)Parent를 상속받은 Child는 Parent의 속성을 포함하고 있기 때문
  2. Parent p = new Child(); // (Parent) new Child()할 필요가 없음
  3. 명시적 형변환 : 캐스팅할 내용을 적어줘야 하는 경우 (다운캐스팅)다운캐스팅은 업캐스팅이 발생한 이후에 작용한다.
  4. Parent p = new Child();
    Child c = (Child) p;

예시 문제

class Parent {
	int age;

	Parent() {}

	Parent(int age) {
		this.age = age;
	}

	void printInfo() {
		System.out.println("Parent Call!!!!");
	}
}

class Child extends Parent {
	String name;

	Child() {}

	Child(int age, String name) {
		super(age);
		this.name = name;
	}

	@Override 
	void printInfo() {
		System.out.println("Child Call!!!!");
	}

}

public class test {
    public static void main(String[] args) {
        Parent p = new Child();
        
        p.printInfo(); // 문제1 : 출력 결과는?
        Child c = (Child) new Parent(); //문제2 : 에러 종류는?
    }
}

문제1 : Child Call!!!!

자바에서는 오버라이딩된 함수를 동적 바인딩하기 때문에, Parent에 담겼어도 Child의 printInfo() 함수를 불러오게 된다.

문제2 : Runtime Error

컴파일 과정에서는 데이터형의 일치만 따진다. 프로그래머가 따로 (Child)로 형변환을 해줬기 때문에 컴파일러는 문법이 맞다고 생각해서 넘어간다. 하지만 런타임 과정에서 Child 클래스에 Parent 클래스를 넣을 수 없다는 것을 알게 되고, 런타임 에러가 나오게 되는것!

'Language > JAVA' 카테고리의 다른 글

Java에서의 Thread  (0) 2022.05.05
Auto Boxing & Unboxing  (0) 2022.05.05
자바 가상 머신(Java Virtual Machine)  (0) 2022.05.05
String StringBuilder StringBuffer 차이  (0) 2022.05.05
Call by value와 Call by reference  (0) 2022.05.05

자바 가상 머신(Java Virtual Machine)

시스템 메모리를 관리하면서, 자바 기반 애플리케이션을 위해 이식 가능한 실행 환경을 제공함

 

 

JVM은, 다른 프로그램을 실행시키는 것이 목적이다.

갖춘 기능으로는 크게 2가지로 말할 수 있다.

 

  1. 자바 프로그램이 어느 기기나 운영체제 상에서도 실행될 수 있도록 하는 것
  2. 프로그램 메모리를 관리하고 최적화하는 것

 

JVM은 코드를 실행하고, 해당 코드에 대해 런타임 환경을 제공하는 프로그램에 대한 사양임

 

개발자들이 말하는 JVM은 보통 어떤 기기상에서 실행되고 있는 프로세스, 특히 자바 앱에 대한 리소스를 대표하고 통제하는 서버를 지칭한다.

자바 애플리케이션을 클래스 로더를 통해 읽어들이고, 자바 API와 함께 실행하는 역할. JAVA와 OS 사이에서 중개자 역할을 수행하여 OS에 구애받지 않고 재사용을 가능하게 해준다.

 

JVM에서의 메모리 관리


JVM 실행에 있어서 가장 일반적인 상호작용은, 힙과 스택의 메모리 사용을 확인하는 것


실행 과정

  1. 프로그램이 실행되면, JVM은 OS로부터 이 프로그램이 필요로하는 메모리를 할당받음. JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리함
  2. 자바 컴파일러(JAVAC)가 자바 소스코드를 읽고, 자바 바이트코드(.class)로 변환시킴
  3. 변경된 class 파일들을 클래스 로더를 통해 JVM 메모리 영역으로 로딩함
  4. 로딩된 class파일들은 Execution engine을 통해 해석됨
  5. 해석된 바이트 코드는 메모리 영역에 배치되어 실질적인 수행이 이루어짐. 이러한 실행 과정 속 JVM은 필요에 따라 스레드 동기화나 가비지 컬렉션 같은 메모리 관리 작업을 수행함

 


자바 컴파일러

자바 소스코드(.java)를 바이트 코드(.class)로 변환시켜줌


클래스 로더

JVM은 런타임시에 처음으로 클래스를 참조할 때 해당 클래스를 로드하고 메모리 영역에 배치시킴. 이 동적 로드를 담당하는 부분이 바로 클래스 로더


Runtime Data Areas

JVM이 운영체제 위에서 실행되면서 할당받는 메모리 영역임

총 5가지 영역으로 나누어짐 : PC 레지스터, JVM 스택, 네이티브 메서드 스택, 힙, 메서드 영역

(이 중에 힙과 메서드 영역은 모든 스레드가 공유해서 사용함)

PC 레지스터 : 스레드가 어떤 명령어로 실행되어야 할지 기록하는 부분(JVM 명령의 주소를 가짐)

스택 Area : 지역변수, 매개변수, 메서드 정보, 임시 데이터 등을 저장

네이티브 메서드 스택 : 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역

 : 런타임에 동적으로 할당되는 데이터가 저장되는 영역. 객체나 배열 생성이 여기에 해당함

(또한 힙에 할당된 데이터들은 가비지컬렉터의 대상이 됨. JVM 성능 이슈에서 가장 많이 언급되는 공간임)

메서드 영역 : JVM이 시작될 때 생성되고, JVM이 읽은 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드 및 메서드 코드, 정적 변수, 메서드의 바이트 코드 등을 보관함



가비지 컬렉션(Garbage Collection)

자바 이전에는 프로그래머가 모든 프로그램 메모리를 관리했음 하지만, 자바에서는 JVM이 프로그램 메모리를 관리함!

JVM은 가비지 컬렉션이라는 프로세스를 통해 메모리를 관리함. 가비지 컬렉션은 자바 프로그램에서 사용되지 않는 메모리를 지속적으로 찾아내서 제거하는 역할을 함.

실행순서 : 참조되지 않은 객체들을 탐색 후 삭제 → 삭제된 객체의 메모리 반환 → 힙 메모리 재사용

'Language > JAVA' 카테고리의 다른 글

Auto Boxing & Unboxing  (0) 2022.05.05
Casting(업캐스팅 & 다운캐스팅)  (0) 2022.05.05
String StringBuilder StringBuffer 차이  (0) 2022.05.05
Call by value와 Call by reference  (0) 2022.05.05
자바 컴파일과정  (0) 2022.05.05
 

String, StringBuffer, StringBuilder


분류 String StringBuffer StringBuilder
변경 Immutable Mutable Mutable
동기화   Synchronized 가능 (Thread-safe) Synchronized 불가능.

 


1. String 특징

  • new 연산을 통해 생성된 인스턴스의 메모리 공간은 변하지 않음 (Immutable)
  • Garbage Collector로 제거되어야 함.
  • 문자열 연산시 새로 객체를 만드는 Overhead 발생
  • 객체가 불변하므로, Multithread에서 동기화를 신경 쓸 필요가 없음. (조회 연산에 매우 큰 장점)

String 클래스 : 문자열 연산이 적고, 조회가 많은 멀티쓰레드 환경에서 좋음

 

2. StringBuffer, StringBuilder 특징

  • 공통점
    • new 연산으로 클래스를 한 번만 만듬 (Mutable)
    • 문자열 연산시 새로 객체를 만들지 않고, 크기를 변경시킴
    • StringBuffer와 StringBuilder 클래스의 메서드가 동일함.
  • 차이점
    • StringBuffer는 Thread-Safe함 / StringBuilder는 Thread-safe하지 않음 (불가능)

 

StringBuffer 클래스 : 문자열 연산이 많은 Multi-Thread 환경

StringBuilder 클래스 : 문자열 연산이 많은 Single-Thread 또는 Thread 신경 안쓰는 환경

'Language > JAVA' 카테고리의 다른 글

Auto Boxing & Unboxing  (0) 2022.05.05
Casting(업캐스팅 & 다운캐스팅)  (0) 2022.05.05
자바 가상 머신(Java Virtual Machine)  (0) 2022.05.05
Call by value와 Call by reference  (0) 2022.05.05
자바 컴파일과정  (0) 2022.05.05

상당히 기본적인 질문이지만, 헷갈리기 쉬운 주제다.

call by value

값에 의한 호출

함수가 호출될 때, 메모리 공간 안에서는 함수를 위한 별도의 임시공간이 생성됨 (종료 시 해당 공간 사라짐)

call by value 호출 방식은 함수 호출 시 전달되는 변수 값을 복사해서 함수 인자로 전달함

이때 복사된 인자는 함수 안에서 지역적으로 사용되기 때문에 local value 속성을 가짐

따라서, 함수 안에서 인자 값이 변경되더라도, 외부 변수 값은 변경안됨


예시

void func(int n) {
    n = 20;
}

void main() {
    int n = 10;
    func(n);
    printf("%d", n);
}

printf로 출력되는 값은 그대로 10이 출력된다.

 

call by reference

참조에 의한 호출

call by reference 호출 방식은 함수 호출 시 인자로 전달되는 변수의 레퍼런스를 전달함

따라서 함수 안에서 인자 값이 변경되면, 아규먼트로 전달된 객체의 값도 변경됨

void func(int *n) {
    *n = 20;
}

void main() {
    int n = 10;
    func(&n);
    printf("%d", n);
}

printf로 출력되는 값은 20이 된다.



Java 함수 호출 방식

자바의 경우, 함수에 전달되는 인자의 데이터 타입에 따라 함수 호출 방식이 달라짐

  • primitive type(원시 자료형) : call by value
  • int, short, long, float, double, char, boolean
  • reference type(참조 자료형) : call by reference
  • array, Class instance

자바의 경우, 항상 call by value로 값을 넘긴다.

C/C++와 같이 변수의 주소값 자체를 가져올 방법이 없으며, 이를 넘길 수 있는 방법 또한 있지 않다.

reference type(참조 자료형)을 넘길 시에는 해당 객체의 주소값을 복사하여 이를 가지고 사용한다.

따라서 원본 객체의 프로퍼티까지는 접근이 가능하나, 원본 객체 자체를 변경할 수는 없다.

아래의 예제 코드를 봐보자.

User a = new User("gyoogle");   // 1

foo(a);

public void foo(User b){        // 2
    b = new User("jongnan");    // 3
}

/*
==========================================

// 1 : a에 User 객체 생성 및 할당(새로 생성된 객체의 주소값을 가지고 있음)
 
 a   -----> User Object [name = "gyoogle"]
 
==========================================

// 2 : b라는 파라미터에 a가 가진 주소값을 복사하여 가짐

 a   -----> User Object [name = "gyoogle"]
               ↑     
 b   -----------
 
==========================================

// 3 : 새로운 객체를 생성하고 새로 생성된 주소값을 b가 가지며 a는 그대로 원본 객체를 가리킴
 
 a   -----> User Object [name = "gyoogle"]
                   
 b   -----> User Object [name = "jongnan"]
 
*/

파라미터에 객체/값의 주소값을 복사하여 넘겨주는 방식을 사용하고 있는 Java는 주소값을 넘겨 주소값에 저장되어 있는 값을 사용하는 call by reference라고 오해할 수 있다.

이는 C/C++와 Java에서 변수를 할당하는 방식을 보면 알 수 있다.

// c/c++ 
 
 int a = 10;
 int b = a;
 
 cout << &a << ", " << &b << endl; // out: 0x7ffeefbff49c, 0x7ffeefbff498
 
 a = 11;
 
 cout << &a << endl; // out: 0x7ffeefbff49c

//java
 
 int a = 10;
 int b = a;
 
 System.out.println(System.identityHashCode(a));    // out: 1627674070
 System.out.println(System.identityHashCode(b));    // out: 1627674070
 
 a = 11;

 System.out.println(System.identityHashCode(a));    // out: 1360875712

C/C++에서는 생성한 변수마다 새로운 메모리 공간을 할당하고 이에 값을 덮어씌우는 형식으로 값을 할당한다. (* 포인터를 사용한다면, 같은 주소값을 가리킬 수 있도록 할 수 있다.)

Java에서 또한 생성한 변수마다 새로운 메모리 공간을 갖는 것은 마찬가지지만, 그 메모리 공간에 값 자체를 저장하는 것이 아니라 값을 다른 메모리 공간에 할당하고 이 주소값을 저장하는 것이다.

이를 다음과 같이 나타낼 수 있다.

  C/C++        |        Java
               |
a -> [ 10 ]    |   a -> [ XXXX ]     [ 10 ] -> XXXX(위치)
b -> [ 10 ]    |   b -> [ XXXX ]
               |
             값 변경
a -> [ 11 ]    |   a -> [ YYYY ]     [ 10 ] -> XXXX(위치)
b -> [ 10 ]    |   b -> [ XXXX ]     [ 11 ] -> YYYY(위치)

b = a;일 때 a의 값을 b의 값으로 덮어 씌우는 것은 같지만, 실제 값을 저장하는 것과 값의 주소값을 저장하는 것의 차이가 존재한다.

즉, Java에서의 변수는 [할당된 값의 위치]를 [값]으로 가지고 있는 것이다.

C/C++에서는 주소값 자체를 인자로 넘겼을 때 값을 변경하면 새로운 값으로 덮어 쓰여 기존 값이 변경되고, Java에서는 주소값이 덮어 쓰여지므로 원본 값은 전혀 영향이 가지 않는 것이다. (객체의 속성값에 접근하여 변경하는 것은 직접 접근하여 변경하는 것이므로 이를 가리키는 변수들에서 변경이 일어난다.)

객체 접근하여 속성값 변경

a : [ XXXX ]  [ Object [prop : ~ ] ] -> XXXX(위치)
b : [ XXXX ]

prop : ~ (이 또한 변수이므로 어딘가에 ~가 저장되어있고 prop는 이의 주소값을 가지고 있는 셈)
prop : [ YYYY ]    [ ~ ] -> YYYY(위치)

a.prop = * (a를 통해 prop를 변경) 

prop : [ ZZZZ ]    [ ~ ] -> YYYY(위치)
                   [ * ] -> ZZZZ

b -> Object에 접근 -> prop 접근 -> ZZZZ

위와 같은 이유로 Java에서 인자로 넘길 때는 주소값이란 값을 복사하여 넘기는 것이므로 call by value라고 할 수 있다.

출처 : Is Java “pass-by-reference” or “pass-by-value”? - Stack Overflow

 

정리

Call by value의 경우, 데이터 값을 복사해서 함수로 전달하기 때문에 원본의 데이터가 변경될 가능성이 없다. 하지만 인자를 넘겨줄 때마다 메모리 공간을 할당해야해서 메모리 공간을 더 잡아먹는다.

Call by reference의 경우 메모리 공간 할당 문제는 해결했지만, 원본 값이 변경될 수 있다는 위험이 존재한다.

'Language > JAVA' 카테고리의 다른 글

Auto Boxing & Unboxing  (0) 2022.05.05
Casting(업캐스팅 & 다운캐스팅)  (0) 2022.05.05
자바 가상 머신(Java Virtual Machine)  (0) 2022.05.05
String StringBuilder StringBuffer 차이  (0) 2022.05.05
자바 컴파일과정  (0) 2022.05.05

들어가기전

자바는 OS에 독립적인 특징을 가지고 있습니다. 그게 가능한 이유는 JVM(Java Vitual Machine) 덕분인데요. 그렇다면 JVM(Java Vitual Machine)의 어떠한 기능 때문에, OS에 독립적으로 실행시킬 수 있는지 자바 컴파일 과정을 통해 알아보도록 하겠습니다.

자바 컴파일 순서

  1. 개발자가 자바 소스코드(.java)를 작성합니다.
  2. 자바 컴파일러(Java Compiler)가 자바 소스파일을 컴파일합니다. 이때 나오는 파일은 자바 바이트 코드(.class)파일로 아직 컴퓨터가 읽을 수 없는 자바 가상 머신이 이해할 수 있는 코드입니다. 바이트 코드의 각 명령어는 1바이트 크기의 Opcode와 추가 피연산자로 이루어져 있습니다.
  3. 컴파일된 바이크 코드를 JVM의 클래스로더(Class Loader)에게 전달합니다.
  4. 클래스 로더는 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data area), 즉 JVM의 메모리에 올립니다.
    • 클래스 로더 세부 동작
      1. 로드 : 클래스 파일을 가져와서 JVM의 메모리에 로드합니다.
      2. 검증 : 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 구성되어 있는지 검사합니다.
      3. 준비 : 클래스가 필요로 하는 메모리를 할당합니다. (필드, 메서드, 인터페이스 등등)
      4. 분석 : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경합니다.
      5. 초기화 : 클래스 변수들을 적절한 값으로 초기화합니다. (static 필드)
  5. 실행엔진(Execution Engine)은 JVM 메모리에 올라온 바이크 코드들을 명령어 단위로 하나씩 가져와서 실행합니다. 이때, 실행 엔진은 두가지 방식으로 변경합니다.
    1. 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행합니다. 하나하나의 실행은 빠르나, 전체적인 실행 속도가 느리다는 단점을 가집니다.
    2. JIT 컴파일러(Just-In-Time Compiler) : 인터프리터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 바이너리 코드로 변경하고 이후에는 해당 메서드를 더이상 인터프리팅 하지 않고, 바이너리 코드로 직접 실행하는 방식입니다. 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일된 바이너리 코드를 실행하는 것이기 때문에 전체적인 실행속도는 인터프리팅 방식보다 빠릅니다.

Reference (추가로 읽어보면 좋은 자료)

[1] https://steady-snail.tistory.com/67

[2] https://aljjabaegi.tistory.com/387

'Language > JAVA' 카테고리의 다른 글

Auto Boxing & Unboxing  (0) 2022.05.05
Casting(업캐스팅 & 다운캐스팅)  (0) 2022.05.05
자바 가상 머신(Java Virtual Machine)  (0) 2022.05.05
String StringBuilder StringBuffer 차이  (0) 2022.05.05
Call by value와 Call by reference  (0) 2022.05.05

+ Recent posts