자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다. ex) InputStream, OutputStream, DB Connection ...

item8에서 알 수 있듯이 안전망으로 finalizer를 활용할 수 있으나, finalizer는 확실하게 믿을 순 없다

 

이를 보완하기 위해 try-with-resources를 사용하도록 하자

// 둘 이상의 자원에서 try-finally: 중첩 되어 지저분해 보이고, exception이 일어난 경우 디버깅이 어렵다
static void copy(String src, String dst) throws IOException {
	InputStream in = new FileInputStream(src);
	try{
		OutputStream out = new FileOutputStream(dst);
		try{
			byte[] buf = new byte[BUFFER_SIZE];
			int n;
			while((n=in.read(buf))>=0){
				out.write(buf, 0, n);
			}
		} finally {
			out.close();
		}
	} finally {
		in.close();
	}
}

위 예시에서 예외는 try 블록과 finally 블록 모두에서 발생할 수 있다.

예를 들어 FileOutputStream 메서드가 예외를 던지고, 같은 이유로 close 메서드도 실패할 때,

두 번째 예외가 첫 번째 예외를 완전히 집어삼켜 버린다. => 스택 추적 내역에 첫 번째 예외에 관한 정보X

 

// try-with-resources: 자원을 회수하는 올바른 선택, 코드는 더 짧고 분명해지며, 예외 케이스도 유용하게 볼 수 있다.
static void copy(String src, String dst) throws IOException {
	try(
			InputStream in = new FileInputStream(src);
			OutputStream out = new FileOutputStream(dst);
	){
			byte[] buf = new byte[BUFFER_SIZE];
			int n;
			while((n=in.read(buf))>=0){
				out.write(buf, 0, n);
			}
	}
}

Java7 부터 Closeable리소스 와 관련하여 아래와 같이 키워드 뒤의 괄호 안에 리소스를 만들 수 있다.

try (initialize resources here) {
   ...
}

그리고 코드 블록이 완료되면 자동으로 닫히기 때문에 finally가 필요 없다 .

 

이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다

AutoCloseable은 단순히 void를 반환하는 close 메서드 하나만 덩그러니 정의한 인터페이스다.

이 경우는 try-finally와는 다르게 양쪽에서 예외가 발생하면, close에서 발생한 예외는 숨겨지고 맨 처음에 발생한 예외가 기록된다.

단, 이렇게 숨겨진 예외들도 그냥 버려지지는 않고, 스택 추적 내역에 ‘숨겨졌다 (suppressed)’는 꼬리표를 달고 출력된다.

 

또한, 자바 7에서 Throwable에 추가 된 getSuppressed 메서드를 이용하면 프로그램 코드에서 가져올 수도 있다.

catch (Throwable e) { 
	Throwable[] suppExe = e.getSuppressed(); 
	for (int i = 0; i < suppExe.length; i++) { 
		System.out.println("Suppressed Exceptions:"); 
		System.out.println(suppExe[i]); 
	} 
} 


보통의 try-finally에서처 럼 try-with-resources에서도 catch 절을 쓸 수 있다. catch 절 덕분에 try 문을 더 중첩하지 않고도 다수의 예외를 처리할 수 있다.

 

 

참고로, 스트림을 닫을 때에는 IOUtils를 쓰는 방법도 있다. (대신 이 방법을 사용할 경우 Exception이 제대로 나오지 않음)

commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/IOUtils.html#closeQuietly(java.io.InputStream)

 

IOUtils (Apache Commons IO 2.8.0 API)

Skips characters from an input character stream. This implementation guarantees that it will read as many characters as possible before giving up; this may not always be the case for skip() implementations in subclasses of Reader. Note that the implementat

commons.apache.org

closeQuietly는 Closeable의 varargs 타입을 파라미터로 받고, 이 closeable들을 모두 닫는다.


 

  • try-catch-finally
package com.example.sypark9646.item09;

import static org.apache.tomcat.util.http.fileupload.IOUtils.copy;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class TryCatchFinallyTest {

		private static String root =
				"/Users/soyeon/Documents/GitHub/Effective_Java_Study/sypark9646/src/main/java/com/example/sypark9646/item09";
		private static String inputFileName = root + "/testin.txt";
		private static String outputFileName = root + "/testout_try_catch_finally.txt";

		public static void main(String[] args) throws IOException {

				try {
						final InputStream in = new FileInputStream(inputFileName);
						try {
								final OutputStream out = new FileOutputStream(outputFileName);
								try {
										copy(in, out);
										out.flush();
								} finally {
										out.close();
								}
						} finally {
								in.close();
						}
				} catch (IOException exc) {
						throw new IOException(exc);
				}
		}
}

 

  • try-with-resources
package com.example.sypark9646.item09;

import static org.apache.tomcat.util.http.fileupload.IOUtils.copy;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class TryWithResourcesTest {

		private static String root =
				"/Users/soyeon/Documents/GitHub/Effective_Java_Study/sypark9646/src/main/java/com/example/sypark9646/item09";
		private static String inputFileName = root + "/testin.txt";
		private static String outputFileName = root + "/testout_try_with_resources.txt";

		public static void main(String[] args) throws IOException {

				try (
						final InputStream in = new FileInputStream(inputFileName);
						final OutputStream out = new FileOutputStream(outputFileName);
				) {
						copy(in, out);
						out.flush();
				} catch (IOException exc) {
						throw new IOException(exc);
				}
		}
}

자바는 두 가지 객체 소멸자는 finalizer와 cleaner가 있다.

이때, finailzer는 예측할 수 없고, 상황에 따라 위험할 수 있어 쓰지 않는 것이 좋다. (오동작, 낮은 성능, 이식성 문제)

cleaner 또한 finalizer 보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

 

1) finalizer와 cleaner는객체에 접근을 할 수 없게 된 후, 이 것이 실행되기까지 얼마가 걸릴지 알 수 없다. 심지어 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다.

=> 즉, 제때 실행되어야 하는 작업은 절대 할 수 없다. (ex. file close, db 공유자원의 영구 lock 해제 등의 인스턴스 자원 회수)

왜냐면 finalizer와 cleaner은 GC에 의해 실행되고, GC 알고리즘마다 다르게 실행되기 때문이다.

 

자바 언어 명세는 어떤 스레드가 finalizer를 수행할지 명시하지 않는 반면, cleaner는 자신을 수행할 스레드를 제어할 수 있다는 면에서 조금 낫다. 하지만 여전히 백그라운드에서 수행되며 가비지 컬렉터의 통제하에 있으니 즉각 수행되리라 는 보장은 없다.

 

item07에서 예시로 사용해 보았던

`System.gc` 와 `System.runFinalization` 또한 gc를 건드려서 finalizer와 cleaner가 실행 될 가능성을 높여줄 수는 있으나, 보장해주진 않는다.

