【JVM实战篇】内存调优:内存问题诊断+案例实战

news2024/12/29 16:49:28

文章目录

  • 诊断
    • 内存快照
      • 在内存溢出时生成内存快照
      • MAT分析内存快照
      • MAT内存泄漏检测的原理
        • 支配树介绍
        • 如何在不内存溢出情况下生成堆内存快照?
        • MAT查看支配树
        • MAT如何根据支配树发现内存泄漏
      • 运行程序的内存快照导出和分析
        • 快照**大文件的处理**
    • 案例实战
      • 案例1:分页查询文章接口的内存溢出
      • 案例2:Mybatis导致的内存溢出
      • 案例3:导出大文件内存溢出(云原生环境解决内存溢出问题)
        • **K8S环境搭建(了解即可)**
        • 内存溢出测试
      • 案例4:ThreadLocal使用时占用大量内存
      • 案例5:文章内容审核接口的内存问题
        • 设计1:使用SpringBoot中的@Async注解进行异步的审核。
        • 设计2:使用生产者和消费者模式进行处理,队列数据可以实现持久化到数据库。
        • 设计3:使用mq消息队列进行处理(推荐)
    • 在线定位问题
      • 诊断问题有两种方法
        • 生成内存快照并分析
        • 在线定位问题
      • 安装Jmeter插件查看接口响应信息
      • 定位内存大的对象创建位置
        • Arthas stack命令
        • btrace在线定位
  • 文章说明

诊断

通过分析工具,诊断问题的产生原因,定位到出现问题的源代码

内存快照

  • 当堆内存溢出时,需要在堆内存溢出时将整个堆内存保存下来,生成内存快照(Heap Profile )文件。
  • 使用MAT打开hprof文件,并选择内存泄漏检测功能,MAT会自行根据内存快照中保存的数据分析内存泄漏的根源,最后推测出几个嫌疑对象。

在这里插入图片描述

在内存溢出时生成内存快照

如何生成内存快照,很简单,启动程序的时候添加Java虚拟机参数:

  • -XX:+HeapDumpOnOutOfMemoryError:发生OutOfMemoryError错误时,自动生成hprof内存快照文件。
  • -XX:HeapDumpPath=<path>:指定hprof文件的输出路径。

使用MAT打开hprof文件,并选择内存泄漏检测功能,MAT会自行根据内存快照中保存的数据分析内存泄漏的根源。

在程序中添加jvm参数(最大内存可以设置小一点,更快排查出问题):

-Xmx256m -Xms256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\jvm\dump\test1.hprof

运行程序之后,使用JMeter来进行压力测试:

在这里插入图片描述

MAT分析内存快照

使用MAT打开hprof文件(操作步骤见前文GC Root小节),首页就展示了MAT检测出来的内存泄漏问题原因。

在这里插入图片描述

在这里插入图片描述

生成的内存泄漏检测报告如下

在这里插入图片描述

  • a代表怀疑对象
  • b表示其他对象

在这里插入图片描述

Keywords中也可以查看一些关键信息来判断内存溢出的原因

点击Details查看详情,这个线程持有了大量的字节数组:

在这里插入图片描述

继续往下来,还可以看到溢出时线程栈,通过栈信息也可以怀疑下是否是因为这句代码创建了大量的对象:

在这里插入图片描述

MAT内存泄漏检测的原理

直接通过GC Root检测内存是否泄漏比较复杂,MAT将引用链转化为支配树结构之后,再进行分析。

支配树介绍

支配树定义:支配树展示的是对象实例间的支配关系。

如下图,A引用B、C;B、C引用D;C引用E;D、E引用F。

  • 通往对象B的路径都经过对象A,都要经过A才能到B,则认为对象A支配对象B。C也同理;
  • 由于E只有C引用,所以C支配E;
  • B和C都引用D,即B和C都无法支配D,只能继续往上层找,最终找到A,因为所有路径都由A出发。F也同理;

在这里插入图片描述

支配树中堆的划分

  • 支配树中对象本身占用的空间称之为浅堆(Shallow Heap)
  • 支配树中对象的子树就是所有被该对象支配的内容,这些内容(当前对象以及子树)组成了对象的深堆(Retained Heap),也称之为保留集(Retained Set)。A的深堆即整棵树。深堆的大小表示该对象如果可以被回收,能释放多大的内存空间
  • 如下图:C自身包含一个浅堆,而C底下挂了E,所以C+E占用的空间大小代表C的深堆
  • MAT根据支配树和深堆的大小,快速发现是哪些对象造成了内存泄漏(优先检查深堆内存比较大的)

在这里插入图片描述

如何在不内存溢出情况下生成堆内存快照?

-XX:+HeapDumpBeforeFullGC可以在FullGC之前就生成内存快照

MAT查看支配树

**需求:**使用如下代码生成内存快照,并分析TestClass对象的深堆和浅堆

package com.itheima.jvmoptimize.matdemo;

import org.openjdk.jol.info.ClassLayout;

import java.util.ArrayList;
import java.util.List;

//-XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=D:/jvm/dump/mattest.hprof
public class HeapDemo {
    public static void main(String[] args) {
        TestClass a1 = new TestClass();
        TestClass a2 = new TestClass();
        TestClass a3 = new TestClass();
        String s1 = "itheima1";
        String s2 = "itheima2";
        String s3 = "itheima3";

        a1.list.add(s1);

        a2.list.add(s1);
        a2.list.add(s2);

        a3.list.add(s3);

        //System.out.print(ClassLayout.parseClass(TestClass.class).toPrintable());
        s1 = null;
        s2 = null;
        s3 = null;
        System.gc();
    }
}

class TestClass {
    public List<String> list = new ArrayList<>(10);
}

上面代码的引用链如下:

在这里插入图片描述

