深入JVM:全面解析GC调优

文章目录

  • 深入JVM:全面解析GC调优
    • 一、序言
    • 二、GC调优指标
    • 三、GC在线监控
      • 1、Jstat工具
      • 2、VisualVM工具
    • 四、GC日志分析
      • 1、收集GC日志
      • 2、GCViewer工具
      • 3、GCeasy工具
    • 五、GC问题调优
      • 1、调整JVM内存大小
        • (1)调整堆内存大小及比例
        • (2)调整元空间内存大小
        • (3)调整Java虚拟机栈大小
      • 2、择优垃圾回收器
        • (1)模拟环境
        • (2)不同垃圾回收器对比
      • 3、优化垃圾回收器参数
    • 六、后记


深入JVM:全面解析GC调优

一、序言

对于Java工程师而言,深入理解JVM(Java虚拟机)不仅是掌握Java程序运行机制的基础,也是提升系统性能、优化应用和解决复杂问题能力的重要一步,更是Java进阶之路的重中之重。

本文小豪将带大家学习GC调优,了解常见的GC监控和日志分析工具,通过一些应用案例,辅助大家理解GC调优的主要思路。

二、GC调优指标

本篇探讨的JVM知识体系中的GC调优相对关键且较复杂,JVM基础不牢的小伙伴,强烈建议先复习小豪之前发布的内容,以便在阅读本篇时,更容易理解内容。

  1. 【深入JVM:详解JVM内存模型及其演变过程】
  2. 【深入JVM:详解垃圾判定与垃圾回收算法】
  3. 【深入JVM:详解G1垃圾回收器原理】

GC调优主要是针对垃圾回收的性能优化,减少产生Full GC的频率和次数

通常来说,GC性能的评判指标包含三个方面:

  • 吞吐量

    吞吐量是指CPU执行用户代码的时间占总运行时间的比例

    吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间)

  • 暂停时间

    暂停时间指垃圾回收时,用户应用线程产生的暂停时间

  • 内存占用

    内存占用指Java应用所占用的系统内存大小

高吞吐量、低暂停时间和内存占用少共同构成GC性能的衡量指标,但鱼与熊掌不可兼得,这三者往往不能同时满足,需要根据业务场景做相应的折中或取舍。

其中内存占用受限于服务器自身系统内存大小,在日常的调优过程中,我们更为关注另外两个指标:高吞吐量和低暂停时间

高吞吐量以程序的吞吐量为优先,CPU用来更多的执行用户业务线程,往往用于大数据处理或对用户响应时间要求没那么高的后台任务,可能会导致单次垃圾回收的STW时间过长

低暂停时间则以程序的最小暂停时间为优先,用于互联网等对用户响应时间要求比较高的应用中,最大程度减少单次STW时间,但往往垃圾回收器会提前进行垃圾回收,频率高,增加GC线程的工作时间,吞吐量会降低

三、GC在线监控

为发现并定位GC问题,我们首要做的就是监控GC的运行统计信息。小豪这里介绍两款比较常见的工具:Jstat和VisualVM,均可以用于监控GC

本文着重讲解GC调优,对工具类的使用仅作简要说明,详细用法大家可自行学习

1、Jstat工具

Jstat是JDK自带的轻量命令行工具,可以用来在线监控GC信息,无需安装,功能也比较简单,具体用法如下:

jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]

举个例子:

  1. 首先通过jps命令获取本机运行的Java进程ID
  2. 使用jstat来监控某个Java进程的GC信息
jstat -gc 8012 5000

此命令代表每隔5000毫秒输出一次Java进程号8012的GC数据。

在这里插入图片描述

输出的GC数据以表格的形式呈现,比较重要的最后五列代表的内容分别是:

  • YGC、YGCT:年轻代垃圾回收次数和总耗时(单位:秒)
  • FGC、FGCT:老年代(Full GC)垃圾回收次数和总耗时(单位:秒)
  • GCT:总垃圾回收耗时(单位:秒)

显然通过Jstat命令能够简要观察到程序的GC统计信息,包括各个内存区域的使用情况,GC的频率和耗时,但仍不够直观,也无法准确捕获GC产生的时间。