(단, `System.runFinalizersOnExit`와 그 쌍둥이 인 `Runtime. runFinalizersOnExit` 다. 하지만 이 두 메서드는 심각한 결함 때문에 수십 년간 지탄받아 왔다.[ThreadStop])

  • System.gc
    • 명시적으로 가비지 컬렉션이 일어나도록 하는 코드이다
    • 모든 스레드가 중단되기 때문에 코드단에서 호출하면 안된다
  • System.runFinalization
    • method는 종료 대기중인 모든 개체의 종료 방법을 실행하지만, 언제 실행될지/실행될지 여부는 보장하지 못한다
  • System.runFinalizersOnExit & Runtime.runFinalizersOnExit
    • 프로세스가 끝날 때 수거하지 않은 개체를 수거하도록 설정한다

 

 

2) finalizer 동작 중 발생한 예외는 무시되고, 처리할 작업이 남았더라도 그 순간 종료된다

=> 만약 다른 스레드가 이처럼 훼손된 객체를 사용하려 한다면 어떻게 동작할지 예측할 수 없다.

일반적으로는 잡지 못한 예외가 스레드를 중단시키고 스택 추적 내역을 출력하겠지만, finalizer에서는 경고조차 출력하지 않는다.

c.f.) cleaner는 자신의 스레드를 통제하기 때문에 이 문제에 대해서는 괜찮다.

 

 

3) finalizer와 cleaner는 심각한 성능 문제도 동반한다

=> finalizer가 가비지 컬렉터의 효율을 떨어뜨리기 때문이다

 

 

4) finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다.

finalizer 공격 원리는 생성자나 직렬화 과정 (readObject와 readResolve 메서드)에서 예외가 발생하면,

이 생성이 되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 된다.

=> 이 경우 finalizer는 정적 필드에 자신의 참조를 할당하여 가비 지 컬렉터가 수집하지 못하게 막을 수 있다.

해결 방법1 : 클래스를 final로 만들자(final 클래스들은 그 누구도 하위 클래스를 만들 수 없기 때문)
해결 방법2: final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언

 

 


finalizer나 cleaner를 대신 할 수 있는 방법은 무엇이 있을까?

 

AutoCloseable 을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다

(일반적으로 예외가 발생해도 제대로 종료되도록 try-withresources를 사용해야 한다. 아이템 9 참조).

추가적으로 각 인스턴스는 자신이 닫혔는지 추적하는 것이 좋다. 즉, close 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렸다면 IllegalStateException을 던지록 하자.


그렇다면 finalizer이나 cleaner를 써야하는 경우는 언제일까?

1) 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할로서 사용할 때

cleaner나 finalizer가 즉시 (혹은 끝까지) 호출되리라는 보장은 없지만, 클라이언트가 하지 않은 자원 회수를 늦게라도 해주는 것이 아예 안 하는 것보다는 낫기 때문이다. 그래도 이런 안전망 역할 의 finalizer를 작성할 때는 그럴만한 값어치가 있는지 생각해 보아야 한다.

 

ex) 안전망 역할의 finalizer: FileInputStream, FileOutputStream, ThreadPoolExecutor

(finalize는 java9부터 deprecated 되어서 java11을 사용하는 나는 메소드 내부는 볼 수 없었다)

  • FileInputStream & FileOutputStream: Ensures that the `close` method of this file stream is called when there are no more references to it.

  • ThreadPoolExecutor: Invokes shutdown when this executor is no longer referenced and it has no threads.

@Override
protected void finalize() throws Throwable {
	try {
		... // cleanup subclass state
	} finally {
		super.finalize();
	}
}

기본적으로는 deprecated된 Object.finalize()를 위와 같이 오버라이드 하고 있다.

docs.oracle.com/javase/9/docs/api/java/lang/Object.html#finalize--

 

Object (Java SE 9 & JDK 9 )

Called by the garbage collector on an object when garbage collection determines that there are no more references to the object. A subclass overrides the finalize method to dispose of system resources or to perform other cleanup. The general contract of fi

docs.oracle.com

 

 

c.f. finalizer 와 달리 cleaner는 클래스의 public API에 나타나지 않는다

// 코드 8-1 cleaner를 안전망으로 활용하는 AutoCloseable 클래스
public class Room implements AutoCloseable {
	private static final Cleaner cleaner = Cleaner.create();

	/* static으로 선언된 중첩 클래스인 State는 cleaner가 방을 청소할 때 수거할 자 원들을 담고 있다. */
	// 청소가 필요한 자원.절대 Room을 참조해서는 안 된다! 만약 참조한다면 순환참조가 생김
	private static class State implements Runnable {
		int numJunkPiles; // 방(Room) 안의 쓰레기 수 => 수거할 자원

		State(int numJunkPiles) {
			this.numJunkPiles = numJunkPiles;
		}

		// close 메서드나 cleaner가 호출한다. => cleanable에 의해 딱 한 번만 호출된다
		@Override
		public void run() {
			System.out.println("방 청소");
			numJunkPiles = 0;
		}

	}

	// 방의 상태. cleanable과 공유한다.
	private final State state;

	// cleanable 객체. 수거 대상이 되면 방을 청소한다. => Room 생성자에서 cleaner에 Room과 State를 등록할 때
	private final Cleaner.Cleanable cleanable;

	public Room(int numJunkPiles) {
		state = new State(numJunkPiles);
		cleanable = cleaner.register(this, state);
	}

	@Override
	public void close() {
		cleanable.clean();
	}

}

run 메서드가 호출되는 상황은 둘 중 하나다. 

1 - 보통은 Room의 close 메서드를 호출할 때이다. close 메서드에서 Cleanable의 clean을 호출하면 이 메서드 안에서 run을 호출

2 - 가비지 컬렉터가 Room을 회수할 때 까지 클라이언트가 close를 호출하지 않는다면, cleaner가 State.run 메서드를 호출

 

이 때, State가 정적 중첩 클래스인 이유는, 정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖게 되기 때문에 GC가 회수할 수 없다.

이와 유사하게 람다 역시 바깥 객체의 참조를 갖기 쉬우니 사용하지 않는 것이 좋다.

 

/* 올바른 예제 */
class Adult {
	public static void main(String[] args) {
		try (Room myRoom = new Room(7)) { // "안녕" "방청소" 순서대로 출력
			System.out.println("안녕〜");
		}
	}
}

/* 틀린 예제 - System.exit을 호출할 때의 cleaner 동작은 구현하기 나름이다. 청소가 이뤄질지는 보장 하지 않는다 */
public class Teenager {
	public static void main(String[] args) {
		new Room(99);
		System.out.println("아무렴"); // "아무렴" 출력 이후 아무 일도 일어나지 않는다 => 예측할 수 없는 상황
	}
}

 

 

