Java程序内存占用优化实践

news2024/11/15 11:33:28

目录

  • 背景
  • 堆内存调整
  • 内存还会继续上涨
  • 减少线程数量
    • Tomcat
    • Dubbo
    • Logback
    • 野线程

背景

上了微服务的当,喜欢将服务各种拆分,公有云模式下服务器比较多,还能玩得转。到了私有化部署,有的客户连个技术人员都没有,只想一键启动就能用,于是将所有服务放在一台物理机上制作母盘,实施安装时省时省力,还能清公司的服务器库存。

但是问题来了,在一台物理机上部署几十个服务,有C++服务,有Java服务,还有中间件,内存非常吃紧。15G的内存,所有服务跑起来,啥也不干,10G就没了,更别提有些服务在运行过程中还会继续申请内存。于是提出资源占用优化。

首当其冲的是Java服务,top看一下,排在上面的是一众Java进程,内存杀手的名号不是白叫的。

堆内存调整

一说到调整内存,最容易想到的就是堆内存了,连-Xms -Xmx这两个参数都不知道的Java程序员不是好curd boy。

  • -Xms:初始堆内存大小,也就是Java进程一起动堆就占这么多物理内存,如果发现不够用就再申请内存(假如能申请的话,通常都喜欢将-Xms和-Xmx的值设置成一样的,因为据说动态扩展会影响性能?)。
  • -Xmx:最大可分配堆内存大小,可以理解成虚拟内存。内存不够用又无法继续扩展时,就会OOM。

如果不需要在堆内存中聚集大量数据(比如:利用堆做缓存、在堆中排序并分页),大部分对象的生命周期都比较短的话,就不需要将堆内存设置的太大。

我个人的经验是,在程序刚启动时和压测后分别统计一次垃圾回收情况,垃圾回收统计使用如下命令:

jstat -gcutil pid

输出:

在这里插入图片描述

重点关注FGC(Full GC 次数)和GCT(GC总耗时),在OOM之前FGC会显著增大,另外,如果花了大量时间来回收垃圾,也能说明堆内存给太少了。

根据对每个服务负载的理解,进行了一波盲调,效果还不错。

内存还会继续上涨

明明通过-Xmx限制了堆内存大小,怎么压测完内存还是有明显上涨捏?我不能接受啊。大家都说是内存泄漏了,我不信!

为了搞清楚原因,我使用NMT追踪Java进程内存使用情况,NMT全称Native Memory Tracking,是HotSpot虚拟机的功能,可跟踪HotSpot虚拟机的内部内存使用情况。

需要在启动参数中加上-XX:NativeMemoryTracking=detail开启NMT,例如:

java -XX:NativeMemoryTracking=detail -jar -Xms96m -Xmx96m ./access-1.8.2.17.jar &

查看:

jcmd pid VM.native_memory summary scale=MB

输出:

在这里插入图片描述

解释:

  • Reserved:reserved memory 是指JVM 通过mmaped PROT_NONE 申请的虚拟地址空间,在页表中已经存在了记录(entries)。
  • Committed:committed memory 是JVM向操做系统实际分配的内存(malloc/mmap),mmaped PROT_READ | PROT_WRITE,相当于程序实际申请的可用内存。committed申请的内存并不是说直接占用了物理内存,由于操作系统的内存管理是惰性的,对于已申请的内存虽然会分配地址空间,但并不会直接占用物理内存,真正使用的时候才会映射到实际的物理内存,所以committed >= res。
  • Java Heap:堆内存,一般它的reserved等于-Xmx设置的值,committed等于-Xms设置的值。
  • Class:加载的类与方法信息。其实就是 metaspace,包含两部分: 一是metadata,被-XX:MaxMetaspaceSize限制最大大小,另外是 classspace,被-XX:CompressedClassSpaceSize限制最大大小。
  • Thread:线程占用的内存,每个线程栈占用大小受-Xss限制,默认是1MB左右,但是总大小没有限制。
  • Code:JIT 即时编译后(C1 C2 编译器优化)的代码占用内存,受-XX:ReservedCodeCacheSize限制。
  • GC:垃圾回收占用内存,例如垃圾回收需要的 CardTable,标记数,区域划分记录,还有标记 GC Root等等,都需要内存。这个不受限制,一般不会很大的。Parallel GC 不会占什么内存,G1 最多会占堆内存 10%左右额外内存,ZGC 会最多会占堆内存 15~20%左右额外内存,但是这些都在不断优化。(注意,不是占用堆的内存,而是大小和堆内存里面对象占用情况相关)。
  • Internal:命令行解析,JVMTI 使用的内存,这个不受限制,一般不会很大的。
  • Symbol:常量池占用的大小,字符串常量池受-XX:StringTableSize个数限制,总内存大小不受限制。
  • Native Memory Tracking:内存采集本身占用的内存大小,如果没有打开采集(那就看不到这个了)。

