【JavaEE】了解JVM

news2024/11/20 15:33:06

JVM的基本认识

在这里插入图片描述

文章目录

  • 【JavaEE】了解JVM
    • 1. JVM中的内存区域划分
      • 1.1 JVM的核心区域
      • 1.2 JVM内存城防图
    • 2. JVM的类加载机制
      • 2.1 loading
      • 2.2 verification
      • 2.3 preparation
      • 2.4 resolution
      • 2.5 initialization
      • 2.6 类加载触发的时机
      • 2.7 双亲委派模型
    • 3. JVM中的垃圾回收策略
      • 3.1 JVM释放的空间
      • 3.2 GC的两个阶段
      • 3.3 垃圾回收算法
        • 3.3.1 引用计数
        • 3.3.2 可达性分析
      • 3.4 释放“垃圾”对象
        • 3.4.1 标记释放
        • 3.4.2 复制算法
        • 3.4.3 标记整理
        • 3.4.4 分代回收
    • 4. JMM

【JavaEE】了解JVM

JVM是Java虚拟机(Java Virtual Machine)的缩写

  • 它是Java编程语言的关键组成部分。
  • JVM是一种用于在计算机上执行Java字节码的虚拟机。
  • 它是Java平台的核心技术,提供了跨平台的特性和Java的主要优点。

JVM的来历可以追溯到20世纪90年代初,当时Sun Microsystems(现在是Oracle Corporation的一部分)开发了Java语言。

  • Java的目标是在不同的硬件和操作系统平台上实现 “一次编写,到处运行” 的理念。
  • 为了实现这一目标,Sun Microsystems开发了JVM,它是Java语言的运行时环境。

接下来就是介绍JVM中重要的几个 芝士点

1. JVM中的内存区域划分

JVM其实就是一个java进程,java进程会从操作系统这里申请一大块内存区域,再进行一系列划分,给java代码使用~

  • 所以java程序不鼓励多进程,而鼓励多线程也是这个原因

而我们重点要学的是内存的进一步划分,不同的区域有不同的用途

1.1 JVM的核心区域

  1. ,java中实例化出来的对象,即成员变量
  2. ,维护方法之间的调用关系,即局部变量
  3. 方法区(旧说法)/元数据区(新说法),类加载之后的类对象,包括静态变量、方法…
    • 类对象就是.class文件的特殊数据结构构成的对象,类名.class去获取
    • 方法不是变量,其实在内存中是字节码(二进制指令)形式存在!

经典的面试题:

  • 给一段代码,问你某个变量处在内存中的哪个区域
  • 根据上面的变量形态与内存区域的对应即可~

注意:

  • 变量的类型与它在内存中的区域无关
Apple a = new Apple();

在这里插入图片描述

  • 我们常常把a叫做引用,或者对象,其实它指向的内容,才是对象,才是属于堆的东西

记住java对象的这个核心关系即可:

在这里插入图片描述

1.2 JVM内存城防图

在这里插入图片描述

灰色:两个栈

  1. 虚拟机栈,给java代码使用的
  2. 本地方法栈,是给JVM内存的native本地方法使用的(JVM的本地方法内部是通过C++实现的)

之前的String常量池…也在元数据区里~

  • 不必多说~

Program Counter Register(程序计数器)

用途是记录当前程序执行到哪个指令~

  • 简单地用long类型的变量去存储 “一个内存地址”
  • 这个内存地址就是下一个要执行的字节码所在的地址

CPU也是有这么一个专门的寄存器,JVM参考了CPU

堆的细节安排,在垃圾回收策略里就体现出来了,随后讲解

堆和元数据区,在一个JVM进程中,只有一份;栈和程序计数器,则存在多份(每个线程只有一份)

2. JVM的类加载机制

类加载:把.class文件,加载到内存,得到类对象,这样的过程~

  • 程序要运行的必要条件:指令和数据

类加载的步骤,其实非常的复杂,而我们只需要理解一些基本流程

  • 可以去看看官方的规范:Java SE Specifications (oracle.com)
  • The Java® Virtual Machine Specification (oracle.com)

提到了**五个词**

  1. 加载 - loading
  2. 验证 - verification
  3. 准备 - preparation
  4. 解析 - resolution
  5. 初始化 - initialization