2、VisualVM工具

VisualVM相较于Jstat就比较强大了,同样也是JDK自带的监控工具,能够可视化的在线监控线程,内存使用情况,GC信息等

VisualVM位于Jdk的安装目录的bin目录下(JDK 9及以后需单独下载),双击打开:

在这里插入图片描述

选择需监控的应用程序后,VisualVM右侧展示相应的信息,我们着重关注里面的监视Visual GC状态栏:

  • 监视:展示当前Java程序的CPU、堆内存、类和线程的运行信息,同时能够快速手动触发垃圾回收以及Dump导出栈堆信息

在这里插入图片描述

  • Visual GC:展示当前Java程序堆内存结构、使用情况和GC统计信息,包括GC的次数和耗时

在这里插入图片描述

VisualVM工具默认不带Visual GC插件,这里需在工具栏手动安装下载Visual GC插件:

在这里插入图片描述

同时IDEA也能够集成VisualVM工具,在Plugins插件中搜索VisualVM Launcher进行安装,随后选用VisualVM图标启动程序,VisualVM监控工具则会自动弹出:

在这里插入图片描述

VisualVM总体功能还是比较强大的,支持安装额外的插件,同时可视化界面也更友好,日常使用频率较多,也更适合用于开发环境。

四、GC日志分析

上面介绍的两款工具主要用于在线监控GC信息,初步排查程序是否存在性能问题。

在发现GC问题后,我们需更进一步的了解整个垃圾回收的详细数据,因此需在程序运行过程中收集其产生的GC日志,以便于诊断问题。

1、收集GC日志

开启GC日志信息收集,我们需要在JVM启动命令中设定如下参数:

// 打印详细的GC垃圾回收日志信息
-XX:+PrintGCDetails

// 将GC垃圾回收日志输出到指定的文件中
-Xloggc:文件名

在JDK 9及之后版本中,已将上述命令整合为一个,可直接指定GC日志输出至文件

-Xlog:gc*:file=文件名

输出至文件内容如图:

在这里插入图片描述

GC日志中详细记录整个GC的发生过程,堆内存的整理变化、暂停时间等,但这部分内容看起来比较费力,往往会借助外部工具。这里介绍两种常见的GC日志分析工具:GCViewerGCeasy

2、GCViewer工具

GCViewer是免费的GC日志分析工具,能够将GC日志转换为可视化的图表。也是用Java编写的,使用也比较简单,运行jar包即可。

首先在GitHub官网下载GCViewer源码(地址在这),在本地maven编译打包,生成gcviewer-1.36.jar,启动时加上日志文件即可:

java -jar gcviewer-1.36.jar 日志文件名

启动后界面如下:

在这里插入图片描述

界面左侧Chart图表显示元素过多,这里可以选择View工具栏,勾选掉不关注的曲线:

在这里插入图片描述

界面右侧显示一些统计信息,Summary、Memory、Pause。

Summary中展示程序GC累计暂停时间吞吐量等:

在这里插入图片描述

Pause中展示程序平均暂停时间最小/最大暂停时间等:

在这里插入图片描述

上面列举的这些参数对我们后续GC调优具有指导意义,其它功能大家可自行研究一下。

3、GCeasy工具

GCeasy也是目前比较流行的GC日志分析工具。官网介绍其是业内首款借助机器学习技术引导的垃圾回收日志分析工具,GCeasy内置有智能功能,可自动检测JVM和Android GC日志中的问题并推荐解决方案(官网在这)

在这里插入图片描述

使用也比较简单,只需要登录后将GC日志上传上去即可,GCeasy会自动分析诊断,但每月只能免费使用5次,超过就需要收费了,小豪这里也只是简单了解了一下,使用不多。

五、GC问题调优

上面介绍的GC在线监控工具和GC日志分析工具旨在帮我们发现并诊断GC存在的问题,最终还是需要落实到解决方案上。