转换成支配树,TestClass简称为tc。

  • tc1 tc2 tc3都是直接挂在main线程对象上;
  • itheima2 itheima3都只能通过tc2和tc3访问,所以直接挂上;
  • itheima1不同,他可以由tc1 tc2访问,所以他要挂载tc1 tc2的上级也就是main线程对象上:

在这里插入图片描述

使用mat来分析,添加虚拟机参数:

在这里插入图片描述

在FullGC之后产生了内存快照文件:

在这里插入图片描述

直接查看MAT的支配树功能:

在这里插入图片描述

输入main进行搜索:

在这里插入图片描述

在这里插入图片描述

可以看到结构与之前分析的是一致的:

  • 第一层

在这里插入图片描述

  • 第二层

在这里插入图片描述

同时可以看到字符串的浅堆大小和深堆大小,单位是字节 :

在这里插入图片描述

为什么字符串对象的浅堆大小是24字节,深堆大小是56字节呢?首先字符串对象引用了字符数组,字符数组的字节大小底下有展示是32字节,那我们只需要搞清楚浅堆大小也就是他自身为什么是24字节就可以了。使用jol框架打印下对象大小(原理篇会详细展开讲解,这里先有个基本的认知)。

在这里插入图片描述

添加依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

使用代码打印:

public class StringSize {
    public static void main(String[] args) {
        //使用JOL打印String对象
        System.out.print(ClassLayout.parseClass(String.class).toPrintable());
    }
}

**结果如下:**对象头占用了12字节,value字符数组的引用占用了4字节,int类型的hash字段占用4字节,还有4字节是对象填充,所以加起来是24字节。至于对象填充、对象头是做什么用的,在《原理篇》中会详细讲解。

在这里插入图片描述

在这里插入图片描述

如果可以将这个String对象回收,可以释放56个字节的内存

MAT如何根据支配树发现内存泄漏

MAT根据支配树,从叶子节点向根节点遍历,如果发现深堆的大小超过整个堆内存的一定比例阈值,就会将其标记成内存泄漏的“嫌疑对象”。

在这里插入图片描述

运行程序的内存快照导出和分析

刚才我们都是在本地导出内存快照的,并且是程序已经出现了内存溢出,接下来我们要做到防范于未然,一旦看到内存大量增长就去分析内存快照,那此时内存还没溢出,怎么样去获得内存快照文件呢

背景:

小李的团队通过监控系统发现有一个服务内存在持续增长,希望尽快通过内存快照分析增长的原因,由于并未产生内存溢出所以不能通过HeapDumpOnOutOfMemoryError参数生成内存快照。

思路:

导出运行中系统的内存快照,比较简单的方式有两种,注意只需要导出标记为存活的对象:

  • 通过JDK自带的jmap命令导出,格式为:jmap -dump:live,format=b,file=文件路径和文件名 进程ID
    • -dump:live:表示只保留存活对象
  • 通过arthas的heapdump命令导出,格式为:heapdump --live 文件路径和文件名

先使用jps或者ps -ef查看进程ID:

在这里插入图片描述

通过jmap命令导出内存快照文件,live代表只保存存活对象,format=b用二进制方式保存:

在这里插入图片描述

也可以在arthas中输出heapdump命令:

在这里插入图片描述

最终生成的文件如下:

在这里插入图片描述

在这里插入图片描述

快照大文件的处理