2.1 loading

找到.class文件,并且读文件内容,获取到字节码

  • 涉及到一个经典的考点:双亲委派模型,在后面单独解释

2.2 verification

.class文件的数据格式:

  1. 二进制
  2. 类似c语言的结构体(JVM本质就是C++写的)

在这里插入图片描述

  • u4 => 无符号整型(四个字节)
  • u2 => 无符号整型(两个字节)
  • 其他就是JVM中定义的其他结构体

这一步就是验证这个.class文章的内容是否符合标准,感兴趣的可以去研究一下每个成员的含义~

2.3 preparation

给类对象分配内存空间

  • 这个是未初始化的空间,内存空间的数据是全是0的

2.4 resolution

解析,则是针对字符串常量,进行初始化

最主要是将“常量池的符号引用替换为直接引用”:

在这里插入图片描述

在这里插入图片描述

2.5 initialization

针对类对象进行初始化(初始化静态成员变量,静态方法,执行静态代码块,加载父类、内部类…)

第四第五步我觉得界限不明显,可能是本就是一步,比较这五个步骤是人为划分出来的

2.6 类加载触发的时机

并不是jvm已启动,就把所有的.class都加载了!整体是一个“懒汉模式”,非必要不加载

  1. 要创建这个类的实例
  2. 使用这个类的静态方法/静态属性
  3. 使用子类,会触发父类的加载
  4. 反射(使用类对象的场合)

2.7 双亲委派模型

因为这个好名字,成为了一个热门面试题,其实这个加载步骤在类加载中并不是什么关键的步骤~

接下来就来好好了解一下吧!

主要工作,在第一个步骤中,找.class文件的一个过程~

JVM中,内置了三个类加载器(加载类,需要用到的一组特殊模块)

  1. BootStrap ClassLoader,负责加载Java标准库中的类
  2. Extension ClassLoader,负责加载一些非标准的类,(Sun/Oracle扩展库的类)
  3. Application ClassLoader,负责加载项目中自己写的类,以及第三方库中的类

也可以自己去定义类加载器~

三个类加载器负责三组不同的目录~

当我们具体加载一个类的时候,需要先给定一个类的全限定类名“一系列包名.类名”,例如:“java.lang.String”

在这里插入图片描述
双亲委派模型被称为“双亲”,是因为它建立了一个父子关系的类加载器层级结构,通过委派加载的方式提供了一种高效、安全和一致的类加载机制。就体现在两个委派方向咯~

其实是翻译的问题,双亲就是family~

3. JVM中的垃圾回收策略

JVM中帮助程序员自动释放内存的~

在C中,malloc的内存必须手动free,否则就容易出现内存泄露(只申请不释放,出现逐渐崩溃)

  • C++中的内存泄露不易发现,并且现象就像温水煮青蛙,很久才会被发现,并且发现的时候就一发不可收拾

java等后续的编程语言,引入了GC来解决这个问题~

GC的全称是垃圾回收(Garbage Collection)。在计算机科学中,垃圾回收是一种自动化的内存管理技术,用于在程序运行时自动回收不再使用的内存资源,以便重新分配给其他需要的对象。

当程序运行时,会动态地创建和销毁对象。由于对象的动态性,手动管理内存资源变得复杂和容易出错。垃圾回收器(GC)的作用就是在程序运行时监测和识别不再使用的对象,然后释放其占用的内存资源。

能够有效的减少内存泄露的出现概率!作死的一样会出现~

申请内存的时机是明确的,使用到了必须要申请

释放内存的时机是模糊的,完全不使用了才能释放

而C/C++将这个释放的时机,全权交给程序员,但是 java的JVM则跳过一系列策略自动判断是否释放

  • 这些策略的准确性是比较高的,但是是需要代价的,那就是时间/空间

3.1 JVM释放的空间

  • 是堆,GC的主要目标

    • 不仅仅是java,C/C++中自主申请和释放的也是堆
  • 因为栈是局部遍历,创建与销毁本就系统自动的行为(随着线程的销毁而销毁,汇编操作中(栈帧创建与销毁,即入栈和出栈)方法结束的出站操作而被销毁)

  • 程序计数器,就一个long,随着线程销毁而销毁

  • 元数据区/方法区,存的类对象,很少会“卸载”,进程结束销毁~