在本文开篇我们提到,GC调优主要是针对垃圾回收的性能优化,减少产生Full GC的频率和次数。因此我们进行GC调优核心目标就是减少GC导致的暂停时间和提高程序运行吞吐量,优化我们可以从这几个方面入手:

  1. 调整JVM内存大小:除内存泄漏等问题,调整JVM内存大小往往能解决绝大多数GC性能问题,过小的内存以及不合适的参数大小都会导致频繁发生GC,影响程序性能
  2. 择优垃圾回收器:JVM目前有众多的垃圾回收器,有专注于减少系统暂停时间的CMS,也有专注于提升系统的吞吐量的Parallel Scavenge + Parallel Old组合,也有更为强悍的G1,可以根据不同的业务需求,选择更合适的垃圾回收器
  3. 优化垃圾回收器参数:同样垃圾回收器参数的合理设置也能有效优化GC,这部分就需要针对专门的垃圾回收器进行不断调试和测试

1、调整JVM内存大小

遇事不决加内存!

可能大家经常性遇到Java程序卡顿,就嚷嚷着加内存,没错,加内存百分之99%的情况下确实能解决性能问题。

剩下1%是因为加的还不够多

但更多的是,我们能在固定的内存大小下优化JVM参数,调整内存比例。

(1)调整堆内存大小及比例

调整堆内存大小会用到两个参数:

// (推荐)初始堆内存大小
–Xms[n]

// (推荐)最大堆内存大小
-Xmx[n]

一般来说,我们首先可以考虑在有限的资源条件下扩大堆内存,毕竟堆内存越大Full GC发生频率越小,同时建议-Xms-Xmx设置的大小比例一致,避免堆内存自己动态地调整大小,向操作系统申请内存,浪费性能。

当然我们在设置堆内存大小时也要考虑服务器上其它软件和操作系统自身占用的内存,不能盲目的扩大,例如现在服务器内存为8G,操作系统和其它软件等共占用3G,那么-Xms-Xmx可设置为4G

另外,也可以调整堆中各区域的大小及比例等:

// (不推荐)年轻代的大小,默认总堆的1/3
-Xmn[n]

// (不推荐)年轻代和老年代的大小比例,默认1:2,设置-Xmn后则不用设置此参数
-XX:NewRatio=n

// (不推荐)Survivor区和Eden区大小比例,默认8:1:1
-XX:SurvivorRatio=n

但其实完全不建议这么做,JDK 8默认的Parallel Scavenge和JDK 9及之后默认的G1垃圾回收器,都会动态的调整内存大小,包括年轻代的比例、老年代大小等细节参数。

(2)调整元空间内存大小

调整元空间大小会用到两个参数:

// (推荐)指定元空间最大内存大小
-XX:MaxMetaspaceSize=n
    
// (不推荐)元空间扩容时触发Full GC的阈值,默认约20M
-XX:MetaspaceSize=n

JDK 8及之后版本,元空间使用操作系统本地内存,默认比较大,一般来说我们用不到那么大,有点浪费资源,我们可以控制一下最大值,但也不能配置的过小,避免频繁Full GC,或导致OOM,这里建议在程序运行一段时间后通过Jstat或VisualVM工具查看一下元空间的内存使用情况,确保配置的值大于它。

同时也可以指定当元空间大小使用到-XX:MetaspaceSize配置的阈值后,触发Full GC,默认当元空间内存使用到约20M时会自动触发一次,很多人提到将此参数与元空间最大内存大小保持一致,但其实不建议这样设置,这样做元空间无法触发Full GC,垃圾对象不会被回收。

(3)调整Java虚拟机栈大小

调整Java虚拟机栈大小会用到下列参数:

// (推荐)指定Java虚拟机栈大小
-Xss[n]

Java虚拟机栈大小默认由操作系统决定,一般为1MB2MB,但其实大多数时间压根使用不到,可以将此参数调小一些,在256KB1MB之间即可。

2、择优垃圾回收器

随着JDK版本的迭代,垃圾回收器也在不断优化,垃圾回收器从单线程到多线程、从并行到并发,选择更优的垃圾回收器提升性能。