在程序员开发用的机器内存范围之内的快照文件,直接使用MAT打开分析即可。但是存在两个问题

  • 服务器上的程序占用的内存达到10G以上,开发机无法正常打开此类内存快照(因为开发机内存要大于服务器程序的内存,MAT才可以打开内存快照文件
  • 而且将服务器的内存快照文件下载到开发机,受限于服务器宽带,下载速度会非常慢,耗时长

解决方案:下载服务器操作系统对应的MAT,直接在服务器上面进行分析。下载地址:https://eclipse.dev/mat/downloads.php

在这里插入图片描述

在这里插入图片描述

在服务器的MAT中运行如下脚本自动根据内存快照生成内存泄漏检测报告:

./ParseHeapDump.sh 快照文件路径 org.eclipse.mat.api:suspects org.eclipse.mat.api:overview org.eclipse.mat.api:top_components

  • org.eclipse.mat.api:suspects表示生成内存泄漏检测报告
  • org.eclipse.mat.api:overview生成总览图
  • org.eclipse.mat.api:top_components生成组件图

在这里插入图片描述

注意:默认MAT分析时只使用了1G的堆内存,如果快照文件超过1G,需要修改MAT目录下的MemoryAnalyzer.ini配置文件调整最大堆内存。(建议将最大内存改成快照文件内存的1.5倍左右)

在这里插入图片描述

在这里插入图片描述

最终会生成报告文件:

在这里插入图片描述

将这些文件下载到本地,解压之后打开index.html文件:

在这里插入图片描述

同样可以看到类似的报告:

在这里插入图片描述

案例实战

修复内存溢出问题的具体问题具体分析,问题总共可以分成三类:

  • 代码中的内存泄漏:在前面的篇章中已经介绍并提供解决方案
  • 并发引起内存溢出 - 设计不当
    • 从数据库获取超大数据量的数据
    • 线程池设计不当
    • 生产者-消费者模型,消费者消费性能问题
    • 解决方案:优化设计方案
  • 并发引起内存溢出 - 参数不当
    • 由于参数设置不当,比如堆内存设置过小,导致并发量增加之后超过堆内存的上限。
    • 解决方案:调整参数

案例1:分页查询文章接口的内存溢出

背景:小李负责的新闻资讯类项目采用了微服务架构,其中有一个文章微服务,这个微服务在业务高峰期出现了内存溢出的现象。

在这里插入图片描述

在这里插入图片描述

解决思路:

1、服务出现OOM内存溢出时,生成内存快照。

2、使用MAT分析内存快照,找到内存溢出的对象。

3、尝试在开发环境中重现问题,分析代码中问题产生的原因。

4、修改代码。

5、测试并验证结果。

代码使用的是com.itheima.jvmoptimize.practice.oom.controller.DemoQueryController

在这里插入图片描述

首先将项目打包,放到服务器上,同时使用如下启动命令启动。设置了最大堆内存为512m,同时堆内存溢出时会生成hprof文件:

在这里插入图片描述

编写JMeter脚本进行压测,size数据量一次性获取10000条,线程150,每个线程执行10次方法调用:

在这里插入图片描述

在这里插入图片描述

执行之后可以发现服务器上已经生成了hprof文件:

在这里插入图片描述

将其下载到本地,通过MAT分析发现是Mysql返回的ResultSet存在大量的数据:

在这里插入图片描述

在这里插入图片描述

通过支配树,可以发现里边包含的数据,如果数据有一些特殊的标识,其实就可以判断出来是哪个接口产生的数据:

在这里插入图片描述

如果想知道每个线程在执行哪个方法,先找到spring的HandlerMethod对象:

在这里插入图片描述

接着去找引用关系:

  • outgoing:当前对象引用的对象
  • incoming:当前对象被哪些对象引用

在这里插入图片描述

通过描述信息就可以看到接口:

在这里插入图片描述

通过直方图的查找功能,也可以找到项目里哪些对象比较多:

到这里有理由怀疑是TbArticle对象导致的内存溢出

在这里插入图片描述

问题还原:

使用JMeter进行压力测试

在这里插入图片描述

问题推测:

文章微服务中的分页接口没有限制最大单次访问条数,并且单个文章对象占用的内存量较大,在业务高峰期并发量较大时这部分从数据库获取到内存之后会占用大量的内存空间。

解决思路:

1、与产品设计人员沟通,限制最大的单次访问条数。(一次性获取那么多数据,用户不一定看得过来)

以下代码,限制了每次访问的最大条数为100条

在这里插入图片描述

2、分页接口如果只是为了展示文章列表,不需要获取文章内容,可以大大减少对象的大小。

把文章内容去掉,减少对象大小:

在这里插入图片描述

3、在高峰期对微服务进行限流保护。(需要考虑可行性,限流会影响一部分用户体验)

案例2:Mybatis导致的内存溢出

背景:小李负责的文章微服务进行了升级,新增加了一个判断id是否存在的接口,第二天业务高峰期出现了内存溢出,小李觉得应该和新增加的接口有关系。

在这里插入图片描述

解决思路:

1、服务出现OOM内存溢出时,生成内存快照。

2、使用MAT分析内存快照,找到内存溢出的对象。

3、尝试在开发环境中重现问题,分析代码中问题产生的原因。

4、修改代码。

5、测试并验证结果。

通过分析hprof发现调用的方法,但是这个仅供参考。这个方法用来判断当前id是否在数据库中存在

在这里插入图片描述

分析支配树,挖掘其他信息,找到大对象来源,是一些字符串,里边还包含SQL

在这里插入图片描述

在这里插入图片描述

通过SQL内容搜索下可以找到对应的方法:

在这里插入图片描述

发现里边用了foreach,如果循环内容很大,会产生特别大的一个SQL语句。

直接打开jmeter,打开测试脚本进行测试:

在这里插入图片描述

本地测试之后,出现了内存溢出:

在这里插入图片描述

问题根源:

Mybatis在使用foreach进行sql拼接时,会在内存中创建对象,如果foreach处理的数组或者集合元素个数过多,会占用大量的内存空间。

解决思路:

1、限制参数中最大的id个数。

2、将id缓存到redis或者内存缓存中,通过缓存进行校验。

案例3:导出大文件内存溢出(云原生环境解决内存溢出问题)

  • 小李负责了一个管理系统,这个管理系统支持几十万条数据的excel文件导出。他发现系统在运行时如果有几十个人同时进行大数据量的导出时,会出现内存谥出。
  • 小李团队使用的是k8s将管理系统部署到了容器中,所以这一次我们使用阿里云的k8s环境还原场景,并解决问题。阿里云的k8s整体规划如下:

在这里插入图片描述

excel文件和内存溢出的内存快照文件都存储到阿里云OSS中

K8S环境搭建(了解即可)

先开通ACK,并创建集群服务

在这里插入图片描述

1、在容器镜像服务中创建镜像仓库

在这里插入图片描述

在这里插入图片描述

2、项目中添加Dockerfile文件,告诉Docker怎么生成镜像

在这里插入图片描述

FROM openjdk:8-jre

MAINTAINER xiadong(作者) <xiadong@itcast.cn(作者邮箱)>

RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime (设置时区)

ADD jvm-optimize-0.0.1-SNAPSHOT.jar /app/ (将jar包放到容器的app目录下)

(设置虚拟机运行参数)
CMD ["java", "-Xmx512m", "-Xms512m", "-Dfile.encoding=UTF-8", "-XX:+HeapDumpOnOutOfMemoryError","-XX:HeapDumpPath=/opt/dump/heapdump.hprof","-jar", "/app/jvm-optimize-0.0.1-SNAPSHOT.jar"]

(暴露SpringBoot启动的端口)
EXPOSE 8881

生成完成之后,需要将jar和dockerfile上传到服务器上面

3、按照阿里云的教程执行命令:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

设置密码

在这里插入图片描述

制作镜像

在这里插入图片描述

使用docker images查看镜像,

在这里插入图片描述

给这个镜像打一个标签

在这里插入图片描述

推送镜像

在这里插入图片描述

4、推送成功之后,镜像仓库中已经出现了镜像:

在这里插入图片描述

5、通过镜像构建k8s中的pod:

在这里插入图片描述

在这里插入图片描述

6、选择刚才的镜像:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

还需要添加和阿里云OSS关联的数据卷

在这里插入图片描述

7、在OSS中创建一个Bucket:

在这里插入图片描述

8、创建存储声明,选择刚才的Bucket:

在这里插入图片描述

到这里,OSS和容器的关联就建立完成了

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

9、选择这个存储声明,并添加hprof文件生成的路径映射,要和Dockerfile中虚拟机参数里的路径相同:

在这里插入图片描述

10、创建一个service,填写配置,方便外网进行访问。

在这里插入图片描述

在这里插入图片描述

服务端口是对外网暴露的端口

在这里插入图片描述

服务的相关信息

在这里插入图片描述

内存溢出测试

打开jmeter文件并测试:

在这里插入图片描述

接口代码如下:

@GetMapping("/export")
public void export(int size, String path) throws IOException {
    // 1 、创建工作薄
    Workbook workbook = new XSSFWorkbook();
    // 2、在工作薄中创建sheet
    Sheet sheet = workbook.createSheet("测试");

    for (int i = 0; i < size; i++) {
        // 3、在sheet中创建行
        Row row0 = sheet.createRow(i);
        // 4、创建单元格并存入数据
        row0.createCell(0).setCellValue(RandomStringUtils.randomAlphabetic(1000));
    }

    // 将文件输出到指定文件
    FileOutputStream fileOutputStream = null;
    try {
        fileOutputStream = new FileOutputStream(path + RandomStringUtils.randomAlphabetic(10) + ".xlsx");
        workbook.write(fileOutputStream);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (fileOutputStream != null) {
            fileOutputStream.close();
        }
        if (workbook != null) {
            workbook.close();
        }
    }

}

测试,将文件生成到OSS中

在这里插入图片描述

12、OSS中出现了这个hprof文件:

在这里插入图片描述

在这里插入图片描述

13、从直方图就可以看到是导出文件导致的内存溢出:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

问题根源:

Excel文件导出如果使用POI的XSSFWorkbook,在大数据量(几十万)的情况下会占用大量的内存。

代码:com.itheima.jvmoptimize.practice.oom.controller.Demo2ExcelController

解决思路:

1、使用poi的SXSSFWorkbook(XSSFWorkbook的优化版本)

2、hutool提供的BigExcelWriter减少内存开销(Hutool的工具类更加好用,专门用来导出大文件)

 //http://www.hutool.cn/docs/#/poi/Excel%E5%A4%A7%E6%95%B0%E6%8D%AE%E7%94%9F%E6%88%90-BigExcelWriter
@GetMapping("/export_hutool")
public void export_hutool(int size, String path) throws IOException {
    List<List<?>> rows = new ArrayList<>();
    for (int i = 0; i < size; i++) {
       rows.add( CollUtil.newArrayList(RandomStringUtils.randomAlphabetic(1000)));
    }

    BigExcelWriter writer= ExcelUtil.getBigWriter(path + RandomStringUtils.randomAlphabetic(10) + ".xlsx");
    // 一次性写出内容,使用默认样式
    writer.write(rows);
    // 关闭writer,释放内存
    writer.close();
}

3、使用easy excel,对内存进行了大量的优化(效果最好,强烈推荐)

不需要把所有要导出的数据放在内存中,可以分批写入,每次写入部分数据

//https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write#%E9%87%8D%E5%A4%8D%E5%A4%9A%E6%AC%A1%E5%86%99%E5%85%A5%E5%86%99%E5%88%B0%E5%8D%95%E4%B8%AA%E6%88%96%E8%80%85%E5%A4%9A%E4%B8%AAsheet
@GetMapping("/export_easyexcel")
public void export_easyexcel(int size, String path,int batch) throws IOException {

    // 方法1: 如果写到同一个sheet
    String fileName = path + RandomStringUtils.randomAlphabetic(10) + ".xlsx";
    // 这里注意 如果同一个sheet只要创建一次
    WriteSheet writeSheet = EasyExcel.writerSheet("测试").build();
    // 这里 需要指定写用哪个class去写
    try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
        // 分100次写入
        for (int i = 0; i < batch; i++) {
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<DemoData> datas = new ArrayList<>();
            for (int j = 0; j < size / batch; j++) {
                DemoData demoData = new DemoData();
                demoData.setString(RandomStringUtils.randomAlphabetic(1000));
                datas.add(demoData);
            }
            excelWriter.write(datas, writeSheet);
            //写入之后datas数据就可以释放了
        }
    }

}