在程序启动时查看一次,运行一段时间后再查看一次,对比之后就知道是哪块内存在增长了,然后有针对性的去优化。

这里可操作空间比较大的就是Java Heap和Thread占用的内存了,其他内存区域一般不会占用很大的空间,也不建议去调整。在我的项目里,所有Java进程的Thread总共占了八百多MB的内存,有点哈人,所以优化方向已经很明确了,那就是减少线程数量。

减少线程数量

要减少线程数量,首先要搞明白这些线程都是由谁创建的,用在哪里。使用:

jstack -l pid > stack.txt

导出线程快照,可以从线程名或线程栈中的方法名大概猜出线程的作用。

下面给出一些常见技术栈的线程数调整方式,仅供参考,线程数应该调整到多少,以自己的实际情况为准

Tomcat

tomcat工作线程(线程名一般是http-nio-port-exec-n这种形式)数:

server:
  tomcat:
    min-spare-threads: 1
    max-threads: 8

非web项目禁用web功能,可以不创建web线程:

spring.main.web-application-type: none

Dubbo

DubboServerHandler线程数:

<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="32" />

Logback

我发现每个项目都有名为logback-1至logback-8的这8个线程,本来想试着调整一下,结果发现这玩意居然是代码里写死的。
在ch.qos.logback.core.util.ExecutorServiceUtil这个类中,有个方法:

static public ScheduledExecutorService newScheduledExecutorService() {
    return new ScheduledThreadPoolExecutor(CoreConstants.SCHEDULED_EXECUTOR_POOL_SIZE, THREAD_FACTORY);
}

再看CoreConstants.SCHEDULED_EXECUTOR_POOL_SIZE

package ch.qos.logback.core;

public class CoreConstants {
	// Apparently ScheduledThreadPoolExecutor has limitation where a task cannot be submitted from 
  // within a running task unless the pool has worker threads already available. ThreadPoolExecutor 
  // does not have this limitation.
  // This causes tests failures in SocketReceiverTest.testDispatchEventForEnabledLevel and
  // ServerSocketReceiverFunctionalTest.testLogEventFromClient.
  // We thus set a pool size > 0 for tests to pass.
  public static final int SCHEDULED_EXECUTOR_POOL_SIZE = 8;
}

上面那段注释翻译一下:

显然,ScheduledThreadPoolExecutor有一个限制,即除非池中已有可用的工作线程,否则无法从正在运行的任务中提交任务。ThreadPoolExecutor没有这个限制。

这会导致SocketReceiverTest.testDispatchEventForEnabledLevel和ServerSocketReceiver FunctionalTest.testLogEventFromClient测试失败。

因此,我们将线程池大小设置为>0,以便测试通过。

好家伙,搞了半天这8个线程是为了让单元测试通过。我使用的logback版本是1.2.3,不知道高版本的logback会不会解决这个问题。

野线程

我发现每个项目里都有一些类似下面的线程:

"pool-3-thread-4" #27 prio=5 os_prio=0 tid=0x00007f9e08042000 nid=0x73a0 waiting on condition [0x00007f9e657d2000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x00000000f9cc0470> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
	at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
	at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
	- None

它们的名称一般叫pool-n-thread-m,从它们的名称和栈里的方法名无法看出由谁创建,用在何处。从线程名的命名风格着手,我在java.util.concurrent.Executors.DefaultThreadFactory中找到了相关代码实现:

static class DefaultThreadFactory implements ThreadFactory {
	private static final AtomicInteger poolNumber = new AtomicInteger(1);
	private final ThreadGroup group;
	private final String namePrefix;
	
	DefaultThreadFactory() {
	    SecurityManager s = System.getSecurityManager();
	    group = (s != null) ? s.getThreadGroup() :
	                          Thread.currentThread().getThreadGroup();
	    namePrefix = "pool-" +
	                  poolNumber.getAndIncrement() +
	                 "-thread-";
	}
}

