JVM知识体系及调优

JVM知识体系

在这里插入图片描述

#查看class
在这里插入图片描述

  • jclasslib 插件 安装 使用
    在这里插入图片描述
  • classfile 构成
u4 maigc(四位)
u2 minor_version(两位)
u2 major_version(两位)
u2  contants_pool_count 常量个体数(两位)
cp_info constant_pool[contants_pool_count-1]

class 加载过程(硬盘到内存)

1.Loading(加载到内存中 双亲委派)

类加载器

  • Class 通过Loader 到内存 一块内存放二进制文件,一块生成该Class类的对象,这个对象,指向存放二进制文件地址
  • method Area(方法七)
    – perm G(1.8之前)
    – metaspace(1.8之后)

类加载器加载层次

在这里插入图片描述

  • Bootstrap 加载 jdk 核心的Class lib/rt.jar等核心类 由C或C++实现
  • Extension 加载扩展包 jre/lib/ext//*.jar
  • APP 加载classpath指定内容
  • CustomClassLoader 自定义加载器

在这里插入图片描述

  • Bootstrap负责加载其他的ClassLoader 让其他ClassLoader 对应的Class,例如程序最后两行,加载ClassLoader 的ClassLoader 是BootStrap

一个class文件被加载的过程(双亲委派)

class 文件通过自己的loadclass 方法到内存,尝试  CustomClassLoad 加载    CustomClassLoad 发现自己的缓存中没,就问AppClassLoader是否加载进来了,一直向上询问,直到Bootstrap,然后向下委托,麻烦下级加载,直到可以加载的类加载器加载上

为什么要搞双亲委派

  • 为了安全

  • 避免重复

  • 除了bootstrap 加载器,其他加载器都是bootstrap加载来的

  • 父加载器,只是当前加载器中parent变量中保存的加载器

  • 如下 bootstrap 是null
    在这里插入图片描述

  • 类加载器范围

  • sun.misc.Launcher$ExtClassLoader

  • sun.misc.Launcher$AppClassLoader
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • BootStrap 加载
    在这里插入图片描述
  • extClassLoader
    在这里插入图片描述
  • appClassLoader
    在这里插入图片描述

自定义类加载器

在这里插入图片描述

  • getClassLoader()方法得到的是AppClassLoader 调用loadClass 加载指定的java文件编译后的class文件到内存,并生成与之对应的Class对象
  • ClassLoader是反射的基石 通过操作Class 类完成反射
  • 自己找调用父类classLoader ,最后自己调动findClass方法
  • 继承ClassLoader 通过重写findClass方法实现自定义类加载器
    在这里插入图片描述
com.xj.ios.HelloWorld 类加载过程  (双亲委派)
首先自定义加载器  调用自己的loadclass方法  问appclassLoader 加载过没有,appclassLoader没有,找exclassloader 加载过没有,没有 再去找bootstarp 没有加载过,bootstarp尝试在自己的区域加载,没有告诉exclassloader 去加载,没有告诉AppclassLoader,还是没有,就会告诉自定义加载器加载,加载的时候调用findclass,找到对应的class类,加载到内存
自定义classloader 的parnet 是appclassloader
自定义classloader 的加载器是appclassloader
  • 自定义classLoader 可以置顶parent(扩展) 在这里插入图片描述
    在这里插入图片描述

  • 通过classLoader 写加密方法

  • -加密

在这里插入图片描述

  • 解密
    在这里插入图片描述

在这里插入图片描述

  • 类加载采用懒加载的方式,用到的时候加载

混合模式

  • 解释器
    • bytecode intepreter
  • JIT
    • just in-time compiler
      在这里插入图片描述

2.Linking

2.1verfication(校验)

  • 校验class是否符合JVM规范

2.2preparation(静态变量赋默认)

  • 给静态成员变量赋默认值

