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

本文涉及的产品
可观测可视化 Grafana 版,10个用户账号 1个月
可观测监控 Prometheus 版,每月50GB免费额度
性能测试 PTS,5000VUM额度
简介: 一文带你实现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


相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
6月前
|
监控 Java 调度
探秘Java虚拟机(JVM)性能调优:技术要点与实战策略
【6月更文挑战第30天】**探索JVM性能调优:**关注堆内存配置(Xms, Xmx, XX:NewRatio, XX:SurvivorRatio),选择适合的垃圾收集器(如Parallel, CMS, G1),利用jstat, jmap等工具诊断,解决Full GC问题,实战中结合MAT分析内存泄露。调优是平衡内存占用、延迟和吞吐量的艺术,借助VisualVM等工具提升系统在高负载下的稳定性与效率。
107 1
|
2月前
|
安全 Java API
🌟探索Java宇宙:深入理解Java技术体系与JVM的奥秘
本文深入探讨了Java技术体系的全貌,从Java语言的概述到其优点,再到Java技术体系的构成,以及JVM的角色。旨在帮助Java开发者全面了解Java生态,提升对Java技术的认知,从而在编程实践中更好地发挥Java的优势。关键词:Java, JVM, 技术体系, 编程语言, 跨平台, 内存管理。
44 2
|
5月前
|
JSON Java BI
一次Java性能调优实践【代码+JVM 性能提升70%】
这是我第一次对系统进行调优,涉及代码和JVM层面的调优。如果你能看到最后的话,或许会对你日常的开发有帮助,可以避免像我一样,犯一些低级别的错误。本次调优的代码是埋点系统中的报表分析功能,小公司,开发结束后,没有Code Review环节,所以下面某些问题,也许在Code Review环节就可以避免。
167 0
一次Java性能调优实践【代码+JVM 性能提升70%】
|
5月前
|
监控 算法 Java
深入探索Java虚拟机:性能监控与调优实践
在面对日益复杂的企业级应用时,Java虚拟机(JVM)的性能监控和调优显得尤为重要。本文将深入探讨JVM的内部机制,分析常见的性能瓶颈,并提供一系列针对性的调优策略。通过实际案例分析,我们将展示如何运用现代工具对JVM进行监控、诊断及优化,以提升Java应用的性能和稳定性。
|
6月前
|
存储 算法 Java
技术笔记:JVM的垃圾回收机制总结(垃圾收集、回收算法、垃圾回收器)
技术笔记:JVM的垃圾回收机制总结(垃圾收集、回收算法、垃圾回收器)
62 1
|
5月前
|
监控 Java 调度
探索JVM性能调优,调优不仅是技术挑战,更是成长过程。
【7月更文挑战第1天】探索JVM性能调优:** 本文深入JVM内存模型,关注堆内存与方法区、栈的优化,通过调整-Xms, -Xmx及垃圾收集器参数减少GC频率。探讨了Serial到G1等垃圾收集器的选择策略,利用jstat、jmap等工具诊断性能瓶颈。实战案例中,通过问题定位、内存分析解决Full GC问题,强调开发者需理解JVM原理,运用工具在复杂场景下实现高效调优。调优不仅是技术挑战,更是成长过程。
47 0
|
6月前
|
存储 缓存 算法
详解JVM内存优化技术:压缩指针
详解JVM内存优化技术:压缩指针
|
6月前
|
存储 缓存 监控
深入解析JVM内存分配优化技术:TLAB
深入解析JVM内存分配优化技术:TLAB
|
7月前
|
Java 数据库连接 Spring
K8S+Docker理论与实践深度集成java面试jvm原理
K8S+Docker理论与实践深度集成java面试jvm原理
|
7月前
|
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收集器。
52 1