자바 최적화

11. 자바 언어 성능 향상 기법

p327

참조형 필드는 힙에 레퍼런스로 저장됩니다. 객체가 순차적으로 저장된다고 대충 말하지만, 사실 컨테이너에 저장되는 건 객체 자신이 아니라, 객체를 가리키는 레퍼런스입니다. 그러므로 C/C++ 형식의 배열이나 벡터를 사용하는 것만큼 성능을 얻을 수는 없습니다.

p329

ArrayList는 고정 크기 배열에 기반한 리스트입니다. 배킹 배열의 최대 크기만큼 원소를 추가할 수 있고 이 배열이 꽉 차면 더 큰 배열을 새로 할당한 다음 기존 값을 복사합니다.
  • 크기 조정이 발생

    • 비용이 꽤 될 것 그러므로 크기를 정확히 결정할 수 있다면 결정하고 객체를 생성하는 것이 나음

    • 벤치마크로 알아볼 수도 있을듯?

p331

LinkedList의 고유 기능이 꼭 필요한 경우가 아니라면, 특히 랜덤 엑세스가 필요한 알고리즘을 구사할 떄는 ArrayList를 권장합니다. 

p333

HashMap은 처음에는 버킷 엔트리를 리스트에 저장합니다. 값을 찾아려면 키 해시값을 계산하고 equals() 메서드로 리스트에서 해당 키를 찾습니다. 키를 해시하고 동등성을 기준으로 리스트에서 값을 찾는 매커니즘으로 키 중복은 허용되지 않습니다. 같은 기를 넣으면 원래 HashMap에 있던 키를 치환합니다.
  • 자바 hashmap에서 키를 통해 값을 찾는 일련의 과정

p334

HashMap의 용량을 현재 생성된 버킷의 수(디폴트 16)를, loadFactor는 버킷 용량을 자동 증가(2배) 시키는 한게치 입니다. 용량을 2배 늘리고 저장된 데이터를 재배치한 다음, 해시를 다시 계산하는 과정을 재해시rehash라 합니다.
  • 해시맵에도 용량이 있고, 보통 2배 늘리고 재배치하는 과정을 가져가는 듯.

  • 다만 생성자에 전달되는 initialCapacity, loadFactor 두 매개 변수를 통해서 위 과정을 좀 더 효율적으로 할 수 도 있을듯

p335

TreeMap은 레드-블랙 트리를 구현한 Map입니다. 레드-블랙 트리는 기본 이진 트리 구조에 메타데이터를 부가해서 트리 규현이 한 쪽으로 치우치는 현상을 방지한 트리입니다.
  • 레드블랙트리는 삽입/삭제/검색의 시간복잡도를 O(logN) 을 보장

p337

Set은 중복값을 허용하지 않습니다. Map의 키 원소와 똑같습니다. HashSet의 add() 메서드가 내부적으로 사용하는 HashMap은 키가 원소 E, 값이 PRESENT라는 더미 객체로 구성됩니다.
  • 즉 HashSet은 내부적으로 HashMap을 사용하고 있으며, Map의 value값에는 더미 객체가 들어가는 구조라는 것

p340

- 가장 흔히 할당되는 자료 구조는 스트링, char 배열, byte 배열, 자바 컬렉션 타입의 인스턴스
- jmap에서 누수되는 데이터는 비정상적으로 비대한 데이터셋으로 나타납니다.

메모리 점유량, 인스턴스 개수 모두 보통 코어 JDK에 있는 자료 구조가 상위권을 형성하는 게 보통입니다. 그런데 어플리케이션에 속한 도메인 객체가 jmap 결과치의 상위 30위 정도안에 든다면 꼭 그렇다고 단정지을 수는 없지만 메모리 누수가 발생한 신호라고 볼 수 있습니다.

p347

자바의 메모리 관리 서브 시스템은 할당할 가용 메모리가 부족하면 그때그떄 반사적으로 가비지 수집기를 실행
가비지 수집이 언제 일어날지는 아무도 모르는 까닭에 객체가 수집될 때에만 실행되는 finalize() 메서드도 언제 실행될지 알 길이 없음
다시 말해, 가비지 수집이 딱 정해진 시간에 실행되는 법이 없으므로 종료화를 통해 자동으로 리소스를 관리한다는 것 자체가 어불성설
  • C와는 다르게 자바는 가비지 콜렉터가 객체에 대한 생명주기를 관리함

  • 문제는 가비지 컬렉터가 언제 이 객체를 청소할지 모른다는 것

    • 그래서 Obejct.finalize() 는 java9 이후 부터 deprecated됨

12. 동시 성능 기법

p361

