JVM 관측 가능성 모범 사례¶
전제 조건¶
공식 웹사이트 Guance에서 계정을 등록하고, 등록된 계정/비밀번호로 로그인합니다.
DataKit 설치¶
명령어 확인¶
[통합] 모듈, [DataKit]을 클릭한 후, 운영체제 및 시스템 유형에 따라 적절한 설치 명령어를 선택합니다.
설치 실행¶
DataKit 설치 명령어를 복사하여 모니터링 대상 서버에서 직접 실행합니다.
- 설치 디렉터리
/usr/local/datakit/ - 로그 디렉터리
/var/log/datakit/ - 주 설정 파일
/usr/local/datakit/conf.d/datakit.conf - 플러그인 설정 디렉터리
/usr/local/datakit/conf.d/
DataKit 기본 설치 플러그인¶
DataKit 설치 완료 후, Linux 호스트 기본 플러그인이 기본적으로 활성화됩니다. 워크스페이스 → 인프라에서 호스트의 기본 정보를 확인할 수 있습니다.
| 수집기 이름 | 설명 |
|---|---|
| cpu | 호스트의 CPU 사용량 수집 |
| disk | 디스크 사용량 수집 |
| diskio | 호스트의 디스크 IO 수집 |
| mem | 호스트의 메모리 사용량 수집 |
| swap | Swap 메모리 사용량 수집 |
| system | 호스트 운영체제 부하 수집 |
| net | 호스트 네트워크 트래픽 수집 |
| host_process | 호스트에서 상주(10분 이상 활성) 중인 프로세스 목록 수집 |
| hostobject | 호스트 기본 정보(운영체제 정보, 하드웨어 정보 등) 수집 |
| docker | 호스트의 컨테이너 객체 및 컨테이너 로그 수집 |
기본 제공 뷰¶
[인프라] 모듈을 클릭하면 DataKit이 설치된 모든 호스트 목록과 기본 정보(호스트명, CPU, 메모리 등)를 확인할 수 있습니다.
JVM 수집 관련 설정:¶
JAVA_OPTS 선언¶
본 예시에서는 ddtrace를 사용하여 Java 애플리케이션의 JVM 지표를 수집합니다. 먼저 요구 사항에 따라 JAVA_OPTS를 정의하고, 애플리케이션 시작 시 JAVA_OPTS를 대체합니다. jar 시작 방식은 다음과 같습니다.
전체 JAVA_OPTS는 다음과 같습니다.
-javaagent:/usr/local/datakit/data/dd-java-agent.jar \
-XX:FlightRecorderOptions=stackdepth=256 \
-Ddd.profiling.enabled=true \
-Ddd.logs.injection=true \
-Ddd.trace.sample.rate=1 \
-Ddd.service.name=your-app-name \
-Ddd.env=dev \
-Ddd.agent.port=9529 \
-Ddd.jmxfetch.enabled=true \
-Ddd.jmxfetch.check-period=1000 \
-Ddd.jmxfetch.statsd.port=8125 \
-Ddd.trace.health.metrics.enabled=true \
-Ddd.trace.health.metrics.statsd.port=8125 \
상세 설명:
-Ddd.env:애플리케이션 환경 유형, 선택 사항
-Ddd.tags:사용자 정의 태그, 선택 사항
-Ddd.service.name: JVM 데이터 출처 애플리케이션 이름, 필수
-Ddd.agent.host=localhost DataKit 주소, 선택 사항
-Ddd.agent.port=9529 DataKit 포트, 필수
-Ddd.version:버전, 선택 사항
-Ddd.jmxfetch.check-period 수집 빈도(밀리초), 기본값 1500, 선택 사항
-Ddd.jmxfetch.statsd.host=127.0.0.1 statsd 수집기 연결 주소(DataKit 주소와 동일), 선택 사항
-Ddd.jmxfetch.statsd.port=8125 DataKit의 statsd 수집기 UDP 연결 포트, 기본값 8125, 선택 사항
-Ddd.trace.health.metrics.statsd.host=127.0.0.1 자체 지표 데이터 수집 전송 주소(DataKit 주소와 동일), 선택 사항
-Ddd.trace.health.metrics.statsd.port=8125 자체 지표 데이터 수집 전송 포트, 선택 사항
-Ddd.service.mapping:애플리케이션이 호출하는 redis, mysql 등의 별칭, 선택 사항
1. jar 사용 방식¶
statsd 활성화
ddtrace 활성화
datakit 재시작
jar를 시작합니다. 아래의 your-app을 애플리케이션 이름으로 바꾸십시오. 애플리케이션이 mysql에 연결되지 않은 경우 -Ddd.service.mapping=mysql:mysql01을 제거하십시오. 여기서 mysql01은 dataflux 애플리케이션 성능 모니터링(APM)에서 확인되는 mysql의 별칭입니다.
nohup java -Dfile.encoding=utf-8 \
-javaagent:/usr/local/datakit/data/dd-java-agent.jar \
-Ddd.service.name=your-app \
-Ddd.service.mapping=mysql:mysql01 \
-Ddd.env=dev \
-Ddd.agent.port=9529 \
-jar your-app.jar > logs/your-app.log 2>&1 &
2. Docker 사용 방식¶
jar 사용 방식과 동일하게 statsd를 활성화하고 ddtrace를 활성화합니다.
외부 네트워크 액세스 포트 개방
/usr/local/datakit/conf.d/vim datakit.conf 파일을 편집하고 listen = "0.0.0.0:9529"로 수정합니다.
datakit 재시작
Dockerfile의 ENTRYPOINT 시작 매개변수에 환경 변수 JAVA_OPTS를 사용하십시오. Dockerfile 예시는 다음과 같습니다.
FROM openjdk:8u292-jdk
ENV jar your-app.jar
ENV workdir /data/app/
RUN mkdir -p ${workdir}
COPY ${jar} ${workdir}
WORKDIR ${workdir}
ENTRYPOINT ["sh", "-ec", "exec java ${JAVA_OPTS} -jar ${jar} "]
이미지 생성
위 내용을 /usr/local/java/Dockerfile 파일에 저장합니다.
/usr/local/datakit/data/dd-java-agent.jar를 /tmp/work 디렉터리로 복사합니다.
Docker run 시작, 172.16.0.215를 서버의 내부 IP 주소로, 9299를 애플리케이션 포트로, your-app을 애플리케이션 이름으로, your-app-image:v1을 이미지 이름으로 바꾸십시오.
docker run -v /tmp/work:/tmp/work -e JAVA_OPTS="-javaagent:/tmp/work/dd-java-agent.jar -Ddd.service.name=your-app -Ddd.service.mapping=mysql:mysql01 -Ddd.env=dev -Ddd.agent.host=172.16.0.215 -Ddd.agent.port=9529 -Ddd.jmxfetch.statsd.host=172.16.0.215 " --name your-app -d -p 9299:9299 your-app-image:v1
Docker compose 시작
Dockerfile은 docker-compose에서 전달된 매개변수를 받기 위해 ARG 매개변수를 선언해야 합니다. 예시는 다음과 같습니다.
FROM openjdk:8u292-jdk
ARG JAVA_ARG
ENV JAVA_OPTS=$JAVA_ARG
ENV jar your-app.jar
ENV workdir /data/app/
RUN mkdir -p ${workdir}
COPY ${jar} ${workdir}
WORKDIR ${workdir}
ENTRYPOINT ["sh", "-ec", "exec java ${JAVA_OPTS} -jar ${jar} "]
위 내용을 /usr/local/java/DockerfileTest 파일에 저장하고, 동일 디렉터리에 docker-compose.yml 파일을 새로 만듭니다. 172.16.0.215를 서버의 내부 IP 주소로, 9299를 애플리케이션 포트로, your-app을 애플리케이션 이름으로, your-app-image:v1을 이미지 이름으로 바꾸십시오. docker-compose.yml 예시는 다음과 같습니다.
version: "3.9"
services:
ruoyi-gateway:
image: your-app-image:v1
container_name: your-app
volumes:
- /tmp/work:/tmp/work
build:
dockerfile: DockerfileTest
context: .
args:
- JAVA_ARG=-javaagent:/tmp/work/dd-java-agent.jar -Ddd.service.name=your-app -Ddd.service.mapping=mysql:mysql01 -Ddd.env=dev -Ddd.agent.host=172.16.0.215 -Ddd.agent.port=9529 -Ddd.jmxfetch.statsd.host=172.16.0.215
ports:
networks:
- myNet
networks:
myNet:
driver: bridge
시작
3 Kubernetes 사용 방식¶
3.1 DataKit 배포¶
Kubernetes에서 DaemonSet 방식으로 DataKit을 배포하려면 <Datakit DaemonSet 설치>를 참조하십시오.
JVM 지표를 수집하려면 ddtrace 및 statsd 수집기를 활성화해야 합니다. DaemonSet 방식으로 배포된 DataKit의 경우, yaml 파일의 ENV_DEFAULT_ENABLED_INPUTS 환경 변수에 statsd, ddtrace를 추가합니다.
- name: ENV_DEFAULT_ENABLED_INPUTS
value: cpu,disk,diskio,mem,swap,system,hostobject,net,host_processes,kubernetes,container,statsd,ddtrace
본 예시의 배포 파일은 /usr/local/k8s/datakit-default.yaml이며, 내용은 다음과 같습니다.
apiVersion: v1
kind: Namespace
metadata:
name: datakit
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: datakit
rules:
- apiGroups:
- rbac.authorization.k8s.io
resources:
- clusterroles
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- nodes
- nodes/proxy
- namespaces
- pods
- pods/log
- events
- services
- endpoints
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- apps
resources:
- deployments
- daemonsets
- statefulsets
- replicasets
verbs:
- get
- list
- watch
- apiGroups:
- batch
resources:
- jobs
- cronjobs
verbs:
- get
- list
- watch
- apiGroups:
- metrics.k8s.io
resources:
- pods
- nodes
verbs:
- get
- list
- nonResourceURLs: ["/metrics"]
verbs: ["get"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: datakit
namespace: datakit
---
apiVersion: v1
kind: Service
metadata:
name: datakit-service
namespace: datakit
spec:
selector:
app: daemonset-datakit
ports:
- protocol: TCP
port: 9529
targetPort: 9529
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: datakit
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: datakit
subjects:
- kind: ServiceAccount
name: datakit
namespace: datakit
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
labels:
app: daemonset-datakit
name: datakit
namespace: datakit
spec:
revisionHistoryLimit: 10
selector:
matchLabels:
app: daemonset-datakit
template:
metadata:
labels:
app: daemonset-datakit
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- env:
- name: HOST_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.hostIP
- name: NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
- name: ENV_DATAWAY
value: https://openway.guance.com?token=<your-token>
- name: ENV_GLOBAL_HOST_TAGS
value: host=__datakit_hostname,host_ip=__datakit_ip,cluster_name_k8s=k8s-prod
- name: ENV_DEFAULT_ENABLED_INPUTS
value: cpu,disk,diskio,mem,swap,system,hostobject,net,host_processes,kubernetes,container,statsd,ddtrace
- name: ENV_ENABLE_ELECTION
value: enable
- name: ENV_HTTP_LISTEN
value: 0.0.0.0:9529
- name: ENV_LOG_LEVEL
value: info
image: pubrepo.guance.com/datakit/datakit:1.2.1
imagePullPolicy: IfNotPresent
name: datakit
ports:
- containerPort: 9529
hostPort: 9529
name: port
protocol: TCP
securityContext:
privileged: true
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-socket
readOnly: true
- mountPath: /usr/local/datakit/conf.d/container/container.conf
name: datakit-conf
subPath: container.conf
- mountPath: /usr/local/datakit/conf.d/log/logging.conf
name: datakit-conf
subPath: logging.conf
- mountPath: /host/proc
name: proc
readOnly: true
- mountPath: /host/dev
name: dev
readOnly: true
- mountPath: /host/sys
name: sys
readOnly: true
- mountPath: /rootfs
name: rootfs
- mountPath: /sys/kernel/debug
name: debugfs
workingDir: /usr/local/datakit
hostIPC: true
hostPID: true
restartPolicy: Always
serviceAccount: datakit
serviceAccountName: datakit
volumes:
- configMap:
name: datakit-conf
name: datakit-conf
- hostPath:
path: /var/run/docker.sock
name: docker-socket
- hostPath:
path: /proc
type: ""
name: proc
- hostPath:
path: /dev
type: ""
name: dev
- hostPath:
path: /sys
type: ""
name: sys
- hostPath:
path: /
type: ""
name: rootfs
- hostPath:
path: /sys/kernel/debug
type: ""
name: debugfs
updateStrategy:
rollingUpdate:
maxUnavailable: 1
type: RollingUpdate
---
apiVersion: v1
kind: ConfigMap
metadata:
name: datakit-conf
namespace: datakit
data:
#### container
container.conf: |-
[inputs.container]
docker_endpoint = "unix:///var/run/docker.sock"
containerd_address = "/var/run/containerd/containerd.sock"
enable_container_metric = true
enable_k8s_metric = true
enable_pod_metric = false
extract_k8s_label_as_tags = false
## Auto-Discovery of PrometheusMonitoring Annotations/CRDs
enable_auto_discovery_of_prometheus_pod_annotations = false
enable_auto_discovery_of_prometheus_service_annotations = false
enable_auto_discovery_of_prometheus_pod_monitors = false
enable_auto_discovery_of_prometheus_service_monitors = false
## Containers logs to include and exclude, default collect all containers. Globs accepted.
container_include_log = []
container_exclude_log = ["image:*logfwd*", "image:*datakit*"]
exclude_pause_container = true
## Removes ANSI escape codes from text strings
logging_remove_ansi_escape_codes = false
## Search logging interval, default "60s"
#logging_search_interval = ""
## If the data sent failure, will retry forevery
logging_blocking_mode = true
kubernetes_url = "https://kubernetes.default:443"
## Authorization level:
## bearer_token -> bearer_token_string -> TLS
## Use bearer token for authorization. ('bearer_token' takes priority)
## linux at: /run/secrets/kubernetes.io/serviceaccount/token
## windows at: C:\var\run\secrets\kubernetes.io\serviceaccount\token
bearer_token = "/run/secrets/kubernetes.io/serviceaccount/token"
# bearer_token_string = "<your-token-string>"
logging_auto_multiline_detection = true
logging_auto_multiline_extra_patterns = []
## Set true to enable election for k8s metric collection
election = true
[inputs.container.logging_extra_source_map]
# source_regexp = "new_source"
[inputs.container.logging_source_multiline_map]
# source = '''^\d{4}'''
[inputs.container.tags]
# some_tag = "some_value"
# more_tag = "some_other_value"
#### logging
logging.conf: |-
[[inputs.logging]]
## required
logfiles = [
"/rootfs/var/log/k8s/demo-system/info.log",
"/rootfs/var/log/k8s/demo-system/error.log",
]
## glob filteer
ignore = [""]
## your logging source, if it's empty, use 'default'
source = "k8s-demo-system"
## add service tag, if it's empty, use $source.
service = "k8s-demo-system"
## grok pipeline script path
#pipeline = ""
## optional status:
## "emerg","alert","critical","error","warning","info","debug","OK"
ignore_status = []
## optional encodings:
## "utf-8", "utf-16le", "utf-16le", "gbk", "gb18030" or ""
character_encoding = ""
## The pattern should be a regexp. Note the use of '''this regexp'''
## regexp link: https://golang.org/pkg/regexp/syntax/#hdr-Syntax
match = '''^\d{4}-\d{2}-\d{2}'''
[inputs.logging.tags]
# some_tag = "some_value"
# more_tag = "some_other_value"
https://console.guance.com/에서 openway 주소를 찾아 아래 그림과 같이 datakit-default.yaml의 ENV_DATAWAY 값을 바꿉니다.
Datakit 배포
본 예시에서 시스템 로그를 수집하려면 아래 내용을 참조하십시오.
#- mountPath: /usr/local/datakit/conf.d/log/demo-system.conf
# name: datakit-conf
# subPath: demo-system.conf
#### kubernetes
demo-system.conf: |-
[[inputs.logging]]
## required
logfiles = [
"/rootfs/var/log/k8s/demo-system/info.log",
"/rootfs/var/log/k8s/demo-system/error.log",
]
## glob filteer
ignore = [""]
## your logging source, if it's empty, use 'default'
source = "k8s-demo-system"
## add service tag, if it's empty, use $source.
service = "k8s-demo-system"
## grok pipeline script path
pipeline = ""
## optional status:
## "emerg","alert","critical","error","warning","info","debug","OK"
ignore_status = []
## optional encodings:
## "utf-8", "utf-16le", "utf-16le", "gbk", "gb18030" or ""
character_encoding = ""
## The pattern should be a regexp. Note the use of '''this regexp'''
## regexp link: https://golang.org/pkg/regexp/syntax/#hdr-Syntax
match = '''^\S'''
[inputs.logging.tags]
# some_tag = "some_value"
# more_tag = "some_other_value"
3.2 sidecar 이미지¶
jar 사용 방식에서는 dd-java-agent.jar가 사용됩니다. 사용자 이미지에 이 jar가 반드시 존재하는 것은 아니므로, 고객의 비즈니스 이미지를 변경하지 않기 위해 dd-java-agent.jar를 포함하는 이미지를 만든 다음, sidecar 방식으로 비즈니스 컨테이너보다 먼저 시작하여 공유 스토리지 방식으로 dd-java-agent.jar를 제공해야 합니다.
3.3 Java 애플리케이션 Dockerfile 작성¶
Dockerfile의 ENTRYPOINT 시작 매개변수에 환경 변수 JAVA_OPTS를 사용하십시오. Dockerfile 예시는 다음과 같습니다.
FROM openjdk:8u292
ENV jar your-app.jar
ENV workdir /data/app/
RUN mkdir -p ${workdir}
COPY ${jar} ${workdir}
WORKDIR ${workdir}
ENTRYPOINT ["sh", "-ec", "exec java ${JAVA_OPTS} -jar ${jar}"]
이미지를 생성하고 harbor 레지스트리에 업로드합니다. 아래의 172.16.0.215:5000/dk를 이미지 레지스트리로 바꾸십시오.
$ cd /usr/local/k8s/agent
$ docker build -t 172.16.0.215:5000/dk/your-app-image:v1 .
$ docker push 172.16.0.215:5000/dk/your-app-image:v1
3.4 deployment 작성¶
/usr/local/k8s/your-app-deployment-yaml 파일을 새로 만들고, 내용은 다음과 같습니다.
apiVersion: v1
kind: Service
metadata:
name: your-app-name
labels:
app: your-app-name
spec:
selector:
app: your-app-name
ports:
- protocol: TCP
port: 9299
nodePort: 30001
targetPort: 9299
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: your-app-name
labels:
app: your-app-name
spec:
replicas: 1
selector:
matchLabels:
app: your-app-name
template:
metadata:
labels:
app: your-app-name
spec:
containers:
- env:
- name: PODE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: JAVA_OPTS
value: |-
-javaagent:/usr/dd-java-agent/agent/dd-java-agent.jar -Ddd.service.name=<your-app-name> -Ddd.tags=container_host:$(PODE_NAME) -Ddd.env=dev -Ddd.agent.port=9529
- name: DD_AGENT_HOST
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.hostIP
name: your-app-name
image: 172.16.0.215:5000/dk/your-app-image:v1
#command: ["sh","-c"]
ports:
- containerPort: 9299
protocol: TCP
volumeMounts:
- mountPath: /usr/dd-java-agent/agent
name: ddagent
initContainers:
- command:
- sh
- -c
- set -ex;mkdir -p /ddtrace/agent;cp -r /datadog-init/* /ddtrace/agent;
image: pubrepo.guance.com/datakit-operator/dd-lib-java-init
imagePullPolicy: Always
name: ddtrace-agent-sidecar
volumeMounts:
- mountPath: /ddtrace/agent
name: ddagent
restartPolicy: Always
volumes:
- emptyDir: {}
name: ddagent
설명: JAVA_OPTS의 -Ddd.tags=container_host:$(PODE_NAME)는 환경 변수 PODE_NAME의 값을 태그 container_host에 전달합니다. 9299를 애플리케이션 포트로, your-app-name을 서비스 이름으로, 30001을 애플리케이션의 외부 노출 포트로, 172.16.0.215:5000/dk/your-app-image:v1을 이미지 이름으로 바꾸십시오.
시작
JVM 관측 가능성 시나리오 생성:¶
Guance에 로그인하여 워크스페이스로 이동한 후, [시나리오 생성]을 클릭합니다.
[JVM 모니터링 시나리오]를 클릭합니다.
시나리오 이름 [JVM 모니터링 시나리오]를 입력하고 확인을 클릭합니다.
위 그림에서 JVM 모니터링 뷰를 찾아 마우스를 올린 후 생성을 클릭합니다.
JVM 모니터링 뷰는 다음과 같습니다.
JVM 및 관련 지표 소개¶
1 JVM 개요¶
1.1 JVM이란?¶
JVM은 Java Virtual Machine의 약자로, 운영체제 위에서 실행되며 Java 바이트코드를 실행하는 가상 컴퓨터입니다.
1.2 클래스 로딩 메커니즘¶
먼저 Java 소스 파일은 Java 컴파일러에 의해 바이트코드로 컴파일된 후, JVM의 클래스 로더가 바이트코드를 로드합니다. 로딩이 완료되면 JVM 실행 엔진이 실행합니다.
1.3 클래스의 생명주기¶
Java 클래스는 시작부터 종료까지 전체 생명주기 동안 7단계를 거칩니다: 로딩(Loading), 검증(Verification), 준비(Preparation), 해석(Resolution), 초기화(Initialization), 사용(Using), 언로딩(Unloading). 이 중 검증, 준비, 해석 세 부분을 통틀어 연결(Linking)이라고 합니다.
1.4 JVM 메모리 구조¶
전체 클래스 로딩 과정에서 JVM은 데이터와 관련 정보를 저장하기 위해 일정 공간을 사용합니다. 이 공간을 흔히 JVM 메모리라고 합니다. JVM 사양에 따라 JVM 메모리는 다음과 같이 구분됩니다.
- 실행 엔진
Java는 플랫폼 독립적인 프로그래밍 언어로, 실행 엔진은 바이트코드를 해당 플랫폼이 인식할 수 있는 기계 명령어로 해석합니다.
- 프로그램 카운터
프로그램 카운터는 작은 메모리 공간으로, 현재 스레드가 실행 중인 바이트코드의 줄 번호를 가리키는 역할을 합니다. 가상 머신의 개념 모델에서 바이트코드 인터프리터는 이 카운터 값을 변경하여 다음에 실행할 바이트코드 명령어를 선택합니다. 분기, 루프, 점프, 예외 처리, 스레드 복원 등의 기본 기능은 이 카운터에 의존합니다.
특징: 점유 메모리가 매우 작아 무시 가능; 스레드 격리; native 로컬 메서드 실행 시 프로그램 카운터 값은 null; 이 메모리 영역은 Java 가상 머신 사양에서 OutOfMemoryError가 발생하지 않도록 지정된 유일한 영역입니다.
- 가상 머신 스택
Java 메서드 실행의 메모리 모델을 설명합니다. 각 메서드가 실행될 때마다 "스택 프레임(Stack Frame)"이 생성됩니다. 즉, 스레드 전용이며 생명주기는 스레드와 일치합니다. 스택 프레임의 구조는 로컬 변수 테이블, 피연산자 스택, 동적 링크, 메서드 출구 등의 부분으로 나뉩니다.
흔히 말하는 "힙 메모리, 스택 메모리"의 "스택 메모리"는 가상 머신 스택을 말하며, 정확히는 가상 머신 스택의 스택 프레임 내 로컬 변수 테이블을 의미합니다. 메서드의 모든 로컬 변수가 여기에 저장되기 때문입니다. 메서드 호출 시 스택 프레임이 생성되어 가상 머신 스택에 푸시되고, 메서드 실행이 완료되면 스택 프레임이 팝되어 소멸됩니다.
JVM은 각 스레드의 가상 머신 스택에 일정 메모리 크기(-Xss 매개변수)를 할당합니다. 단일 스레드가 요청한 스택 깊이가 가상 머신이 허용하는 깊이를 초과하면 StackOverflowError(스택 오버플로 오류)가 발생합니다. 전체 가상 머신 스택 메모리가 모두 소진되고 더 이상 새 메모리를 할당할 수 없으면 OutOfMemoryError 예외가 발생합니다.
- 로컬 메서드 스택
로컬 메서드 스택의 기능과 특징은 가상 머신 스택과 유사하며, 스레드 격리 기능이 있고 StackOverflowError 및 OutOfMemoryError 예외가 발생할 수 있습니다.
차이점은 로컬 메서드 스택이 서비스하는 대상은 JVM이 실행하는 native 메서드이고, 가상 머신 스택이 서비스하는 대상은 JVM이 실행하는 Java 메서드라는 점입니다. native 메서드를 서비스하는 방법, native 메서드가 사용하는 언어, 스택 프레임과 같은 메서드 서비스용 데이터 구조를 구성하는 방법 등은 가상 머신 사양에서 강제 규정하지 않으므로, 각 가상 머신이 자유롭게 구현할 수 있습니다. 일반적으로 사용되는 HotSpot 가상 머신은 가상 머신 스택과 로컬 메서드 스택을 병합하도록 선택했습니다.
- 메서드 영역
JDK8에서는 PermGen(영구 세대)이 폐기되고, 각 클래스의 실행 상수 풀, 컴파일된 코드는 힙과 연결되지 않은 별도의 로컬 메모리인 메타스페이스(Metaspace)로 이동되었습니다.
메타스페이스(Metaspace): 메타스페이스는 HotSpot JVM에서 메서드 영역을 구현한 것입니다. 메서드 영역은 주로 클래스 정보, 상수 풀, 메서드 데이터, 메서드 코드, 심볼 참조 등을 저장하는 데 사용됩니다. 메타스페이스의 본질은 PermGen(영구 세대)과 유사하며, 모두 JVM 사양의 메서드 영역을 구현한 것입니다. 그러나 메타스페이스와 PermGen의 가장 큰 차이점은 메타스페이스가 가상 머신 내부에 있지 않고 로컬 메모리를 사용한다는 점입니다. 이론적으로 32비트/64비트 시스템 메모리 크기에 따라 달라지며, -XX:MetaspaceSize 및 -XX:MaxMetaspaceSize를 통해 메모리 크기를 구성할 수 있습니다.
메타스페이스에는 두 가지 매개변수가 있습니다. MetaspaceSize: 초기 메타스페이스 크기, GC 발생 임계값을 제어합니다. MaxMetaspaceSize: 메타스페이스 크기 상한을 제한하여 비정상적인 과도한 물리 메모리 점유를 방지합니다.
- 힙
힙 영역은 모든 스레드가 공유하며, 주로 객체 인스턴스와 배열을 저장합니다. 물리적으로 연속되지 않은 공간에 위치할 수 있지만 논리적으로는 연속적이어야 합니다.
힙 메모리는 Young Generation(젊은 세대)과 Old Generation(노년 세대)으로 나뉩니다. Young Generation은 다시 Eden 영역과 Survivor 영역으로 나뉩니다. Survivor 영역은 FromSpace와 ToSpace로 구성됩니다. Eden 영역은 큰 용량을 차지하고, Survivor 두 영역은 작은 용량을 차지하며, 기본 비율은 8:1:1입니다.
Java 힙에서 인스턴스 할당을 위한 메모리가 부족하고 힙을 더 이상 확장할 수 없게 되면 Java 가상 머신은 OutOfMemoryError 예외를 발생시킵니다.
JVM 힙 메모리 주요 매개변수
| 매개변수 | 설명 |
|---|---|
| -Xms | 힙 메모리 초기 크기, 단위 m, g |
| -Xmx(MaxHeapSize) | 힙 메모리 최대 허용 크기, 일반적으로 물리 메모리의 80%를 초과하지 않아야 함 |
| -XX:PermSize | 비힙 메모리 초기 크기, 일반 애플리케이션은 초기 200m, 최대 1024m로 설정하면 충분 |
| -XX:MaxPermSize | 비힙 메모리 최대 허용 크기 |
| -XX:NewSize(-Xns) | Young Generation 메모리 초기 크기 |
| -XX:MaxNewSize(-Xmn) | Young Generation 메모리 최대 허용 크기, 축약 가능 |
| -XX:SurvivorRatio=8 | Young Generation에서 Eden 영역과 Survivor 영역의 용량 비율, 기본값 8, 즉 8:1 |
| -Xss | 스택 메모리 크기 |
- 런타임 데이터 영역
Java 가상 머신은 Java 프로그램을 실행하는 과정에서 관리하는 메모리를 여러 데이터 영역으로 나눕니다. 각 영역은 각자의 용도와 생성 및 소멸 시간을 가지며, 일부 영역은 가상 머신 프로세스 시작과 함께 존재하고, 다른 영역은 사용자 스레드 시작 및 종료에 따라 생성 및 소멸됩니다.
《Java 가상 머신 사양(Java SE 8 Edition)》에 따르면 Java 가상 머신이 관리하는 메모리에는 다음과 같은 런타임 데이터 영역이 포함됩니다: 프로그램 카운터, Java 가상 머신 스택, 로컬 메서드 스택, Java 힙, 메서드 영역.
- 직접 메모리
직접 메모리는 가상 머신 런타임 데이터 영역의 일부가 아니며 Java 가상 머신 사양에 정의된 메모리 영역도 아닙니다. Java 힙 크기의 제한을 받지 않으며, 로컬 시스템의 총 메모리 크기에 의해 제한됩니다.
직접 메모리도 -XX:MaxDirectMemorySize로 지정할 수 있습니다. 직접 메모리 공간 할당은 더 높은 성능을 소모하지만, 직접 메모리의 IO 읽기/쓰기 성능은 일반 힙 메모리보다 우수합니다. 메모리가 모두 소진되면 OutOfMemoryError 예외가 발생합니다.
- 가비지 컬렉션
프로그램 카운터, 가상 머신 스택, 로컬 메서드 스택의 3개 영역은 스레드의 생멸에 따라 함께합니다(스레드 전용이므로). 스택의 스택 프레임은 메서드의 진입 및 종료에 따라 체계적으로 팝 및 푸시 작업이 실행됩니다. 그러나 Java 힙과 메서드 영역은 다릅니다. 인터페이스의 여러 구현 클래스가 필요로 하는 메모리는 다를 수 있으며, 메서드의 여러 분기가 필요로 하는 메모리도 다를 수 있습니다. 프로그램이 실행 중일 때만 어떤 객체가 생성될지 알 수 있습니다. 이 부분의 메모리 할당 및 회수는 모두 동적이며, 가비지 컬렉터가 주목하는 부분이 바로 이 메모리입니다.
가비지 컬렉터:
직렬 컬렉터(Serial)
병렬 컬렉터(Parallel)
CMS 컬렉터(Concurrent Mark Sweep)
G1 컬렉터(Garbage First)
가비지 컬렉션 알고리즘(GC, Garbage Collection):
표시-제거(Mark-Sweep)
복사(Copy)
표시-정리(Mark-Compact)
1.5 GC, Full GC¶
세대별 가비지 컬렉션을 위해 Java 힙 메모리는 3개 세대로 나뉩니다: Young Generation, Old Generation, Permanent Generation. Permanent Generation에서 GC가 실행되는지 여부는 사용하는 JVM에 따라 다릅니다. 새로 생성된 객체는 우선 Young Generation의 Eden 영역에 배치되며, 큰 객체는 직접 Old Generation으로 이동합니다. Eden 영역에 충분한 공간이 없으면 Minor GC가 실행됩니다. 살아남은 객체는 Survivor0 영역으로 이동하고, Survivor0 영역이 가득 차면 Minor GC가 실행되며, Survivor0 영역의 살아남은 객체는 Survivor1 영역으로 이동합니다. 이렇게 하면 일정 시간 동안 항상 하나의 survivor 영역이 비어 있게 됩니다. 여러 번의 Minor GC(기본 15회) 후에도 여전히 살아있는 객체는 Old Generation으로 이동합니다. Old Generation은 오래 살아남은 객체를 저장하며, Old Generation으로 승격된 객체의 크기가 Old Generation의 남은 공간보다 크면 Major GC가 발생합니다. Old Generation의 공간이 부족하면 Full GC가 발생합니다. Major GC 발생 시 사용자 스레드는 일시 중지되어 시스템 성능과 처리량이 저하됩니다. 따라서 응답 시간이 중요한 애플리케이션은 Major GC 발생을 최소화하여 응답 시간 초과를 방지해야 합니다. GC 후에도 Survivor 영역에서 복사된 객체를 저장할 수 없으면 OOM(Out of Memory)이 발생합니다.
1.6 OutOfMemoryError 발생 원인¶
OOM(Out of Memory) 예외는 일반적으로 다음과 같은 원인으로 발생합니다.
1) Old Generation 메모리 부족: java.lang.OutOfMemoryError:Javaheapspace
2) Permanent Generation 메모리 부족: java.lang.OutOfMemoryError:PermGenspace
3) 코드 버그로 인해 점유된 메모리를 적시에 회수하지 못함. OOM은 이러한 메모리 영역 중 어디에서든 발생할 수 있습니다. 실제로 OOM이 발생하면 예외 정보를 통해 어떤 영역에서 메모리 오버플로가 발생했는지 확인할 수 있습니다. -XX:+HeapDumpOnOutMemoryError 매개변수를 추가하면 가상 머신이 메모리 오버플로 예외 발생 시 현재 메모리 힙 덤프 스냅샷을 덤프하여 사후 분석에 활용할 수 있습니다.
1.7 JVM 튜닝¶
JAVA 메모리 관리 메커니즘 및 구성 매개변수를 숙지한 후, JAVA 애플리케이션 시작 옵션 튜닝 구성은 다음과 같습니다.
1 힙 메모리 최소값 -Xms와 최대값 -Xmx를 동일하게 설정하여 GC 후 메모리 재할당을 방지합니다.
2 GC 가비지 컬렉터를 G1으로 설정합니다. -XX:+UseG1GC
3 GC 로그를 활성화하여 추후 분석에 활용합니다. -Xloggc:../logs/gc.log
2 기본 제공 뷰¶
3 성능 지표¶
| 지표 | 설명 | 데이터 유형 | 단위 |
|---|---|---|---|
| buffer_pool_direct_capacity | 직접 버퍼 총 크기 | int | Byte |
| buffer_pool_direct_count | 직접 버퍼 개수 | int | count |
| buffer_pool_direct_used | 직접 버퍼 사용 크기 | int | Byte |
| buffer_pool_mapped_capacity | 메모리 매핑 버퍼 총 크기 | int | Byte |
| buffer_pool_mapped_count | 메모리 매핑 버퍼 개수 | int | count |
| buffer_pool_mapped_used | 메모리 매핑 버퍼 사용 크기 | int | Byte |
| cpu_load_process | 프로세스 CPU 점유율 | 소수 | 백분율 |
| cpu_load_system | 시스템 CPU 점유율 | 소수 | 백분율 |
| gc_eden_size | Young Generation Eden 영역 크기 | int | Byte |
| gc_survivor_size | Young Generation Survivor 영역 크기 | int | Byte |
| gc_old_gen_size | Old Generation 크기 | int | Byte |
| gc_metaspace_size | 메타스페이스 크기 | int | Byte |
| gc_major_collection_count | Old Generation GC 횟수 | int | count |
| gc_major_collection_time | Old Generation GC 소요 시간 | int | ms |
| gc_minor_collection_count | Young Generation GC 횟수 | int | count |
| gc_minor_collection_time | Young Generation GC 소요 시간 | int | ms |
| heap_memory_committed | 힙 메모리 커밋 바이트 수 | int | Byte |
| heap_memory_init | 힙 메모리 초기 바이트 수 | int | Byte |
| heap_memory_max | 힙 메모리 최대 바이트 수 | int | Byte |
| heap_memory | 힙 메모리 사용 바이트 수 | int | Byte |
| loaded_classes | 클래스 로드 수 | int | count |
| non_heap_memory_committed | 비힙 메모리 커밋 바이트 수 | int | Byte |
| non_heap_memory_init | 비힙 메모리 초기 바이트 수 | int | Byte |
| non_heap_memory_max | 비힙 메모리 최대 바이트 수 | int | Byte |
| non_heap_memory | 비힙 메모리 사용 바이트 수 | int | Byte |
| os_open_file_descriptors | 열린 파일 디스크립터 수 | int | count |
| thread_count | 총 스레드 수 | int | count |












