JVM 이해하기
JVM
•
자바 가상 머신으로 자바 바이트 코드를 OS에 특화된 코드로 변환(인터프리터와 JIT컴파일러)하여 실행한다.
•
바이트 코드를 실행하는 표준이자 구현체
•
JVM 스펙, JVM벤더(오라클, 아마존, Azul)
•
특정 플랫폼에 종속적: Native 코드에 맞춰서 실행해야하기 때문에
JRE
•
자바 애플리케이션을 실행할 수 있도록 구성된 배포판
•
JVM과 핵심 라이브러리 및 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일을 가지고 있다.
•
개발 관련 도구는 제공하지 않음. (실행은 할 수 있다. 단, 컴파일할 때 썼던 javac같은 거는 못 씀)
JDK
•
JRE + 개발에 필요한 툴
•
소스 코드를 작성할 때 사용하는 자바 언어는 플랫폼에 독립적
•
오라클은 자바 11부터 JDK 만 제공하며, JRE를 따로 제공하지 않는다.
JAVA
•
프로그래밍 언어
•
JDK에 들어있는 자바 컴파일러를 사용하여 바이트코드(.class파일) 로 컴파일 할 수 있다.
•
자바 유료화? 오라클에서 만든 Oracle JDK 11버전부터 상용으로 사용할 때 유료
◦
오라클에서 만든 Oracle Open JDK라고 있다. 이거는 무료
◦
오라클에서 만들지 않은 Open JDK
타 프로그래밍 언어 지원
•
JVM 기반으로 동작
•
클로져, 그루비, 루비, 코틀린 등
JVM 이해하기
1.
클래스 로더 시스템
2.
메모리
3.
실행엔진
4.
JNI, 네이티브 메소드 라이브러리
클래스 로더 시스템
바이트 코드를 읽어들여서 메모리에 적절히 올려놓는 것
로딩: 바이트 코드를 읽어오는 것
링크: 레퍼런스를 연결하는 과정
초기화: static한 값들을 초기화한다.
메모리
영역: 스택, PC, 네이티브 메소드 스택, 힙, 메소드
메소드 영역: 클래스 수준의 정보( 클래스 이름, 부모 클래스 이름, 메소드, 변수) 저장, 고유 자원(다른 영역에서 참조할 수 있는 거다)
힙 영역: 객체를 저장(인스턴트들을 저장). 공유 자원
쓰레드 단위로 공유되는 자원
스택 영역: 쓰레드마다 런타임 스택을 만들고, 그 안에 메소드 호출을 스택 프레임이라 부르는 블럭으로 쌓는다.(메소드 콜), 쓰레드 종료하면 런타임 스택도 사라진다.
PC(Program Counter) 레지스터: 쓰레드마다 쓰레드 내 현재 실행할 스택 프레임을 가리키는 포인터가 생성된다.
네이티브 메소드 스택: 네이티브 메소드가 쌓이는 스택:
(네이티브 메소드라 하믄?, 네이티브라는 키워드가 있고, C나 C++로 구현되어있는 것)
실행엔진
바이트 코드들을 이해할 수 있다.
한줄 한줄씩 실행하면서 네이티브 코드로 바꿔서 이해하면서 실행하는 것
네이티브 언어로 한줄씩 컴파일하는 것
JIT 컴파일러: 인터프리터 효율을 높이기 위해, 인터프리터가 반복되는 코드를 발견하면 JIT 컴파일러로 반복되는 코드를 모두 네이티브 코드로 바궈둔다. 그 다음부터 인터프리터는 네이티브 코드로 컴파일된 코드를 바로 사용
GC(Garbage Collector): 더이상 참조되지 않는 객체를 모아서 정리
•
Stop-The-World : 많은 객체를 생성하고 응답 시간이 중요하다
•
Throw-put:
클래스 로더 시스템
로딩
•
클래스 로더가 .class 파일을 읽고 그 내용에 따라 적절한 바이너리 데이터를 만들고 "메소드" 영역에 저장
•
이 때 메소드 영역에 저장하는 데이터
◦
FQCN(Full Qualified Class Name)
◦
클래스 , 인터페이스, Enum
◦
메소드와 변수
•
로딩이 끝나면 해당 클래스 타입의 class객체를 생성하여 '힙'영역에 저장
계층 구조로 이뤄져있다
부트스트랩 클래스 로더 → 플랫폼 클래스로더 → 애플리케이션 클래스로더
부트스트랩: JAVA_HOME\lib에 있는 코어 자바 API를 제공, 최상위 우선순위
플랫폼 클래스로더 : JAVA_HOME\lib\ext 폴더 또는 java.ext.dirs 시스템 변수에 해당하는 위치에 있는 클래스를 읽는다
애플리케이션 클래스로더: 애플리케이션 클래스패스에서 클래스를 읽는다.
링크
Verify → Prepare → Resolve(optional)
Verify: .class파일 형식이 유효한지 체크
Preparation: 클래스 변수와 기본값에 필요한 메모리
Resolve: 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체
심볼릭 메모리 레퍼런스: 논리적인 레퍼런스다. 실제 Heap에 있는 레퍼런스로 가리키게끔 하는게 Resolve
초기화
static 변수의 값을 할당한다.
바이트 코드 조작
코드 커버리지는 어떻게 측정할까?
코드 커버리지 → 테스트 코드가 확인한 소스 코드를 %로 나타내준다.
코드 커버리지 툴의 동작 원리
•
바이트 코드를 읽어서 코드 커버리지를 챙겨야하는 부분을 개수를 샌다.
•
코드가 실행될 때, 어디가 실행됬는지 비교하는 거다.
노란색: 완벽하게 지나간게 아니다. if문의 분기를 다 지나간게 아니다
초록색: 지나간 부분
빨간색: 아예 지나가지 않은 부분
모자에서 토끼를 꺼내는 마술
public class Moja{
public String pullOut(){
return "";
}
}
Java
복사
public class Masulsa{
public static void main(String[]args){
System.out.println(new Moja().pullOut());
}
}
Java
복사
bytebuddy → 클래스가 바이트 코드로 변경될 때, 바이트코드를 조작하는 것이다.
javaagent → 클래스 로더가 클래스를 읽어올 대 javaagent를 거쳐서 변경된 바이트코드를 읽어들여 사용한다.
바이트 코드 활용 예
1.
프로파일러: 메모리를 얼마나 쓰는지, 쓰레드는 얼마나 쓰는지 등등, 성능 분석 툴
2.
최적화, 로깅 등등
3.
프록시 객체를 만드는데도 쓰인다.
Ex) 스프링이 컴포넌트 스캔을 하는 방법(asm)
컴포넌트 스캔으로 빈으로 등록할 후보 클래스 정보를 찾는데 사용
특정한 애노테이션 정보가 붙어있는 클래스들을 찾는 과정
ClassPathScanningCandidateComponentProvider → SimpleMetadataReader
ClassReader와 Visitor를 사용해서 클래스나 메소드에 있는 애노테이션 정보를 들고온다.
코드를 분석하는데 쓰는 것!
리플렉션
리플렉션 API 1부: 클래스 정보 조회
Class<Book> bookClass = Book.class;
Book book = new Book();
Class<? extends Book> aClass = book.getClass();
Class<?> aClass1 = Class.forName("me.whiteship.Book");
Java
복사
모든 클래스를 로딩 한 다음 Class의 인스턴스가 생긴다.
•
“타입.class”로 접근할 수있다.
•
모든 인스턴스는 getClass() 메소드를 가지고 있다. “인스턴스.getClass()”로 접근할 수
있다.
•
클래스를 문자열로 읽어오는 방법
◦
Class.forName(“FQCN”)
◦
클래스패스에 해당 클래스가 없다면 ClassNotFoundException이 발생한다.
class<T>를 통해 할 수 있는 것
•
필드 가져오기
•
메소드 가져오기
•
상위 클래스 가져오기
•
인터페이스 가져오기
•
애노테이션 가져오기
•
생성자 가져오기
애노테이션과 리플렉션
애노테이션은 근본적으로 주석과 같은 정보다
소스에서도 남고, 클래스에서도 남는데, byte Code를 로딩했을 때 메모리에서는 남지 않는다.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.Type, ElementType.FIELD}) // 애노테이션을 어디에 쓸 수 있는지
@Inherited // 상속 관계에서 애노테이션이 상속된다.
public @interface myAnnotation(){
String name() default "keesun";
int number() default 100;
}
Java
복사
이런 식으로 애노테이션을 정의해야 메모리에도 애노테이션 정보가 남는다.
Arrays.stream(Book.class.getAnnotations()).forEach(System.out::println);
Java
복사
이런 식으로 애노테이션을 확인할 수 있다.
클래스 정보 수정 또는 실행
public class Book{
public static String A = "A";
private STring B = "B";
public Book(){
}
private void c(){
System.out.println("C");
}
public int sum(int left, int right){
return left * right;
}
}
Java
복사
public class App{
public static void main(String[] args] throws classNotFoundException{
Class<?> bookClass = class.forName("me.whiteship.Book");
Constructor<?> constructor = bookClass.getConstructor(String.class);
Book book = (Book) constructor.newInstance("myBook");
System.out.println(book);
Field a = Book.class.getDeclaredField("A");
a.get(null);
a.set(null,"AAAA");
Field b = Book.class.getDeclaredField("B");
b.setAccessible(true);
b.get(book);
b.set(book,"BBBB");
}
}
Java
복사
나만의 DI 프레임워크 만들기
리플렉션을 사용해서, 클래스 정보를 받아오고, 인스턴스를 만드는 방식으로 사용할 수 있다.
리플렉션 정리 및 활용
스프링
•
의존성 주입
•
MVC 뷰에서 넘어온 데이터를 객체에 바인딩
하이버네이트
•
@Entity 클래스에 Setter가 없다면 리플렉션을 사용
주의할점!
컴파일 타임에 확인되지 않고 런타임 시에만 발생하는 문제를 만들 가능성이 있다.
접근 지시자를 무시할 수 있다.
지나친 사용은 성능 이슈를 야기할 수 있다. 반드시 필요한 경우에만 사용할 것
다이나믹 프록시
스프링 데이터 JPA는 어떻게 동작하나?
public interface BookRepository extends JpaRepository<Book,Integer>{
}
//???? 어떻게 이게 인터페이슨데, 인터페이스 인스턴스가 만들어졌냐?
//사용시 이렇게 사용한다.
@Auowired BookRepository bookRepository;
Java
복사
인터페이스에 정의만 되어있는 애들인데 어떻게 동작을 하냐?
누가 어떻게 구현을 해서 객체를 만드는가? —> 프록시라는 클래스
데이터 JPA는 AOP코드를 쓰고 있다.
AOP → ProxyFactory를 제공한다.
프록시 패턴
프록시도 리얼서브젝트도 같은 인터페이스를 구현하고 있다.
프록시는 리얼 서브젝트를 참조하고 있다.
클라이언트는 프록시를 사용함.
•
프록시와 리얼 서브젝트가 공유하는 인터페이스가 있고, 클라이언트는 해당 인터페이스 타입으로 프록시를 사용한다
•
클라이언트는 프록시를 거쳐서 리얼 서브젝트를 사용하기 때문에 프록시는 리얼 서브젝트에 대한 접근을 관리하거나 부가기능을 제공하거나 리턴값을 변경할 수도 있다.
•
리얼 서브젝트는 자신이 해야할 일만 하면서(SRP) 프록시를 사용해서 부가적인 기능(접근 제한, 로깅, 트랜잭션 등)을 제공할 때 이런 패턴을 주로 사용함.
public class BookServiceProxy implement BookService{
BookService bookService;
public BookServiceProxy(BookService bookService){
this.bookService = bookService;
}
@Override
public void rent(Book book){
System.out.println("aaaaa");
bookService.rent(book);
}
@Override
public void returnBook(Book book){
System.out.println("aaaa");
bookService.returnBook(book);
}
}
//프록시 패턴으로 할 때 비슷한 기능들이 계속 생겨난다.
// 이런 것들을 클래스로 정의해놓는게 아니라, 런타임에 특정 인터페이스들을 구현하는
// 클래스 또는 인스턴스를 만드는 기술
Java
복사
다이나믹 프록시
런타임에 특정 인터페이스들을 구현하는 클래스 또는 인스턴스를 만드는 기술
public class BookServiceTeset{
BookService bookService = (BookSerivce) Proxy.newProxyInstance(BookService.class.getClassLoader(),
new Class[]{BookService.class},
new InvocationHandler(){
BookService bookService = new DefaultBookService();
@Override
public Object invoke(Object proxy, Method method, Object[]args) throws Throwable{
if(method.getName().equals("rent")){
System.out.println("aaaa");
Object invoke = method.invoke(bookService,args);
System.out.println("bbbb");
return invoke;
}
return method.invoke(bookSerivce,args);
}
});
Java
복사
코드가 너무 더럽다...
스프링 AOP → 프록시 기반의 AOP
이건 자바가 제공하는 프록시 기술
다이나믹 프록시 → 클래스 기반의 프록시를 만들어주지 못한다.
클래스의 프록시가 필요하다면?
서브 클래스를 만들 수 있는 라이브러리를 사용하여 프록시를 만들 수 있다.
CGlib
MethodInterceptor handler = new MethodInterceptor(){
BookService bookService = new BookService();
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("aaa");
Object invoke = method.invoke(bookSerivce,args);
System.out.println("bbb");
return invoke;
}
}
BookService bookService = (BookSerivce) Enhancer.create(BookService.class, handler)
Java
복사
ByteBuddy —> 너무 어려운데..?하핳..
서브 클래스를 만드는 방법의 단점
•
상속을 사용하지 못하는 경우 프록시를 만들 수 없다.
◦
private한 생성자만 있는 경우
◦
final 클래스인 경우
•
인터페이스가 있을 때는 인터페이스의 프록시를 만들어 사용할 것.
사용처
스프링 데이터 JPA
스프링 AOP
Mockito
하이버네이트 lazy initialization
??? 지금까지는 바이트코드를 조작, 바이트 코드 이전에는 조작할 수 없는가?, 소스코드 레벨에서 컴파일에서 또 다른 소스코드를 생성해낼 수 있는 기능 → Lombok
애노테이션 프로세서
롬복은 어떻게 동작할까?
애노테이션 프로세서 → 애노테이션이 붙어있는 소스코드가 컴파일 될 때 또 다른 소스코드를 만들어낸다.
•
소스코드의 AST(abstract syntax tree) 를 조작한다.
•
공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스 코드를 조작한다.
애노테이션 프로세서
사용 예
•
롬복
•
AutoService: java.util.ServiceLoader 용 파일 생성 유틸리티 → 리소스 파일을 생성
•
@Override
런타임 비용이 제로 but 기존 클래스 코드를 변경할 때는 약간의 hack이 필요하다