2.3resolution (常量池用到的符号引用转换为内存引用)

  • 类 方法 属性等符号引用解析为直接引用
  • 常量池中各种符号引用解析为指针,偏移量等内存地址的直接引用

3.Initialzing

  • 静态变量赋值为初始值

面试题:
在这里插入图片描述

  • count 输出几 2
解析:T.count    load  T.class到内存中,linking   校验、静态变量赋初始值 Tt = null,count=0,  initialing   t = new T() 执行构造count++ 为1     count 复制为2 

在这里插入图片描述

  • count 输出几 3
T  load到内存   到linking 阶段  静态变量赋 默认值 count=0  T t= null , initiling  count=2   t = new T()  count 为3

面试题 volatile 双重校验单例 为什么要用volatile
在这里插入图片描述

  • new 对象分为两步 T t = new T() 申请内存-赋默认值-赋初始值
1.申请内存
2.内存中,成员变量默认值
3.调用构造  赋初始值

由于new 会分为申请内存  给成员变量赋默认值  再赋初始值,  这个赋完默认值 其实Instance其实就不为空了 ,这时候另一个线程拿到的INSTANCE 中的成员变量,的值是默认值 ,并不是初始化后的值,加volatile防止指令重排序

在这里插入图片描述

  • 1new 是申请空间
  • invokespecial 调用构造方法
  • astore 将内存地址复制给变量
  • 可能出现指令重排序问题,所以用voletial 防止指令重排序

在这里插入图片描述

  • loading过程
    在这里插入图片描述

JMM(java 内存模型)

硬件层数据一致性

  • 离CPU越近,更小,更快
    在这里插入图片描述

在这里插入图片描述

  • 老的CPU数据同步方案,为了防止两个cpu 数据不一致 ,用的总线锁,第一个Cpu操作x的时候,其他cpu无法用,所以效率低下在这里插入图片描述

缓存行 (一块连续的内存)

读取缓存以cache line为基本,度为64个字节

在这里插入图片描述

    1. 对象创建过程
class  loading   linking (检验  静态变量赋默认值  符号引用化)    静态变量初始化 -》 申请内存对象-》成员变量赋默认值-》  调用构造方法-》成员变量赋初始值,执行构造方法语句
  • 观察虚拟机配置命令
    在这里插入图片描述

  • -XX:InitialHeapSize 初始堆大小

  • -XX:MaxHeapSize 最大堆大小

  • -XX:+UseCompressedClassPointers

  • 2.对象在内存中的存储布局

- 普通对象
- 1.对象头  markdown 8个字节
- 2.ClassPointer   指向class对象
- 3.实例数据
- 4.Padding 对齐  8字节
- 数组对象
- 1.对象头  markdown 8个字节
- 2.ClassPointer   指向class对象
- 3.数组长度:4字节
- 4. 数组数据
- 5.Padding 对齐  8字节
  • 对象头具体包括什么(没懂)
  • 对象怎么定位(没懂)
  • 对象怎么分配(没懂)

JAVA 运行时数据区域和JVM指令集

  • run-time data areas
    在这里插入图片描述
  • ProgramCounter 存放指令的位置 下一条指令

在这里插入图片描述

  • Heap 堆
  • JVM static 每个线程对应一个栈 每个方法对应一个栈帧
  • native method stacks 对用jni 调C C++
  • Direct Memory 直接内存
  • MethodArea 方法区
- 1.Perm Space(<1.8) 
- 2.Meta Space(>1.8)

在这里插入图片描述

栈帧 Frame(重点)

  • 一个线程在JVM中是一个JVM栈,每个方法是一个栈帧, 每一个栈帧有自己的操作数栈和动态链接

局部变量表(Local Variable Table)

操作数栈(Operand Stack)

动态链接(Dynamic Linking)

指向class文件中常量池的符号链接 (方法叫什么名字,方法的返回值类型等等)
a()中调用b()方法 指向class文件的常量池中b 方法的描述信息,就是动态链接

返回地址(return address)