案例4:ThreadLocal使用时占用大量内存

背景:小李负责了一个微服务,但是他发现系统在没有任何用户使用时,也占用了大量的内存,导致可以使用的内存大大减少。

在这里插入图片描述

1、打开jmeter测试脚本

在这里插入图片描述

在这里插入图片描述

【接口内容】

import com.itheima.jvmoptimize.practice.demo.service.TbArticleService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;

/**
 * -Xmx1g -Xms1g
 * -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/jvm/dump/heapdemo.hprof
 */
@RestController
@RequestMapping("/threadlocal")
public class DemoThreadLocal {

    @GetMapping
    public ResponseEntity test() {
        // 一进来就抛异常
        error();
        return ResponseEntity.ok().build();
    }

    private void error() {
        throw new RuntimeException("出错了");
    }

}

测试结果

在这里插入图片描述

在这里插入图片描述

2、内存有增长,但是没溢出。所以通过jmap命令导出hprof文件

在这里插入图片描述

3、MAT分析之后发现每个线程中都包含了大量的对象:

在这里插入图片描述

4、在支配树中可以发现是ThreadLocalMap导致的内存增长:

在这里插入图片描述

10M大小的来源

在这里插入图片描述

当用户请求过来的时候,微服务的拦截器根据用户的请求头信息在内存中组装一个对象放到ThreadLocal中,方便后续在Controller层、Service层进行使用。按理说,请求结束之后,拦截器就要把这部分内存释放掉,但是这里可能存在某些问题导致内存没有释放

5、ThreadLocalMap就是ThreadLocal对象保存数据的地方,所以只要分析ThreadLocal代码即可。在拦截器中,ThreadLocal清理的代码被错误的放在postHandle中,如果接口发生了异常,这段代码不会调用到,这样就产生了内存泄漏,将其移动到afterCompletion(这个方法一定会执行)就可以了。