而GC就是以对象为单位进行释放的,即释放内存=释放对象

  • 总不能释放半拉对象吧,🤣

3.2 GC的两个阶段

  1. 找,谁是垃圾(涉及垃圾回收算法)
  2. 释放,将垃圾对象的内存整体释放掉

接下来我们要了解一下垃圾回收算法,我们学习思想,不代表JVM的真实实现方法,JVM的实现方法是在此基础上的优化~

3.3 垃圾回收算法

一个对象,后续不再使用,就可以认为是垃圾~

java中使用一个对象,只能通过“引用”~

  1. 如果一个对象,没有引用指向它,此时这个对象一定是无法被使用的(妥妥的垃圾)
  2. 如果一个对象,已经不想用了,但是这个引用可能还被指向着(这个携带程序员主观意愿,JVM无法判断)

3.3.1 引用计数

不针对JVM的判断方法,python和PHP采取了一个算法:引用计数

就是给对象安排一个额外的空间,保存一个整数,表示该对象有几个引用指向它~

在这里插入图片描述

  • 随着引用的销毁,计数就会减少,为0的时候,立即释放~

缺陷就是:

  1. 每个对象都需要怎么一个可见来存放这个计数

  2. 还有个漏洞,就是一些“循环引用”引起的问题,最典型的就是循环链表

在这里插入图片描述

  • 这个时候,c1 = null; c2 = null;

在这里插入图片描述

  • 计数并为0,不能释放~

3.3.2 可达性分析

可达鸭 的图像结果

而这个算法,才是JVM采取的方案,并没有采取引用计数~

  • 我们可以把对象之间的引用关系,理解成一个“有向图结构”,从一些特殊的起点出发,进行遍历,只要能便利访问到的对象,就是“可达”,否则就是“不可达”
    • 不可达的就是垃圾咯~

例如下图,则就是一种复杂的引用关系~
在这里插入图片描述

  • 边的箭头我省略了

  • 然后我们就寻找其中特殊的起点(蓝),开始遍历,每个蓝色的都便利一边,访问的到的标记为“可达”,所有蓝点都便利完后,未被标记的就是“不可达”

  • 即顺藤摸瓜

在这里插入图片描述

  • 而这个蓝色起点,就是引用中的“局部变量”,因为我们是在方法内部可以先通过局部引用变量去访问堆区空间的,进而延申出后面的引用关系!
  • 其次,还可以是常量池上引用的对象,方法区中静态成员引用的对象
  • 这些蓝点有一个名称:GCroots

缺点:

  • 耗时及其大
  • 这个过程中,必须保证原代码的引用关系不要发生变化,所以要加锁,即STW问题
    • 加锁 -> 其他业务暂停工作,Stop the world!

随着java的发展,JVM的垃圾回收不断的更新优化,STW问题也被很好的应对,不能完全消除,但是STW的时间可以忽略不及了~

3.4 释放“垃圾”对象

接下来介绍三种典型的策略

3.4.1 标记释放

通过找的过程后,我们已经知道哪些是需要释放的了~

在这里插入图片描述

而“标记释放”则是直接将被标记未“不可达”的内存空间直接释放,虽然速率快,但显然,释放出来的空间,七零八落,这导致这些空间释放了之后,完整性不高,甚至可能导致之后无法再被申请!

  • 因为申请空间是需要一段完整的空间!
  • “产生内存碎片”

就相当于,你有2G的内存,但是不连续,都是内存碎片,这样就导致100M的空间都申请不出来~

3.4.2 复制算法

这种算法是将原本的堆去分为两个部分,一次只用其中一半:

在这里插入图片描述

而删除的时候,将“可达”的内存搬运到另一侧

在这里插入图片描述

然后这一侧,全部统一释放~

在这里插入图片描述

解决了“内存碎片”的问题,但是也很明显

  1. 若大部分要保留则搬运成本大
  2. 空间利用率低

3.4.3 标记整理

这种算法则是类似顺序表删除元素操作的方式:

  • 就是一个搬运的过程~
    在这里插入图片描述

  • “不可达”的数据被覆盖

  • “搬运完成之后”,再对后面“不可达”的数据进行释放