2) 네이티브 피어와 연결된 객체에서 사용할 때(단, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않은 경우)

  • 네이티브 피어(native peer)
    • 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체
    • 네이티브 피어는 자바 객체 가 아니니 가비지 컬렉터슨 그 존재를 알지 못한다. 그 결과 자바 피어를 회수 할 때 네이티브 객체까지 회수하지 못한다.

=> 성능 저하를 감당할 수 없거나 네이티브 피어가 사용하는 자원을 즉시 회수해야 한다면 `close` 메서드를 사용해야 한다.

 

 

그룹화 하기 위해 sorted된 string을 key로 해서 map에 넣도록 했다.

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class Solution {
	public List<List<String>> groupAnagrams(String[] strs) {
		List<List<String>> answer = new ArrayList<>();

		HashMap<String, ArrayList<String>> map = new HashMap<>();
		for (String str : strs) {
			String key = Stream.of(str.split("")).sorted().collect(Collectors.joining());

			if (!map.containsKey(key)) {
				map.put(key, new ArrayList<String>());
			}
			map.get(key).add(str);
		}

		for (String key : map.keySet()) {
			answer.add(map.get(key));
		}

		return answer;
	}
}

 

single string을 정렬하는 방법으로는 스트림으로 정렬하는 방법도 있고,

char로 바꾼 다음에 Arrays.sort를 쓴 후 다시 스트링으로 합치는 방법이 있는데 테스트 해 보니 후자가 더 빠르다는 것을 알게 되었다.

import java.util.*;
class Solution {
	public List<List<String>> groupAnagrams(String[] strs) {
		List<List<String>> answer = new ArrayList<>();
		HashMap<String, ArrayList<String>> map = new HashMap<>();

		for (String str : strs) {
			char[] keys = str.toCharArray();
			Arrays.sort(keys);
			String key = new String(keys);

			if (!map.containsKey(key))
				map.put(key, new ArrayList<>());
			map.get(key).add(str);
		}

		for (String key : map.keySet()) {
			answer.add(map.get(key));
		}

		return answer;
	}
}

'알고리즘 공부 > leetcode' 카테고리의 다른 글

leetcode: Decode String  (0) 2021.02.14
leetcode: Task Scheduler  (0) 2021.02.07
leetcode: Sliding Window Maximum  (0) 2021.02.07
leetcode: Longest Increasing Subsequence  (0) 2021.02.07
leetcode: Jump Game II  (0) 2021.01.31

자리수가 가장 높은 알파벳에 순서대로 높은 값을 부여하면 되는 문제이다.

그렇지만 캐리 등이 있을 수 있다는 점을 염두해서 문제를 풀도록 해야한다.

 

이를 위해 예시 GCF + ACDEB의 경우

(100G+10C+1F)+(10000A+1000C+100D+10E+1B)로 변환하여

10000A+1010C+100D+100G+10E+1F+1B로 계산하고, 계수가 높은 순으로 높은 숫자를 부여해서 계산하도록 하면 된다.

import java.util.*;

public class Main {
	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		int n = in.nextInt();
		int alphabets[] = new int[26];

		for (int i = 0; i < n; i++) {
			String str = in.next();
			char ch[] = str.toCharArray();
			int index = 1;

			for (int j = ch.length - 1; j >= 0; j--) {
				alphabets[ch[j] - 'A'] += index;
				index *= 10;
			}
		}

		PriorityQueue<Alphabet> queue = new PriorityQueue<Alphabet>();
		for (int i = 0; i < alphabets.length; i++) {
			queue.add(new Alphabet(i, alphabets[i]));
		}

		int index = 9;
		int sum = 0;
		while (!queue.isEmpty()) {
			Alphabet alphabet = queue.poll();
			sum += alphabet.value * index;
			index--;
		}

		System.out.println(sum);
	}
}

class Alphabet implements Comparable {
	int index;
	int value;

	public Alphabet(int index, int value) {
		this.index = index;
		this.value = value;
	}

	@Override
	public int compareTo(Object o) {
		Alphabet alphabet = (Alphabet) o;
		return -Integer.compare(this.value, alphabet.value);
	}
}

'알고리즘 공부 > boj' 카테고리의 다른 글

boj 2417: 정수 제곱근  (0) 2021.03.15
boj 1789: 수들의 합  (1) 2021.03.15
boj 2638: 치즈  (0) 2021.02.14
boj 1766: 문제집  (0) 2021.02.12
boj 1504:특정한 최단경로  (0) 2021.02.07

치즈는 4변 중에서 적어도 2변 이상이 실내온도의 공기와 접촉하면 녹는다.

이 때, dfs를 돌며 바로바로 치즈를 녹이는 식으로 문제를 풀면 안되고, 미리 어떤 치즈가 녹을지 체크를 한 뒤 해당 체크 부분만 녹이도록 해야 한다.

풀이는 아래와 같다.

import java.util.*;

public class Main {
	public static int[] dr = { 0, 0, 1, -1 };
	public static int[] dc = { 1, -1, 0, 0 };

	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		int n = in.nextInt();
		int m = in.nextInt();
		int mat[][] = new int[n][m];

		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				mat[i][j] = in.nextInt();
			}
		}

		int time = 0;
		while (true) {
			boolean isVisited[][] = new boolean[n][m];
			boolean isMelted = false;

			dfs(0, 0, n, m, mat, isVisited);
			for (int i = 0; i < n; i++) {
				for (int j = 0; j < m; j++) {
					if (mat[i][j] == 1 && meltCheese(i, j, n, m, mat)) {
						isMelted = true;
					}
				}
			}

			if (!isMelted) {
				break;
			}
			time++;
		}
		System.out.println(time);
	}

	private static void dfs(int r, int c, int n, int m, int[][] mat, boolean[][] isVisited) {
		mat[r][c] = -1;
		isVisited[r][c] = true;
		for (int i = 0; i < 4; i++) {
			int row = r + dr[i];
			int col = c + dc[i];
			if (isBoundary(row, col, n, m) && mat[row][col] != 1 && !isVisited[row][col]) {
				dfs(row, col, n, m, mat, isVisited);
			}
		}
	}

	private static boolean meltCheese(int r, int c, int n, int m, int mat[][]) {
		int airs = 0;
		for (int i = 0; i < 4; i++) {
			int row = r + dr[i];
			int col = c + dc[i];
			if (isBoundary(row, col, n, m) && mat[row][col] == -1) {
				airs++;
			}
		}
		if (airs >= 2) {
			mat[r][c] = 0;
			return true;
		}
		return false;

	}

	private static boolean isBoundary(int row, int col, int maxRow, int maxCol) {
		if (row < 0 || col < 0) {
			return false;
		}
		if (row >= maxRow) {
			return false;
		}
		if (col >= maxCol) {
			return false;
		}
		return true;
	}
}