a() 调用 b() ,方法a调用方法b, b 方法的返回值放在什么地方 存储这个返回值的地址 就是栈帧的返回地址

在这里插入图片描述

在这里插入图片描述
一个线程一个jvm 栈,每个方法对应一个栈帧,栈帧包括四个 局部变量表,操作数栈,动态链接,返回地址

  • 局部变量表 (方法用到的局部变量)
  • 操作数栈
    通过 jclasslib 可以看到 main方法栈帧包括两张表,LineNumberTable(行号表),LocalVariableTable (用到的局部变量)
    用到变量名字,cp_info_#15 对用ContantsPool 中
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 0 binpush 8 (8 当成byte 扩展成int入操作数栈)
  • 2 istore_1 (把栈顶出栈 复制下标值为1的局部变量中 给8赋值给i)
  • 3 iload_1 (把局部变量表下标为1的数,入栈)
  • 4 iinc 1 by 1 (把局部变量表下标为1的数+1 ) 此时栈中是8 局部变量表中是9
  • 7 istore_1((把栈顶出栈 8 赋值给下标值为1的局部变量中 从9 变到8 )
  • 。。。
  • 11 iload_1 (把局部变量表下标为1的数,入栈)
  • 13 return
    在这里插入图片描述

在这里插入图片描述

结果为9

  • 0 binpush 8 (8 当成byte 扩展成int入操作数栈)
  • 2 istore_1 (把栈顶出栈 赋值下标值为1的局部变量中 给8赋值给i)
  • 3 iinc 1 by 1 (把局部变量表下标为1的数+1 ) 局部变量8 -》9
  • 6 iload_1 (把局部变量表下标为1的数,入栈) 9 入栈
  • 7 istore_1((把栈顶出栈 9 赋值给下标值为1的局部变量中 )
  • 11 iload_1 (把局部变量表下标为1的数,入栈) 9 入栈
  • return

在这里插入图片描述
1.一个方法对应一个栈帧,会和堆还有方法区打交道

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

非静态方法,默认局部变量表中,第0个是this
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Hello_02 h = new Hello_02()

1. new xxxxx
new Hello_02() 申请内存空间 赋初始值  将内存地址压栈 
2. dup
复制对象内存地址,再次压栈
3.invokespecial----<init>
弹栈、给对象初始化,调用构造方法
4.astore_1  弹栈  将地址复制给h 
5.aload_1 将局部变量表下标为1的h 压栈
6.invokespecial----<m> 调用m方法
7.return


m 方法
1. sipush 200
2.istore_1
3.return

DCL 为什么使用volatile

  • 因为new对象过程,会出现申请空间,默认值,压栈,出栈,赋初始值,过程中会出现异步问题,所以用volatile

在这里插入图片描述

return 100
将100压栈    放在main 栈帧栈顶
弹栈

在这里插入图片描述

return 100
将100压栈
istore_2  100 弹栈 赋值给局部变量表的下标为2的变量

在这里插入图片描述

垃圾回收

ROOT Searching

jvm 栈   、 本地方法栈  、常量池、方法区静态变量

垃圾回收算法

  • 标记删除 (两次扫描,容易产生碎片)
  • 标记压缩 (两次扫描,效率比较低)
  • 复制清除 (浪费空间,需要对象调整)

堆内存 逻辑分代模型(青年代 老年代)

在这里插入图片描述

1.eden(伊甸区)(新生代) (copy)
2.servivor (幸存区1)(新生代)回收一次 到这里(copy)
3.servivor (幸存区2)(新生代)回收一次 到这里(copy)
4.老年代(mark comact mark sweep)

一个独享从出生到消亡的过程

在这里插入图片描述

对象产生后,尝试在栈上分配,如果分配不下,进入伊甸区,被回收一次进入s1,再回收一次,进入s2,在回收进入s1 ,如此重复,知道年龄达标,进入old区域

GC概念(YGC FULLGC)

  • 年轻代空间耗尽,触发YGC
  • 老年代空间耗尽,触发FULLGC (年轻代和老年代同时回收)
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

  • 栈上分配
  • 线程本地分配 TLAB 在这里插入图片描述
去掉逃逸分析
去掉标量替换
去掉TLAB

在这里插入图片描述

在这里插入图片描述


一个对象产生,是否可以入栈,如果不可以,判断对象是否大,通过固定的参数配置,如果够大直接进入old 区,触发(FULLGC)才会消亡,
如果不够大,进入伊甸区(TLAB),伊甸区进行YGC 清除,对象进入s1,在清除,如果年龄够了,进入old,如果不够进入s2 ,直到年龄达标,进入old区域。 

(永久代 Perm Generation(1.7)、元数据区(1.8)Metaspace)

  • MethodArea
  • Perm Generation(<1.8)
  • Metaspace(1.8以后)
class 的源信息
代码的编译信息
jni 产生的中间jit
等等

java 虚拟机参数

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

常见的垃圾回收器

1.Serial (单线程 )
2.Parallel(多线程)
Serial+SerialOld
Parallel Scavenge+Parallel Old
ParNew+CMS
在这里插入图片描述

  • 1.Seria
  • ATW stop-the-work 线程停止 进行垃圾回收
  • 2.Seria Old
  • 老年代的Seria
    在这里插入图片描述
    2.Parallel Scavenge
  • 停止线程,多线程回收垃圾

在这里插入图片描述

parallel old
在这里插入图片描述
PS+PO

  • ParNew+CMS

  • ParNew(parallel new)
    在 ps 的基础上增加了对CMS的支持

  • CMS (1.4 版本引入 CMS是里程碑式的 并发回收 CMS毛病较多 没有任何一个jdk 版本默认是CMS 但是很重要)

concurrent  mark  sweep
并发标记清除

在这里插入图片描述
在这里插入图片描述

  • 初始标记 标记ROOT对象

在这里插入图片描述

  • 并发标记
    在这里插入图片描述
  • 重新标记
  • 重新标记就是标记并发标记过程中新产生的对象

在这里插入图片描述

  • 并发清理

CMS 缺点

  • 由于浮动垃圾问题,预制在50%-68% ,保证有足够的空间可以产生浮动垃圾 ,
  • 默认值 -XX:CMSInitiatiingOCCpancyFraction=92% 由于必须有预留移动的空间给浮动垃圾,所以阈值为92% 阈值达到92% 就会触发FGC,将阈值降低到50%-68%,让它有足够的空间产生浮动垃圾以及

在这里插入图片描述

GC 和GC Tuning

垃圾回收器组合参数的设定

在这里插入图片描述
在这里插入图片描述

JVM调优第一步,了解JVM常用命令行参数

  • HostSport 参数分类
标准:-开头 所有的HotSpot
非标准: -X开头,特定版本HotSpot支持特定命令
不稳定定: -XX 开头  下一版本可能取消
  • Java 虚拟机-XX 所有的参数
java -XX:+PrintFlagsFinal -version |grep CMS
总参数700-800

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 1.概念区分:内存泄漏 memory leak ,内存溢出 out of memory
  • 2.java -XX:+PrintCommandLineFlags HelloGC

在这里插入图片描述

-XX:InitialHeapSize = 16070592  初始虚拟机堆大小
-XX:MaxHeapSize=257129472 最大堆大小
-XX:+UseComperssedClassPointers   -XX:+UseCompressedOops
指针压缩
    1. java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC
-Xms40M -Xmx60M 最小堆大小  最大堆大小  一般两个值一样
-Xmn 新生代大小
    1. java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息

在这里插入图片描述

  • 5.java -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC

cms 回收会更频繁
初始化标记
[GC (CMS Initial Mark)  72771K(126720K), 0.0009446 secs]
并发标记
重新标记
并发清除

在这里插入图片描述

在这里插入图片描述

GC 日志详解

  • java -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC

在这里插入图片描述

[GC (Allocation Failure) [ParNew: 38579K->4100K(39296K), 0.0284189 secs] 1420303K->1419617K(1455924K), 0.0284476 secs] [Times: user=0.03 sys=0.02, real=0.03 secs] 
GC 哪种回收器
Allocation Failure 产生原因
ParNew: 38579K->4100K(39296K), 0.0284189 secs]  年轻代  (回收前大小->回收后大小)(年轻代总大小) 时间 
1420303K->1419617K(1455924K) 回收前堆大小-回收后堆大小(堆总大小) 时间
[Times: user=0.03 sys=0.02, real=0.03 secs]  执行的时候 用户状  内核态 总多少 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at HelloGC.main(HelloGC.java:9)
Heap
 par new generation   total 306688K, used 306433K [0x0000000740000000, 0x0000000754cc0000, 0x0000000754cc0000)
  eden space 272640K, 100% used [0x0000000740000000, 0x0000000750a40000, 0x0000000750a40000)
  from space 34048K,  99% used [0x0000000750a40000, 0x0000000752b40598, 0x0000000752b80000)
  to   space 34048K,   0% used [0x0000000752b80000, 0x0000000752b80000, 0x0000000754cc0000)
 concurrent mark-sweep generation total 1756416K, used 1755477K [0x0000000754cc0000, 0x00000007c0000000, 0x00000007c0000000)
 Metaspace       used 2687K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 290K, capacity 386K, committed 512K, reserved 1048576K

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

起始内存地址  使用空间结束地址 整体空间结束地址
total = eden+ 1个survivor

调优前基础概念

  • 1.吞吐量 用户代码执行时间/(用户代码执行时间+垃圾回收时间)
  • 2.响应时间: STW越短 相应时间越好
所为调优,首先确定追求啥,吞吐量优先,还是响应时间优先 还是在满足一定相应时间的情况下,要求达到多大的吞吐量

问题:
科学计算   吞吐量优先   --数据挖掘   一般采用 (PS+PO)
相应时间优先:网站,代界面的程序 (1.8 G1 或 ParNew+CMS)

什么是调优

  • 1.根据需求进行JVM预调优
  • 2.优化运行JVM运行环境
  • 3.解决JVM出现的各种问题(OOM)

调优,从规划开始

QPS per second (1000并发 好几十万用户在线)
TPS
PPS

  • 调优 : 根据业务场景
  • 无监控,不调优 (压力测试)
  • 步骤:
  1. 熟悉业务场景(没有最好的垃圾回收器,只有合适的)
    1. 响应时间、停顿时间【CMS G1 ZGC】
    2. 吞吐量 = 用户时间/(用户时间+GC时间)[PS]
      2.选择垃圾回收器组合
      3.计算内存需求
      4.选定CPU
      5.设定年代大小,升级年龄
      6.设定日志参数
如下:5个日志文件,每个20M,循环插入

在这里插入图片描述
7.观察日志情况

在这里插入图片描述
在这里插入图片描述

1.系统硬件升级反而卡顿

在这里插入图片描述

为什么慢  很多数据load到内存,内存不足频繁GC,相应时间变慢
为什么更卡顿,内存越大, FGC时间越长
咋办:
ps+po    修改为   ProNew+CMS 或者G1

2.系统CPU经常100% 如何调优

1.找出那个进程(top mingling)
2.进程中那个线程高(top 	-Hp )
3.到处该线程的堆栈(jstack)
4查找那个方法(栈帧)耗费时间(js)
5.工作线程占比高  还是 垃圾回收线程占比高

3.系统内存飙高

1. 到处堆内存(jmap)
2. 分析(jhat jvisualvm jprofiler)

4.如何监控jvm

jstat jvisualvm arthas top ...

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1. java  -Xms200M  -Xmx200M -XX:+PrintGC  com.xj.jvm.gc.T01_FullGC
堆内存大小200M  打印GC信息
2.一般是运维团队收到报警信息(内存 或者CPU)
3.top 命令  查看cpu 占比比较高的进程  如果是java 进程 记住pid(进程id)
4.top -Hp 1364(pid)  打印java进程的线程占比  观察那个线程cpu 和内存占比高
5.jstack 1364 (pid)

在这里插入图片描述

  • 查看线程 top
    在这里插入图片描述

  • 查看java 进程 top -Hp 8390
    在这里插入图片描述
    在这里插入图片描述

  • jps 定位具体进程

  • jstack 8390
    在这里插入图片描述

  • 多个程序等待抢一个对象 同一个锁
    在这里插入图片描述
    在这里插入图片描述

  • jinfo 8568

在这里插入图片描述

  • jconsole
  • jstat -gc
  • java VisualVM 图像化界面 可以查看内存使用情况 定位问题
    在这里插入图片描述
    • 面试官问怎么定位OOM问题的
不要说是图形化界面  因为生产环境下不允许   只有测试的时候用  测试的时候才会用  
已经上线的系统用什么  cmdline  arthas
  • jmap 解决oom 查看对象占用空间
  • jmap -histo 8568| head -20 线上内存特别大 不如100G 就不适合了
    在这里插入图片描述
1.设定HeapDump参数  oom时候会自动产生堆转储文件
2.很多服务器备份(高可用),停掉这台服务器对其他服务器不影响
3.在线定位
  • arthas 在线监测jvm
下载  https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar

在这里插入图片描述
挂载到8568
在这里插入图片描述

  • 常用命令

  • jvm 查看jvm信息 相当于jinfo 在这里插入图片描述

  • thread 那些线程线程的使用情况 在这里插入图片描述

  • heapdump 相当于 jmap 到处堆内存情况

  • heapdump /home/wupeng/11111.hprof

在这里插入图片描述

分析堆内存 dump文件分析

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 类似于 jmap
    在这里插入图片描述
  • 如果服务器已经挂掉怎么倒出堆内存
  • jmap 或者参数
    在这里插入图片描述
  • jad 反编译
  • 动态代理生成的类 或者 是否是被使用了的代码

在这里插入图片描述

  • 按理汇总
    在这里插入图片描述
    在这里插入图片描述

G1

复习CMS

  • PerNew+CMS
  • 缺点
1.初始标记
2.并发标记
3.重新标记
4.并发清除

G1 逻辑分代 物理不分代

主要用于服务器,多核、大内存
压缩空闲时间不会延长GC的暂停时间
G1的内存区域不是固定的eden 或者old区域

在这里插入图片描述

  • G1基本概念
  • 1.card table

在这里插入图片描述
在这里插入图片描述

  • RSet RememberedSet

在这里插入图片描述

  • 3.CSet Collection Set

在这里插入图片描述

在这里插入图片描述

  • G1 有FullGC吗? 如果产生怎么处理
    在这里插入图片描述
  • 当年轻代的空间,使用超多一定值 45% 以后,就会触发MixedGC 就相当于cms
    在这里插入图片描述

并发标记算法

三色标记 (cms G1 都在使用)

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

颜色指针

  • 通过三个字节标识对象引用是否改变 ,回收的时候扫描变化过的

csm 日志

  • java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC com.xj.jvm.gc.T01_FullGC

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

G1 日志

  • java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseG1 com.xj.jvm.gc.T01_FullGC

  • G1 由于region 组成 里面有 年轻代 幸存 老年 还有大对象

  • GC 指定暂停时间 比如20ms 回收年轻代如果比20Ms大 那就调节年轻代大小
    在这里插入图片描述
    在这里插入图片描述

实战参数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

发布了10 篇原创文章 · 获赞 2 · 访问量 1000

猜你喜欢

转载自blog.csdn.net/wpaycn/article/details/105100887
今日推荐