解决了“内存碎片”的问题,但是缺点很明显:

  • 搬运开销还是比较大

    • 因素:
    1. 需保留的内存
    2. “不可达”的数据在内存中排的比较前

3.4.4 分代回收

对于前面三种策略其实各有千秋,但是都有缺点,而我们现在要做的就是,在他们适合存在的场合使用他们,将利益最大化,就衍生出算法“分代回收”,这也联系到“堆”的布局:

在这里插入图片描述

这就有一个概念:“年龄”

  • 对象的年龄代表,他没经过一轮扫描(可达性分析),就涨一岁
  • 一个对象刚出生,认为是0岁

还有一个普遍的经验规律,或者说是一个代码习惯:

  • 一个对象年龄越长,这个对象就更可能存活更久时间;
  • 一个对象年龄曰小,这个对象就更可能被销毁

即,“要死的话早就死了”

在这里插入图片描述

  • 伊甸是西方的说法,伊甸园是上帝创造的第一个人出生的地方~

分代回收过程如下:

  1. 新创建的对象,放到伊甸区,在伊甸区的对象,进行”标记释放“
  • 在第一轮”GC“存活下来的对象,移动至幸存区,S0/S1
  • 有绝大多数的对象在第一轮”GC“中就别释放了,采取”标记删除“的方式直接释放即可,因为不会出现”内存碎片“的问题,因为原”可达“数据已经走了

在这里插入图片描述

  1. 在幸存区的对象,进行”复杂算法“
  • 因为在这个区域内的对象需要保留的比较少
  • 并且这个区域本来就小,不用担心内存利用率低的问题
  • 经过若干轮”GC“存活下来的对象,移动到老年代

在这里插入图片描述

  1. 在老年代的对象,进行”标记整理“
  • 因为在这个区域内能存活更长时间的内存都排列在前面了
  • 前面的能活得更久,后面的则更容易被释放,所以挪动次数被尽可能降低了
  • 不仅如此,在老年代,“GC”的频率降低
    在这里插入图片描述
  1. 特殊情况:在第一轮“GC”后,内存很大的存活对象,直接放进老年代
  • 内存很大,复制算法成本太高,放在老年代被后面的数据覆盖会更好
  • 并且大内存的对象,也不会很多

在这里插入图片描述

贯彻这个过程的一句话就是:”要死的对象早就死了,活下来的就是有两把刷子的“

感兴趣的同学可以去学习“垃圾收集器”,这就是具体的实现方法了,有改进和优化…

  • CMS
  • G1
  • ZGC

认识越新的越好~

4. JMM

JMM的全称是Java内存模型(Java Memory Model)。

  • Java内存模型定义了Java程序中多线程并发访问共享内存的行为规范,确保多线程程序的正确性和可预测性。

Java内存模型主要关注的是多线程环境下的共享内存访问问题。

  • 在多线程编程中,多个线程同时访问和修改共享的变量和对象可能会导致不可预料的结果,如数据竞争、内存可见性问题等。
  • Java内存模型提供了一套规范,定义了一系列规则和原则,来约束线程如何协作和访问共享内存。

Java内存模型包含了对于线程之间的操作顺序、变量的可见性、原子性操作、内存屏障等方面的规范。

  • 它确保在满足规定的条件下,程序员可以正确地编写并发程序,而无需担心数据不一致或未定义行为。

通过定义内存访问规则和操作顺序,Java内存模型提供了happens-before关系的概念。

  • happens-before关系指定了对于不同线程之间操作的顺序性,确保线程之间的操作按一定的顺序发生,从而保证了程序的正确性。

Java内存模型的规范不仅仅适用于Java语言本身,也适用于运行在Java虚拟机上的其他语言。

  • 它为多线程编程提供了标准化的原则和规则,使得程序员能够更加方便地控制线程的行为,编写并发安全的程序。

总结而言,JMM的全称是Java内存模型,它定义了Java程序中多线程并发访问共享内存的行为规范,确保多线程程序的正确性和可预测性,提供了一套规则和原则来约束线程的协作和访问共享内存。

