从Android程序员的角度理解JVM内存布局

     从Android程序员的角度理解JVM内存布局



前言

   这个标题有点怪,啥叫从Android程序员的角度理解JVM呢?从Android程序员的角度出发来说,感觉JVM(Java Virtual Machine)离我们有点远啊。因为我们一般开发都是在ART环境,最不济也是在Dalivk环境下运行App程序,JVM吗是有点远!但是,理解JVM可以帮助我们更好的了解Java内存的布局,对象的创建和访问,垃圾的回收等等。引申到Android 虚拟机也是类似的原理。这个篇章就是我作为一个Android程序员角度出发来了解和掌握JVM中学习的一些知识点,这里不会教大家怎么设置各种参数调优,但是会重点放在一些JVM一些基础点的讲解上。这样做的目的是为了Android程序员更好的掌握JVM基础知识点,然后将其运用到Android开发中。所以若想要更加系统更加详细的学习 JVM 知识,还是需要去阅读专业的书籍和文档。



一.JVM内存布局

我们知道内存在操作系统终端中是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。同样的内存相对于JVM来说也是,根据java虚拟机规范,JVM 内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行。
在这里插入图片描述
上图描述的比较经典的JVM五大内存布局区域,这里重点需要注意的是框区的大小并不是代表每个内存区域的大小,这里仅仅是为了演示使用。
如果按照内存区域是否共享来划分的话,如下图所示:
在这里插入图片描述
通过前面的两个图,我们应该对JVM的内存分布有了一个大致的了解了。至于线程是否共享这点,实际上理解了每块区域的实际用处之后,就很自然而然的就记住了。不需要死记硬背,当然死记硬背也不可能会记住的。不要问为什么,因为这是从经验得来的。

下面让我们来了解下各个区域。


