• 性能调优读书笔记(下篇)


    一、并行程序开发优化

    1、Future 设计模式

    public class Client {
    
        public Data request(final String queryStr){
            final FutureData future=new FutureData();
            new Thread(){
                public void run(){
                    RealData realData=new RealData(queryStr);
                    future.setRealData(realData);
                }
            }.start();
            return future;
        }
    }
    
    public interface Data {
        String getResult();
    }
    
    
    public class FutureData implements Data {
        //FutureData是RealData的包装
        protected RealData realData=null;
    
        protected boolean isReady=false;
    
        public synchronized void setRealData(RealData realData){
            if(isReady)
                return;
            this.realData=realData;
            isReady=true;
            notifyAll();
        }
        @Override
        public synchronized  String getResult() {
            while (!isReady){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return realData.result;
        }
    }
    
    
    
    public class RealData implements Data {
    
        protected final String result;
    
        public RealData(String para){
            //RealData的构造很慢
            StringBuffer sb=new StringBuffer();
            for (int i=0;i<10;i++){
                sb.append(para);
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            result=sb.toString();
    
        }
    
        @Override
        public String getResult() {
            return result;
        }
    }
    
    2、Master-Worker 模式

    Master-Worker 的好处是可以将大任务分为若干个小任务,并行执行。

    
    public class Master {
        //任务队列
        protected Queue<Object> workQueue=new ConcurrentLinkedQueue<>();
        //Woker线程队列
        protected Map<String,Thread> threadMap=new HashMap<>();
        //子任务的结果集
        protected Map<String,Object> resultMap=new ConcurrentHashMap<>();
    
    
        //是否所有子任务都结束了
        public  boolean isComplete(){
            for (Map.Entry<String,Thread> entry:threadMap.entrySet()){
                if(entry.getValue().getState()!=Thread.State.TERMINATED){
                    return false;
                }
            }
            return true;
        }
    
        public Master(Worker worker,int countWorker){
            worker.setWorkQueue(workQueue);
            worker.setResultMap(resultMap);
            for (int i = 0; i put(Integer.toString(i),new Thread(worker,Integer.toString(i)));
            }
        }
    
    
        //提交一个任务
        public void submit(Object object){
            workQueue.add(object);
        }
    
        //返回子任务结果集
        public Map<String,Object> getResultMap(){
            return resultMap;
        }
    
        //开始运行所有的Worker进程
        public void execute(){
            for (Map.Entry<String,Thread> entry:threadMap.entrySet()){
                entry.getValue().start();
            }
        }
    
    
    }
    
    
    public class Worker implements Runnable {
    
        //任务队列,用于取得子任务
        protected Queue<Object> workQueue;
    
        //子任务处理结果集
        protected Map<String,Object> resultMap;
    
        public void setWorkQueue(Queue<Object> workQueue) {
            this.workQueue = workQueue;
        }
    
        //子任务的处理逻辑,在子类中具体实现
        public  Object handle(Object input){
            return input;
        }
    
        public void setResultMap(Map<String, Object> resultMap) {
            this.resultMap = resultMap;
        }
    
        @Override
        public void run() {
            while (true){
                Object input=workQueue.poll();
                if(input==null)break;
                Object result=handle(input);
                resultMap.put(Integer.toString(input.hashCode()),result);
            }
        }
    }
    
    
    public class PlusWorker extends Worker {
    
        @Override
        public Object handle(Object input) {
            if(input instanceof Integer){
                Integer i=(Integer)input;
                return i*i*i;
            }
            return 0;
        }
    }
    

    测试代码:

    @Test
        public void test26(){
            long start=System.currentTimeMillis();
            Master master=new Master(new PlusWorker(),5);
            for (int i = 0; i <100 ; i++) {
                master.submit(i);
            }
            master.execute();
            long re=0;
            Map<String,Object> resultMap=master.getResultMap();
            while (resultMap.size()>0||!master.isComplete()){
                Set<String> keys = resultMap.keySet();
                String key=null;
                for (String k:keys){
                    key=k;
                    break;
                }
                Integer i=null;
                if(key!=null)
                    i=(Integer)resultMap.get(key);
                if(i!=null)
                    re+=i;
                if(key!=null)
                    resultMap.remove(key);
    
            }
            System.out.println(re);
            System.out.println(System.currentTimeMillis()-start);
        }
    
    3、优化线程池大小

    Ncpu:CPU 的数量

    Ucpu:目标 Cpu 的使用率 0<=Ucpu<=1

    W/C:等待时间和计算时间的比率

    最优线程池大小为:
    Nthreads=NcpuUcpu(1+W/C);

    4、扩展 ThreadPoolExecutor

    ThreadPoolExecutor 是一个可以扩展的线程池,它提供了 beforeExecutor()和 afterExecutor()和 terminated()3 个方法进行扩展。

    5、并发数据结构
    1. 并发 List

      CopyOnWriteArrayList:适用于读多写少的场景
    2. 并发 Set

      CopyOnWriteArraySet:适用于读多写少的场景,如果有并发写的情况,也可使用 Collections.synchronizedSet(Set s)方法得到一个线程安全的 Set
    3. 并发 Map
      ConcurrentHashMap
    4. 并发 Queue
      JDK 提供了两套实现,一套是 ConcurrentLinkedQueue 为代表的高性能队列,一个是以 BlockingQueue 接口为代表的阻塞队列
    5. 并发 Deque(双端队列)
      LinkedBlockingDeque
    6、锁的性能优化
    1. 避免死锁
    2. 减小锁的作用范围
    3. 减小锁的粒度
    4. 读写分离锁代替独占锁
    5. 锁分离
    6. 重入锁(ReentrantLock)和内部锁(Synchronized)
    7. 锁粗化:不断地获取和释放锁也很耗资源,比如在 for 循环里使用 synchronized
    8. 自旋锁:线程没有取得锁时不被挂起,转而去执行一个空循环。
    9. 锁消除:JVM 在即时编译时通过多运行上下文的扫描,去除不可能有共享资源竞争的锁。如方法内部的 StringBuffer。
      逃逸分析和锁消除分别可以使用-XX:+DoEscapeAnalysis 和-XX:+EliminateLocks 开启(锁消除必须工作在-server 模式下)
      实例如下:
      分别在-server -XX:-DoEscapeAnalysis -XX:-EliminateLocks 和-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks 下运行程序
    public class LockTest {
    
        private static final int CIRCLE=20000000;
    
        public static void main(String[] args) {
            long start=System.currentTimeMillis();
            for (int i = 0; i <CIRCLE ; i++) {
                createStringBuffer("java","Performance");
            }
    
            long bufferCost=System.currentTimeMillis()-start;
            System.out.println("CreateStringBuffer:"+bufferCost+"ms");
        }
    
        public static String createStringBuffer(String s1,String s2){
            StringBuffer sb=new StringBuffer();
            sb.append(s1);
            sb.append(s2);
            return sb.toString();
        }
    }
    
    
    1. 锁偏向:如果程序没有竞争,则取消之前获得锁的线程的同步操作。通过设置-XX:+UseBiasedLocking 可以设置启用偏向锁
    2. Amino 集合:无锁的线程安全集合框架。包下载地址:https://sourceforge.net/projects/amino-cbbs/files/cbbs/
      相关集合类有 LockFreeList 和 LockFreeVector,LockFreeSet,LockFreeBSTree。另外该框架还实现了两个 Master-Worker 模式。一个静态的和动态的。分别需要实现 Doable 和 DynamicWorker 接口。
    
    /**
     * Amino框架实现了Master-woker模式
     */
    public class Pow3 implements Doable<Integer,Integer> {
    
        //业务逻辑
        @Override
        public Integer run(Integer integer) {
            return integer*integer*integer;
        }
    
        public static void main(String[] args) {
            MasterWorker mw=MasterWorkerFactory.newStatic(new Pow3());
            List keyList=new Vector<>();
            for (int i = 0; i <100 ; i++) {
                keyList.add(mw.submit(i));
            }
            mw.execute();
            int re=0;
            while (keyList.size()>0){
                MasterWorker.ResultKey k=keyList.get(0);
                Integer i=mw.result(k);
                if(i!=null){
                    re+=i;
                    keyList.remove(0);
                }
    
            }
            System.out.println(re);
        }
    }
    
    
    7、协程

    为了增加系统的并发性,人们提出了线程,随着应用程序日趋复杂,并发量要求越来越高,线程也变得沉重了起来。为了使系统有更高的并行度,便有了协程的概念。如果说线程是轻量级进程,那么协程就是轻量级的线程。

    相关框架:kilim。

    介绍:略。

    二、JVM 调优

    1、虚拟机内存模型

    1、程序计数器

    每一个线程都有一个独立的程序计数器,用于记录下一条要执行的指令,各个线程互不影响。

    2、Java 虚拟机栈

    它和 Java 线程在同一时间创建,保存方法的局部变量,部分结果,并参与方法的调用和返回。

    • 当线程在计算过程中请求的栈深度大于最大可用的栈深度,抛出 StackOverflowError 异常。
    • 如果 Java 栈可以可以动态扩展,在扩展的过程中没有足够的空间支持,则抛出 OutOfMemoryError
    • -Xss1M 该命令可以调整栈的深度。

    public void test28(){   //gc无法回收,因为b还在局部变量表中
            {
                byte[] b=new byte[6*1024*1024];
    
            }
            System.gc();
            System.out.println("first explict gc over");
        }
    
         public void test29(){   //gc可以回收,因为变量a复用了b的字
            {
                byte[] b=new byte[6*1024*1024];
    
            }
            int a=0;
            System.gc();
            System.out.println("first explict gc over");
        }
    

    3、本地方法栈
    本地方法栈和 java 虚拟机栈的功能很像,本地方法栈用于管理本地方法的调用。

    4、Java 堆
    几乎所有的对象和数组都是在堆中分配空间的。

    5、方法区
    主要保存类的元数据,所有线程共享的。其中最为重要的是类的类型信息,常量池,静态变量,域信息,方法信息。在 Hot Spot 虚拟机中,方法区也被称为永久区。

    2、JVM 内存分配参数
    1. 最大堆内存:可以用-Xmx 参数指定。是指新生代和老年代的大小之和。
    2. 最小堆内存:JVM 启动时,所占据的操作系统内存大小,可以用-Xms 指定。
    3. 设置新生代:新生代一般设置未 1/4 到 1/3 之间。用参数-Xmn 指定
    4. 持久代(方法区):-XX:PermSize 可以设置初始大小,-XX:MaxPermSize 可以设置最大值
    5. 线程栈:可以使用-Xss 设置大小。-Xss1M 表示每个线程拥有 1M 的空间。
    6. 堆的比例分配:-XX:SurvivorRatio:eden/s0=eden/s1。 -XX:NewRatio=老年代/新生代
    7. 参数总结:
    3、垃圾回收

    1、引用计数法

    如果有引用,计数器就加 1,当引用失效时,计数器就减 1.这种方法无法处理循环引用,不适用与 JVM 的回收。

    2、标记-清除算法

    第一阶段,标记所有从根节点开始可达的对象。第二阶段,清理所有未标记的对象。缺点是回收后的空间是不连续的。

    3、复制算法

    将原有的空间分为两块,将正在使用的那个空间的活对象复制到未使用的那个空间,在垃圾回收时,只需要清理正在使用的内存空间的所有对象,然后交换两个内存空间的角色。缺点是要将系统内存折半。

    4、标记压缩算法

    标记完成之后,将所有的存活对象压缩到内存的一端,然后清除边界外所有的空间。避免了碎片的产生。

    5、增量算法

    一次性的垃圾清理会造成系统长时间停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。在垃圾回收的过程中间断性的执行应用程序代码。

    6、分代

    对新生代使用复制算法,老年代使用标记-压缩算法。

    7、串行收集器

    1. -XX:+UseSerialGC 新生代,老年代都使用串行回收器
    2. -XX:+UseParNewGC 新生代使用并行,老年代使用串行
    3. -XX:+UseParallelGC 新生代使用并行,老年代使用串行
    4. -XX:+UseConcMarkSweepGC 新生代使用并行,老年代使用 CMS(-XX:ParallelGCThreads 可以指定线程数量,Cpu 小于 8 时,就设置 cput 的数量,大于 8 时设置为(3+5*cpucount/8))

    8、CMS 收集器

    使用的是标记清除算法,并行的垃圾收集器

    9、G1 收集器

    基于标记-压缩算法,目标是一款服务器的收集器,在吞吐量和停顿控制上,要优于 CMS 收集器。

    观察 GC 情况:

    package com.mmc.concurrentcystudy.test;
    
    import java.util.HashMap;
    
    public class StopWorldTest {
    
        public static class MyThread extends Thread{
            HashMap map=new HashMap();
    
            @Override
            public void run() {
                try{
                    while (true){
                        if(map.size()*512/1024/1024>=400){   //防止内存溢出
                            map.clear();
                            System.out.println("clean map");
                        }
                        byte[] b1;
                        for (int i = 0; i <100 ; i++) {
                            b1=new byte[512];     //模拟内存占用
                            map.put(System.nanoTime(),b1);
                        }
                        Thread.sleep(1);
                    }
                }catch (Exception e){}
            }
        }
    
        public static class PrintThread extends Thread{   //每毫秒打印时间信息
            public static final long starttime=System.currentTimeMillis();
    
            @Override
            public void run() {
                try{
                    while (true){
                        long t=System.currentTimeMillis()-starttime;
                        System.out.println(t/1000+"."+t%1000);
                        Thread.sleep(100);
                    }
                }catch (Exception e){}
            }
        }
    
        public static void main(String[] args) {
            MyThread t=new MyThread();
            PrintThread p=new PrintThread();
            t.start();
            p.start();
        }
    
    }
    
    
    4、常用调优案例

    1、将新对象预留在新生代

    避免新对象因空间不够进入了年老代,可以适当增加新生代的 from 大小。

    1. 可以通过-XX:TargetSurvivorRatio 提高 from 区的利用率
    2. 通过-XX:SurvivorRatio 设置更大的 from 区

    2、大对象直接进入老年代

    在大部分情况下,新对象分配在新生代是合理的,但是,对于大对象,很可能扰乱新生代,使得大量小的新生代对象因空间不足移到老年代。

    1. 使用-XX:PretenureSizeThreshold 设置大对象进入老年代的阈值。当对象大小超过这个值时,对象直接分配在老年代。

    3、设置对象进入老年代的年龄

    1. 可以通过设置-XX:MaxTenuringThreshold 阈值年龄的最大值。

    4、稳定与震荡的堆大小

    1、一般来说稳定的堆大小是对回收有利的,获得一个稳定的堆大小的方法就是设置-Xms 和-Xmx 的大小一致。稳定的堆空间可以减少 GC 的次数,但是会增加每次 GC 的时间。
    基于这样的考虑,JVM 提供了压缩和扩展堆空间的参数。

    1. -XX:MinHeapFreeRatio 设置堆空间最小空闲比例,默认是 40,当堆空间内存小于这个数值时,JVM 会扩展堆空间。
    2. -XX:MaxHeapFreeRatio 设置堆空间的最大空闲比例,默认是 70,当堆空间内存大于这个数值时,JVM 会压缩堆空间。
    3. 注意:当设置-xms 和-xmx 一样时,这两个参数会失效。

    5、吞吐量优先案例

    在拥有 4G 内存和 32 核 CPU 的计算机上,进行吞吐量的优化

    6、使用大页案例

    在 Solaris 系统中,可以支持大页的使用,大的内存分页可以增强 CPU 的内存寻址能力。

    1. -XX:LargePageSizeInBytes:设置大页的大小。

    7、降低停顿案例

    为了降低停顿,应尽可能将对象预留在新生代,因为新生代的 GC 成本远小于老年代。

    5、实用 JVM 参数

    1、JIT 编译参数

    JIT 编译器可以将字节代码编译成本地代码,从而提高执行效率。

    1. -XX:CompileThreshold 为 JIT 编译的阈值,当函数的调用次数超过他,JIT 就将字节码编译成本地机器码。
    2. -XX:+CITime 可以打印出编译的耗时
    3. -XX:+PrintCompilation 可以打印 JIT 编译信息

    2、堆快照

    在性能优化中,分析堆快照是必不可少的环节。

    1. 使用-XX:+HeapDumpOnOutOfMemoryError 参数可以在程序发生 OOM 异常时导出当前堆快照
    2. 使用-XX:HeapDumpPath 可以指定堆快照保存的位置。(例:
    -Xmx20M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d://log
    

    1. 使用 Visual VM 工具可以分析堆文件。

    3、错误处理

    可以在系统发生 OOM 时,运行第三方脚本。如重置系统

    1. -XX:OnOutOfMemoryError=c:\reset.bat

    4、获取 GC 信息

    1. -XX:+PrintGC
    2. -XX:+PrintGCDetails 打印更详细的 GC 信息
    3. -XX:+PrintHeapAtGC 打印堆的使用情况

    5、类和对象跟踪

    1. -XX:+TraceClassLoading 用于跟踪类加载情况
    2. -XX:+TraceClassUnloading 用于跟踪类卸载情况
    3. -XX:+PrintClassHistogram 开关打印运行时实例的信息,开关打开后,当按下 Ctrl+Break 时,会打印系统内类的统计信息

    6、控制 GC

    1. -XX:+DisableExplicitGC 用于禁止显示的 GC
    2. -Xnoclassgc 禁止类的回收
    3. -Xincgc 增量式的 GC

    7、使用大页

    1. -XX:+UseLargePages 启用大页
    2. -XX:+LargePageSizeInBytes 指定大页的大小

    8、压缩指针

    在 64 位虚拟机上,应用程序占的内存要远大于 32 位的,因为 64 位系统拥有更宽的寻址空间,指针对象进行了翻倍。

    1. +XX:+UseCompressedOops 打开指针压缩,减少内存消耗(但是压缩和解压指针性能会影响)
    6、实战案例

    1、tomcat 优化

    1. 在 catalina.bat 中增加打印 GC 日志,以便调试:
      set CATALINA_OPTS=-Xloggc:gc.log -XX:+PrintGCDetails

    2.当发现产生了 GC 时,看下是否需要增加新生代大小
    set CATALINA_OPTS=%CATALINA_OPTS% -Xmx32M -Xms32M

    3.如果发现有人使用了显示的 GC 调用,可以禁止掉 set CATALINA_OPTS=%CATALINA_OPTS% -XX:+DesableExplicitGC

    4.扩大新生代比例 set CATALINA_OPTS=%CATALINA_OPTS% -XX:NewRatio=2

    5.给新生代使用并行回收 set CATALINA_OPTS=%CATALINA_OPTS% -XX:UseParallelGC

    6.当确保 class 安全的时候,可以关闭 class 校验 set CATALINA_OPTS=%CATALINA_OPTS% -Xverify:none

    2、JMeter 介绍和使用

    JMeter 是一款性能测试和压力测试工具。
    下载路径:https://jmeter.apache.org/download_jmeter.cgi

    1. 下载解压之后,进入 bin 目录,点击 jmeter.bat 启动。

    2. 右键添加线程组

    3. 右键添加请求

    4. 添加结果视图

    这里有很多结果视图,都可以选来试试。

    1. 点击运行

    3、案例

    确认堆大小(-Xmx,-Xms),合理分配新生代和老年代(-XX:NewRatio,-Xmn,-XX:SurvivorRatio),确定永久区大小(-XX:PermSize,-XX:MaxPermSize),选择垃圾收集器,除此之外,禁用显示的 GC,禁用类元素回收,禁用类验证对性能也有提升。

    实战的调优:

    三、Java 性能调优工具

    1、Linux 命令行工具

    1、top 命令

    2、sar 命令

    3、vmstat 命令
    统计 CPU、内存情况

    4、iostat 命令
    统计 IO 信息

    5、pidstat
    可以监控线程的

    2、JDK 命令行工具
    1. jps
    2. jstat
    3. jinfo 查看 jvm 参数
    4. jmap 生成堆快照和对象的统计信息 jmap -histo 2972 >c:/s.txt
      生成当前程序的堆快照:jmap -dump:format=b,file=c:\heap.hprof 2972
    5. jhat 分析堆快照文件
    6. jstack 导出 java 程序的线程堆栈,可以打印锁的信息 jstack -l 149864>d:/a.txt
    7. jstatd 有些工具(jps,jstat)可以支持对远程计算机的监控,这就需要 jstatd 的配合
      开启 jstatd:

      1、在 d 盘新建了个文件 jstatd.all.policy。内容为
    grant codebase "file:D:/Java/jdk1.8.0_112/lib/tools.jar" {
    	permission java.security.AllPermission;
    };
    

    2、执行开启
    jstatd -J-Djava.security.policy=D:/jstatd.all.policy
    3、新开一个 cmd 窗口,执行 jps localhost:1099 即可远程监听

    8.hprof 工具,程序运行时加上-agentlib:hprof=cpu=times,interval=10 可以导出函数执行时间。还可以使用-agentlib:hprof=heap=dump,format=b,file=d:/core.hprof 导出文件

    3、JConsole 工具
    4、Visual VM 工具

    1、BTrace 插件,可在不修改代码情况下给代码加日志

    /* BTrace Script Template */
    import com.sun.btrace.annotations.*;
    import static com.sun.btrace.BTraceUtils.*;
    
    @BTrace
    public class TracingScript {
    	/* put your code here */
    private static long startTime=0;
    
    //方法开始时调用
    @OnMethod(clazz="com.mmc.concurrentcystudy.test.BTraceTest",method="writeFile")          //监控任意类的writeFile方法
    public static void startMethod(){
        startTime=timeMillis();
    
    }
    
    @OnMethod(clazz="com.mmc.concurrentcystudy.test.BTraceTest",method="writeFile",location=@Location(Kind.RETURN))      //方法返回时触发
    public static void endM(){
        print(strcat(strcat(name(probeClass()),"."),probeMethod()));
        print("[");
        print(strcat("Time taken:",str(timeMillis()-startTime)));
        println("]");
        }
    }
    

    实例 2:

    /* BTrace Script Template */
    import com.sun.btrace.annotations.*;
    import static com.sun.btrace.BTraceUtils.*;
    
    @BTrace
    public class TracingScript {
    	/* put your code here */
    @OnMethod(clazz="/.*Test/",location=@Location(value=Kind.LINE,line=27))       //,监控Test结尾的类,指定程序运行到第27行触发
    public static void onLine(@ProbeClassName String pcn,@ProbeMethodName String pmn,int line){
        print(Strings.strcat(pcn,"."));
         print(Strings.strcat(pmn,":"));
        println(line);
        }
    }
    

    实例 3:每秒执行

    
    @BTrace
    public class TracingScript {
    	/* put your code here */
    
    @OnTimer(1000)    //每秒钟运行
    public static void getUpTime(){
        println(Strings.strcat("l000 msec:",Strings.str(Sys.VM.vmUptime())));   //虚拟机启动时间
        }
    
        @OnTimer(3000)
        public static void getStack(){
            jstackAll();     //导出线程堆栈
            }
    }
    

    实例 4:获取参数

    
    @BTrace
    public class TracingScript {
    	/* put your code here */
     @OnMethod(clazz="com.mmc.concurrentcystudy.test.BTraceTest",method="writeFile")
       public static void any(String filename){
            print(filename);
        }
    }
    
    5、MAT 内存分析工具

    1、下载 mat
    https://www.eclipse.org/mat/

    2、深堆和浅堆
    浅堆:一个对象结构所占用的内存大小。
    深堆:一个对象被 GC 后,可以真实释放的内存大小

    6、JProfile


    __EOF__

  • 本文作者: 女友在高考
  • 本文链接: https://www.cnblogs.com/javammc/p/16640774.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    JS 常用数组
    第五章:最新版零基础学习 PYTHON 教程—Python 字符串操作指南(第二节 - Python 字符串—Python 字符串 len()的语法)
    nodejs+vue 网上招聘系统elementui
    el-cascader级联选择器-懒加载+多选+回显功能
    测试老鸟整理,从手工测试到自动化测试的进阶全程...
    LIO-SAM总结笔记
    CSS的Grid布局与Flex布局
    TNF1LTX_TNF2LTX_TNF3LTX 全新板卡10路(TNF1LTX)/11路(TNF2LTX/TNF3LTX)高速任意业务汇聚波长转换板
    中国制霸生成器「GitHub 热点速览 v.22.42」
    【高等の数学】e^-3x的一阶导数
  • 原文地址:https://www.cnblogs.com/javammc/p/16640774.html