在这里插入图片描述

修正之后的代码:

import com.itheima.jvmoptimize.practice.demo.common.UserDataContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 拦截器的实现,模拟放入数据到threadlocal中
 */
public class UserInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserDataContextHolder.userData.set(new UserDataContextHolder.UserData());
         return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserDataContextHolder.userData.remove();
    }
}

按理说,请求结束之后,tomcat中线程池的线程长时间不用的话,会被回收,那线程里面的数据也会被回收,这里为什么不会被回收呢?

原因:核心线程就算空闲,也不会回收,这部分线程里面的对象内存会积压

在这里插入图片描述

min-spare计算设置为0,最后还是会有10个核心线程是保留的

问题根源和解决思路:

很多微服务会选择在拦截器preHandle方法中去解析请求头中的数据,并放入一些数据到ThreadLocal中方便后续使用。在拦截器的afterCompletion方法中,必须要将ThreadLocal中的数据清理掉。

案例5:文章内容审核接口的内存问题

背景:文章微服务中提供了文章审核接口,会调用阿里云的内容安全接口进行文章中文字和图片的审核,在自测过程中出现内存占用较大的问题。
在这里插入图片描述

设计1:使用SpringBoot中的@Async注解进行异步的审核。

com.itheima.jvmoptimize.practice.oom.controller.Demo1ArticleController类中的article1方法

使用线程池:做异步审核,提交审核就返回给用户,不用等到审核结束。

Spring Bootr中使用@Async注解,即可完成异步功能实现。

在这里插入图片描述

【Controller】

package com.itheima.jvmoptimize.practice.demo.controller;

import cn.hutool.json.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itheima.jvmoptimize.practice.demo.pojo.ArticleDto;
import com.itheima.jvmoptimize.practice.demo.service.ArticleService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * -XX:+HeapDumpOnOutOfMemoryError
 */
@RestController
@RequestMapping("/article")
public class Demo1ArticleController {

    @Autowired
    private ArticleService articleService;

    @PostMapping("/demo1/{id}")
    public void article1(@PathVariable("id") long id, @RequestBody ArticleDto article){
        article.setId(id);
        articleService.asyncSaveArticle(article);
    }

}

【Service】

import com.itheima.jvmoptimize.practice.demo.pojo.ArticleDto;
import com.itheima.jvmoptimize.practice.demo.service.ArticleService;
import com.itheima.jvmoptimize.practice.demo.utils.AliyunUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import static com.itheima.jvmoptimize.practice.demo.config.ThreadPoolTaskConfig.BUFFER_QUEUE;

@Service
public class ArticleServiceImpl implements ArticleService {

    @Autowired
    private AliyunUtil aliyunUtil;

    @Override
    public void saveArticle(ArticleDto article) {
        BUFFER_QUEUE.add(article);
        int size = BUFFER_QUEUE.size();
        if( size > 0 && size % 10000 == 0){
            System.out.println(size);
        }
    }

    @Override
    // asyncTaskExecutor是线程池的名字,通过线程池来执行下面的方法
    @Async("asyncTaskExecutor")
    public void asyncSaveArticle(ArticleDto article) {
        // 阿里云安全审核
        aliyunUtil.verify(article.getTitle() + "。" + article.getContent());
    }
}


package com.itheima.jvmoptimize.practice.demo.utils;

import org.springframework.stereotype.Component;

/**
 * 休眠是模拟安全审核慢,速度跟不上文章产生的速度,才会造成内存积压
 */
@Component
public class AliyunUtil {

