不懂如何实现JVM可观测?技术大咖优秀实践分享来啦

本文涉及的产品
可观测可视化 Grafana 版,10个用户账号 1个月
云拨测,每月3000次拨测额度
简介: 一文带你实现JVM可观测

前置条件

账号注册

前往官方网站 观测云 - 云时代的系统可观测平台 注册账号,使用已注册的账号/密码登录。

image.png

安装 Datakit

获取命令

点击 [集成] 模块, [DataKit],根据您的操作系统和系统类型选择合适的安装命令。

image.png

执行安装

复制 Datakit 安装命令在需要被监控的服务器上直接运行。

  • 安装目录 /usr/local/datakit/
  • 日志目录 /var/log/datakit/
  • 主配置文件 /usr/local/datakit/conf.d/datakit.conf
  • 插件配置目录 /usr/local/datakit/conf.d/

datakit默认已安装如下插件:

Datakit 安装完成后,已经默认开启 Linux 主机常用插件,可以在DF——基础设施——内置视图查看。

image.png

内置视图

点击 [基础设施] 模块,查看所有已安装 Datakit 的主机列表以及基础信息,如主机名,CPU,内存等。

image.png

点击 [主机名] 可以查看该主机的详细系统信息,集成运行情况 (该主机所有已安装的插件),内置视图(主机)。

image.png

点击 [集成运行情况] 任意插件名称 [查看监控视图] 可以看到该插件的内置视图。

image.png

JVM采集相关配置:

JAVA_OPTS声明

本示例使用ddtrace采集java应用的jvm指标,先根据您的需求定义JAVA_OPTS,在启动应用的时候替换JAVA_OPTS,启动jar方式如下:

java  ${JAVA_OPTS} -jar your-app.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=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:JVM数据来源的应用名称,必填  
-Ddd.agent.host=localhost    DataKit地址 
-Ddd.agent.port=9529         DataKit端口,必填  
-Ddd.version:版本
-Ddd.jmxfetch.check-period 表示采集频率,单位为毫秒,必填  
-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等别名

如需详细了解JVM,请参考JVM采集器

1 jar使用方式

开启statsd

1. $ cd /usr/local/datakit/conf.d/statsd
2. $ cp statsd.conf.sample statsd.conf

开启ddtrace

1. $ cd /usr/local/datakit/conf.d/ddtrace
2. $ cp ddtrace.conf.sample  ddtrace.conf

重启datakit

$ datakit --restart

启动jar,请用您的应用名替换下面的your-app,如果应用未连接mysql,请去掉-Ddd.service.mapping=mysql:mysql01,其中mysql01是观测云应用性能监控看到的mysql的别名

nohup java -Dfile.encoding=utf-8  \
 -javaagent:/usr/local/datakit/data/dd-java-agent.jar \
 -XX:FlightRecorderOptions=stackdepth=256 \
 -Ddd.service=your-app   \
 -Ddd.service.mapping=mysql:mysql01   \
 -Ddd.env=dev  \
 -Ddd.agent.port=9529   \
 -Ddd.jmxfetch.enabled=true   \
 -Ddd.jmxfetch.check-period=1000   \
 -Ddd.jmxfetch.statsd.port=8125   \
 -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"

image.png

重启datakit

$ datakit --restart

请在您的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文件中

$ cd /usr/local/java
$ docker build -t your-app-image:v1 .

拷贝/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 -XX:FlightRecorderOptions=stackdepth=256 -Ddd.service=your-app  -Ddd.service.mapping=mysql:mysql01 -Ddd.env=dev  -Ddd.agent.host=172.16.0.215 -Ddd.agent.port=9529 -Ddd.jmxfetch.enabled=true -Ddd.jmxfetch.check-period=1000 -Ddd.jmxfetch.statsd.host=172.16.0.215 -Ddd.jmxfetch.statsd.port=8125 " --name your-app -d -p 9299:9299 your-app-image:v1

Docker compose启动

Dockerfile需要声明ARG参数来接收docker-compose传过来的参数,示例如下:

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 -XX:FlightRecorderOptions=stackdepth=256 -Ddd.logs.injection=true  -Ddd.trace.sample.rate=1  -Ddd.service=your-app  -Ddd.service.mapping=mysql:mysql01 -Ddd.env=dev  -Ddd.agent.host=172.16.0.215 -Ddd.agent.port=9529 -Ddd.jmxfetch.enabled=true -Ddd.jmxfetch.check-period=1000 -Ddd.jmxfetch.statsd.host=172.16.0.215 -Ddd.jmxfetch.statsd.port=8125 
    ports:
    networks:
      - myNet