我们使用GCViewer工具对比一下上一篇中介绍到的几种垃圾回收器性能,分别是:

  • ParNew + CMS
  • Parallel Scavenge + Parallel Old
  • G1
(1)模拟环境

首先编写一个对外接口,这里使用软引用模拟缓存,当每次调用该接口时,默认往内存中存放10M数据,当内存不足时会触发Full GC释放软引用对象:

@RestController
@RequestMapping("/fullGcTest")
public class FullGcController {

    public static Map<String, SoftReference> map;

    // 简略模拟软引用集合,当内存不足时会释放软引用对象
    static {
        map = new HashMap<>();
    }

    @GetMapping("/addMemory")
    public void addMemory() {
        // 随机ID
        String autoId = UUID.randomUUID().toString();
        // 创建软引用对象,占用的内存大小为10M
        SoftReference memory = new SoftReference(new byte[1024 * 1024 * 10]);
        map.put(autoId, memory);
    }
    
}

同时使用Postman中Runner功能模拟高并发下用户访问程序,每次模拟100个用户同时请求,共模拟8轮,累计调用800次该接口。

在这里插入图片描述

在对比过程中,控制其它变量,程序统一运行约1分40秒,堆内存大小等保持一致:

-Xms1g -Xmx1g -Xss256k -XX:MaxMetaspaceSize=256m -XX:+PrintGCDetails -Xloggc:C:\Users\12748\Desktop\log\gclog.txt
(2)不同垃圾回收器对比
  • ParNew + CMS

启用参数:

-XX:+UseParNewGC -XX:+UseConcMarkSweepGC

并发请求8轮次后,GCViewer工具分析ParNew + CMS垃圾回收器的GC日志结果如下:

在这里插入图片描述

吞吐量:95.51%

平均暂停时间:0.04834s

最小暂停时间:0.00049s

最大暂停时间:0.25087s

  • Parallel Scavenge + Parallel Old

PS + PO为JDK 8默认垃圾回收器,不需要在启动参数中指定。

在同样的运行时间和并发请求数下,PS + PO分析结果如下:

在这里插入图片描述

吞吐量:96.22%

平均暂停时间:0.09947s

最小暂停时间:0.01395s

最大暂停时间:0.27486s

可以看出,PS + PO吞吐量高于ParNew + CMS,但平均暂停时间、最小/最大暂停时间均长于ParNew + CMS,也进一步证实了其专注于程序的高吞吐量,更适合于处理大数据或后台任务的应用场景中。反之ParNew + CMS专注于减少系统暂停时间,更适合用于对用户响应时间要求较高的互联网系统中。

  • G1

启用参数(JDK 9及之后版本为默认垃圾回收器):

-XX:+UseG1GC

同样在相同控制变量条件下,G1分析结果如下:

在这里插入图片描述

吞吐量:98.95%

平均暂停时间:0.01446s

最小暂停时间:0.00033s

最大暂停时间:0.04514s

结果很明显,即使G1在JDK 8中还不够成熟(JDK 8之后做了大量优化),但G1其吞吐量、平均暂停时间、最小/最大暂停时间等指标均优于ParNew + CMS和PS + PO。在JDK 8最新版本和JDK 9之后版本,在能选用G1垃圾回收器的情况下,都建议直接使用G1

3、优化垃圾回收器参数

最后,在我们选用合适的垃圾回收器之后,也可以根据垃圾回收器的特点,针对性的进行参数调节。

例如在我们选用G1垃圾回收器之后,合理配置其定制化的参数来优化性能,G1调优的常用参数有:

// 指定Region的内存大小,n必须是2的指数幂,其取值范围是从1M到32M
-XX:G1HeapRegionSize=n

// 设置最大暂停时间,默认200ms
-XX:MaxGCPauseMillis=n

// 指定垃圾回收工作的线程数量
-XX:ParallelGCThreads=n

// 保留空闲的内存区域,默认10%
-XX:G1ReservePercent=n

大多数情况下,这些参数的默认值是经过其设计人员验证下的最优解,因此在配置这些参数后,我们也需要进行大量的测试工作,确认是否对程序性能有改善,否则结果很有可能会适得其反。