'알고리즘 공부 > boj' 카테고리의 다른 글

boj 1789: 수들의 합  (1) 2021.03.15
boj 1339: 단어 수학  (0) 2021.02.14
boj 1766: 문제집  (0) 2021.02.12
boj 1504:특정한 최단경로  (0) 2021.02.07
boj 11812: K진 트리  (0) 2021.02.07

문제 조건

  • k[encoded_string] => encoded_string을 k번 반복
  • 3a 또는 2[4] 등의 경우는 없다고 생각한다

 

먼저, 괄호 중 ']' 을 만나는 순간, 뒤에서부터 가장 먼저 만나게 되는 '[' 의 위치를 찾아 그 부분을 반복하는 문제이다.

=> 뒤에서 부터 꺼내는 것이니까 스택을 사용하자

 

예를 들어 예시2와 같이 `3[a2[c]]` 가 주어졌다고 해 보자

아래와 같이 일단 닫는 괄호를 만나기 전까지는 스택에 push를 한다.

그 다음 여는 괄호를 만나기 전까지 pop을 하며 stringbuilder에 더해주고,

여는 괄호까지 pop한 뒤에 top이 숫자가 될 수 있는지 확인한다. 숫자이면 pop해주고, stringbuilder를 그 숫자만큼 반복해준다.

결과를 스택에 다시 넣어주고 위 규칙을 반복한다.

 

한가지 더 문제를 풀면서 주의할 점은 예시 4번과 같이 `abc3[cd]xyz`로 들어오게 되면 숫자와 문자의 분리가 필요하고,

스택에는 문자열로 집어넣기 때문에 문자열을 숫자로 바꿀 수 있는지 확인이 필요하다. 이는 아래와 같은 방법으로 해결했다.

class Solution {
	public String decodeString(String s) {
		Pattern p = Pattern.compile("(\\D)+|(\\d)+"); // 숫자와 숫자가 아닌것에 대한 분리
		Matcher m = p.matcher(s);
		
		while(m.find()) {
			System.out.println(m.group());
		}
	}

	private int isNumber(String str) { // 숫자인지 확인
		try {
			int n = Integer.parseInt(str);
			return n; // 맞다면 정수 리턴
		} catch (Exception e) {
			return -1; // 아니라면 -1 리턴
		}
	}
}

 

이를 코드로 옮겨보면 아래와 같다.

import java.util.*;
import java.util.regex.*;

class Solution {
	public String decodeString(String s) {
		Stack<String> stack = new Stack<String>();
		Queue<String> queue = new LinkedList<String>();

		Pattern p = Pattern.compile("(\\D)+|(\\d)+");
		Matcher m = p.matcher(s);

		while (m.find()) {
			StringTokenizer st = new StringTokenizer(m.group(), "[]", true);
			while (st.hasMoreElements()) {
				queue.add(st.nextToken());
			}
		}
		
		StringBuilder sb = new StringBuilder("");
		while (!queue.isEmpty()) {
			String str = queue.poll();

			if (str.equals("]")) {
				while (true) {
					String buffer = stack.pop();

					if (buffer.equals("[")) {
						break;
					}
					sb.insert(0, buffer);
				}

				if (!stack.isEmpty()) {
					int k = isNumber(stack.peek());
					if (k != -1) {
						stack.pop();
						for (int i = 0; i < k; i++) {
							stack.add(sb.toString());
						}
					}
				}
				
				sb.setLength(0);
			} else {
				stack.add(str);
			}
		}
		
		while(!stack.isEmpty()) {
			sb.insert(0, stack.pop());
		}
		return sb.toString();
	}

	private int isNumber(String str) {
		try {
			int n = Integer.parseInt(str);
			return n;
		} catch (Exception e) {
			return -1;
		}
	}
}

'알고리즘 공부 > leetcode' 카테고리의 다른 글

leetcode: Group Anagrams  (0) 2021.02.14
leetcode: Task Scheduler  (0) 2021.02.07
leetcode: Sliding Window Maximum  (0) 2021.02.07
leetcode: Longest Increasing Subsequence  (0) 2021.02.07
leetcode: Jump Game II  (0) 2021.01.31

자바는 가비지 컬렉터를 가지고 있어서 다 쓴 객체를 알아서 회수 해 간다.

처음엔 나도 그래서 메모리 관리에 전혀 신경을 쓰지 않았는데, 직접 배포를 하고 메모리 사용률 등을 보니 그렇지 않다는 것을 알게 됐던 기억이 있다..

 

 

메모리 관리에 대한 내용을 알아보기 전에 자바의 가비지 컬렉터에 대한 내용을 먼저 알아두는 것이 좋다.

2021/02/13 - [Java (+ Spring)] - JVM과 Garbage Collector

 

JVM과 Garbage Collector

자바를 이용하여 개발하는 개발자라면 누구나 자바 바이트코드가 JRE 위에서 동작한다는 사실을 잘 알고 있습니다. 이 JRE에서 가장 중요한 요소는 자바 바이트코드를 해석하고 실행하는 JVM(Java V

sysgongbu.tistory.com

 

메모리 누수를 주의 깊게 보아야 하는 경우 아래 3가지 정도가 있다. = 객체들이 다 쓴 참조(obsolete reference)를 여전히 가지고 있는 경우

 

1) 아래 코드는 스택을 구현한 코드인데, pop()을 보면 스택에서 꺼내진 객체를 더 이상 사용하지 않는데도 회수하지 않고 있다.

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    ...
    
    public Object pop() {
        if(size == 0){
            throw new EmpthStackException();
        }
        
        return elements[--size]; // 메모리 누수: elements[size] = null을 해 주어야
    }
}

가비지 컬렉터는 객체 참조 하나 뿐만 아니라, 이 객체가 참조하는/참조되는 모든 객체들을 회수하지 못하게 된다.

그래서 elements[size] = null 을 해 주는 부분이 필요하다. (이 부분을 추가할 경우 외부 영역 접근 시 NullPointerException을 던지게 됨으로써 오류를 조기에 발견할 수 있게 된다)

 

하지만, 모든 곳에 이 것을 해 줄 필요는 없다.

객체 참조를 꼭 null 처리 해 주어야 하는 경우는 위 스택 예제와 같이 자기 메모리를 직접 관리하는 클래스를 구현할 때이다.

위 스택 예제에서는 객체 자체가 아닌 객체 참조를 담는 elements 배열로 저장소 pool을 만들어 원소들을 관리한다.

 

 

 

