Written by
Poogle
on
on
Java 면접 질문 모음(1)
참고 링크
Naver D2 - Java Garbage Collection
f-lab - 자바 백엔드 기술 면접 대비하기 - 1편
📌 Java 구조, JVM
❓ JVM, JRE, JDK에 대해 설명해주세요.
JVM(Java Virtual Machine); 자바 가상 머신
- 자바 바이트 코드를 OS에 특화된 코드로 변환하여 실행하는 구현체입니다.
- 표준화된 JVM 스펙을 기준으로 이를 구현한 다양한 JVM 벤더 들이 있습니다.(ex. Oracle, Amazon …)
❓❓ ‘플랫폼 종속적’이라는 키워드를 설명해주세요.
- Java 프로그램은 JVM 덕분에 플랫폼에 독립적입니다.
- 중간 단계 언어인 바이트 파일로 컴파일 되기 때문에 OS마다 따로 코드를 작성해야 하는 번거로움이 없습니다.
- JVM은 플랫폼에 종속됩니다.
- Mac, Windows에 설치되는 JVM이 구분됩니다.
- JDK, JRE는 플랫폼에 독립적입니다.
JRE(Java Runtime Environment); 자바 실행 환경
- 자바 애플리케이션을 실행할 수 있도록 구현된 배포판입니다.
JVM + 라이브러리
- JVM과 핵심 라이브러리 및 자바 런타임 환경에서 사용되는 프로퍼티 세팅이나 리소스 파일을 가지고 있습니다.
- 개발 관련 도구들은 포함하지 않습니다.
JDK(Java Development Kit); 자바 개발 키트
JRE + 개발 관련 도구
- 과거 JRE와 JDK를 구분해서 배포했던 것과 달리 오라클은 Java11부터는 JDK만 제공합니다.
❓ JVM의 구조에 대해 설명해주세요.
[Class Loader]
.class
에서 바이트코드를 읽고 메모리에 저장해 JVM 내로 클래스를 로드하는 모듈입니다.
Loading
- Bootstrap -> Extension(Platform) -> Application
- 클래스 로더가
.class
파일을 읽어 그 내용에 따라 적절한 바이너리 데이터를 만들고Method Area
에 저장합니다. Method Area
에 저장하는 데이터- FQCN(Fully Qualified Class Name); 패키지 경로까지 포함한 이름
- class, interface, enum
- method와 변수
- 로딩이 끝나면 해당 클래스 타입의
클래스 객체
를 생성해Heap
영역에 저장합니다. - 해당 시점에서 클래스가 없으면
ClassNotFoundException
이 발생합니다.
Link
- 레퍼런스를 연결합니다.
- Verify:
.class
파일이 유효한지 체크합니다. - Prepare: 클래스 변수(static 변수)와 기본값에 필요한 메모리를 준비합니다.
- Resolve: 심볼릭 메모리 레퍼런스(논리적인 레퍼런스)를
method area
에 있는 실제 레퍼런스로 교체합니다. (optional)
Initialization
- 준비한 메모리에 static 값들을 초기화하고, 변수에 할당합니다.
[Execution Engine]
- 바이트 코드를 실행시키는 역할을 합니다.
- Interpreter: 바이트 코드를 한 줄씩 실행합니다.
- JIT(Just-In-Time Compiler) Compiler
- 인터프리터 효율을 높이기 위한 컴파일러로 인터프리터가 반복되는 코드를 발견하면 JIT컴파일러가 반복되는 코드를 네이티브 코드(캐시에 보관)로 바꿔줍니다.
- 그 후 인터프리터는 네이티브 코드로 컴파일된 코드를 바로 사용합니다.
- Garbage Collector
- 힙 영역에서 사용되지 않는 객체들을 제거합니다.
- 유효하지 않은 메모리, 즉 주소를 잃어버려 사용할 수 없는 메모리들을 해제 시켜 다른 용도로 사용할 수 있게 해줍니다.
[Runtime Data Areas]
- 프로그램 실행 중에 사용되는 다양한 영역입니다.
Stack Area
- 스레드마다 런타임 스택을 만들고 그 안에 메소드 호출을 스택 프레임 블럭으로 쌓습니다. 스레드가 종료되면 런타임 스택도 사라집니다.
- Method 안에서 사용되는 값들(매개변수, 지역변수, 리턴 값 등)이 생성되며 실제 객체는 Heap에 할당되고 해당 레퍼런스만 Stack에 저장됩니다.
PC Register
- 스레드가 시작될 때 생성되며 현재 수행중인 JVM 명령의 주소를 갖고 있습니다.
- 스레드마다 스레드 내 현재 실행할 스택 프레임을 가리키는 포인터가 생성됩니다.
Native Method Stack
- 각 스레드마다 생성됩니다.
- 다른 언어(C, C++, 어셈블리어로 구축)의 메서드 호출을 위해 할당되는 구역입니다.
- 이미 다른 언어로 작성되어 자바로 재작성할 필요가 없거나,
- 시스템 디바이스에 접근하거나 플랫폼 특정 작업을 할 떄 성능 향상을 위해 이러한
native
메서드를 사용합니다.
Heap Area
- 모든 스레드에게 공유되는 자원입니다.
- 동적으로 생성된 오브젝트(인스턴스와 객체)와 배열이 저장되는 곳입니다.
- 공간이 부족해지면 GC의 대상이 되는 영역입니다.
Method Area(Static Area)
- 모든 스레드에게 공유되는 자원입니다.
- 클래스 수준의 정보(클래스 멤버 변수, 메소드 정보, Type 정보, Constant Pool(모든 Symbolic Reference를 포함), static, final 변수) 등이 생성됩니다.
[Native Method Interface; JNI(Java Native Interface)]
- 자바 어플리케이션에서 C, C++ 어셈블리어로 작성된 함수를 사용할 수 있는 방법(표준 규약)을 제공해줍니다.
- 자바 프로그램이 네이티브 메서드를 호출하는 기술입니다.
native
키워드를 사용해서 메서드를 호출합니다.- ex. Thread의
currentThread()
cf.) Effective Java Item.66: 네이티브 메서드는 신중히 사용하라
- Native Method 네이티브 메서드의 쓰임
- 레지스트리 같은 플랫폼 특화 기능 사용 시 (-> 자바가 성숙해지면서 사용할 필요가 줄어들음)
- 네이티브 코드로 작성된 기존 라이브러리 사용 시
- 성능 개선 영향 영역만 따로 작성 시
- ⚠️ BUT 네이티브 언어는 메모리 훼손 오류로부터 안전하지 않음, 네이티브 메서드가 성능을 개선해주는 일은 많지 않음(때로는 이식성 문제가 있거나 디버깅이 어려워서 성능을 더 낮추기도 함)
[Native Method Library]
- C, C++로 작성된 라이브러리 입니다.
❓ JVM의 구조와 함께 Java의 실행방식을 설명해주세요.
- 자바 컴파일러(javac)가 자바 소스코드(
.java
)를 읽어 자바 바이트코드(.class
)로 변환시킵니다. - Class Loader를 통해 class 파일들을 JVM으로 로딩합니다.
- 로딩된 class파일들은 Execution Engine을 통해 해석됩니다.
- 해석된 바이트코드는 Runtime Data Areas 에 배치되어 실질적인 수행이 이루어집니다.
❓ 바이트 코드(byte code)란 무엇인가요?
.java
소스파일을 컴파일 후 생성되는.class
(확장자) 파일로 JVM이 읽어서 기계어로 해석할 수 있는 소스 코드를 의미합니다.- 자바 컴파일러에 의해 변환되는 코드의 명령어 크기가 1byte이기 때문에 byte code라고 불립니다.
❓ Java 성능에 대한 이슈와 관련지어 JIT에 대해 설명해주세요.
- 자바의 특징으로 인터프리터를 하는 과정, 그리고 그 전의 컴파일 과정 때문에 느리다는 인식이 있었습니다.
- 바이트코드로 한 번 컴파일하는 과정
- 바이트코드를 인터프리터로 읽어가는 과정(런타임 시 한 줄씩)
- 자바는 컴파일 방식과 인터프리터 방식을 모두 사용하기 때문에 JIT 컴파일러를 도입해 반복되는 코드를 네이티브 코드로 컴파일해 인터프리터가 바로 사용할 수 있도록, 그로 인해 빠른 성능을 가질 수 있도록 합니다.
❓❓ AOT 컴파일은 무엇일까요?
- Ahead-Of-Time Compile: 소스 코드를 미리 컴파일하는 방식으로 실행 전에 바이트코드를 기계어로 바꾸는 컴파일러입니다.(JIT는 런타임에 네이티브 코드로 컴파일)
- JIT와는 반대의 원리를 갖고 있기에 JIT의 단점이 AOT의 장점이 됩니다.
- 실행 전에 전체 파일을 빌드해야 하기 때문에 빌드 속도가 느려지고 설치 시 기계어로 번역하는 작업까지 포함되기 때문에 설치 속도나 용량이 큰 편이지만 CPU 사용량이 JIT에 비해 상대적으로 낮습니다.
- 주로 C나 C++에 사용합니다.
❓❓ C1 컴파일러와 C2 컴파일러는 무엇이고 역할이 어떻게 다를까요?
- C1, C2 컴파일러는 JIT 컴파일러 내부에 존재합니다.
- C1 컴파일러: 런타임에 바이트 코드를 기계어로 변환하는 과정을 수행합니다.
- C2 컴파일러: 런타임에 바이트 코드를 기계어로 변환한 다음 캐시에 저장하는 캐시에 저장하는 과정까지 수행합니다.
❓❓ 컴파일 과정에서 컴파일러가 최적화해주는 것들은 무엇이 있을까요?
- HotSpot JVM: 미국 Longview Technologies LLC에서 1999년 처음 발표한 JVM, 이후 SUN에 인수되어 현재 가장 일반적인 JVM 중 하나입니다.
- Hot한 Spot을 찾아서 JIT 컴파일러를 사용하기 때문에 이름이 HotSpot입니다. 반복되는 부분을 네이티브 코드로 생성하는데 Client, Server 두 가지 방법을 사용합니다.
- HotSpot JVM Client: 프로그램 시작 시간 최소화(부분적인 코드 실행에 집중)
- HotSpot JVM Server: 전체적인 성능 최적화에 집중
- Dead Code Elimination
- Loop Unrolling
- Escape Analysis(탈출 분석)
- GlobalEscape, ArgEscape, NoEscape
- Lock Coarsening
- Eliminate locks if monitor is not reachable from other threads
- Join adjacent synchronized blocks on the same object
- Method Inlining(자주 호출되는 메서드의 성능이 향상됨)
- Fast instanceOf/checkCast
- Range Check Elimination
- Drop memory write for non-volatile variables
❓ GC가 무엇이고 필요한 이유와 동작 방식에 대해 설명해주세요.
- Java에서는 개발자가 프로그램 코드로 메모리를 명시적으로 해제하지 않기 때문에 가비지 컬렉터(Garbage Collector)가 더 이상 필요 없는 (쓰레기) 객체를 찾아 지우는 작업을 합니다.
- GC는 힙 메모리만 다룹니다.
- GC의 대상
- 객체가 NULL인 경우
- 블럭 실행 종료 후 블럭 안에서 생성된 객체
- 부모 객체가 NULL인 경우, 포함하는 자식 객체
- 관련 가설;
weak generational hypothesis
- 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.
- 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
❓❓ 개발자가 메모리에 대해 신경을 덜 쓸 수 있어서 편해지는데, 그에 따른 단점은 없을까요?
- stop-the-world
- GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것을 의미하는데, stop-the-world가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춥니다.
- GC 작업을 완료한 후 중단한 작업을 다시 시작합니다.
- => GC 튜닝은 stop-the-world 시간을 줄이는 것을 의미합니다.
GC의 물리적 공간 구분
Young Generation 영역
- 새롭게 생성한 객체의 대부분이 위치하는 영역입니다.
- 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 많은 객체가 Young 영역에 생성되었다가 사라집니다.
- 이 때
Minor GC
가 발생합니다. Eden
+Survivor 1
+Survivor 2
- 새로 생성한 대부분의 객체가 위치
- GC 한 번 발생 후 살아남은 객체는 Survivor 중 하나로 이동
- GC 발생하면 위 Survivor 영역에 객체가 계속 쌓임
- 하나의 Survivor가 가득 차면 그 중 살아남은 객체를 다른 Survivor로 이동, 가득찬 곳은 아무 데이터도 없는 상태로 됨(둘 중 하나는 비어있음)
- 위 과정 반복 후 살아남은 객체는 Old영역으로 이동
Old Generation 영역
- 접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사됩니다.
- 대부분 Young 영역보다 크게 할당하며, 큰 만큼 Young보다 GC가 적게 발생합니다.
- 이 때
Major GC
가 발생합니다. (stop-the-world
발생) - Serial GC, Parallel GC, Parallel Old GC, CMS, G1GC(Garbage First GC) 등
❓❓ 개발자가 GC 튜닝을 하는 궁극적인 목표는 무엇일까요?
- 빠른 처리 속도를 달성하면서
stop-the-world
의 최소화를 충족시키는 것이 목표입니다.
❓❓❓ G1GC부터는 GC튜닝에 크게 손이 가진 않는데, G1GC는 어떻게 만들었길래 개발자가 튜닝을 이전보다 덜 해도 되는걸까요?
- G1GC는 바둑판의 각 영역에 객체를 할당하고 GC를 실행합니다.
- 그러다가, 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행합니다. (Young -> Old 단계 사라짐, CMS GC의 문제 해결)
- 기존 GC와는 다르게 Eden, Survivor, Old 영역이 고정된 크기가 아니며 전체 힙 메모리 영역을
Region
이라는 특정한 크기로 나누고Region
의 상태에 따라 역할이 동적으로 변동합니다. - 장점: GC 방식 중에
stop-the-world
시간이 제일 짧습니다.
📌 Object 클래스 - equals()
, hashCode()
❓ 자바의 모든 클래스는 Object 클래스를 상속받습니다. 그리고 Object클래스에는 equals() 와 hashCode() 라는 메소드가 선언되어 있습니다. 이 메소드들은 각각 어떤 역할일까요? 이 둘의 차이점은 무엇일까요?
- 동일성(identity): 메모리상 주소값이 일치하는지를 의미합니다.
==
비교- 객체 인스턴스의 주소 값을 비교합니다.
- primitive data type의 경우
==
을 통해 값 비교가 가능합니다.- 변수 선언부 -> Stack Area / 해당 변수에 저장된 상수 -> Runtime Constant Pool
- 변수 선언부는 해당 Runtime Constant Pool의 주소값을 가지게 되고 만약 다른 변수도 같은 상수를 저장하고 있다면 같은 주소값을 가지게 되기 때문에 주소값 비교가 가능합니다.
hashCode()
- 객체의 주소값을 이용하여 객체 고유의 해시코드를 반환합니다.
- 동등성(equality): 논리적 지위가 동등한지를 의미합니다.
- 객체 내부의 값을 비교합니다.
equals()
- 객체의 값의 일치 여부를 반환하는데 논리적으로 동등한지(참조값이 다르더라도 객체 내부 value는 같음)를 판단합니다.
- String class는
eqauls()
메서드를 재정의해서 문자열 값을 비교합니다.
* 참고 - 잘못 답변할 수 있는 케이스
hashCode()
는 객체의 메모리 주소를 리턴합니다. -> (X)- 반론받을 수 있는 답변: 그럼 우리가
hashCode()
를 오버라이드 했을 때에도 메모리 주소를 리턴하게 할 수 있나요? 자바에서는 개발자가 직접 메모리에 접근할 수 있나요?
❓ String은 왜 문자열이 일치하면 ==
연산의 결과값이 true로 나올까요?
- String은 두 가지 생성방식이 존재합니다.
- 1)
new
연산자를 이용한 방식 -> Heap 영역에 존재 - 2)
리터럴
을 이용한 방식 -> String Constant Pool에 존재
- 1)
String object = new String("literal");
String literal = "literal";
System.out.println(literal == object); //false
System.out.println(literal.equals(object)); //true
- 모든 객체는
new
로 생성해야하는 반면 String은new
없이리터럴
(""
)만으로 객체를 생성할 수 있는 특수한 클래스입니다. - constant pool은 해시테이블 구조로 되어 있어 동일한 문자열은 해시테이블 상 같은 키를 갖게 되어 동일한 주소를 갖게 됩니다.
equals()
는 문자열을 비교하기 때문에 같은 문자열에 대해서는 true가 나오지만 일반 객체처럼 String 객체를 Heap에 생성시킨다면 리터럴로 저장된 String 객체와의 주소값이 다르게 설정됩니다.- 리터럴로 선언할 시 내부적으로
intern()
메서드가 실행되어 주어진 문자열이 String Constant Pool에 존재하는지 검색하고 있다면 그 주소값을 반환, 없을 시 String Constant Pool에 넣고 새로운 주소값을 반환합니다.
String object = new String("literal");
String literal = "literal";
String intern = object.intern();
System.out.println(literal == object); //false
System.out.println(literal.equals(object)); //true
System.out.println(literal == intern); //true
System.out.println(literal.equals(intern)); //true
❓ equals()
와 hashCode()
를 함께 override 해줘야 하는 이유는 무엇인가요?
equals()
메서드를 오버라이딩하는 것은 메모리 주소가 다른 두 객체의 논리적 지위가 동일하다고 선언하는 것입니다.- 만약
hashCode()
를equals()
와 함께 재정의하지 않으면 hash 값을 사용하는 Collection(HashSet, HashMap, HashTable)을 사용할 때 중복을 판단할 때 문제가 발생합니다.- HashTable(다른 Hash Collection 역시 동일)은
<key, value>
형태로 데이터를 저장합니다. 이 때 해시 함수를 이용해 키값 기준 고유 식별값인 해시값을 만듭니다. 이 해시값을 버킷에 저장합니다. - 하지만 HashTable의 크기는 한정적이기 때문에 서로 다른 객체라 하더라도 같은 해시값을 가질 수 있습니다. (=> Hash Collisions; 해시충돌)
- 이처럼 같은 해시값의 버킷 안에 다른 객체가 있는 경우
equals()
를 사용합니다.
- HashTable(다른 Hash Collection 역시 동일)은
- if)
hashCode()
를 재정의하지 않으면- 같은 값 객체라도 해시 값이 다를 수 있습니다. => HashTable에서 해당 객체가 저장된 버킷을 찾을 수 없습니다.
- if)
equals()
를 재정의하지 않으면hashCode()
가 만든 해시값을 이용해 객체가 저장된 버킷을 찾을 수는 있지만 해당 객체가 자신과 같은 객체인지 값을 비교할 수 없기 때문에 null을 리턴하게 됩니다.