상태를 공유하는 워크로드는 무조건 정교한 보호/제어 장치가 필요합니다.
자바 플랫폼은 JVM에서 실행되는 워크로드에 JMM 이라는 메모리 보증 세트를 제공합니다.
  • 업데이트 소설

    • 멀티 스레딩 환경에서 발생할 수 있는 문제

    • A 스레드에서 업데이트 했는데, 다른(B) 스레드에는 그 업데이트 상황이 반영되지 않아 결국엔 A 스레드에서 업데이트한 것이 없어지는 현상

  • sychronized 블록 없이 레퍼런스를 여러 스레드에서 공유할 경우(그리고 수정한다면) 문제가 발생할 가능성이 다분

    • 더 문제는 문제가 발생할 수도 혹은 발생하지 않을 수도 있다는 점

    • 버그 재현이 힘들 수도

p365

JMM은 다음 질문에 답을 찾는 모델입니다.
- 두 코어가 같은 데이터를 액세스 하면 어떻게 되는가?
- 언제 두 코어가 같은 데이터를 바라본다고 장담할 수 있는가?
- 메모리 캐시는 위 두 질문의 답에 어떤 영향을 미치는가?

자바 플랫폼은 공유 상태를 어디서 액세스하든지 JMM이 약속한 내용을 반드시 이행합니다.
그 약속이란, 순서에 관한 보장과 여러 스레드에 대한 업데이트 가시성 보장, 두가지로 분류됩니다.
  • JMM의 메모리 모델 (고수준)

    • 강한 메모리 모델, 전체 코어가 항상 같은 값을 바라봄

    • 약한 메모리 모델, 코어 마다 다른 값을 바라볼 수 있고 그 시점을 제어하는 특별한 캐시 규칙이 있다.

  • 자바에서는 강한 메모리 모델이 아닌 약한 메모리 모델을 선택

    • 왜?

      • 언어 자체가 아키텍쳐에 독립적인 이라는 점 (강한 메모리 모델을 지원하지 않는 하드웨어의 경우.. 이 부분을 지원하기 위해 어떻게 해야할지 난감한듯)

      • 멀티코어 체제에 부적절하기 떄문

p 367

  • JMM의 기본개념

Happen-Before
한 이벤트는 무조건 다른 이벤트보다 먼저 발생한다.

Synchronizes-With
이벤트가 객체 뷰를 메인 메모리와 동기화시킨다.

As-If-Serial
실행 스레드 밖에서 명령어가 순차 실행되는 것처럼 보인다.

Release-Before-Acquire
한 스레드에 걸린 락을 다른 스레드가 그 락을 획득하기 전에 해제한다.
  • 자바에서 스레드?

    • 객체 상태 정보를 스스로 들고 다니며,

    • 스레드가 변경한 내용은 메인 메모리에 곧장 반영괻고,

    • 같은 데이터를 액세스하는 다른 스레드가 읽는 구조

  • 자바의 Synchronized keyword?

    • JMM 기본 개념의 Synchronizes-With 와 동일

    • 즉 모니터를 장악한 스레드의 로컬 뷰가 메인 메모리와 동기화 되었다는 뜻

  • 자바에서 동기화되지 않는 액세스는, 그러니까 이 스레드에서 변경한 부분을 언제 다른 스레드에서 읽을 수 있는지 보장하지 않음.

    • 약한 메모리 모델이기 때문인듯

p368

  • 그러면 Synchronized 락의 한계점?

- 락이 걸린 객체에서 일어나는 동기화 작업은 모두 균등하게 취급한다.
- 락 획득/해제는 반드시 메서드 수준이나 메서드 내부의 동기화 블록 안에서 이루어져야 한다.
- 락을 얻지 못한 스레드는 블로킹 된다. 락을 얻지 못할 경우, 락을 얻어 처리를 계속하려고 시도하는 것조차 불가능한다.
  • 쓰기 작업에만 Synchronized 를 적용하면, lost update 현상이 일어난다.

    • 즉 읽기 작업을 할 때 변경된 값을 읽어 오리라는 보장이 없다.

p371

CAS(Compare and swap)는 예상되는 현재 값과 원하는 새 값, 그리고 메모리 위치를 전달 받아 다음 두가지 일을 하는 아토믹 유닉
1. 예상되는 현자 값을 메모리 위치에 있는 콘텐츠와 비교
2. 두 값이 일치하면 현재 값을 원하는 새 값으로 교체

Unsafe

  • sum.misc.Unsafe 는 내부 구현 클래스

    • 저수준 하드웨어 명령어에 액세스할 수 있음

  • 표준 자바 플랫폼 API가 아니다.

  • 애플리케이션 개발자가 이 클래스를 직접 이용할 일은 거의 없다.

  • 할 수 있는 일?

    • 객체는 할당, 생성자는 실행하지 않음

    • raw memory에 액세스 하고 포인터 수준의 연산을 수행

    • 프로세스별 하드웨어 특성 (e.g. CAS)을 이용

  • 이를 통해

    • 직렬화, 역직렬화

    • 스레드 안전한 네이티브 메모리 액세스

    • atomic 메모리 연산

    • 효율적인 객체/메모리 레이아웃

    • custom memory fence

    • 네이티브 코드와의 상호작용

    • JNI에 관한 다중 운영체체 대체물..

    • 배열 원소에 volatile하게 액세스..

  • p373에서 Unsafe.getUnsafe() 로 직접적으로 사용하면 SecurityException 발생

    • 스프링을 사용하면 UnsafeUtils 로 접근하도록 하자

    • https://stackoverflow.com/questions/13003871/how-do-i-get-the-instance-of-sun-misc-unsafe