JVM和JMM是紧密联系的,JVM作为Java程序的运行环境,依赖于JMM的规范来保证多线程程序的正确性和可预测性。JVM执行的字节码遵循JMM的规则,通过指定的线程安全机制和内存可见性保证线程间的正确通信和协作。

之前在讲多线程线程安全(内存可见性)的时候讲过了,传送门:【JavaEE】线程安全问题_s:103的博客-CSDN博客

在这里插入图片描述


文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆

总的来说,JVM不需要了解的太深,如果你能理解本篇文章,就足够了~

JavaEE的初阶内容已经结束,接下来将学习JavaEE的进阶内容,比如一些框架,敬请期待!


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

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

相关文章

cmake 提前结束处理命令: return

有时候,我们有这样的需求,当处理到某个地方的时候,后面的我们都不想处理或者不需要处理的时候,就可以提前结束当前的处理逻辑,回到父级去处理.在C/C中,我们有break关键字跳出当前循环,continue关键字进入下一次循环,return关键字返回当前处理的函数. cmake也提供了break(),con…

浅谈电脑城的衰退是好是坏社会现象_kaic

在过去很长一段时间里,想要购买电子设备都逃不开一个叫“电脑城”的地方,那里鱼龙混杂良莠不齐,是令许多人记忆深刻分外难忘之处。 但是随着时代发展电商兴起,采用传统线下销售的电脑城却逐渐衰退甚至面临消失,对此你怎…

7-3 打怪升级

B0->途经堡垒1->...->B 总耗费能量 武器总价值输入样例: 6 12 1 2 10 5 2 3 16 20 3 1 4 2 2 4 20 22 4 5 2 2 5 3 12 6 4 6 8 5 6 5 10 5 6 1 20 25 1 5 8 5 2 5 2 1 2 6 8 5 4 2 3 6 5输出样例: 5 5->2 2 1 5->1->3 12 7 5->4->6 10 7 5 0 0#inclu…

目标检测——R-CNN网络基础

目录 Overfeat模型RCNN模型算法流程候选区域生成CNN网络提取特征目标分类(SVM)目标定位预测过程 算法总结 Fast RCNN模型算法流程ROI Pooling目标分类和回归 模型训练模型总结 Overfeat模型 RCNN模型 算法流程 候选区域生成 CNN网络提取特征 目标分类&am…

osg osgText::Text 中文乱码问题修复 解决中

osg osgText::Text 中文乱码问题修复 解决中 #include <osgDB/ConvertUTF>osg::Camera* osgWidget::createTextHUD() { osgText::Font* fontHei osgText::readFontFile("Fonts/simkai.ttf");text->setFont(fontHei);// 步骤二&#xff1a;设置 文字颜…

浅谈分布式系统 - 架构演进

目录 1. 架构演进 1.1 单机架构 1.2 什么是分布式架构 1.3 数据库和应用分离 1.4 引入负载均衡 1.5 引入数据库读写分离 1.6 引入缓存 1.7 数据库分库分表 1.8 微服务架构 2. 分布式系统下的常见概念 1. 架构演进 1.1 单机架构 单机架构只有一台服务器, 这个服务器…

HTML+CSS+JavaScript:渲染柱形统计图

一、需求 用户输入四个季度的数据&#xff0c;根据数据生成柱形统计图&#xff0c;浏览器预览效果如下 二、完整代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content&q…

Java利用POI导入Excel数据(多个sheet、模板)

需求&#xff1a;根据excel模板导入数据 sheet1&#xff1a;1-6行为固定格式&#xff0c;且需要取值({xxx});7行开始为数据集合(list) sheet2&#xff1a;都为固定格式&#xff0c;取值地方&#xff1a;{xxx} 1、数据格式&#xff08;两个Sheet&…

从零开始理解Linux中断架构(19)--中断线程化irq_thread

前面一节讲到的中断流处理流程是在hard_irq 流程上&#xff0c;工作在中断堆栈上。还有一种情况是使用中断线程的情形。request_threaded_irq参数中有两个处理函数handler,thread_fn是有区别的。handler主中断处理例程&#xff0c;运行hard_irq 流程上。而如果驱动程序填写thre…

利用R分别绘制配对连线散点图、云雨图、山脊图