2) 객체 참조를 캐시에 두고 계속 캐시에 두는 것 또한 메모리 누수를 일으키는 주범이다. 해결 방법은 아래와 같다.

- WeakHashMap을 사용하여 캐시를 만들면 다 쓴 엔트리는 그 즉시 자동으로 제거될 것이다.

package com.example.sypark9646.item07;

import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapTest {

		public static void main(String[] args) {
				MyKey k1 = new MyKey("Hello");
				MyKey k2 = new MyKey("World");
				MyKey k3 = new MyKey("Java");
				MyKey k4 = new MyKey("Programming");

				Map<MyKey, String> wm = new WeakHashMap<>();

				wm.put(k1, "Hello");
				wm.put(k2, "World");
				wm.put(k3, "Java");
				wm.put(k4, "Programming");
				k1 = null;
				k2 = null;
				k3 = null;
				k4 = null;
				System.gc(); // 강제로 GC 발생
				System.out.println("Weak Hash Map :" + wm.toString()); //Weak Hash Map :{}
		}
}

 

- 캐시를 만들 때 ScheduledThreadPoolExecuter 등을 이용하여 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다.

- LinkedHashMap을 사용할 경우에는 removeEldestEntry 메소드를 사용해서 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다.

- java.lang.ref 패키지를 활용하여 만드는 방법

java.lang.ref 패키지는 전형적인 객체 참조인 strong reference 외에도 soft, weak, phantom 3가지의 새로운 참조 방식을 각각의 Reference 클래스로 제공합니다. 이 3가지 Reference 클래스를 애플리케이션에 사용하면 앞서 설명하였듯이 GC에 일정 부분 관여할 수 있고, LRU(Least Recently Used) 캐시 같이 특별한 작업을 하는 애플리케이션을 더 쉽게 작성할 수 있습니다. 이를 위해서는 GC에 대해서도 잘 이해해야 할 뿐 아니라, 이들 참조 방식의 동작도 잘 이해할 필요가 있습니다.

 

WeakReference<Sample> wr = new WeakReference<Sample>(new Sample());  
Sample ex = wr.get(); // 그림 1
// ...
ex = null; // 그림 2

https://d2.naver.com/helloworld/329631 그림 1
https://d2.naver.com/helloworld/329631 그림 2

 

Strong 레퍼런스를 Weak 레퍼런스로 감싸면 Weak 레퍼런스가 된다.

Garbage Collection 대상이 되려면 해당 객체를 가리키는 레퍼런스가 전부 없어져야 한다.

Weak 레퍼런스는 Strong한 레퍼런스가 없어지면 Weak 레퍼런스도 GC의 대상이 될 수가 있다.

 

즉, 정리하자면, 원래 GC 대상 여부는 reachable/unreachable인가로만 구분하였고 이를 사용자 코드에서는 관여할 수 없었다.

그러나 java.lang.ref 패키지를 이용하여 reachable 객체들을 strongly reachable, softly reachable, weakly reachable, phantomly reachable로 더 자세히 구별하여 GC 때의 동작을 다르게 지정할 수 있게 되었다. = GC 대상 여부를 판별하는 부분에 사용자 코드가 개입할 수 있다

https://d2.naver.com/helloworld/329631 Reachability

녹색으로 표시한 중간의 두 객체는 WeakReference로만 참조된 weakly reachable 객체이고, 파란색 객체는 strongly reachable 객체이다. GC가 동작할 때, unreachable 객체뿐만 아니라 weakly reachable 객체도 가비지 객체로 간주되어 메모리에서 회수된다.

root set으로부터 시작된 참조 사슬에 포함되어 있음에도 불구하고 GC가 동작할 때 회수되므로, 참조는 가능하지만 반드시 항상 유효할 필요는 없는 LRU 캐시와 같은 임시 객체들을 저장하는 구조를 쉽게 만들 수 있다.

 

위 그림에서 WeakReference 객체 자체는 weakly reachable 객체가 아니라 strongly reachable 객체이다. 또한, 그림에서 A로 표시한 객체와 같이 WeakReference에 의해 참조되고 있으면서 동시에 root set에서 시작한 참조 사슬에 포함되어 있는 경우에는 weakly reachable 객체가 아니라 strongly reachable 객체이다.

 

GC가 동작하여 어떤 객체를 weakly reachable 객체로 판명하면, GC는 WeakReference 객체에 있는 weakly reachable 객체에 대한 참조를 null로 설정한다. 이에 따라 weakly reachable 객체는 unreachable 객체와 마찬가지 상태가 되고, 가비지로 판명된 다른 객체들과 함께 메모리 회수 대상이 된다.

 

참고) Strengths of Reachability

  • strongly reachable: root set으로부터 시작해서 어떤 reference object도 중간에 끼지 않은 상태로 참조 가능한 객체, 다시 말해, 객체까지 도달하는 여러 참조 사슬 중 reference object가 없는 사슬이 하나라도 있는 객체
  • softly reachable: strongly reachable 객체가 아닌 객체 중에서 weak reference, phantom reference 없이 soft reference만 통과하는 참조 사슬이 하나라도 있는 객체
  • weakly reachable: strongly reachable 객체도 softly reachable 객체도 아닌 객체 중에서, phantom reference 없이 weak reference만 통과하는 참조 사슬이 하나라도 있는 객체
  • phantomly reachable: strongly reachable 객체, softly reachable 객체, weakly reachable 객체 모두 해당되지 않는 객체. 이 객체는 파이널라이즈(finalize)되었지만 아직 메모리가 회수되지 않은 상태이다.
  • unreachable: root set으로부터 시작되는 참조 사슬로 참조되지 않는 객체

 

 

3) Listener와 Callback 등록 후 해지 하지 않는 것도 메모리 누수의 주범이 될 수 있다.

이 경우 마찬가지로 콜백을 약한 참조로(WeakHashMap에) 저장하면 가비지 컬렉터가 즉시 수거해 갈 수 있다.

자바를 이용하여 개발하는 개발자라면 누구나 자바 바이트코드가 JRE 위에서 동작한다는 사실을 잘 알고 있습니다. 이 JRE에서 가장 중요한 요소는 자바 바이트코드를 해석하고 실행하는 JVM(Java Virtual Machine)입니다. JRE는 자바 API와 JVM으로 구성되며, JVM의 역할은 자바 애플리케이션을 클래스 로더(Class Loader)를 통해 읽어 들여서 자바 API와 함께 실행하는 것입니다.

 

JVM(Java Virtual Machine)

JVM은 운영체제 위에서 동작하는 프로세스로 자바 코드를 컴파일해서 얻은 바이트 코드를 해당 운영체제가 이해할 수 있는 기계어로 바꿔 실행시켜주는 역할을 한다. (java 언어와 직접적인 연관이 있는 것이 아니라, class 파일만 있으면 실행해 준다. ex. 코틀린 등)