在这个构造器里打个断点,然后debug,就能找到是哪里调用它了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/431804.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java——《面试题——基础篇》

1、 Java语言有哪些特点 1、简单易学、有丰富的类库 2、面向对象&#xff08;Java最重要的特性&#xff0c;让程序耦合度更低&#xff0c;内聚性更高&#xff09; 3、与平台无关性&#xff08;JVM是Java跨平台使用的根本&#xff09; 4、可靠安全 5、支持多线程 2、面向对象…

Baumer工业相机堡盟工业相机如何联合BGAPI SDK和OpenCVSharp实现Mono12和Mono16位深度的图像保存(C#)

Baumer工业相机堡盟工业相机如何联合BGAPI SDK和OpenCVSharp实现Mono12和Mono16位深度的图像保存&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机保存位深度12/16位图像的技术背景代码案例分享1&#xff1a;引用合适的类文件2&#xff1a;BGAPI SDK在图像回调中联合O…

【GCU体验】基于PaddlePaddle + GCU跑通模型并测试GCU性能

一、环境 地址&#xff1a;启智社区:https://openi.pcl.ac.cn/ 二、计算卡介绍 云燧T20是基于邃思2.0芯片打造的面向数据中心的第二代人工智能训练加速卡&#xff0c;具有模型覆盖面广、性能强、软件生态开放等特点&#xff0c;可支持多种人工智能训练场景。同时具备灵活的可…

STM32理论 —— 定时器、时钟

文章目录 1. 定时器1.1 分类与简介1.1.1 分类与主要功能特点1.1.2 三种常用的定时器简介1.1.3 三种计数模式1.1.4 定时器计数原理 1.2 时钟来源1.3 通用定时器简介1.4 计数溢出时间公式1.4 定时器中断的原理1.5 输入捕获1.6 核心代码1.6.1 通用定时器初始化1.6.2 高级定时器初始…

【Python_Scrapy学习笔记(十三)】基于Scrapy框架的图片管道实现图片抓取

基于Scrapy框架的图片管道实现图片抓取 前言 本文中介绍 如何基于 Scrapy 框架的图片管道实现图片抓取&#xff0c;并以抓取 360 图片为例进行展示。 正文 1、Scrapy框架抓取图片原理 利用 Scrapy 框架提供的图片管道类 ImagesPipeline 抓取页面图片&#xff0c;在使用时需…

领域驱动设计理论实践

战略设计 战略设计是将“混沌”解构成“清晰”的过程&#xff0c;在该过程从开始到结束的历程之中&#xff0c;我们会划分出领域、界定通用语言范围、确定出系统限界上下文以及上下文之间的映射方式。 领域划分 战略设计在领域驱动设计中起着关键作用&#xff0c;因为其决定了…

使用Bazel构建前端Sass

注&#xff1a;本文假设对Bazel有一定的了解。本文基于Bazel 4.2.2 版本 在web前端领域&#xff0c;前端样式&#xff0c;web浏览器只认CSS样式语言。而CSS样式语言又过于低级。于是有人发明了更高级的语言&#xff1a;Sass[1]&#xff0c;用于生成CSS代码。 这样的方案&#x…

【C++】队列模拟问题

文章目录队列模拟问题12.7.1 ATM问题12.7.2 队列类12.7.3 Queue类的接口12.7.4 **Queue类的实现**12.7.5 是否需要其他函数&#xff1f;12.7.6 Customer类queue.hqueue.cpp12.7.7 ATM模拟main.cpp队列模拟问题 12.7.1 ATM问题 Heather银行打算在Food Heap超市开设一个自动柜员…

【C++STL精讲】vector的基本使用与常用接口

文章目录&#x1f490;专栏导读&#x1f490;文章导读&#x1f337;vector是什么&#xff1f;&#x1f337;vector的基本使用&#x1f337;vector常用函数接口&#x1f490;专栏导读 &#x1f338;作者简介&#xff1a;花想云&#xff0c;在读本科生一枚&#xff0c;致力于 C/C…

HAL库版FreeRTOS(上)

目录 FreeRTOS 简介初识FreeRTOS什么是FreeRTOS?为什么选择FreeRTOS&#xff1f;FreeRTOS 的特点商业许可 磨刀不误砍柴工查找资料FreeRTOS 官方文档Cortex-M 架构资料 FreeRTOS 源码初探FreeRTOS 源码下载FreeRTOS 文件预览 FreeRTOS 移植FreeRTOS 移植移植前准备添加FreeRTO…

浏览器断点调试说明

断点调试 断点调试面板 功能按钮介绍 描述&#xff1a;继续执行脚本 或者叫&#xff08;逐过程执行&#xff09; 快捷键 &#xff08;F8&#xff09;或者是&#xff08;Ctrl\&#xff09; 作用&#xff1a;打断点了的地方&#xff08;比如有是三个断点地方&#xff09;就会 第一…

大数据能力提升项目|学生成果展系列之四

导读 为了发挥清华大学多学科优势&#xff0c;搭建跨学科交叉融合平台&#xff0c;创新跨学科交叉培养模式&#xff0c;培养具有大数据思维和应用创新的“π”型人才&#xff0c;由清华大学研究生院、清华大学大数据研究中心及相关院系共同设计组织的“清华大学大数据能力提升项…

13.vue-cli

单页面应用程序&#xff1a;所有的功能只在index.html中完成 vue-cli是vue版的webpack 目录 1 安装vue-cli 2 创建项目 3 使用预设 4 删除预设 5 开启项目 6 项目文件内容 6.1 node_moduls 中是项目依赖的库 6.2 public 6.2.1 favicon.ico 是浏览器页签内部…

尚融宝——整合OpenFeign与Sentinel实现兜底方法——验证手机号码是否注册功能

一、整合过程 在项目添加依赖&#xff1a;添加位置 <!--服务调用--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency> 在需要的服务中添加启动注…

spring中常见的注解

DI(依赖注入中常见的注解) Autowired&#xff1a;按类型自动装配Resource&#xff1a;按名称或类型自动装配&#xff0c;Qualifier&#xff1a;按名称自动装配&#xff0c;Value &#xff1a;注入int、float、String等基本数据类型&#xff0c;只能标注在成员变量、setter方法上…

【Gradle-1】入门Gradle,前置必读

1、为什么要学习Gradle Gradle作为Android开发默认的构建工具&#xff0c;你的每一次编译都会用到它。招聘要求从以前的熟悉加分&#xff0c;到现在的必备技能&#xff0c;可见Gradle的重要性。 做开发这么久了&#xff0c;你是否对Gradle又爱又恨&#xff1f;是否对Gradle的…

第三章(1):自然语言处理概述:应用、历史和未来

第三章&#xff08;1&#xff09;&#xff1a;自然语言处理概述&#xff1a;应用、历史和未来 目录第三章&#xff08;1&#xff09;&#xff1a;自然语言处理概述&#xff1a;应用、历史和未来1. 自然语言处理概述&#xff1a;应用、历史和未来1.1 主要应用1.2 历史1.3 NLP的新…

【科普】PCB为什么常用50Ω阻抗?6大原因

在PCB设计中&#xff0c;阻抗通常是指传输线的特性阻抗&#xff0c;这是电磁波在导线中传输时的特性阻抗&#xff0c;与导线的几何形状、介质材料和导线周围环境等因素有关。 对于一般的高速数字信号传输和RF电路&#xff0c;50Ω是一个常用的阻抗值。 为什么是50Ω&#xff1f…

《程序员面试金典(第6版)》面试题 10.09. 排序矩阵查找(观察法,二分法,分治算法入门题目,C++)

题目描述 给定MN矩阵&#xff0c;每一行、每一列都按升序排列&#xff0c;请编写代码找出某元素。 示例: 现有矩阵 matrix 如下&#xff1a;[[1, 4, 7, 11, 15],[2, 5, 8, 12, 19],[3, 6, 9, 16, 22],[10, 13, 14, 17, 24],[18, 21, 23, 26, 30] ]给定 target 5&…

wma格式怎么转换mp3,4种方法超快学

其实我们在任何电子设备上所获取的音频文件都具有自己的格式&#xff0c;每种格式又对应着自己的属性特点。比如wma就是一种音质优于MP3的音频格式&#xff0c;虽然很多小伙伴比较青睐于wma所具有的音质效果&#xff0c;但也不得不去考虑因wma自身兼容性而引起很多播放器不能支…