1.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间。是线程私有的。它可以看作是当前线程所执行的字节码的行号指示器。什么意思呢?比如如下字节码内容,在每个字节码`前面都有一个数字(行号),我们可以认为它就是程序计数器存储的内容。
在这里插入图片描述

记录这些数字(指令地址)有啥用呢,我们知道 Java 虚拟机的多线程是通过线程轮流切换并分配处理器的时间来完成的,在任何一个时刻,一个处理器只会执行一个线程,如果这个线程被分配的时间片执行完了(线程被挂起),处理器会切换到另外一个线程执行,当下次轮到执行被挂起的线程(唤醒线程)时,怎么知道上次执行到哪了呢,通过记录在程序计数器中的行号指示器即可知道,所以程序计数器的主要作用是记录线程运行时的状态,方便线程被唤醒时能从上一次被挂起时的状态继续执行,需要注意的是,程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OOM 情况的区域,所以这块区域也不需要进行 GC的。

因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。 此区域也是JVM内存中有且仅有的不会发生内存溢出异常的区域


1.2 Java虚拟机栈

Java虚拟机栈是JVM内存区域中最具有迷惑性的一个区域,从字面意思理解还以为这个是多线程共享的呢,而其实是恰恰相反的。大胆猜测是当初翻译的问题,我觉得叫做Java线程栈比较恰当。

对于每一个线程,JVM 都会在线程被创建的时候,创建一个单独的栈,这就是我们所说的Java虚拟机栈。也就是说虚拟机栈的生命周期和线程是一致,并且是线程私有的。除了 Native 方法以外,Java 方法都是通过 Java 虚拟机栈来实现调用和执行过程的(需要程序技术器、堆、元空间内数据的配合)。所以 Java 虚拟机栈是虚拟机执行引擎的核心之一。而 Java 虚拟机栈中出栈入栈的元素就称为「栈帧」。

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

  • 栈对应线程,栈帧对应方法
  • 栈帧大小实在编译期确定,不受运行期数据影响

在活动线程中, 只有位于栈顶的帧才是有效的, 称为当前栈帧。正在执行的方法称为当前方法。在执行引擎运行时, 所有指令都只能针对当前栈帧进行操作。而 StackOverflowError 表示请求的栈溢出, 导致内存耗尽, 通常出现在递归方法中。如下实例所示:

public class JavaStack {
    
    //无限递归,导致虚拟机栈溢出
    public static int f(int num) {
        num++;
        return f(num);

    }

    public static void main(String[] args) {
        f(0);
    }
}

在这里插入图片描述
虚拟机栈通过 pop 和 push 的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。在执行的过程中,如果出现了异常,会进行异常回溯,返回地址通过异常处理表确定。

可以看出栈帧在整个 JVM 体系中的地位颇高。栈帧的大概示意图如下:
在这里插入图片描述
通过前面的示意图我们对栈帧有了一个轮廓上的认识,下面让我们对其一一介绍一番:


1.2.1 局部变量表

局部变量表(Local Variable),这个先从字面意思看看就是存储局部变量的一个表。从术语上来讲局部变量表代表的是一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型)。

  • reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。

如果对上述的描述还不是很明白,让我们上代码和示意图,这样就好理解了。

    public int fun(int a, int b){
        Object obj = new Object();
        return a + b;
    }

如果局部变量是 Java 的 8 种基本基本数据类型,则存在局部变量表中,如果是引用类型。如 new 出来的 String,局部变量表中存的是引用,而实例在堆中。如下示意图所示:
在这里插入图片描述

关于局部变量表需要重点注意的是:局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。


1.2.2 操作数栈

操作数栈(Operand Stack)看名字可以知道是一个栈结构,是操作变量的内存模型。Java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。当 JVM 为方法创建栈帧的时候,在栈帧中为方法创建一个操作数栈,保证方法内指令可以完成工作。当方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。
说得再多不如实操一遍来得痛快。

/**
 * 
 * @author tangkw
 * 演示操作数栈使用
 *
 */
public class OperandStackTest {
    public int sum(int a, int b) {
        return a + b;
    }
}

通过javah编译生成.class文件之后,再通过javap返回编译查看汇编指令:

                                                                                                                   
E:\workspace\TestForJava\src\com\pax\api\fun                                                                       
λ javac OperandStackTest.java                                                                                      
                                                                                                                   
E:\workspace\TestForJava\src\com\pax\api\fun                                                                       
λ javap -v OperandStackTest.class                                                                                  
Classfile /E:/workspace/TestForJava/src/com/pax/api/fun/OperandStackTest.                                          
class                                                                                                              
  Last modified 2020-4-14; size 278 bytes                                                                          
  MD5 checksum b9bdf29ae16869b405a6b285a98e4625                                                                    
  Compiled from "OperandStackTest.java"                                                                            
public class com.pax.api.fun.OperandStackTest                                                                      
  minor version: 0                                                                                                 
  major version: 52                                                                                                
  flags: ACC_PUBLIC, ACC_SUPER                                                                                     
Constant pool:                                                                                                     
   #1 = Methodref          #3.#12         // java/lang/Object."<init>":()                                          
V                                                                                                                  
   #2 = Class              #13            // com/pax/api/fun/OperandStack                                          
Test                                                                                                               
   #3 = Class              #14            // java/lang/Object                                                      
   #4 = Utf8               <init>                                                                                  
   #5 = Utf8               ()V                                                                                     
   #6 = Utf8               Code                                                                                    
   #7 = Utf8               LineNumberTable                                                                         
   #8 = Utf8               sum                                                                                     
   #9 = Utf8               (II)I                                                                                   
  #10 = Utf8               SourceFile                                                                              
  #11 = Utf8               OperandStackTest.java                                                                   
  #12 = NameAndType        #4:#5          // "<init>":()V                                                          
  #13 = Utf8               com/pax/api/fun/OperandStackTest                                                        
  #14 = Utf8               java/lang/Object                                                                        
{                                                                                                                  
  public com.pax.api.fun.OperandStackTest();                                                                       
    descriptor: ()V                                                                                                
    flags: ACC_PUBLIC                                                                                              
    Code:                                                                                                          
      stack=1, locals=1, args_size=1                                                                               
         0: aload_0                                                                                                
         1: invokespecial #1                  // Method java/lang/Object.                                          
"<init>":()V                                                                                                       
         4: return                                                                                                 
      LineNumberTable:                                                                                             
        line 3: 0                                                                                                  
                                                                                                                   
  public int sum(int, int);                                                                                        
    descriptor: (II)I                                                                                              
    flags: ACC_PUBLIC                                                                                              
    Code:                                                                                                          
      stack=2, locals=3, args_size=3  //最大栈深度为2 局部变量个数为3                                                                       
         0: iload_1           // 局部变量1 压栈                                                                                     
         1: iload_2           // 局部变量2 压栈                                                                                     
         2: iadd              // 栈顶两个元素相加,计算结果压栈                                                                                     
         3: ireturn           //返回                                                                                     
      LineNumberTable:                                                                                             
        line 5: 0                                                                                                  
}                                                                                                                  
SourceFile: "OperandStackTest.java"                                                                                                                                                                                                                                                          

1.2.3 动态链接

每个栈帧中都包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接。


1.2.4 方法返回地址

方法执行时有两种退出情况:

  • 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等
  • 异常退出

无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

  • 返回值压入上层调用栈帧
  • 异常信息抛给能够处理的栈帧
  • PC 计数器指向方法调用后的下一条指令

1.2.5 Java虚拟机栈常见的异常

Java虚拟机栈可能出现两种类型的异常:

  • 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。这个在前面已经通过实例讲解了,就不过多举例了。
  • 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。这个要怎么通过实例来演示呢,可以通过不断创建线程的方法,在这种情况下,为每个线程分配的内存越大,就会越容易产生OOM异常。下面在测试之前,先特别提示一下,如果想测试栈的OOM异常,记得先保存当前的工作。由于Windows平台的虚拟机中,Java的线程是映射到操作系统的内核上的,因此以下代码可能会导致操作系统假死。不要问我为啥知道,因为谁用谁知道。
/**
 * 
 * @author tangkw
 * 演示Java虚拟机栈溢出
 */
public class JavaVMStackOOM {

    private void dontStop() {
        while (true) {
        }
    }

    public void stackLeakByThread() {
        while (true) {
            new Thread(new Runnable() {
                public void run() {
                    dontStop();
                }
            }).start();
        }
    }

    public static void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

1.3 Java本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。


1.4 堆区(Heap)


1.4.1 堆区的介绍

堆是 OOM 故障最主要的发生区域。它是内存区域中最大的一块区域,被所有线程共享,存储着几乎所有的实例对象、数组。所有的对象实例以及数组都要在堆上分配,但是随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代。再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。


1.4.2 堆区的调整

根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以在运行时动态地调整。

怎么调整
由于我们是从Android程序员的角度出发,所以我们对于怎么调整不在本篇的讲解范围之内,对其有一个基本理论知识即可。

通过设置如下参数,可以设定堆区的初始值和最大值,比如 -Xms256M -Xmx 1024M,其中 -X 这个字母代表它是 JVM 运行时参数,ms 是 memory start 的简称,中文意思就是内存初始值,mx 是 memory max 的简称,意思就是最大内存。

值得注意的是,在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,会形成不必要的系统压力所以在线上生产环境中 JVM 的 Xms 和 Xmx 会设置成同样大小,避免在 GC 后调整堆大小时带来的额外压力。
这里以eclipse运行Java程序为例说明
右击工程 Run AS -->选最下面Run…–> Arguments–>在VM arguments里面填 -Xmx256m。这样就可以设置它运行时最大内存为256m,其它的参数设置也可以类似设置。
在这里插入图片描述


1.4.3 JVM堆的默认空间分配

让我们看一下堆空间内存分配的大体情况。示意图如下:
在这里插入图片描述
这里可能就会有人来问了,你从哪里知道的呢?如果我想配置这个比例,要怎么修改呢?

我先来告诉你怎么看虚拟机的默认配置。命令行上执行如下命令,就可以查看当前 JDK 版本所有默认的 JVM 参数。

java -XX:+PrintFlagsFinal -version

对应的输出应该有几百行,我们这里去看和堆内存分配相关的两个参数

>java -XX:+PrintFlagsFinal -version
[Global flags]
    ...
    uintx InitialSurvivorRatio                      = 8
    uintx NewRatio                                  = 2
    ...
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

参数解释:

参数 作用
-XX: InitialSurvivorRatio 新生代Eden/Survi vor空间的初始比例
-XX: NewRatio Old区/Young区的内存比例

因为新生代是由 Eden + S0 + S1 组成的,所以按照上述默认比例,如果 eden 区内存大小是 40M,那么两个 survivor 区就是 5M,整个 young 区就是 50M,然后可以算出 Old 区内存大小是 100M,堆区总大小就是 150M。


1.4.3 Android进程堆区情况

这里我们也过一下Android虚拟机的堆分布情况,如下所示:

130|msm8953_64:/ # getprop | grep dalvik.vm
[dalvik.vm.appimageformat]: [lz4]
[dalvik.vm.dex2oat-Xms]: [64m]
[dalvik.vm.dex2oat-Xmx]: [512m]
[dalvik.vm.dex2oat-minidebuginfo]: [true]
[dalvik.vm.dexopt.secondary]: [true]
[dalvik.vm.heapgrowthlimit]: [192m]
[dalvik.vm.heapmaxfree]: [8m]
[dalvik.vm.heapminfree]: [4m]
[dalvik.vm.heapsize]: [512m]
[dalvik.vm.heapstartsize]: [16m]
[dalvik.vm.heaptargetutilization]: [0.75]
[dalvik.vm.image-dex2oat-Xms]: [64m]
[dalvik.vm.image-dex2oat-Xmx]: [64m]
[dalvik.vm.isa.arm.features]: [default]
[dalvik.vm.isa.arm.variant]: [cortex-a53]
[dalvik.vm.isa.arm64.features]: [default]
[dalvik.vm.isa.arm64.variant]: [generic]
[dalvik.vm.lockprof.threshold]: [500]
[dalvik.vm.stack-trace-dir]: [/data/anr]
[dalvik.vm.usejit]: [true]
[dalvik.vm.usejitprofiles]: [true]
[persist.sys.dalvik.vm.lib.2]: [libart.so]
[ro.dalvik.vm.native.bridge]: [0]

让我们对这些值大概解释一下:

  • dalvik.vm.heapstartsize=16m,相当于虚拟机的 -Xms配置,该项用来设置堆内存的初始大小。
  • dalvik.vm.heapgrowthlimit=192m,相当于虚拟机的 -XX:HeapGrowthLimit配置,该项用来设置一个标准的应用的最大堆内存大小。一个标准的应用就是没有使用android:largeHeap的应用。
  • dalvik.vm.heapsize=512m,相当于虚拟机的 -Xmx配置,该项设置了使用android:largeHeap的应用的最大堆内存大小。
  • dalvik.vm.heaptargetutilization=0.75相当于虚拟机的 -XX:HeapTargetUtilization,该项用来设置当前理想的堆内存利用率。其取值位于0与1之间。当GC进行完垃圾回收之后,Dalvik的堆内存会进行相应的调整,通常结果是当前存活的对象的大小与堆内存大小做除法,得到的值为这个选项的设置,即这里的0.75
  • dalvik.vm.heapminfree=4m与 dalvik.vm.heapmaxfree=8m
    dalvik.vm.heapminfree对应的是-XX:HeapMinFree配置,用来设置单次堆内存调整的最小值。
    dalvik.vm.heapmaxfree 对应的是-XX:HeapMaxFree配置,用来设置单次堆内存调整的最大值。

关于这些参数是如何得来,这个就牵涉到Android虚拟机的启动流程了,不在本篇的讨论范围之内。
源码的路径如下:

frameworks/base/core/jni/AndroidRuntime.cpp

具体参见如下函数,感兴趣的自行研究:

int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{
    JavaVMInitArgs initArgs;
    char propBuf[PROPERTY_VALUE_MAX];
    char stackTraceFileBuf[sizeof("-Xstacktracefile:")-1 + PROPERTY_VALUE_MAX];
    char jniOptsBuf[sizeof("-Xjniopts:")-1 + PROPERTY_VALUE_MAX];
    char heapstartsizeOptsBuf[sizeof("-Xms")-1 + PROPERTY_VALUE_MAX];
    char heapsizeOptsBuf[sizeof("-Xmx")-1 + PROPERTY_VALUE_MAX];
    char heapgrowthlimitOptsBuf[sizeof("-XX:HeapGrowthLimit=")-1 + PROPERTY_VALUE_MAX];
    char heapminfreeOptsBuf[sizeof("-XX:HeapMinFree=")-1 + PROPERTY_VALUE_MAX];
    char heapmaxfreeOptsBuf[sizeof("-XX:HeapMaxFree=")-1 + PROPERTY_VALUE_MAX];
    char usejitOptsBuf[sizeof("-Xusejit:")-1 + PROPERTY_VALUE_MAX];
    char jitmaxsizeOptsBuf[sizeof("-Xjitmaxsize:")-1 + PROPERTY_VALUE_MAX];
    char jitinitialsizeOptsBuf[sizeof("-Xjitinitialsize:")-1 + PROPERTY_VALUE_MAX];
    char jitthresholdOptsBuf[sizeof("-Xjitthreshold:")-1 + PROPERTY_VALUE_MAX];
    char useJitProfilesOptsBuf[sizeof("-Xjitsaveprofilinginfo:")-1 + PROPERTY_VALUE_MAX];
	...
}


1.4.4 JVM堆栈溢出实例

人狠话不过直接上实例代码,如下:

/**
 * 
 * @author tangkw
 * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError,设置堆的最大为10M
 */
public class HeapOOMFun {
    public static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        List<byte[]> byteList = new ArrayList<>(10);
        for (int i = 0; i < 6; i++) {//这里演示的是12M,所以堆溢出了
            byte[] bytes = new byte[2 * _1MB];
            byteList.add(bytes);
        }
    }
}

直接运行,关于JVM参数的设置方法,前面有讲解了怎么在eclipse里面设置,请参见前面篇章。运行结果如下:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid25400.hprof ...
Heap dump file created [7520617 bytes in 0.014 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.pax.api.fun.HeapOOMFun.main(HeapOOMFun.java:12)

在这里插入图片描述
注意:-XX:+HeapDumpOnOutOfMemoryError 可以让 JVM 在遇到 OOM 异常时,输出堆内信息,特别是对相隔数月才出现的 OOM 异常尤为重要。


1.5 方法区和运行时常量池

  • 方法区:
    方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。
    方法区用于存储已被虚拟机加载的类信息、常量、静态变量等,如static修饰的变量加载类的时候就被加载到方法区中。

  • 运行时常量池:
    是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。

  • 需要注意的点:
    这块知识我们需要掌握的一个重点就是在jdk1.7之前字符常量池是存放在方法区中的,但是在jdk1.7及之后就从方法区中移除了字符串常量池,放在了堆中。方法区也是多个线程共享的,存放已经被虚拟机加载的类信息,常量和静态变量等数据信息,在方法区中还存在一个运行时常量池,这个是值得好好研究的。首先是Class文件中除了有类的字段,方法和接口等描述信息之外还有一个常量池,这些内容会在类加载后进入方法区的运行时常量池。而这个常量池是用于存放编译阶段生成的各种字面量和符号引用。



结语

修行至此,恭喜读者你已经对JVM内存布局有了一个比较清晰的认识了,但是如果想要深入掌握,这里我只能说臣妾做不到。这个就得找专业书籍深入研究了,这边文章最多就是一个入门的东西了。



写在最后

   好了JVM内存布局就写到这了,后续的篇章我会继续从Android程序员的角度出发来讲解和学习JVM其它的知识,主要是个人能力有限,只能入门级别啊。在最后麻烦读者朋友们如果本篇对你有帮助,关注和点赞一下,当然如果有错误和不足的地方也可以拍砖。

参考:
图文并茂,傻瓜都能看懂的 JVM 内存布局

发布了119 篇原创文章 · 获赞 131 · 访问量 36万+

猜你喜欢

转载自blog.csdn.net/tkwxty/article/details/105409166
今日推荐