JVM에 대해 알아보기 전에 먼저 Java와 프로그램 실행 과정에 대해 알아보자.

Java의 가장 큰 특징 중 하나는 하드웨어/OS 에 상관없이 컴파일된 바이트코드가 플랫폼 독립적이라는 점이다.

(이 자바 바이트코드가 자바 코드를 배포하는 가장 작은 단위이다.)

그 이유는 다른 언어들과 다르게 자바는 OS 위에 JVM이 돌아가고, JVM이 컴파일된 코드(바이트코드)를 실행시켜주기 때문이다.

(참고로 JVM은 H/W와 OS 위에서 실행되기 때문에 JVM 자체는 플랫폼에 종속적 즉, 플랫폼에 따라 호환되는 JVM을 실행시켜줘야함)

 

좀 더 자세히 소스코드 실행 과정을 보자

1. .java 소스 코드 파일을 작성

2. 코드를 실행하게 되면 Java 컴파일러를 호출한다. 컴파일러는 코드에서 구문 오류 및 기타 컴파일 타임 오류를 확인하고, 오류가없는 경우에는 바이트 코드라는 중간 코드 .class 파일로 변환한다.

  • 바이트 코드는 플랫폼에 독립적이다. 또한, 바이트 코드는 중간 코드이므로 user/HW/OS 계층이 아닌 JVM에서만 이해할 수 있다.

3. 바이트 코드는 class loader (JVM 내부의 또 다른 내장 프로그램)에 의해 JVM으로 로드 된다.

  • 클래스로더는 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data area), 즉 JVM의 메모리에 올린다.

4. 바이트 코드 검증 도구(JVM 내부에 내장 된 프로그램)가 바이트 코드의 무결성을 확인하고 문제가 발견되지 않으면 인터프리터에 전달

  • JIT 컴파일러의 경우 기계 코드로 변환하기 위해 바이트 코드가 인터프리터에 전달되기 전, 전체 코드를 스캔하여 최적화 할 수 있는지 확인 (중복 제거 등)
  • JIT 컴파일러는 코드에 필요한 패키지만 포함 하거나, 코드 최적화, 중복 코드 제거 등과 같은 일을 하며 전체적으로 프로세스를 매우 빠르고 효율적으로 만든다.
  • JIT 컴파일러는 종류 마다 다르게 작동하며, optional 하다. (매번 호출되는 것은 아님)

5. JVM 내부의 인터프리터는 바이트 코드의 각 행을 실행 가능한 기계 코드로 변환하고, 이를 실행할 CPU와 같은 OS / 하드웨어에 전달

  • 실행엔진(Execution Engine)은 JVM메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행

 

 

JVM 내부 구조에 대해 조금 더 자세히 살펴보면 아래와 같다.

JVM은 크게 Class Loader,GC, Runtime Data Area, Excute engine 세가지로 나뉜다. (여기에 GC까지 4가지로 보기도 한다)

위에서도 볼 수 있듯이, 클래스 로더(Class Loader)가 컴파일된 자바 바이트코드를 런타임 데이터 영역(Runtime Data Areas)에 로드하고, 실행 엔진(Execution Engine)이 자바 바이트코드를 실행한다.

 

1) 클래스 로더

자바는 동적 로드, 즉 컴파일타임이 아니라 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 특징이 있다. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다. 아래는 클래스 로더의 특징이다.

  • 계층 구조: 클래스 로더끼리 부모-자식 관계를 이루어 계층 구조로 생성된다. 최상위 클래스 로더는 부트스트랩 클래스 로더(Bootstrap Class Loader)이다.
  • 위임 모델: 계층 구조를 바탕으로 클래스 로더끼리 로드를 위임하는 구조로 동작한다. 클래스를 로드할 때 먼저 상위 클래스 로더를 확인하여 상위 클래스 로더에 있다면 해당 클래스를 사용하고, 없다면 로드를 요청받은 클래스 로더가 클래스를 로드한다.
  • 가시성(visibility) 제한: 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만, 상위 클래스 로더는 하위 클래스 로더의 클래스를 찾을 수 없다.
  • 언로드 불가: 클래스 로더는 클래스를 로드할 수는 있지만 언로드할 수는 없다. 언로드 대신, 현재 클래스 로더를 삭제하고 아예 새로운 클래스 로더를 생성하는 방법을 사용할 수 있다.

각 클래스 로더는 로드된 클래스들을 보관하는 네임스페이스(namespace)를 갖는다. 클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위해서 네임스페이스에 보관된 FQCN(Fully Qualified Class Name)을 기준으로 클래스를 찾는다. 비록 FQCN이 같더라도 네임스페이스가 다르면, 즉 다른 클래스 로더가 로드한 클래스이면 다른 클래스로 간주된다.

클래스 로더 위임 모델

 

클래스 로더가 클래스 로드를 요청받으면, 클래스 로더 캐시, 상위 클래스 로더, 자기 자신의 순서로 해당 클래스가 있는지 확인한다. 즉, 이전에 로드된 클래스인지 클래스 로더 캐시를 확인하고, 없으면 상위 클래스 로더를 거슬러 올라가며 확인한다. 부트스트랩 클래스 로더까지 확인해도 없으면 요청받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾는다.

  • 부트스트랩 클래스 로더: JVM을 기동할 때 생성되며, Object 클래스들을 비롯하여 자바 API들을 로드한다. 다른 클래스 로더와 달리 자바가 아니라 네이티브 코드로 구현되어 있다.
  • 익스텐션 클래스 로더(Extension Class Loader): 기본 자바 API를 제외한 확장 클래스들을 로드한다. 다양한 보안 확장 기능 등을 여기에서 로드하게 된다.
  • 시스템 클래스 로더(System Class Loader): 부트스트랩 클래스 로더와 익스텐션 클래스 로더가 JVM 자체의 구성 요소들을 로드하는 것이라 한다면, 시스템 클래스 로더는 애플리케이션의 클래스들을 로드한다고 할 수 있다. 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드한다.
  • 사용자 정의 클래스 로더(User-Defined Class Loader): 애플리케이션 사용자가 직접 코드 상에서 생성해서 사용하는 클래스 로더이다.

웹 애플리케이션 서버(WAS)와 같은 프레임워크는 웹 애플리케이션들, 엔터프라이즈 애플리케이션들이 서로 독립적으로 동작하게 하기 위해 사용자 정의 클래스 로더를 사용한다. 즉, 클래스 로더의 위임 모델을 통해 애플리케이션의 독립성을 보장하는 것이다. 이와 같은 WAS의 클래스 로더 구조는 WAS 벤더마다 조금씩 다른 형태의 계층 구조를 사용하고 있다.