更多细节的调优参数,大家也可自行查阅【Oracle官方调优文档】

六、后记

本文从GC调优的指标开始介绍,带领大家认识常用的GC监控和GC日志分析工具,最后讲解了进行GC调优的三个方面,过程中也通过GCViewer分析工具对比了不同垃圾回收器的性能,相信大家对GC调优也有了一定的认知。

当然大多数情况下是不需要进行调优的,一般内存出现问题,更多的可能是代码逻辑问题造成的内存泄露。当然程序的并发访问量较高也会导致频繁GC,那可能更多的就是引入中间件了,进行GC调优往往是最后的解决方案了,同时也随着垃圾回收器的不断改进,近期Azul System公司新推出的C4垃圾回收器几乎可以实现零暂停时间了,应用于我们工作中的GC调优占比会更小,但GC调优作为高阶程序员的必备技能,还是很有必要掌握的。

下一篇,小豪将会更新排查并定位内存泄漏的相关文章,如果大家觉得内容还不错,可以先点点关注,共同进步~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ChatGPT-4o 有何特别之处?

文章目录 多模态输入&#xff0c;多模态输出之前的模型和现在模型对比 大家已经知道&#xff0c;OpenAI 在 GPT-4 发布一年多后终于推出了一个新模型。它仍然是 GPT-4 的一个变体&#xff0c;但具有前所未见的多模态功能。 有趣的是&#xff0c;它包括实时视频处理等强大功能&…

疫情物资捐赠和分配系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;机构管理&#xff0c;用户管理&#xff0c;发放管理&#xff0c;物资管理 前台账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;物资论坛&#xff0c;公告信息…

7.1 Go 错误的概念

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

Python:由b站临时短链接获取到永久链接(去除分享中的杂项)

&#x1f4da;博客主页&#xff1a;knighthood2001 ✨公众号&#xff1a;认知up吧 &#xff08;目前正在带领大家一起提升认知&#xff0c;感兴趣可以来围观一下&#xff09; &#x1f383;知识星球&#xff1a;【认知up吧|成长|副业】介绍 ❤️如遇文章付费&#xff0c;可先看…

LabVIEW在高校电力电子实验中的应用

概述&#xff1a;本文介绍了如何利用LabVIEW优化高校电力电子实验&#xff0c;通过图形化编程实现参数调节、实时数据监控与存储&#xff0c;并与Simulink联动&#xff0c;提高实验效率和数据处理能力。 需求背景高校实验室在进行电机拖动和电力电子实验时&#xff0c;通常使用…

MongoDB CRUD操作:插入文档

MongoDB CRUD操作&#xff1a;插入文档 文章目录 MongoDB CRUD操作&#xff1a;插入文档使用MongoDB Atlas UI插入文档插入单个文档插入多个文档插入行为自动创建集合_id字段原子性写确认 在MongoDB中插入文档的集中方式&#xff1a; 使用编程语言提供的驱动程序&#xff0c;在…

Table表格组件不请求接口,实现表格里某条数据的本地编辑功能(Vue3+ArcoDesign)

【背景】 在 Vue3 ArcoDesign项目中&#xff0c;使用ArcoDesign-Table表格组件不请求接口&#xff0c;实现表格里某条数据的本地编辑功能。最后统一通过接口发送数据。 【步骤】 1. 在表格每条数据列后添加一个“编辑”按钮&#xff0c;点击该按钮弹出一个对话框&#xff0c…

flink 作业报日志类冲突的解决方案

文章目录 背景思考初步解决方案深入思考下终极解决方案总结 背景 实时作业在页面提交任务后&#xff0c;报NoSuchMethodException 方法&#xff0c;看了下是关于log4j的&#xff0c;首先是作业升级了很多依赖的版本&#xff0c;其次flink 也升级 到了1.19版本 思考 打的Jar有…

计算一个3x3矩阵对角线和其它两条线的元素之和