networks:
  myNet:
    driver: bridge

启动

$ cd /usr/local/java
#制作镜像
$ docker build -t your-app-image:v1 .
#启动
$ docker-compose up -d

3 Kubernetes 使用方式

3.1部署Datakit

在kubernetes中使用DaemonSet方式部署Datakit,如需详细了解,请参考 Datakit DaemonSet安装

新建/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:
  - ""
  resources:
  - nodes
  - nodes/proxy
  - namespaces
  - pods
  - services
  - endpoints
  - persistentvolumes
  - persistentvolumeclaims
  - ingresses
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - apps
  resources:
  - deployments
  - daemonsets
  - statefulsets
  - replicasets
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - extensions
  resources:
  - ingresses
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - batch
  resources:
  - jobs
  - cronjobs
  verbs:
  - get
  - list
  - watch
- nonResourceURLs: ["/metrics"]
  verbs: ["get"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: datakit
  namespace: datakit
---
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.dataflux.cn?token=<your-token>
        - name: ENV_GLOBAL_TAGS
          value: host=__datakit_hostname,host_ip=__datakit_ip
        - name: ENV_ENABLE_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.jiagouyun.com/datakit/datakit:1.1.8-rc0
        imagePullPolicy: Always
        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/kubernetes/kubernetes.conf
          name: datakit-conf
          subPath: kubernetes.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
        workingDir: /usr/local/datakit
      hostIPC: true
      hostNetwork: true
      hostPID: true
      restartPolicy: Always
      serviceAccount: datakit
      serviceAccountName: datakit
      terminationGracePeriodSeconds: 30
      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
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 1
    type: RollingUpdate
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: datakit-conf
  namespace: datakit
data:
    #### container
    container.conf: |-
      [inputs.container]
        endpoint = "unix:///var/run/docker.sock"
        enable_metric = true
        enable_object = true
        enable_logging = true
        metric_interval = "10s"
        ## TLS Config
        # tls_ca = "/path/to/ca.pem"
        # tls_cert = "/path/to/cert.pem"
        # tls_key = "/path/to/key.pem"
        ## Use TLS but skip chain & host verification
        # insecure_skip_verify = false
        [inputs.container.kubelet]
          kubelet_url = "http://127.0.0.1:10255"
          ## Use bearer token for authorization. ('bearer_token' takes priority)
          ## If both of these are empty, we'll use the default serviceaccount:
          ## at: /run/secrets/kubernetes.io/serviceaccount/token
          # bearer_token = "/path/to/bearer/token"
          ## OR
          # bearer_token_string = "abc_123"
          ## Optional TLS Config
          # tls_ca = /path/to/ca.pem
          # tls_cert = /path/to/cert.pem
          # tls_key = /path/to/key.pem
          ## Use TLS but skip chain & host verification
          # insecure_skip_verify = false
        #[[inputs.container.logfilter]]
        #  filter_message = [
        #    '''<this-is-message-regexp''',
        #    '''<this-is-another-message-regexp''',
        #  ]
        #  source = "<your-source-name>"
        #  service = "<your-service-name>"
        #  pipeline = "<pipeline.p>"
        [inputs.container.tags]
          # some_tag = "some_value"
          # more_tag = "some_other_value"
    #### kubernetes
    kubernetes.conf: |-
      [[inputs.kubernetes]]
          # required
          interval = "10s"
          ## URL for the Kubernetes API          
          url = "https://kubernetes.default:443"
          ## Use bearer token for authorization. ('bearer_token' takes priority)
          ## at: /run/secrets/kubernetes.io/serviceaccount/token
          bearer_token = "/run/secrets/kubernetes.io/serviceaccount/token"
          ## Set http timeout (default 5 seconds)
          timeout = "5s"
           ## Optional TLS Config
          tls_ca = "/run/secrets/kubernetes.io/serviceaccount/ca.crt"
          ## Use TLS but skip chain & host verification
          insecure_skip_verify = false
          [inputs.kubernetes.tags]
           #tag1 = "val1"
           #tag2 = "valn"
    #### 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 = '''^\S'''
          [inputs.logging.tags]
          # some_tag = "some_value"
          # more_tag = "some_other_value"

https://console.guance.com 找到openway地址,如下图所示,替换datakit-default.yaml中的ENV_DATAWAY的值

image.png

部署Datakit

$ cd /usr/local/k8s
$ kubectl apply -f datakit-default.yaml
$ kubectl get pod -n datakit

image.png

本示例如果采集系统日志,请参考下面的内容:

#- 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.2sidecar镜像

在jar使用方式中使用到了dd-java-agent.jar,而在用户的镜像中并不一定存在这个jar,为了不侵入客户的业务镜像,我们需要制作一个包含dd-java-agent.jar的镜像,再以sidecar的方式先于业务容器启动,以共享存储的方式提供dd-java-agent.jar。

pubrepo.jiagouyun.com/datakit/dk-sidecar:1.0

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 -XX:FlightRecorderOptions=stackdepth=256 -Ddd.service=<your-app-name> -Ddd.tags=container_host:$(PODE_NAME)  -Ddd.env=dev  -Ddd.agent.port=9529 -Ddd.jmxfetch.enabled=true -Ddd.jmxfetch.check-period=1000 -Ddd.jmxfetch.statsd.port=8125 -Ddd.version=1.0
        - 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 /usr/dd-java-agent/agent/* /ddtrace/agent;
        image: pubrepo.jiagouyun.com/datakit/dk-sidecar:1.0
        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为您的镜像名

image.png

启动

1. $ cd /usr/local/k8s/
2. $ kubectl apply -f your-app-deployment-yaml

新建JVM可观测场景:

登录观测云进入observer空间,点击新建场景

image.png

点击新建空白场景

image.png

image.png

输入场景名称JVM监控,点击确定

image.png

找到上图中的JVM监控视图,鼠标移到上面,点击创建

JVM监控视图如下:

image.png

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)。

image.png

1.4 JVM内存结构

在整个类的加载过程中,JVM会用一段空间来存储数据和相关信息,这段空间就是我们常说的JVM内存。根据JVM规范,JVM内存共分为:


  • 执行引擎

java是一种跨平台的编程语言,执行引擎就是把字节码解析成对应平台能识别的机器指令。


  • 程序计数器

程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。


特点:占用内存很小,可忽略不计;线程隔离;执行native本地方法时,程序计数器的值为空;此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。


  • 虚拟机栈

描述Java方法执行的内存模型,每个方法在执行时都会创建一个“栈帧(Stack Frame)”,即线程私有,生命周期和线程一致。栈帧的结构分为局部变量表、操作数栈、动态链接、方法出口几个部分。


我们常说的“堆内存、栈内存”中的“栈内存”指的便是虚拟机栈,确切地说,指的是虚拟机栈的栈帧中的局部变量表,因为这里存放了一个方法的所有局部变量。方法调用时,创建栈帧,并压入虚拟机栈;方法执行完毕,栈帧出栈并被销毁。


JVM会为每个线程的虚拟机栈分配一定的内存大小(-Xss参数),若单个线程请求的栈深度大于虚拟机允许的深度,则会抛出StackOverflowError(栈溢出错误)。当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时抛出OutOfMemoryError异常。


  • 本地方法栈

本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常。


不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。如何去服务native方法?native方法使用什么语言实现?怎么组织像栈帧这种为了服务方法的数据结构?虚拟机规范并未给出强制规定,因此不同的虚拟机可以进行自由实现,我们常用的HotSpot虚拟机选择合并了虚拟机栈和本地方法栈。


  • 方法区

JDK8废弃永久代,每个类的运行时常量池、编译后的代码移到了另一块与堆不相连的本地内存--元空间。


元空间(Metaspace):元空间是方法区的在 HotSpot JVM 中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码、符号引用等。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统内存大小,可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 配置内存大小。


元空间有两个参数:MetaspaceSize :初始化元空间大小,控制发生GC阈值。MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存。


堆区由所有线程共享,主要是存放对象实例和数组。可以位于物理上不连续的空间,但是逻辑上要连续。


堆内存分为年轻代(Young Generation)、老年代(Old Generation)。年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。


如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出 OutOfMemoryError 异常。


JVM堆内存常用参数

image.png

  • 运行时数据区

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都各有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。


根据《Java 虚拟机规范(Java SE 8版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。

image.png

  • 直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是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代:年轻代,老年代和永久代。永久代是否执行GC,取决于采用的JVM。新生成的对象优先放到年轻代Eden区,大对象直接进入老年代,当Eden区没有足够空间时,会发起一次Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC(默认15次)仍然存活的对象移动到老年代。老年代存储长期存活的对象,当升到老年代的对象大于老年代剩余空间时会发生Major GC,当老年代空间不足时会引发Full GC。发生Major GC时用户线程会暂停,会降低系统性能和吞吐量。所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。如果GC后,还是无法存放从Survivor区复制过来的对象,就会出现OOM(Out of Memory)。

1.6 引发OutOfMemoryError原因

OOM(Out of Memory)异常常见有以下几个原因:1)老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace2)永久代内存不足:java.lang.OutOfMemoryError:PermGenspace3)代码bug,占用内存无法及时回收。OOM在这几个内存区都有可能出现,实际遇到OOM时,能根据异常信息定位到哪个区的内存溢出。可以通过添加个参数-XX:+HeapDumpOnOutMemoryError,让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。

1.7 JVM调优

熟悉了JAVA内存管理机制及配置参数,下面是对JAVA应用启动选项调优配置:


  1. 设置堆内存最小-Xms和最大值-Xmx相等,避免每次垃圾回收后重新分配内存
  2. 设置GC垃圾收集器为G1, -XX:+UseG1GC
  3. 启用GC日志,方便后期分析-Xloggc:../logs/gc.log

2 内置视图

image.png

3 性能指标

image.png

image.png

更多请关注:

如何利用观测云采集JVM指标

如何利用观测云监控JVM


相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
目录
相关文章
|
4天前
|
算法 Java
太狠了!阿里技术专家撰写的电子版JVM&G1 GC实战,颠覆了传统认知
JVM是Java语言可以跨平台、保持高发展的根本,没有了 JVM, Java语言将失去运行环境。针对 Java 程序的性能优化一定不可能避免针对JVM 的调优,随着 JVM 的不断发展,我们的应对措施也在不断地跟随、变化,内存的使用逐渐变得越来越复杂。所有高级语言都需要垃圾回收机制的保护,所以 GC 就是这么重要。
|
4天前
|
监控 Oracle Java
《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,探索各大JVM虚拟机特色 —— JVM故障排除指南(先导篇)
《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,探索各大JVM虚拟机特色 —— JVM故障排除指南(先导篇)
45 0
|
4天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
92 0
|
3天前
|
Java 数据库连接 Spring
K8S+Docker理论与实践深度集成java面试jvm原理
K8S+Docker理论与实践深度集成java面试jvm原理
|
4天前
|
Kubernetes Oracle Java
JVM调参实践总结
这篇文档探讨了JVM调优的最佳实践,重点关注Oracle的HotSpot VM。内容涵盖Java版本选择,建议使用Java 11、17或稳定版Java 8,尤其是对于需要免费版本的用户,可以选择OpenJDK。接着讨论了JVM类型,推荐根据需求平衡稳定性、性能和费用。在部署时,应考虑64位JVM,并确保与操作系统和CPU架构兼容。JVM运行模式建议使用Java 8及以后版本默认的混合模式。垃圾收集器的选择是一个关键点,需根据应用需求平衡吞吐量、延迟和内存占用。默认的垃圾收集器如Parallel Scavenge和Parallel Old适用于多数情况,但特定场景可能需要CMS或G1收集器。
12 1
|
4天前
|
监控 Java 调度
探秘Java虚拟机(JVM)性能调优:技术要点与实战策略
【4月更文挑战第17天】本文探讨了JVM性能调优的关键技术,包括内存模型调优(关注堆内存和垃圾回收),选择和优化垃圾收集器,利用JVM诊断工具进行问题定位,以及实战调优案例。强调了开发者应理解JVM原理,善用工具,结合业务场景进行调优,以应对高并发和大数据量的挑战。调优是持续的过程,能提升系统稳定性和效率。
|
4天前
|
存储 Java 索引
《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(核心结构剖析)
《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(核心结构剖析)
35 0
|
4天前
|
存储 算法 Java
精华推荐 | 【JVM深层系列】「GC底层调优专题」一文带你彻底加强夯实底层原理之GC垃圾回收技术的分析指南(GC原理透析)
精华推荐 | 【JVM深层系列】「GC底层调优专题」一文带你彻底加强夯实底层原理之GC垃圾回收技术的分析指南(GC原理透析)
60 0
|
4天前
|
Cloud Native Java Docker
【Spring云原生】Spring官宣,干掉原生JVM,推出 Spring Native!整体提升性能!Native镜像技术在Spring中的应用
【Spring云原生】Spring官宣,干掉原生JVM,推出 Spring Native!整体提升性能!Native镜像技术在Spring中的应用
|
4天前
|
存储 监控 Java
JVM监控和分析技术在实践中可能会面临什么?
JVM监控和分析技术在实践中可能会面临什么?