클래스 로더가 아직 로드되지 않은 클래스를 찾으면, 다음 그림과 같은 과정을 거쳐 클래스를 로드하고 링크하고 초기화한다.

 

클래스 로드 매커니즘

  • 로드: 클래스를 파일에서 가져와서 JVM의 메모리에 로드한다.
  • 검증(Verifying): 읽어 들인 클래스가 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사한다. 클래스 로드의 전 과정 중에서 가장 까다로운 검사를 수행하는 과정으로서 가장 복잡하고 시간이 많이 걸린다. JVM TCK의 테스트 케이스 중에서 가장 많은 부분이 잘못된 클래스를 로드하여 정상적으로 검증 오류를 발생시키는지 테스트하는 부분이다.
  • 준비(Preparing): 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비한다.
  • 분석(Resolving): 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
  • 초기화: 클래스 변수들을 적절한 값으로 초기화한다. 즉, static initializer들을 수행하고, static 필드들을 설정된 값으로 초기화한다.

 

2) 런타임 데이터 영역 (=메모리)

런타임 데이터 영역

런타임 데이터 영역은 JVM이라는 프로그램이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다. 런타임 데이터 영역은 6개의 영역으로 나눌 수 있다. 이중 PC 레지스터(PC Register), JVM 스택(JVM Stack), 네이티브 메서드 스택(Native Method Stack)은 스레드마다 하나씩 생성되며 힙(Heap), 메서드 영역(Method Area), 런타임 상수 풀(Runtime Constant Pool)은 모든 스레드가 공유해서 사용한다.

  • PC 레지스터: PC(Program Counter) 레지스터는 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. PC 레지스터는 현재 수행 중인 JVM 명령의 주소를 갖는다.

JVM 스택

  • JVM 스택: JVM 스택은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택으로, JVM은 오직 JVM 스택에 스택 프레임을 추가하고(push) 제거하는(pop) 동작만 수행한다. 예외 발생 시 printStackTrace() 등의 메서드로 보여주는 Stack Trace의 각 라인은 하나의 스택 프레임을 표현한다.
    • 스택 프레임: JVM 내에서 메서드가 수행될 때마다 하나의 스택 프레임이 생성되어 해당 스레드의 JVM 스택에 추가되고 메서드가 종료되면 스택 프레임이 제거된다. 각 스택 프레임은 지역 변수 배열(Local Variable Array), 피연산자 스택(Operand Stack), 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다. 지역 변수 배열, 피연산자 스택의 크기는 컴파일 시에 결정되기 때문에 스택 프레임의 크기도 메서드에 따라 크기가 고정된다.
    • 지역 변수 배열: 0부터 시작하는 인덱스를 가진 배열이다. 0은 메서드가 속한 클래스 인스턴스의 this 레퍼런스이고, 1부터는 메서드에 전달된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역 변수들이 저장된다.
    • 피연산자 스택: 메서드의 실제 작업 공간이다. 각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드 호출 결과를 추가하거나(push) 꺼낸다(pop). 피연산자 스택 공간이 얼마나 필요한지는 컴파일할 때 결정할 수 있으므로, 피연산자 스택의 크기도 컴파일 시에 결정된다.
    • 런타임 상수 풀
  • 네이티브 메서드 스택: 자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다. 즉, JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C 스택이나 C++ 스택이 생성된다.
  • 메서드 영역: 메서드 영역은 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, Static 변수, 메서드의 바이트코드 등을 보관한다. 메서드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM(HotSpot JVM)에서는 흔히 Permanent Area, 혹은 Permanent Generation(PermGen)이라고 불린다. 메서드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항이다.
  • 런타임 상수 풀: 클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역이다. 메서드 영역에 포함되는 영역이긴 하지만, JVM 동작에서 가장 핵심적인 역할을 수행하는 곳이기 때문에 JVM 명세에서도 따로 중요하게 기술한다. 각 클래스와 인터페이스의 상수뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 즉, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조한다.
  • 힙: 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션 대상이다. JVM 성능 등의 이슈에서 가장 많이 언급되는 공간이다. 힙 구성 방식이나 가비지 컬렉션 방법 등은 JVM 벤더의 재량이다.

 

3) 실행 엔진

클래스 로더를 통해 JVM 내의 런타임 데이터 영역에 배치된 바이트코드는 실행 엔진에 의해 실행된다. 실행 엔진은 자바 바이트코드를 명령어 단위로 읽어서 실행한다. CPU가 기계 명령어을 하나씩 실행하는 것과 비슷하다. 바이트코드의 각 명령어는 1바이트짜리 OpCode와 추가 피연산자로 이루어져 있으며, 실행 엔진은 하나의 OpCode를 가져와서 피연산자와 함께 작업을 수행한 다음, 다음 OpCode를 수행하는 식으로 동작한다.

그런데 자바 바이트코드는 기계가 바로 수행할 수 있는 언어보다는 비교적 인간이 보기 편한 형태로 기술된 것이다. 그래서 실행 엔진은 이와 같은 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경하며, 그 방식은 다음 두 가지가 있다.

  • 인터프리터: 바이트코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠른 대신 인터프리팅 결과의 실행은 느리다는 단점을 가지고 있다. 흔히 얘기하는 인터프리터 언어의 단점을 그대로 가지는 것이다. 즉, 바이트코드라는 '언어'는 기본적으로 인터프리터 방식으로 동작한다.
  • JIT(Just-In-Time) 컴파일러: 인터프리터의 단점을 보완하기 위해 도입된 것이 JIT 컴파일러이다. 인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행되게 된다.

JIT 컴파일러가 컴파일하는 과정은 바이트코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸리므로, 만약 한 번만 실행되는 코드라면 컴파일하지 않고 인터프리팅하는 것이 훨씬 유리하다. 따라서, JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일을 수행한다.

 

 

 

마지막으로 , JVM의 특징으로는 아래와 같은 것들이 있다.

  • 스택 기반의 가상 머신 (<-> 레지스터 기반 동작)
  • 심볼릭 레퍼런스: 기본 자료형(primitive data type)을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.
  • 가비지 컬렉션(garbage collection): 클래스 인스턴스는 사용자 코드에 의해 명시적으로 생성되고 가비지 컬렉션에 의해 자동으로 파괴된다.
  • 기본 자료형을 명확하게 정의하여 플랫폼 독립성 보장: C/C++ 등의 전통적인 언어는 플랫폼에 따라 int 형의 크기가 변하는데 반해, JVM은 기본 자료형을 명확하게 정의하여 호환성을 유지하고 플랫폼 독립성을 보장한다.
  • 네트워크 바이트 오더(network byte order): 자바 클래스 파일은 네트워크 바이트 오더를 사용한다. 인텔 x86 아키텍처가 사용하는 리틀 엔디안이나, RISC 계열 아키텍처가 주로 사용하는 빅 엔디안 사이에서 플랫폼 독립성을 유지하려면 고정된 바이트 오더를 유지해야 하므로 네트워크 전송 시에 사용하는 바이트 오더인 네트워크 바이트 오더를 사용한다. 네트워크 바이트 오더는 빅 엔디안이다.

 