大家好&#xff0c;我是带我去滑雪&#xff01; 精美的科研绘图总会给人眼前一亮&#xff0c;今天学习利用R绘制多组配对连线散点图、云雨图、山脊图&#xff0c;这三幅图最近都曾出现在Nature Communications (IF 16.6)中&#xff0c;比如配对连线散点图&#xff0c;如下所示&…

AI智能助手的未来:与人类互动的下一代人工智能技术

自我介绍⛵ &#x1f4e3;我是秋说&#xff0c;研究人工智能、大数据等前沿技术&#xff0c;传递Java、Python等语言知识。 &#x1f649;主页链接&#xff1a;秋说的博客 &#x1f4c6; 学习专栏推荐&#xff1a;人工智能&#xff1a;创新无限、MySQL进阶之路、C刷题集、网络安…

AIGC文生图:使用ControlNet 控制 Stable Diffusion

1 ControlNet介绍 1.1 ControlNet是什么&#xff1f; ControlNet是斯坦福大学研究人员开发的Stable Diffusion的扩展&#xff0c;使创作者能够轻松地控制AI图像和视频中的对象。它将根据边缘检测、草图处理或人体姿势等各种条件来控制图像生成。 论坛地址&#xff1a;Adding…

操作系统进行设备控制的方式

一.I/O控制方式 上一篇的博客介绍了设备管理的一些概念基础知识点&#xff0c;其中I/O控制方式这一块没有详细说明。设备管理的主要任务之一是控制设备和内存或CPU之间的数据传送。外围设备和内存之间的输入/输出控制方式有4种&#xff0c;下面分别加以介绍。 二.程序直接控制…

ITIL 4—事件管理实践

1 关于本文 本文档提供事件管理实践实用指南。分为五个主要部分&#xff0c;内容包括&#xff1a; 本实践的一般信息实践的流程和活动及其在服务价值链中的作用参与实践的组织和人员支持实践的信息和技术对实践的合作伙伴和供应商的考虑。 1.1 ITIL4 认证方案 本文档的部分…

Ubuntu 安装 Docker

本文目录 1. 卸载旧版本 Docker2. 更新及安装工具软件2.1 更新软件包列表2.2 安装几个工具软件2.3 增加一个 docker 的官方 GPG key2.4 下载仓库文件 3. 安装 Docker3.1 再次更新系统3.2 安装 docker-ce 软件 4. 查看是否启动 Docker5. 验证是否安装成功 1. 卸载旧版本 Docker …

图片文字对齐 图片文字居中对齐

方法一: 用 vertical-align: middle; <view class="container"><view class="search"><image src="../../images/icon/search.png" alt="" /><text class="tex">搜索</text></view>&…

手写对象浅比较(React中pureComponent和Component区别)

PureComponent和Component的区别 PureComponent会给类组件默认加一个shouldComponentUpdate这样的周期函数 //PureComponent类似自动加了这段shouldComponentUpdate(nextProps,nextState){let { props, state } this;// props/state:修改之前的属性状态// nextProps/nextState…

layui入门增删改查

layui入门增删改查 创建Lauiyi对象1.后台准备1.dao方法2.子实现类 2.R工具类的使用3.查询前端代码实现前端页面 4.增删改实现2.浮层3分离的js代码1.userManage.js2.userEdit.js3.index.js 5.运行效果 作为一名开发人员&#xff0c;我们经常需要对数据库中的数据进行增删改查&am…

120、仿真-51单片机温湿度光照强度C02 LCD1602 报警设计(Proteus仿真+程序+元器件清单等)

方案选择 单片机的选择 方案一&#xff1a;STM32系列单片机控制&#xff0c;该型号单片机为LQFP44封装&#xff0c;内部资源足够用于本次设计。STM32F103系列芯片最高工作频率可达72MHZ&#xff0c;在存储器的01等等待周期仿真时可达到1.25Mip/MHZ(Dhrystone2.1)。内部128k字节…

刷题 DAY4

题目1 给定一个有序数组arr&#xff0c;给定一个正数aim (1)返回累加和为aim的&#xff0c;所有不同二元组 (2)返回累加和为aim的&#xff0c;所有不同三元组 问题一 暴力的解法就是 遍历每一个二元组 找和为aim的 当然 只用暴力解很难ac 想一想有序 那肯定就是能加速 我们…