`

探讨PHP的内存管理

阅读更多
内存管理对于长期运行的程序,例如服务器守护程序,是相当重要的影响;因此,理解PHP是如何分配与释放内存的对于创建这类程序极为重要。本文将重点探讨PHP的内存管理问题。

 

一、 内存

  在PHP中,填充一个字符串变量相当简单,这只需要一个语句"<?php $str = 'hello world '; ?>"即可,并且该字符串能够被自由地修改、拷贝和移动。而在C语言中,尽管你能够编写例如"char *str = "hello world ";"这样的一个简单的静态字符串;但是,却不能修改该字符串,因为它生存于程序空间内。为了创建一个可操纵的字符串,你必须分配一个内存块,并且通过一 个函数(例如strdup())来复制其内容。

{
 char *str;
 str = strdup("hello world");
 if (!str) {
  fprintf(stderr, "Unable to allocate memory!");
 }
}


  由于后面我们将分析的各种原因,传统型内存管理函数(例如malloc(),free(),strdup(),realloc(),calloc(),等等)几乎都不能直接为PHP源代码所使用。

  二、 释放内存

  在几乎所有的平台上,内存管理都是通过一种请求和释放模式实现的。首先,一个应用程序请求它下面的层(通常指"操作系统"):"我想使用一些内存空间"。如果存在可用的空间,操作系统就会把它提供给该程序并且打上一个标记以便不会再把这部分内存分配给其它程序。
当 应用程序使用完这部分内存,它应该被返回到OS;这样以来,它就能够被继续分配给其它程序。如果该程序不返回这部分内存,那么OS无法知道是否这块内存不 再使用并进而再分配给另一个进程。如果一个内存块没有释放,并且所有者应用程序丢失了它,那么,我们就说此应用程序"存在漏洞",因为这部分内存无法再为 其它程序可用。

  在一个典型的客户端应用程序中,较小的不太经常的内存泄漏有时能够为OS所"容忍",因为在这个进程稍后结束时该泄漏内存会被隐式返回到OS。这并没有什么,因为OS知道它把该内存分配给了哪个程序,并且它能够确信当该程序终止时不再需要该内存。

  而对于长时间运行的服务器守护程序,包括象Apache这样的web服务器和扩展php模块来说,进程往往被设计为相当长时间一直运行。因为OS不能清理内存使用,所以,任何程序的泄漏-无论是多么小-都将导致重复操作并最终耗尽所有的系统资源。

   现在,我们不妨考虑用户空间内的stristr()函数;为了使用大小写不敏感的搜索来查找一个字符串,它实际上创建了两个串的各自的一个小型副本,然 后执行一个更传统型的大小写敏感的搜索来查找相对的偏移量。然而,在定位该字符串的偏移量之后,它不再使用这些小写版本的字符串。如果它不释放这些副本, 那么,每一个使用stristr()的脚本在每次调用它时都将泄漏一些内存。最后,web服务器进程将拥有所有的系统内存,但却不能够使用它。

  你可以理直气壮地说,理想的解决方案就是编写良好、干净的、一致的代码。这当然不错;但是,在一个象PHP解释器这样的环境中,这种观点仅对了一半。

  三、 错误处理

   为了实现"跳出"对用户空间脚本及其依赖的扩展函数的一个活动请求,需要使用一种方法来完全"跳出"一个活动请求。这是在Zend引擎内实现的:在一个 请求的开始设置一个"跳出"地址,然后在任何die()或exit()调用或在遇到任何关键错误(E_ERROR)时执行一个longjmp()以跳转到 该"跳出"地址。

  尽管这个"跳出"进程能够简化程序执行的流程,但是,在绝大多数情况下,这会意味着将会跳过资源清除代码部分(例如free()调用)并最终导致出现内存漏洞。现在,让我们来考虑下面这个简化版本的处理函数调用的引擎代码:

void call_function(const char *fname, int fname_len TSRMLS_DC){
 zend_function *fe;
 char *lcase_fname;
 /* PHP函数名是大小写不敏感的,
 *为了简化在函数表中对它们的定位,
 *所有函数名都隐含地翻译为小写的
 */
 lcase_fname = estrndup(fname, fname_len);
 zend_str_tolower(lcase_fname, fname_len);
 if (zend_hash_find(EG(function_table),lcase_fname, fname_len + 1, (void **)&fe) == FAILURE) {
  zend_execute(fe->op_array TSRMLS_CC);
 } else {
  php_error_docref(NULL TSRMLS_CC, E_ERROR,"Call to undefined function: %s()", fname);
 }
 efree(lcase_fname);
}

 


  当执行到php_error_docref()这一行时,内部错误处理器就会明白该错误级别是critical,并相应地调用 longjmp()来中断当前程序流程并离开call_function()函数,甚至根本不会执行到efree(lcase_fname)这一行。你可 能想把efree()代码行移动到zend_error()代码行的上面;但是,调用这个call_function()例程的代码行会怎么样 呢?fname本身很可能就是一个分配的字符串,并且,在它被错误消息处理使用完之前,你根本不能释放它。

  注意,这个 php_error_docref()函数是trigger_error()函数的一个内部等价实现。它的第一个参数是一个将被添加到docref的可选 的文档引用。第三个参数可以是任何我们熟悉的E_*家族常量,用于指示错误的严重程度。第四个参数(最后一个)遵循printf()风格的格式化和变量参 数列表式样。

 

  四、 Zend内存管理器

  在上面的"跳出"请求期间解决内存泄漏的方案之一是:使用Zend内存管理 (ZendMM)层。引擎的这一部分非常类似于操作系统的内存管理行为-分配内存给调用程序。区别在于,它处于进程空间中非常低的位置而且是"请求感知" 的;这样以来,当一个请求结束时,它能够执行与OS在一个进程终止时相同的行为。也就是说,它会隐式地释放所有的为该请求所占用的内存。图1展示了 ZendMM与OS以及PHP进程之间的关系。

深入探讨PHP中的内存管理问题
图1.Zend内存管理器代替系统调用来实现针对每一种请求的内存分配。


   除了提供隐式内存清除功能之外,ZendMM还能够根据php.ini中memory_limit的设置控制每一种内存请求的用法。如果一个脚本试图请 求比系统中可用内存更多的内存,或大于它每次应该请求的最大量,那么,ZendMM将自动地发出一个E_ERROR消息并且启动相应的"跳出"进程。这种 方法的一个额外优点在于,大多数内存分配调用的返回值并不需要检查,因为如果失败的话将会导致立即跳转到引擎的退出部分。

  把PHP内 部代码和OS的实际的内存管理层"钩"在一起的原理并不复杂:所有内部分配的内存都要使用一组特定的可选函数实现。例如,PHP代码不是使用 malloc(16)来分配一个16字节内存块而是使用了emalloc(16)。除了实现实际的内存分配任务外,ZendMM还会使用相应的绑定请求类 型来标志该内存块;这样以来,当一个请求"跳出"时,ZendMM可以隐式地释放它。

  经常情况下,内存一般都需要被分配比单个请求持 续时间更长的一段时间。这种类型的分配(因其在一次请求结束之后仍然存在而被称为"永久性分配"),可以使用传统型内存分配器来实现,因为这些分配并不会 添加ZendMM使用的那些额外的相应于每种请求的信息。然而有时,直到运行时刻才会确定是否一个特定的分配需要永久性分配,因此ZendMM导出了一组 帮助宏,其行为类似于其它的内存分配函数,但是使用最后一个额外参数来指示是否为永久性分配。

  如果你确实想实现一个永久性分配,那么 这个参数应该被设置为1;在这种情况下,请求是通过传统型malloc()分配器家族进行传递的。然而,如果运行时刻逻辑认为这个块不需要永久性分配;那 么,这个参数可以被设置为零,并且调用将会被调整到针对每种请求的内存分配器函数。

  例如,pemalloc(buffer_len,1)将映射到malloc(buffer_len),而pemalloc(buffer_len,0)将被使用下列语句映射到emalloc(buffer_len):

#define in Zend/zend_alloc.h:
#define pemalloc(size, persistent) ((persistent)?malloc(size): emalloc(size))


  所有这些在ZendMM中提供的分配器函数都能够从下表中找到其更传统的对应实现。

  表格1展示了ZendMM支持下的每一个分配器函数以及它们的e/pe对应实现:

  表格1.传统型相对于PHP特定的分配器。

分配器函数 e/pe对应实现
void *malloc(size_t count); void *emalloc(size_t count);void *pemalloc(size_t count,char persistent);
void *calloc(size_t count); void *ecalloc(size_t count);void *pecalloc(size_t count,char persistent);
void *realloc(void *ptr,size_t count); void *erealloc(void *ptr,size_t count);
void *perealloc(void *ptr,size_t count,char persistent);
void *strdup(void *ptr); void *estrdup(void *ptr);void *pestrdup(void *ptr,char persistent);
void free(void *ptr); void efree(void *ptr);
void pefree(void *ptr,char persistent);


   你可能会注意到,即使是pefree()函数也要求使用永久性标志。这是因为在调用pefree()时,它实际上并不知道是否ptr是一种永久性分配。 针对一个非永久性分配调用free()能够导致双倍的空间释放,而针对一种永久性分配调用efree()有可能会导致一个段错误,因为内存管理器会试图查 找并不存在的管理信息。因此,你的代码需要记住它分配的数据结构是否是永久性的。

  除了分配器函数核心部分外,还存在其它一些非常方便的ZendMM特定的函数,例如:

void *estrndup(void *ptr,int len);


  该函数能够分配len+1个字节的内存并且从ptr处复制len个字节到最新分配的块。这个estrndup()函数的行为可以大致描述如下:

void *estrndup(void *ptr, int len)
{
 char *dst = emalloc(len + 1);
 memcpy(dst, ptr, len);
 dst[len] = 0;
 return dst;
}


   在此,被隐式放置在缓冲区最后的NULL字节可以确保任何使用estrndup()实现字符串复制操作的函数都不需要担心会把结果缓冲区传递给一个例如 printf()这样的希望以为NULL为结束符的函数。当使用estrndup()来复制非字符串数据时,最后一个字节实质上都浪费了,但其中的利明显 大于弊。

void *safe_emalloc(size_t size, size_t count, size_t addtl);
void *safe_pemalloc(size_t size, size_t count,size_t addtl,char persistent);


   这些函数分配的内存空间最终大小是((size*count)+addtl)。你可以会问:"为什么还要提供额外函数呢?为什么不使用一个 emalloc/pemalloc呢?"原因很简单:为了安全。尽管有时候可能性相当小,但是,正是这一"可能性相当小"的结果导致宿主平台的内存溢出。 这可能会导致分配负数个数的字节空间,或更有甚者,会导致分配一个小于调用程序要求大小的字节空间。而safe_emalloc()能够避免这种类型的陷 井-通过检查整数溢出并且在发生这样的溢出时显式地预以结束。

  注意,并不是所有的内存分配例程都有一个相应的p*对等实现。例如,不存在pestrndup(),并且在PHP 5.1版本前也不存在safe_pemalloc()。

  五、 引用计数

  慎重的内存分配与释放对于PHP(它是一种多请求进程)的长期性能有极其重大的影响;但是,这还仅是问题的一半。为了使一个每秒处理上千次点击的服务器高效地运行,每一次请求都需要使用尽可能少的内存并且要尽可能减少不必要的数据复制操作。请考虑下列PHP代码片断:

<?php
$a = 'Hello World';
$b = $a;
unset($a);
?>


  在第一次调用之后,只有一个变量被创建,并且一个12字节的内存块指派给它以便存储字符串"Hello World",还包括一个结尾处的NULL字符。现在,让我们来观察后面的两行:$b被置为与变量$a相同的值,然后变量$a被释放。

   如果PHP因每次变量赋值都要复制变量内容的话,那么,对于上例中要复制的字符串还需要复制额外的12个字节,并且在数据复制期间还要进行另外的处理器 加载。这一行为乍看起来有点荒谬,因为当第三行代码出现时,原始变量被释放,从而使得整个数据复制显得完全不必要。其实,我们不妨再远一层考虑,让我们设 想当一个10MB大小的文件的内容被装载到两个变量中时会发生什么。这将会占用20MB的空间,此时,10已经足够了。引擎会把那么多的时间和内存浪费在 这样一种无用的努力上吗?

  你应该知道,PHP的设计者早已深谙此理。

  记住,在引擎中,变量名和它们的值实际上是两个不同的概念。值本身是一个无名的zval*存储体(在本例中,是一个字符串值),它被通过zend_hash_add()赋给变量$a。如果两个变量名都指向同一个值,会发生什么呢?

{
 zval *helloval;
 MAKE_STD_ZVAL(helloval);
 ZVAL_STRING(helloval, "Hello World", 1);
 zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
 zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL);
}


   此时,你可以实际地观察$a或$b,并且会看到它们都包含字符串"Hello World"。遗憾的是,接下来,你继续执行第三行代码"unset($a);"。此时,unset()并不知道$a变量指向的数据还被另一个变量所使 用,因此它只是盲目地释放掉该内存。任何随后的对变量$b的存取都将被分析为已经释放的内存空间并因此导致引擎崩溃。

  这个问题可以借 助于zval(它有好几种形式)的第四个成员refcount加以解决。当一个变量被首次创建并赋值时,它的refcount被初始化为1,因为它被假定 仅由最初创建它时相应的变量所使用。当你的代码片断开始把helloval赋给$b时,它需要把refcount的值增加为2;这样以来,现在该值被两个 变量所引用:

{
 zval *helloval;
 MAKE_STD_ZVAL(helloval);
 ZVAL_STRING(helloval, "Hello World", 1);
 zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
 ZVAL_ADDREF(helloval);
 zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval,sizeof(zval*),NULL);
}


  现在,当unset()删除原变量的$a相应的副本时,它就能够从refcount参数中看到,还有另外其他人对该数据感兴趣;因此,它应该只是减少refcount的计数值,然后不再管它。

  六、 写复制(Copy on Write)

  通过refcounting来节约内存的确是不错的主意,但是,当你仅想改变其中一个变量的值时情况会如何呢?为此,请考虑下面的代码片断:

<?php
$a = 1;
$b = $a;
$b += 5;
?>


  通过上面的逻辑流程,你当然知道$a的值仍然等于1,而$b的值最后将是6。并且此时,你还知道,Zend在尽力节省内存-通过使$a和$b都引用相同的zval(见第二行代码)。那么,当执行到第三行并且必须改变$b变量的值时,会发生什么情况呢?

  回答是,Zend要查看refcount的值,并且确保在它的值大于1时对之进行分离。在Zend引擎中,分离是破坏一个引用对的过程,正好与你刚才看到的过程相反:

zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC)
{
 zval **varval, *varcopy;
 if (zend_hash_find(EG(active_symbol_table),varname, varname_len + 1, (void**)&varval) == FAILURE) {
  /* 变量根本并不存在-失败而导致退出*/
  return NULL;
 }
 if ((*varval)->refcount < 2) {
  /* varname是唯一的实际引用,
  *不需要进行分离
  */
  return *varval;
 }
 /* 否则,再复制一份zval*的值*/
 MAKE_STD_ZVAL(varcopy);
 varcopy = *varval;
 /* 复制任何在zval*内的已分配的结构*/
 zval_copy_ctor(varcopy);
 /*删除旧版本的varname
 *这将减少该过程中varval的refcount的值
 */
 zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);
 /*初始化新创建的值的引用计数,并把它依附到
 * varname变量
 */
 varcopy->refcount = 1;
 varcopy->is_ref = 0;
 zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,&varcopy, sizeof(zval*), NULL);
 /*返回新的zval* */
 return varcopy;
}


  现在,既然引擎有一个仅为变量$b所拥有的zval*(引擎能知道这一点),所以它能够把这个值转换成一个long型值并根据脚本的请求给它增加5。

  七、 写改变(change-on-write)

  引用计数概念的引入还导致了一个新的数据操作可能性,其形式从用户空间脚本管理器看来与"引用"有一定关系。请考虑下列的用户空间代码片断:

<?php
$a = 1;
$b = &$a;
$b += 5;
?>


   在上面的PHP代码中,你能看出$a的值现在为6,尽管它一开始为1并且从未(直接)发生变化。之所以会发生这种情况是因为当引擎开始把$b的值增加5 时,它注意到$b是一个对$a的引用并且认为"我可以改变该值而不必分离它,因为我想使所有的引用变量都能看到这一改变"。

  但是,引 擎是如何知道的呢?很简单,它只要查看一下zval结构的第四个和最后一个元素(is_ref)即可。这是一个简单的开/关位,它定义了该值是否实际上是 一个用户空间风格引用集的一部分。在前面的代码片断中,当执行第一行时,为$a创建的值得到一个refcount为1,还有一个is_ref值为0,因为 它仅为一个变量($a)所拥有并且没有其它变量对它产生写引用改变。在第二行,这个值的refcount元素被增加为2,除了这次is_ref元素被置为 1之外(因为脚本中包含了一个"&"符号以指示是完全引用)。

  最后,在第三行,引擎再一次取出与变量$b相关的值并且检查是否有必要进行分离。这一次该值没有被分离,因为前面没有包括一个检查。下面是get_var_and_separate()函数中与refcount检查有关的部分代码:

if ((*varval)->is_ref || (*varval)->refcount < 2) {
 /* varname是唯一的实际引用,
 * 或者它是对其它变量的一个完全引用
 *任何一种方式:都没有进行分离
 */
 return *varval;
}


  这一次,尽管refcount为2,却没有实现分离,因为这个值是一个完全引用。引擎能够自由地修改它而不必关心其它变量值的变化。

  八、 分离问题

  尽管已经存在上面讨论到的复制和引用技术,但是还存在一些不能通过is_ref和refcount操作来解决的问题。请考虑下面这个PHP代码块:

<?php
$a = 1;
$b = $a;
$c = &$a;
?>


   在此,你有一个需要与三个不同的变量相关联的值。其中,两个变量是使用了"change-on-write"完全引用方式,而第三个变量处于一种可分离 的"copy-on-write"(写复制)上下文中。如果仅使用is_ref和refcount来描述这种关系,有哪些值能够工作呢?

  回答是:没有一个能工作。在这种情况下,这个值必须被复制到两个分离的zval*中,尽管两者都包含完全相同的数据(见图2)。

深入探讨PHP中的内存管理问题
图2.引用时强制分离


  同样,下列代码块将引起相同的冲突并且强迫该值分离出一个副本(见图3)。

深入探讨PHP中的内存管理问题
图3.复制时强制分离

 

<?php
$a = 1;
$b = &$a;
$c = $a;
?>


  注意,在这里的两种情况下,$b都与原始的zval对象相关联,因为在分离发生时引擎无法知道介于到该操作当中的第三个变量的名字。

  九、 总结

  PHP是一种托管语言。从普通用户角度来看,这种仔细地控制资源和内存的方式意味着更为容易地进行原型开发并导致出现更少的冲突。然而,当我们深入"内里"之后,一切的承诺似乎都不复存在,最终还要依赖于真正有责任心的开发者来维持整个运行时刻环境的一致性。

来源:http://www.jb51.net/article/28166.htm

分享到:
评论

相关推荐

    深入探讨PHP中的内存管理问题

    总的来说,深入理解PHP内存管理,编写出高效、无泄漏的代码,是每个PHP开发者必备的技能。这不仅涉及到内存的分配和释放,还包括错误处理策略以及如何避免因程序设计不当而产生的内存问题。只有这样,才能确保长时间...

    解析PHP中的内存管理,PHP动态分配和释放内存

    本文将深入探讨PHP内存管理的各个方面。 首先,PHP中的内存管理与C语言有所不同。在PHP中,创建和操作字符串非常简单,如`$str = 'hello world';`,字符串可以被自由修改、拷贝和移动。相比之下,C语言中的静态字符...

    java php python erlang 千万级内存数据性能比较

    尽管PHP在内存管理和性能上可能不如Java或Erlang,但在特定场景下,如快速原型开发和轻量级数据处理,PHP依然表现出色。 Python,以其简洁的语法和强大的科学计算库而备受青睐,"python_dict_turple_test.py"可能...

    PHP7内核剖析,包括php基本框架,变量,Zend虚拟机,php基本语法实现,内存管理,线程安全,扩展开发,命名空间等

    《PHP7内核剖析》是一本深入探讨PHP7核心机制的专著,涵盖了从基本框架到高级特性的广泛内容。本书旨在帮助读者理解PHP7的内部运作原理,从而提升编程能力,优化性能,并为扩展开发提供坚实的理论基础。 首先,PHP7...

    深度探讨 PHP 之性能.doc

    尽管 PHP 在内存管理、变量处理、函数调用和运行机制等方面进行了大量优化,但由于其动态特性,仍然面临一定的性能挑战。具体而言: - **动态解析**:所有变量、函数、对象调用等都在运行时动态解析,这增加了运行...

    PHP与C++进行IPC命名管道共享内存通信

    本文将深入探讨如何使用PHP和C++通过IPC的两种常见机制——命名管道(Named Pipes)和共享内存(Shared Memory)来实现跨进程通信。这两种技术在高效率的系统设计中尤其重要。 首先,我们来看命名管道。命名管道是...

    nginx+php-fpm模式php内存泄漏探究1

    本文主要探讨的是在Nginx+PHP-FPM模式下,PHP内存泄漏问题及其解决方案。 标题中的"nginx+php-fpm模式php内存泄漏探究1"是指在使用Nginx作为Web服务器,PHP-FPM作为PHP解释器的环境中,出现PHP内存泄漏的现象。描述...

    深入解析PHP内存管理之谁动了我的内存

    深入理解PHP内存管理机制,是每个PHP开发者必备的技能之一。在PHP中,内存管理并非像C语言那样直观,而是有着一套独特的系统。本文将探讨内存分配、释放以及`unset`函数的工作原理。 首先,PHP的内存管理并不直接...

    深入理解PHP内核

    本内容将深入探讨PHP内核的工作原理,帮助读者更好地理解和利用PHP。 ### PHP内核与Web应用 PHP的内核是其最为核心的部分,包含了PHP运行的底层实现。内核中处理了变量类型、语法解析、函数定义、内存管理等众多...

    服务器大量php-cgi.exe进程,导致CPU占用100%的解决[定义].pdf

    3. **内存管理机制**: - PHP官方文档指出,`php-cgi`进程在每次请求结束后会回收脚本使用的内存,但不会将其释放回操作系统,而是保留下来以便快速响应下一次请求。这种机制虽有助于减少内存碎片化,但可能导致...

    phpCAS源码以及调用示例(redis管理session,解决无法logout的问题)

    本文将深入探讨phpCAS的源码和调用示例,特别是在使用Redis管理session以及解决无法正常logout问题方面的应用。 首先,理解phpCAS的工作原理至关重要。phpCAS是一个基于PHP的客户端库,它遵循CAS协议与CAS服务器...

    深入理解PHP内核-php宝典

    4. **内存管理**:内存管理是任何编程语言的核心组成部分。PHP通过一系列复杂的机制来确保内存的有效利用,避免内存泄漏等问题的发生。 #### 三、深入解析 ##### 1. 变量与数据类型 - **变量存储结构**:PHP中的...

    php_swoole_loader_php72_zts_x64.rar

    它还支持进程管理、事件驱动、内存缓存、协程等特性。 2. **协程(Coroutines)**:Swoole的协程模型使得开发者可以编写出类似同步的代码来实现异步操作,降低了异步编程的复杂性。在PHP中,这意味着你可以避免回调...

    解决Linux下php-fpm进程过多导致内存耗尽问题

    如果发现CPU使用率不高,但内存使用异常,那么问题很可能出在内存管理上。 在排查过程中,通过`ps auxw|head -1;ps auxw|sort -rn -k4|head -40`命令,我们可以查看消耗内存最多的前40个进程。如果发现php-fpm进程...

    PHP7源码分享

    本文将深入探讨PHP7中的内存管理技术,包括内存分配器、内存对齐、内存预分配等核心概念,并通过具体的例子来说明这些技术的应用。 ##### 内存分配器 在PHP7中,内存分配主要通过两种方式实现:传统的`malloc`和`...

    PHP的性能测试全过程分享.doc

    在探讨PHP性能测试的全过程中,我们首先需要澄清一个普遍的误解:PHP是否真的性能低下。这篇文章正是为了挑战这一观点,通过深入分析PHP的源码、应用场景、基准性能以及与其他语言的对比,来揭示PHP的真实性能表现。...

Global site tag (gtag.js) - Google Analytics