Garbage Collector

이제 JVM의 가장 큰 특징, 가비지 컬렉터에 대해 알아보자.

gc는 동적으로 할당된 메모리 영역 중 사용하지 않는 영역을 방지하여 해제하는 기능을 말한다. (자바에서 동적으로 할당된 메모리 = 힙 영역)

 

Java GC는 객체가 가비지인지 판별하기 위해서 reachability라는 개념을 사용한다. 어떤 객체에 유효한 참조가 있으면 'reachable'로, 없으면 'unreachable'로 구별하고, unreachable 객체를 가비지로 간주해 GC를 수행한다. 한 객체는 여러 다른 객체를 참조하고, 참조된 다른 객체들도 마찬가지로 또 다른 객체들을 참조할 수 있으므로 객체들은 참조 사슬을 이룬다. 이런 상황에서 유효한 참조 여부를 파악하려면 항상 유효한 최초의 참조가 있어야 하는데 이를 객체 참조의 root set이라고 한다.

 

https://d2.naver.com/helloworld/329631 런타임 데이터 영역

런타임 데이터 영역은 위와 같이 스레드가 차지하는 영역들과, 객체를 생성 및 보관하는 하나의 큰 힙, 클래스 정보가 차지하는 영역인 메서드 영역, 크게 세 부분으로 나눌 수 있다. 위 그림에서 객체에 대한 참조는 화살표로 표시되어 있다.

 

힙에 있는 객체들에 대한 참조는 다음 4가지 종류 중 하나이다.

  • 힙 내의 다른 객체에 의한 참조
  • Java 스택, 즉 Java 메서드 실행 시에 사용하는 지역 변수와 파라미터들에 의한 참조
  • 네이티브 스택, 즉 JNI(Java Native Interface)에 의해 생성된 객체에 대한 참조
  • 메서드 영역의 정적 변수에 의한 참조

이들 중 힙 내의 다른 객체에 의한 참조를 제외한 나머지 3개가 root set으로, reachability를 판가름하는 기준이 된다.

 

reachability를 더 자세히 설명하기 위해 root set과 힙 내의 객체를 중심으로 다시 그리면 다음과 같다.

https://d2.naver.com/helloworld/329631 reachability

위 그림에서 보듯, root set으로부터 시작한 참조 사슬에 속한 객체들은 reachable 객체이고, 이 참조 사슬과 무관한 객체들이 unreachable 객체로 GC 대상이다. 오른쪽 아래 객체처럼 reachable 객체를 참조하더라도, 다른 reachable 객체가 이 객체를 참조하지 않는다면 이 객체는 unreachable 객체이다.

 

 

그렇다면 GC 매커니즘을 보자. 아래와 같은 메모리 영역에 대해,

  • Stack: 정적으로 할당한 메모리 영역으로, 원시 타입의 데이터가 값과 함께 할당된다. 힙 영역에 생성된 Object 타입의 데이터의 참조값 할당
  • Heap: 동적으로 할당한 메모리 영역으로, 모든 Object 타입의 데이터가 할당된다. 힙 영역의 Object를 가리키는 참조 변수가 스택 영역에 저장된다. 힙 영역은 New Generation 영역과 Old Generation 영역으로 이루어져 있다.
    • New generation:
      • Eden: 새로운 객체는 Eden 영역에 할당된다. Eden영역이 모두 사용되면 GC가 발생하는데, 이때 일어나는 가비지 컬렉터가 Minor GC라고 한다.
      • Servival 0: 이 이후에 살아남은 객체(Eden 영역의 Reachable 객체)를 Servival 0 영역으로 이동한다. Eden영역의 Unreachable 객체는 메모리에서 해제한다. Servival 0 영역이 다 차면 또 다시 Mark & Sweep 과정을 반복한다.
      • Servival 1: Servival 0 영역에서 살아남은 객체들을 Survival 1 영역으로 이동한다. 이동한 객체는 Age값 증가한다. 그 다음에 새로운 객체가 Eden 영역으로 들어와서 Minor GC가 발생하면 아까처럼 Servival 0 으로 가는 것이 아니라, 객체가 차 있는 곳으로 이동하기 때문에 바로 Servival 1 로 이동하게 된다. (즉, Servival 0/1중 둘 중 하나는 항상 비어있는 상태로 유지된다.) 만약 Servival 1 이 다 차면 Servival 1에 대해 Mark & Sweep 과정이 일어나고 Servival 0 으로 이동 + Age 1 증가하게 된다.
    • Old generation: Servival 영역의 Age 값이 증가하다가 특정 Age 값을 넘어서면, 그 때 Old generation으로 이동한다. 이 과정을 Promotion 과정이라고 한다. 만약 Old generation영역이 다 사용되면 Major GC가 발생한다.
    • -> 이 과정이 반복되면서 가비지 컬렉터가 메모리를 관리한다.

Garbage Collecter 과정을 Mark & Sweep이라고 하는데,

  • 가비지 컬렉터가 스택 영역의 모든 변수를 스캔하면서 각각 어떤 객체를 참조하고 있는지 찾아서 마킹한다.
  • Reachable Object가 참조하고 있는 객체도 찾아서 마킹한다.
  • 마킹되지 않은 객체를 힙 영역에서 제거한다.

 

 

용어

  • JRE(Java Runtime Environment): JVM + 라이브러리
    • 자바 애플리케이션을 실행할 수 있도록 구성된 배포판 (최소한의 배포 단위)
    • JVM과 핵심 라이브러리 및 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일을 가지고 있다.
    • 개발 관련 도구는 포함하지 않는다. (java compile을 위한 javac 등은 없음)
  • JDK (Java Development Kit): JRE + 개발 툴
    • JRE + 개발에 필요할 툴
    • 소스 코드를 작성할 때 사용하는 자바 언어는 플랫폼에 독립적.
    • 오라클은 자바 11부터는 JDK만 제공하며 JRE를 따로 제공하지 않는다.
    • Write Once Run Anywhere

 

Reference

docs.oracle.com/javase/specs/jvms/se8/html/index.html

 

The Java® Virtual Machine Specification

Tim Lindholm Frank Yellin Gilad Bracha Alex Buckley

docs.oracle.com

d2.naver.com/helloworld/329631

d2.naver.com/helloworld/1230

 

+ Recent posts