[CS 면접 질문] Java
Java의 특징
- 장점
- JVM 위에서 동작하기 때문에 운영체제에 독립적이다.
- 가비지 컬렉터가 메모리를 관리해주기 때문에 편리하다.
- 단점
- JVM 위에서 동작하기 때문에 실행 속도가 상대적으로 느리다.
- 다중 상속이나 타입에 엄격하며, 제약이 많다.
자바 프로그램 실행 과정
- 프로그램이 실행되면 JVM은 OS로부터 메모리를 할당받는다.
- 자바 컴파일러(javac)가 자바 소스(.java)를 읽어 자바 바이트 코드(.class)로 변환시킨다.
- 바이트 코드를 JVM의 클래스 로더(class loader)에 전달한다.
- 클래스 로더를 통해 class 파일들을 JVM 메모리에 로드한다.
- 로딩된 class 파일들은 실행 엔진을 통해 운영체제에 맞는 기계어로 해석된다.
- 해석된 바이트코드는 Runtime Data Area에 배치되어 실질적인 수행이 이루어지게 된다.
JVM이란?
JVM이란 스택 기반의 가상 머신으로, 자바 애플리케이션이 OS에 구애받지 않고 실행될 수 있도록 환경을 제공하고, 시스템 메모리를 관리하는 역할을 한다.
JVM의 구성
- 클래스 로더(class loader) : 클래스 파일들을 엮어서 JVM이 OS로부터 할당받은 메모리 영역인 Runtime Data Area에 적재하는 역할을 한다.
- 실행 엔진(execution engine) : 클래스 로더에 의해 메모리에 적재된 클래스(바이트 코드)들을 기계어로 변경해 명령어 단위로 실행하는 역할을 한다.
- 가비지 컬렉터(garbage collector) : Heap 영역에 생성된 객체들 중 더이상 참조되지 않는 객체들을 탐색 후 제거하는 역할을 한다. GC가 수행되는 동안 GC를 수행하는 스레드가 아닌 다른 모든 스레드는 일시정지된다.
- Runtime Data Area : 프로그램 수행을 위해 OS로부터 할당받은 메모리 공간이다. 모든 스레드가 공유하는
Heap
,Method
영역과 개별 스레드에서 관리하는PC Register
,JVM Stack
,Native Method Stack
영역으로 구성되어 있다.
JVM 메모리 구조 (Runtime Data Area)
- PC Register : 현재 스레드가 실행되는 부분의 주소와 명령을 저장하는 영역이다.
- JVM Stack : 지역 변수, 파라미터, 리턴 값, 연산에 사용되는 임시 값이 생성되는 영역이다.
- Native Method Stack : 자바 외 언어(C/C++ 등)로 작성된 코드를 위한 메모리 영역이다. JNI를 통해 사용된다.
- Method Area : 클래스와 관련된 정보(Field 이름, Field 타입, Class 이름 등등)를 저장한다. 그 외에도 static 변수, 인터페이스 등이 저장된다.
- Heap Area : new 키워드로 생성된 객체와 배열의 인스턴스를 저장하는 곳이다. 가비지 컬렉터는 힙 영역을 청소하며 메모리를 확보한다.
Garbage Collection의 과정
JVM은 GC를 실행하기 위해 애플리케이션의 실행을 멈추는 stop-the-world를 먼저 실행하게 된다. stop-the-world를 실행하면 GC를 실행하는 스레드를 제외한 모든 스레드가 작업을 멈춘다. 그리고 GC가 끝나면 다시 작업을 재개한다.
GC의 작업은Young 영역에 대한 Minor GC와 Old 영역에 대한 Major GC(혹은 Full GC)로 구분된다.
- Young 영역 : 새롭게 생성한 객체들이 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다.
- Old 영역 : Young 영역에서 계속 사용되어 살아남은 객체가 복사되는 영역이다. Young 영역보다 크게 할당하며, 더 적은 GC가 발생한다.
Young 영역은 Eden 영역과 2개의 Survivor 영역으로 구성된다.
- 새로운 객체가 Eden 영역에 만들어진다.
- Eden 영역에서 GC가 동작하고, 그 중 살아남은 객체가 Survivor1로 이동한다.
- Survivor1이 가득 차면 Survivor1 영역에서 GC가 동작한다. 그 중 살아남은 객체들이 Survivor2로 이동하고, Survivor1은 비워진다.
- 이 과정을 반복하여 특정 횟수만큼 살아남은 객체는 Old 영역으로 이동한다.
그리고 Old 영역이 가득차서 Survivor 영역에서 Old 영역으로 이동이 불가능할 때, Old 영역에 대한 GC(Major GC)가 실행된다.
OOM이란?
OOM(Out of Memory)은 JVM이 힙 메모리에 새로운 객체를 할당할 수 없을 때 발생하는 에러이다. 자바 애플리케이션이 실행되면서 동적으로 만들어진 객체는 힙 메모리에 저장된다. 그러다 해당 객체에 대한 참조가 사라지면 가비지 컬렉터에 의해 청소되어야 하는데, 청소되지 않아 해당 메모리가 계속 쌓이면 새로운 객체를 힙 메모리에 할당하는 것이 불가능해진다. 이러한 경우 발생하는 것이 OOM이다.
원시 타입 vs 참조 타입
원시 타입은 실제 값을 저장하기 위한 타입으로, JVM의 스택 영역에 생성된다. 참조 타입보다 메모리를 훨씬 적게 사용한다.
참조 타입은 기본 타입을 제외한 타입으로, 객체의 주소를 저장하는 타입이다. JVM의 힙 영역에 실제 객체가 저장되고, 스택 영역에 객체의 주소를 저장한다.
원시 타입 | 메모리 | 참조 타입 | 메모리 |
---|---|---|---|
boolean | 1 bit | Boolean | 128 bits |
byte | 8 bits | Byte | 128 bits |
short, char | 16 bits | Short, Character | 128 bits |
int, float | 32 bits | Integer, Float | 128 bits |
long, double | 64 bit | Long, Double | 196 bits |
제네릭(Generic)이란?
제네릭은 데이터 타입을 하나로 지정하지 않고 사용할 때 외부에서 지정하는 것으로, 컴파일 과정에서 타입 체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 타입 안전성을 높이고, 형변환이 필요없어 자연스럽게 코드도 간결해진다는 장점이 있다.
+) 단, static 멤버에는 타입 변수 T를 사용할 수 없다. static 멤버는 인스턴스 변수를 참조할 수 없는데, 타입 변수 T는 인스턴스 변수로 간주되기 때문이다. 또한 제네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, new T[10]
와 같이 배열을 생성하는 것은 불가능하다. new 연산자
는 컴파일 시점에 타입 T가 무엇인지 명확히 알아야 하기 때문이다.
타입인자
제네릭을 표현하는 형태는 다양한데, 어느 용도로 사용되는지 알 수 있게 제시해주는 가이드 라인이 있다.
타입인자 | 설명 |
---|---|
T | Type |
E | Element (used extensively by the Java Collections Framework) |
K | Key |
V | Value |
N | Number |
R | Result |
제네릭 구현
- 클래스
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
public class GenericClass<T> { private T t; public GenericClass(T t) { this.t = t; } public GenericClass() {} public T getT() { return t; } public void setT(T t) { this.t = t; } }
1 2 3 4 5 6 7
public static void main(String[] args) { GenericClass<String> stringGenericClass = new GenericClass<>(); stringGenericClass.setT("suyeon"); GenericClass<Integer> integerGenericClass = new GenericClass<>(); integerGenericClass.setT("20"); // 컴파일 에러 }
- 인터페이스
1 2 3 4
public interface GenericInterface<T> { public void set(T t); public T get(); }
1 2 3 4 5 6 7 8 9 10 11 12 13
public class GenericImpl<T> implements GenericInterface<T> { private T t; @Override public void set(T t) { this.t = t; } @Override public String get() { return t; } }
- 멀티 타입
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
public class GenericMulti<A, B, C>{ private A a; private B b; private C c; public A getA() { return a; } public void setA(A a){ this.a = a; } public B getB() { return b; } public void setB(B b) { this.b = b; } } public C getC() { return c; } public void setC(C c) { this.c = c; } }
1 2 3 4 5 6
public static void main(String[] args) { GenericMulti<String, Integer, Double> genericMulti = new GenericMulti<>(); genericMulti.setA("suyeon"); genericMulti.setB(20); genericMulti.setC(20.0); }
제한된 타입 파라미터 (Bounded type parameter)
타입 파라미터들은 바운드(bound) 될 수 있다. 바운드 된다는 것은 받을 수 있는 타입을 특정 타입의 서브 타입으로만 제한한다는 것이다. 예를 들면, 어떤 타입과 그 타입의 모든 서브 클래스들을 허용하거나, 어떤 타입과 그 타입의 모든 부모클래스들을 허용하도록 메소드를 작성할 수 있다.
1
2
3
4
5
6
7
8
9
public class GenericTest<T extends Number> {
public void set(T t) {}
}
public static void main(String[] args) {
GenericTest<Integer> genericTest = new GenericTest<>();
genericTest.set("Hi"); // 컴파일 에러
}
위와 같은 코드에서, Integer는 Number의 서브타입이기 때문에 GenericTest<Integer>
같은 선언이 가능하지만, set
메소드의 인자로 문자열(String)을 전달하려고 했기 때문에 컴파일 에러가 발생하게 된다.
Wrapper Class란?
기본 자료형(Primitive data type)을 객체로 다루기 위해서 사용하는 클래스들을 Wrapper class라고 한다. 컬렉션에서 제네릭을 사용하기 위해서는 Wrapper class를 사용해야 한다. 또한 래퍼 클래스로 감싸고 있는 기본 타입 값은 외부에서 변경할 수 없다. 만약 값을 변경하고 싶다면 새로운 포장 객체를 만들어야 한다.
오버라이딩(Overriding)과 오버로딩(Overloading)
오버라이딩(Overriding)은 상위 클래스에 있는 메소드를 하위 클래스에서 재정의 하는 것이고, 오버로딩(Overloading)은 매개변수의 개수나 타입을 다르게 하여 같은 이름의 메소드를 여러 개 정의하는 것이다.
String, StringBuffer, StringBuilder
String 객체가 불변인 이유
캐싱을 통한 메모리 절약과 속도 향상
String 객체들은 Heap의 String Pool 이라는 공간에 저장된다. 이 때 참조하려는 문자열이 String Pool에 존재하면 새로 생성하지 않고 Pool에 있는 객체를 사용한다. 때문에 특정 문자열의 재사용 빈도가 높을수록 성능 향상을 기대할 수 있다.스레드 안정성 (thread-safe)
String 객체는 불변이기 때문에 여러 스레드에서 동시에 참조하더라도 안전하다.보안
중요한 데이터를 문자열로 다루는 경우 강제로 값을 바꾸는 것이 불가능하기 때문에 보안에 유리하다.
StringBuffer vs StringBuilder
StringBuffer와 StringBuilder는 모두 가변(mutable)성을 가진다. StringBuffer는 동기화를 지원하여 멀티스레드 환경에서 주로 사용하며, StringBuilder는 동기화를 지원하지 않아 싱글스레드 환경에서 주로 사용한다. 동기화를 고려하지 않는 만큼, 싱글스레드에서의 성능은 StringBuffer보다 뛰어나다.
Vector vs List
Vector는 데이터 삽입시 원소를 밀어내지만, List는 노드를 연결만 하기 때문에, 삽입 삭제 연산에서 리스트가 더 빠른 시간복잡도를 갖는다. 그러나 Vector는 random access가 가능한 반면, List는 double-linked-list로 되어있어 random access가 불가능하기 때문에, 검색 연산에서 벡터가 더 빠르다.
또한 벡터는 리스트와 달리 항상 동기화되기 때문에 멀티스레드 환경에서 안전하게 객체를 추가하고 삭제할 수 있다는 장점이 있다. 하지만 이로 인해 싱글스레드 환경에서 리스트보다 성능이 좋지 않다는 단점이 있다.
스레드 (Thread)
스레드 구현 방법
1. Runnable 인터페이스 구현
Runnable은 run()
이라는 단 하나의 메소드를 제공한다. Runnable 인터페이스로 구현한 클래스를 스레드로 바로 시작할 수는 없고, Thread 클래스의 생성자에 해당 객체를 추가해 시작해야 한다.
1
2
3
4
5
public static class ThreadTest implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
1
2
// 메인 메서드에서 인스턴스 생성 방법
Thread t1 = new Thread(new ThreadTest()); // new Thread(Runnable target) 생성자 이용
2. Thread 클래스 상속
Thread 클래스는 많은 메소드를 포함하고 있다. Thread 클래스를 상속하여 만든 클래스는 start()
메소드를 바로 호출할 수 있다. 스레드를 시작하는 메소드는 start()이며, 스레드가 시작하면 수행되는 메소드는 run()이다.
1
2
3
4
5
public static class ThreadTest extends Thread {
public void run() {
System.out.println(getName());
}
}
1
2
// 메인 메서드에서 인스턴스 생성 방법
Thread t1 = new ThreadTest();
synchronized 키워드
스레드의 동기화를 위해서는 임계 영역(critical section)과 잠금(lock)을 활용한다. 임계 영역을 지정하고, 임계 영역을 가지고 있는 lock을 단 하나의 스레드에게만 주는 것이다. 임계 영역 안에서 수행할 코드가 완료되면 스레드는 lock을 반납한다.
java에서는 synchronized
키워드를 사용해 메소드나 특정 블럭에 대해 임계 영역을 지정할 수 있다. synchronized 블럭을 점유한 스레드가 작업을 마칠 때까지 다른 스레드들은 접근할 수 없게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Account {
int balance = 1000;
public synchronized void withdraw(int money) {
if(balance >= money) {
try {
Thread.sleep(1000);
} catch (Exception e) {
}
balance -= money;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A implements Runnable {
Account account = new Account();
public void run() {
while(account.balance > 0) {
int money = (int)(Math.random() * 3 + 1) * 100; // 100, 200, 300 중 임의 금액 출금
account.withdraw(money);
System.out.println("balance : " + acc.balance);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Runnable r = new A();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
}
}
한 스레드에 의해서 먼저 withdraw()가 호출되면 해당 스레드가 종료될 때까지 다른 스레드가 withdraw()를 호출하더라도 대기 상태에 머물게 된다. 즉, withdraw()는 한 순간에 단 하나의 스레드만 사용할 수 있다.
참고 자료
Interview_Question_for_Beginner
신입 개발자 기술면접 질문 정리 - 자바
[기술면접] CS 기술면접 질문 - 개발 언어 (7/8)
#자바가상머신, JVM(Java Virtual Machine)이란 무엇인가?
Thread(쓰레드)