    //阿里云审核接口调用
    public void verify(String content){
        try {
            /**
             *  调用第三方接口审核数据,但是此时网络出现问题,
             * 第三方接口长时间没有响应,此处使用休眠来模拟30秒
             */
            Thread.sleep(30 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

【线程池配置】
package com.itheima.jvmoptimize.practice.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
public class AsyncThreadPoolTaskConfig {

    private static final int corePoolSize = 50;            // 核心线程数(默认线程数)
    private static final int maxPoolSize = 100;           // 最大线程数
    private static final int keepAliveTime = 10;          // 允许线程空闲时间(单位:默认为秒)
    private static final int queueCapacity = 200;         // 缓冲队列数
    private static final String threadNamePrefix = "Async-Task-"; // 线程池名前缀

    @Bean("asyncTaskExecutor")
    public ThreadPoolTaskExecutor getAsyncExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        //executor.setMaxPoolSize(Integer.MAX_VALUE);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setThreadNamePrefix(threadNamePrefix);
        // 线程池对拒绝任务的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}

1、打开jmeter脚本,已经准好了一段测试用的文本。

在这里插入图片描述

2、运行测试,发现线程数一直在增加:

在这里插入图片描述

3、发现是因为异步线程池的最大线程数设置了Integer的最大值,所以只要没到上限就一直创建线程:

在这里插入图片描述

4、接下来修改为100,再次测试:

在这里插入图片描述

5、这次线程数相对来说比较正常:

在这里插入图片描述

存在问题:

1、线程池参数设置不当,会导致大量线程的创建或者队列中保存大量的数据。

2、任务没有持久化,一旦走线程池的拒绝策略或者服务宕机、服务器掉电等情况很有可能会丢失任务。

设计2:使用生产者和消费者模式进行处理,队列数据可以实现持久化到数据库。

在这里插入图片描述

【controller】

package com.itheima.jvmoptimize.practice.demo.controller;

import cn.hutool.json.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itheima.jvmoptimize.practice.demo.pojo.ArticleDto;
import com.itheima.jvmoptimize.practice.demo.service.ArticleService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * -XX:+HeapDumpOnOutOfMemoryError
 */
@RestController
@RequestMapping("/article")
public class Demo1ArticleController {

    @Autowired
    private ArticleService articleService;

    @PostMapping("/demo2/{id}")
    public void article2(@PathVariable("id") long id, @RequestBody ArticleDto article){
        article.setId(id);
        articleService.saveArticle(article);
    }
}

【队列】

package com.itheima.jvmoptimize.practice.demo.config;

import com.itheima.jvmoptimize.practice.demo.pojo.ArticleDto;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
public class ThreadPoolTaskConfig  {

    public static final BlockingQueue<ArticleDto> BUFFER_QUEUE = new LinkedBlockingQueue<>();
    private static final int corePoolSize = 50;            // 核心线程数(默认线程数)
    private static final int maxPoolSize = 100;           // 最大线程数
    private static final int keepAliveTime = 10;          // 允许线程空闲时间(单位:默认为秒)
    private static final int queueCapacity = 200;         // 缓冲队列数
    private static final String threadNamePrefix = "Async-Service-"; // 线程池名前缀

    @Bean("taskExecutor")
    public ThreadPoolTaskExecutor getAsyncExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(Integer.MAX_VALUE);
        //executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setThreadNamePrefix(threadNamePrefix);
        // 线程池对拒绝任务的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}

【任务】

import com.itheima.jvmoptimize.practice.demo.pojo.ArticleDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

import static com.itheima.jvmoptimize.practice.demo.config.ThreadPoolTaskConfig.BUFFER_QUEUE;

//@Component
public class ArticleSaveTask {
    @Autowired
    @Qualifier("taskExecutor")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @PostConstruct
    public void pullArticleTask(){
        for (int i = 0; i < 50; i++) {
            // 使用50个线程从队列里面获取任务来执行
            threadPoolTaskExecutor.submit((Runnable) () -> {
                while (true){
                    try {
                        ArticleDto data = BUFFER_QUEUE.take();
                        /**
                         * 获取到队列中的数据之后,调用第三方接口审核数据,但是此时网络出现问题,
                         * 第三方接口长时间没有响应,此处使用休眠来模式30秒
                         */
                        Thread.sleep(30 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

1、测试之后发现,出现内存泄漏问题(其实并不是泄漏,而是内存中存放了太多的对象,但是从图上看着像内存泄漏了):

在这里插入图片描述

2、每次接口调用之后,都会将数据放入队列中。

在这里插入图片描述

3、而这个队列没有设置上限,导致队列中积压了太多任务:

在这里插入图片描述

4、调整一下上限设置为2000:

在这里插入图片描述

需要实现一套机制,如果队列满了,就将任务信息存储到数据库中,后期再将任务拿出来重新提交计算

5、这次就没有出现内存泄漏问题了:

在这里插入图片描述

存在问题:

1、队列参数设置不正确,会保存大量的数据。

2、实现复杂,需要自行实现持久化的机制,否则数据会丢失。

设计3:使用mq消息队列进行处理(推荐)

由MQ来保存文章的数据。发送消息的服务和拉取消息的服务可以是同一个,也可以不是同一个。MQ可以单独部署,而且具有持久化机制。

在这里插入图片描述

package com.itheima.jvmoptimize.practice.demo.controller;

import cn.hutool.json.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itheima.jvmoptimize.practice.demo.pojo.ArticleDto;
import com.itheima.jvmoptimize.practice.demo.service.ArticleService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * -XX:+HeapDumpOnOutOfMemoryError
 */
@RestController
@RequestMapping("/article")
public class Demo1ArticleController {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    // 用来实现json序列化
    @Autowired
    private ObjectMapper objectMapper;

    @PostMapping("/demo3/{id}")
    public void article3(@PathVariable("id") long id, @RequestBody ArticleDto article) throws JsonProcessingException {
        article.setId(id);
        rabbitTemplate.convertAndSend("jvm-test",null,  objectMapper.writeValueAsString(article));
    }
}

【监听器】

package com.itheima.jvmoptimize.practice.demo.listener;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class SpringRabbitListener {
    // 启动10个线程来并行处理
    @RabbitListener(queues = "queue1",concurrency = "10")
    public void listenSimpleQueue(String msg) throws InterruptedException {
        System.out.println(msg);
        Thread.sleep(30 * 1000);
    }
}

测试结果

内存没有出现膨胀的情况

在这里插入图片描述

问题根源和解决思路

  • 在项目中如果要使用异步进行业务处理,或者实现生产者 – 消费者的模型,如果在Java代码中实现,会占用大量的内存去保存中间数据。
  • 尽量使用Mq消息队列,可以很好地将中间数据单独进行保存,不会占用Java的内存。同时也可以将生产者和消费者拆分成不同的微服务。

在线定位问题

诊断问题有两种方法

生成内存快照并分析

优点:

  • 通过完整的内存快照准确地判断出问题产生的原因

缺点:

  • 内存较大时,生成内存快照较慢,这个过程中没办法响应用户的请求
  • 通过MAT分析内存快照,至少要准备快照文件1.5 – 2倍大小的内存空间
在线定位问题

优点:

  • 无需生成内存快照,整个过程对用户的影响较小

缺点:

  • 无法查看到详细的内存信息
  • 需要通过arthas或者btrace工具调测发现问题产生的原因,需要具备一定的经验(例如要监测哪个方法)

安装Jmeter插件查看接口响应信息

为了监控接口的响应时间RT、每秒事务数TPS等指标,需要在Jmeter上安装gc插件。

1、打开资料中的插件包并解压。

在这里插入图片描述

2、按插件包中的目录,复制到jmeter安装目录的相应目录下。

在这里插入图片描述

3、重启之后就可以在监听器中看到三个选项

  • 活跃线程数
  • 响应时间
  • 每秒事务数

在这里插入图片描述

查看响应时长

在这里插入图片描述

定位内存大的对象创建位置

Arthas stack命令

在这里插入图片描述

1、使用jmap -histo:live 进程ID > 文件名 命令将内存中存活对象以直方图的形式保存到文件中,这个过程会影响用户的时间,但是时间比较短暂(比生成堆内存快照对用户的影响小很多)

在这里插入图片描述

2、分析内存占用最多的对象,一般这些对象就是造成内存泄漏的原因 打开1.txt文件,从图中可以看到,有一个UserEntity对象占用非常多的内存。只要知道这个对象是在哪里创建出来的,就很容易定位到问题了

在这里插入图片描述

3、使用arthas的stack命令,追踪对象创建的方法被调用的调用路径,找到对象创建的根源(即构造方法在哪里调用的)。也可以使用btrace工具编写脚本追踪方法执行的过程。

在这里插入图片描述

接下来启动jmeter脚本,会发现有大量的方法调用这样不利于观察。

在这里插入图片描述

加上 -n 1 参数,限制只查看一笔调用:

在这里插入图片描述

这样就定位到了是login接口中创建的对象:

在这里插入图片描述

在这里插入图片描述

btrace在线定位
  • 相比较arthas的stack命令,btrace允许我们自己编写代码获取感兴趣的内容,灵活性更高。
  • BTrace 是一个在Java 平台上执行的追踪工具,可以有效地用于线上运行系统的方法追踪,具有侵入性小、对性能的影响微乎其微等特点。
  • 项目中可以使用btrace工具,打印出方法被调用的栈信息。

使用方法:

1、下载btrace工具, 官方地址:https://github.com/btraceio/btrace/releases/latest

在资料中也给出了:

在这里插入图片描述

在这里插入图片描述

编写代码的时候,需要引入这些jar包

在这里插入图片描述

这里提供了很多案例,编写代码的时候可以参考

在这里插入图片描述

2、编写btrace脚本,通常是一个java文件

<dependencies>
    <dependency>
        <groupId>org.openjdk.btrace</groupId>
        <artifactId>btrace-agent</artifactId>
        <version>${btrace.version}</version>
        // 本地文件引入,scope是system
        <scope>system</scope>
        <systemPath>D:\tools\btrace-v2.2.4-bin\libs\btrace-agent.jar</systemPath>
    </dependency>

    <dependency>
        <groupId>org.openjdk.btrace</groupId>
        <artifactId>btrace-boot</artifactId>
        <version>${btrace.version}</version>
        <scope>system</scope>
        <systemPath>D:\tools\btrace-v2.2.4-bin\libs\btrace-boot.jar</systemPath>
    </dependency>

    <dependency>
        <groupId>org.openjdk.btrace</groupId>
        <artifactId>btrace-client</artifactId>
        <version>${btrace.version}</version>
        <scope>system</scope>
        <systemPath>D:\tools\btrace-v2.2.4-bin\libs\btrace-client.jar</systemPath>
    </dependency>
</dependencies>

代码:

代码非常简单,就是打印出栈信息。clazz指定类,method指定监控的方法。

import org.openjdk.btrace.core.annotations.*;

import static org.openjdk.btrace.core.BTraceUtils.jstack;
import static org.openjdk.btrace.core.BTraceUtils.println;

@BTrace// 表示当前类是一个btrace脚本
public class TracingUserEntity {
    // 条件满足的时候,就会出发这个方法
    // method="/.*/" 表示监控所有的方法
    @OnMethod(
        clazz="com.itheima.jvmoptimize.entity.UserEntity",
        method="/.*/")
    public static void traceExecute(){
        // btace内置方法,打印栈信息
        jstack();
    }
}

3、将btrace工具和脚本上传到服务器,在服务器上运行 btrace 进程ID 脚本文件名

配置btrace环境变量,与JDK配置方式基本相同:

在这里插入图片描述

在服务器上运行 btrace 进程ID 脚本文件名:

在这里插入图片描述

4、观察执行结果。 启动jmeter之后,同样获取到了栈信息:

在这里插入图片描述

文章说明

该文章是本人学习 黑马程序员 的学习笔记,文章中大部分内容来源于 黑马程序员 的视频黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对 黑马程序员 的优质课程表示感谢。

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

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

相关文章

交换机和路由器的工作流程

1、交换机工作流程&#xff1a; 将接口中的电流识别为二进制&#xff0c;并转换成数据帧&#xff0c;交换机会记录学习该数据帧的源MAC地址&#xff0c;并将其端口关联起来记录在MAC地址表中。然后查看MAC地址表来查找目标MAC地址&#xff0c;会有一下一些情况&#xff1a; MA…

java.sql.SQLException: Before start of result set

情况描述&#xff0c;在通过JDBC连接数据库时&#xff0c;想直接判断获取的值是否存在&#xff0c;运行时报错。 翻译&#xff1a; 在开始结果集之前 报错截图 解决问题的方法&#xff1a;对结果集ResultSet进行操作之前&#xff0c;一定要先用ResultSet.next()将指针移动至…

4K60无缝一体矩阵 HDMI2.0功能介绍

关于GF-HDMI0808S 4K60无缝一体矩阵的功能介绍&#xff0c;由于直接针对GF-HDMI0808S型号的具体信息较少&#xff0c;我将结合类似4K60无缝HDMI矩阵的一般功能特性和可能的GF-HDMI0808系列产品的特点来进行说明。请注意&#xff0c;以下信息可能不完全针对GF-HDMI0808S型号&…

vienna整流器的矢量分析

Vienna整流器使用六个二极管和六个IGBT&#xff08;或MOSFET&#xff09;组成&#xff0c;提供三个电平&#xff1a;正极电平&#xff08;P&#xff09;、中性点电平&#xff08;O&#xff09;和负极电平&#xff08;N&#xff09;。通过对功率管的控制&#xff0c;Vienna整流器…

xmind梳理测试点,根据这些测试点去写测试用例

基本流&#xff08;冒烟用例必写&#xff09; 备选流 公共测试点&#xff1a;

算法笔记——LCR

一.LCR 152. 验证二叉搜索树的后序遍历序列 题目描述&#xff1a; 给你一个二叉搜索树的后续遍历序列&#xff0c;让你判断该序列是否合法。 解题思路&#xff1a; 根据二叉搜索树的特性&#xff0c;二叉树搜索的每一个结点&#xff0c;大于左子树&#xff0c;小于右子树。…

【企业级监控】Zabbix实现邮箱报警

Zabbix监控自动化 文章目录 Zabbix监控自动化资源列表基础环境前言四、Zabbix邮件告警4.1、实现报警所需的条件4.1.1、告警媒介4.1.2、触发器&#xff08;trigger&#xff09;4.1.3、动作&#xff08;action&#xff09; 4.2、配置告警媒介4.2.1、设置告警媒介参数4.2.2、启用此…

SpringCloud教程 | 第八篇: 使用seata处理分布式事务

1、参考程序员江小北 2、打算降低nacos版本&#xff0c;先学通再看看升级到高版本nacos能不能正常使用。 3、nacos用1.4.1&#xff0c;正常启动单机版的了 4、seata用2.0.0 我看江小北说用的1.4.0的seata&#xff0c;但是图片中的目录文件都找不到&#xff0c;倒是在2.0.0的…

弥合人类与人工智能的知识差距:AlphaZero 中的概念发现和迁移(1)

文章目录 一、摘要二、简介三、相关工作3.1 基于概念的解释3.2 强化学习中生成解释3.3 国际象棋与人工智能 四、什么是概念&#xff1f;五、发掘概念5.1 挖掘概念向量5.1.1 静态概念的概念约束5.1.2 动态概念的概念约束 5.2 过滤概念 一、摘要 人工智能&#xff08;AI&#xff…

计算机网络--tcpdump和iptable设置、内核参数优化策略

tcpdump工具 tcpdump命令&#xff1a; 选项字段&#xff1a; 过滤表达式&#xff1a; 实用命令&#xff1a; TCP三次握手抓包命令&#xff1a; #客户端执行tcpdump 抓取数据包 tcpdump -i etho tcp and host 192.168.12.36 and port 80 -W timeout.pcapnetstat命令 netst…

昇思25天学习打卡营第12天 | ResNet50图像分类

昇思25天学习打卡营第12天 | ResNet50图像分类 文章目录 昇思25天学习打卡营第12天 | ResNet50图像分类ResNet网络模型残差网络结构Building BlockBottleneck 训练数据准备训练数据可视化 网络构建Build Block结构Bottleneck结构ResNet50网络 模型训练与评估训练验证迭代数据集…

Swiftui中几种常用的数据存储方式@AppStorage/UserDefaults/CoreData/File Storage/Keychain等

在 SwiftUI 中&#xff0c;有多种常用的数据存储方式&#xff0c;根据需求的不同&#xff0c;可以选择适合的存储方案。以下是几种常用的数据存储方式&#xff1a; AppStorage/UserDefaults/CoreData/File Storage/Keychain&#xff0c;分别来看一下他们的使用场景和区别。 1.…

【Linux 文件读写描述符重定向 Linux 一切皆文件缓冲区】

文章目录 一、文件的读写操作二、文件描述符三、文件重定向四、理解 Linux 一切皆文件五、文件缓冲区 一、文件的读写操作 文件内容属性 当文件没有被操作的时候&#xff0c;一般文件还是在磁盘当中 文件操作文件内容的操作文件属性的操作&#xff0c;文件操作有可能即改变内容…

实战:docker式部署frp内网穿透-2024.7.13(测试成功)

前提 首先就需要准备好一台云服务器&#xff0c;用于提供公网 IP 和流量转发。至于购买哪家的云服务产品&#xff0c;本着能省则省的原则&#xff0c;这个当然是哪家便宜用哪家呢。 我手上目前有闲置的腾讯云的服务器&#xff0c;刚好可以用来作为内网穿透的机器&#xff0c;…

MySQL篇十:事务

文章目录 前言1. 什么是事务&#xff1f;2. 为什么会出现事务3. 事务的版本支持4. 事务提交方式5. 事务常见操作方式6. 事务隔离级别6.1 如何理解隔离性16.2 如何理解隔离性26.2.1 读-写6.2.2 undo 日志6.2.3 模拟 MVCC6.2.4 RR 与 RC的本质区别 前言 CURD不加控制&#xff0c;…

【探索Linux】P.39(传输层 —— TCP的三次 “握手” 和四次 “挥手” )

阅读导航 引言一、TCP的三次握手1. 简介2. 图解三次握手3. 名词解释&#xff08;1&#xff09;SYN&#xff08;同步序列编号&#xff09;包&#xff08;2&#xff09;SYN-ACK&#xff08;同步确认&#xff09;包&#xff08;3&#xff09;ACK&#xff08;确认&#xff09;包 4.…

【CTF-Crypto】数论基础-02

【CTF-Crypto】数论基础-02 文章目录 【CTF-Crypto】数论基础-021-16 二次剩余1-20 模p下-1的平方根*1-21 Legendre符号*1-22 Jacobi符号*2-1 群*2-2 群的性质2-3 阿贝尔群*2-4 子群2-11 群同态2-18 原根2-21 什么是环2-23 什么是域2-25 子环2-26 理想2-32 多项式环 1-16 二次剩…

C语言的神髓

从应用的角度出发&#xff0c;聊一聊C语言最精妙的部分。 ​​​​​​​ ​​​​​​​

unity 2020版本packManager没有AssetBundles

1.Packages->manifest.json打开manifest.json文件 2.添加"com.unity.assetbundlebrowser": "1.7.0", 保存即可

Readiris PDF Corporate / Business v23 解锁版安装教程 (PDF管理软件)

前言 Readiris PDF Corporate / Business 是一款高性能的 OCR&#xff08;光学字符识别&#xff09;软件&#xff0c;能够帮助用户将纸质文档、PDF 文件或图像文件转换为可编辑和可搜索的电子文本。该软件提供专业级的功能和特性&#xff0c;非常适合企业和商业使用。使用 Rea…