计算一个3x3矩阵对角线和其它两条线的元素之和 #include <stdio.h> int main () { int d0,b0,s,i,j; int a[3][3]{1,2,3,4,5,6,7,8,9}; for(i0,j2;i<3;i,j--) dda[i][i]a[i][j]; for(i0,j0;i<3;) {bba[i][j]a[i][j2]; ii2;} sdb; printf("d%d\nb%d\ns%d\n&qu…

远程继电器模块实现(nodemcu D1 + 继电器)

前言 接下来将实现一个远程继电器&#xff0c;实时远程控制和查询的开关状态。用 5v 直流电控制 220v 交流电。 硬件上&#xff1a; 使用 nodemcu D1 和 JQC-3FF-S-Z 继电器。 软件上&#xff1a; 使用 nodejs 作为服务端&#xff0c;和 html 作为客户端。 在开始之前在电脑…

数模混合芯片设计中的修调技术是什么?

一、修调目的 数模混合芯片需要修调技术主要是因为以下几个原因&#xff1a; 工艺偏差&#xff08;Process Variations&#xff09;&#xff1a; 半导体制造过程中存在不可避免的工艺偏差&#xff0c;如晶体管尺寸、阈值电压、电阻和电容值等&#xff0c;这些参数的实际值与…

2024年海南省三支一扶报名指南,照片要求

2024年海南省三支一扶报名指南&#xff0c;照片要求 一、考试时间安排&#xff1a; 报名时间&#xff1a;6月1日8:00至6月7日18:00 准考证打印时间&#xff1a;6月17日8:00 考试时间&#xff1a;6月22日 二、招聘人数 海南省计划招募390名高校毕业生

Golang | Leetcode Golang题解之第125题验证回文串

题目&#xff1a; 题解&#xff1a; func isPalindrome(s string) bool {s strings.ToLower(s)left, right : 0, len(s) - 1for left < right {for left < right && !isalnum(s[left]) {left}for left < right && !isalnum(s[right]) {right--}if l…

Golang | Leetcode Golang题解之第126题单词接龙II

题目&#xff1a; 题解&#xff1a; //bfsdfs(如果是双向bfs&#xff0c;效果会更好) func findLadders(beginWord string, endWord string, wordList []string) [][]string {//字典表&#xff08;将wordList中的单词放入hash表中&#xff0c;方便查找&#xff09;dict:make(m…

学习笔记——网络参考模型——TCP/IP模型(物理层)

一、TCP/IP模型-物理层 1、数据传输(交换)的形式 (1)电路交换 特点&#xff1a;通信双方独占通信链路。 优点&#xff1a;数据传输时延小&#xff0c;适用于实时通信&#xff1b;数据按序发送&#xff0c;不存在失序问题&#xff1b;适合模拟信号和数字信号传输。 缺点&am…

指纹采集技术

目录 1.概述 1.1 捺印油墨采集 1.2 现场指纹提取 1.3 在线指纹采集 2. 指纹采集器的关键技术指标 2.1 采集面积 2.2 分辨率 2.3 图像质量 2.4 耐用性 1.概述 最早的指纹采集技术是油墨法&#xff0c;至少已经有上百年的历史。1990年代出现了活体指纹采集器&#xff0c…

国内AI工具访问量第一的竟然是它?!不是Kimi,也不是文心一言

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…

spoon基础使用-第一个转换文件

新建一个转换&#xff0c;文件->新建->转换&#xff0c;也可以直接ctralN新建。 从右边主对象树拖拽一个输入->表输入&#xff1b;输出->文本文档输出&#xff1b;也可以直接在搜索框搜素表输入、文本文档输出。 双击表输入新建一个数据库连接 确定后就可以在S…

AndroidStudio中debug.keystore的创建和配置使用

1.如果没有debug.keystore,可以按照下面方法创建 首先在C:\Users\Admin\.android路径下打开cmd窗口 之后输入命令:keytool -genkey -v -keystore debug.keystore -alias androiddebugkey -keyalg RSA -validity 10000 输入两次密码(密码不可见,打码处随便填写没关系) 2.在build…

JavaScript拖拽API的简单使用

演示效果&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><st…