java.util.concurrent 락

  • lock()

    • 락을 획득하고, 락을 사용할 수 있을 때 까지 블록킹

  • newCondition()

    • 락 주위에 조건을 설정해 좀 더 유연하게 락을 확용

    • 락 내부에서 관심사 분리(읽기 혹은 쓰기)가 가능하다

  • tryLock()

    • 락을 획득하려고 시도(타임아웃 설정가능)

    • 덕분에 스레드가 락을 사용할 없는 경우에도, 처리를 진행할 수 있음.

    • 논 블로킹 방식

  • unlock()

    • 락 해제

  • ReentrantLock

    • Lock의 주요 구현체

    • 내부적으로는 int 값이 compareAndSwap(0 함)

  • LockSupport

    • 스레드에게 permit을 발급 (퍼밋 == 세마포어..?)

    • 만약 발급할 수 있는 permit 이 없다면 스레드는 기다려야함

    • 0, 1 binary semaphore만을 permit으로 발급함

    • 스레드가 permit을 발급받지 못한 경우 parking 되고

    • 유효한 permit을 발급받을 수 있다면 해당 스레드는 unparking됨

    • 이 클래스가 Thread.suspend() -> parking, Thread.resume() -> unparking을 대체 한다

  • ReentrantReadWriteLock

    • 읽기와 쓰기에서 다른 락을 사용하는 방법

    • 즉 읽기 작업 중에는 다른 읽기 스레드는 블로킹 되지 않도록 하지만

    • 쓰기 작업 중에는 읽기나 혹은 쓰기 스레드는 블로킹 될 것

세마포어

  • '최대 ~개의 객체까지만 액세스를 허용한다' 는 전제하에 정해진 수량의 permit으로 액세스를 제어하는 것

  • permit이 하나 뿐인 세마포어(binary semaphore)는 뮤텍스 mutex와 동등

    • 뮤텍스는 뮤텍스가 걸린 스레드가 해제할 수 있는 반면

    • 세마포어는 비소유 스레드도 세마포어를 해제할 수 있는 점이 다르다

p381

... 따라서 읽기/쓰기 락을 둘 다 소유한 상태에서 여러 스레드가 ConcurrentHashMap 곳곳을 읽는 동안, 쓰기가 필요한 경우 어느 한 세그먼트만 락을 거는 행위도 가능합니다.
일반적으로 락을 걸지 않는 읽기 스레드는 안심하고 put(), remove() 작업을 중첩시켜도 되고, 완료된 업데이트 작업에 대해서는 Happens-Before 순서대로 읽습니다.
  • Happens-Before?

    • 한 이벤트는 무조건 다른 이벤트보다 먼저 발생한다.

p384

일반 자바 프로그래머는 저수준의 스레드 문제를 직접 처리하려고 하기 보다는, java.util.concurrent 패키지에서 적절한 수준으로 추상화된 동시 프로그래밍 지원 기능을 골라 쓰는 편이 좋다

Fork/Join

  • ForkJoinPool 클래스는 두가지 특성이 있음

    • 하위 분할 태스크를 효율적으로 처리할 수 있음

    • 작업 빼앗기 알고리즘을 구현

  • 하위 분할 태스크?

    • 자바 스레드보다 가벼운, 스레드와 비슷한 엔티티

    • ForkJoinTask 클래스가 지원하는 기능

      • 자기 자신을 더 작은 서브 태스크를 분할하는 능력이 핵심

    • 적은 수의 스레드가 아주 많은 태스크/서브 태스크를 담당해야하는 경우 사용

p390

자바 8의 가장 큰 변경사항은 람다와 스트림
  • 자바 스트림은 데이터 소스에서 원소를 퍼 나르는 불변 데이터 시퀀스

  • parallelStream() 을 이용하면 병렬로 데이터를 작업 후, 그 결과를 재조합 할 수 있음

    • 실제로 컬렉션이 작을수록 직렬 연산이 병렬 연산보다 훨씬 빠르다

    • 그러므로 항상 패러럴스트림을 사용할 때는 성능 테스트를 해봐야한다

액터 기반 기법

  • actor 실행기

    • 가번적인 상태는 일체 공유하지 않고 오직 불변 메세지를 통해서만 액터끼리 상호 교류함

    • 이를 통해 상태 관리

  • 전통적인 락킹 체계보다 액터가 더 좋다. 왜?

    • 락을 쓰면 데드락..

    • 락을 쓰면 CPU 처리율이 떨어질 수도..

Reference

  • http://www.yes24.com/Product/Goods/72161685

Last updated