热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Jikes研究虚拟机(RVM)五结论

相关工作用Java代码实现一个Java虚拟机和它的相关子系统(包括优化编译器)提出了几个挑战。Taivalsaari36也描述了一个“用Java写Java”的JVM

相关工作

用 Java 代码实现一个 Java 虚拟机和它的相关子系统(包括优化编译器)提出了几个挑战。Taivalsaari 36 也描述了一个“用 Java 写 Java”的 JVM 实现,设计它是为了检查用 Java 写的高质量的虚拟机的可行性。这个方案的一个缺点是它运行在另一台 JVM 上,这增加了性能开销,因为有两级解释过程。麻省理工学院(Massachusetts Institute of Technology(MIT))的 Rivet JVM 37 也运行在另一台 JVM 上面。通过自举系统,我们的方案不需要另一台 JVM(请参阅 附录 B)。IBM VisualAge for Java 38 的 JVM 是用 Smalltalk 写的。其它的 JVM 39 是用本机代码写的。

最令人兴奋的传统 JVM 可能是 HotSpot。 40 HotSpot 的对象模型和 Jalapeo 的有点相似:对象直接被引用(而不是通过句柄)而且对象有一个两个字的头。在两个模型中,关于对象的类的信息都通过对象头中的引用可用。

HotSpot 最初解释字节码,编译(并内联)被频繁调用的方法。Jalapeo 的快速编译器将扮演与 HotSpot 的解释器相似的角色。其它所有都是同等的,这对 HotSpot 会带来启动方面的好处,对 Jalapeo 会带来性能方面的好处。我们不期望哪个好处会特别大,但这仍然可以看到。如果未优化的 Jalapeo 代码比解释后的 HotSpot 代码表现更好,这将允许 Jalapeo 优化编译器把更多的资源集中在它优化的代码上。用 Java 代码实现 Jalapeo 允许优化编译器内联和优化频繁被调用的运行时服务,HotSpot 通过调用本机代码(极好地优化了的 C 例程)访问这些服务。

HotSpot 把 Java 线程作为宿主操作系统线程实现。这些线程是完全抢先的。Jalapeo 调度它自己的准抢先线程。我们希望这将允许支持更多的线程、更轻量的同步和使从正常操作到垃圾回收的更平滑切换(特别是在有极多线程的情况下)。HotSpot 的每线程方法激活堆栈符合宿主操作系统的调用约定。这应会给 Jalapeo 较小的空间和性能方面的好处(尽管 Jalapeoill 在它 确实调用 C 代码时获得了性能方面的好处)。

HotSpot 和 Jalapeo 都支持类型确切的垃圾回收。Jalapeo 支持一系列内存管理器。Jalapeo 的回收器没有一个象 HotSpot 的那么成熟,但在 SMP 上,Jalapeo 的回收器并行地运行在所有可用的 CPU 上。HotSpot 使用一个带有针对主要回收的“标记并压缩”繁衍方案。为尽可能减少暂停时间,HotSpot 可以使用一个增量的“火车(train)” 回收器。 44 这个回收器执行频繁的短回收。注意,这将加剧任何的过渡到回收(transition-to-collection)延迟。

我们没有关于 HotSpot 的锁定机制的信息。

Squeak 45 是一个用 Smalltalk 写的 Smalltalk 虚拟机。它通过把虚拟机转换为 C 以编译和链接接来生成产品版。转换程序也是用 Smalltalk 写的。

动态编译(称为动态转换或即时编译)已经成为很多面向对象语言的以前的实现中的一个关键因素。Deutsch 和 Schiffman 的 Smalltalk-80 的高性能实现动态地把 Smalltalk 字节码转换成本机代码; 46 他们的编译器与 Jalapeo 的基线编译器非常相似。Self 语言的实现也依赖于动态编译来达到高性能。 47 Self 编译使用与 Jalapeo 的优化编译器所用的中间表示大体相当的的基于寄存器的中间表示。最近,大量 Java 语言的即时编译器已被开发出来。 3148 这些编译器中,有一些编译器把字节码转换成三地址代码,进行简单的优化和寄存器分配,然后就生成目标机器代码。

DAISY 49 是一个 VLIW(very long instruction word(超长指令字))仿真器,它“快速”地把不同体系结构指令集,包括 Java 字节码,转换成 VLIW 体系结构。它使用 VLIW 类似于树的表示以进行指令调度和寄存器分配。

很多以前的系统使用了动态编译的更专门的形式,以通过使用“运行时常数”来有选择地优化程序的“热点”。 50 一般来说,这些系统都强调极其快速的动态编译,经常执行大量的脱机预计算,以避免构造正在编译的程序片段的任何显式表示。

有大量的工作用于处理特定于面向对象语言的优化,例如类分析(过程内的 54 和过程间的 55 ),类层次结构分析和优化 5657 ,接收器类预测 465859 ,方法规范 56 和调用图构造。 55 其它与 Java 编译有关的优化包括边界检查清除 60 和语义扩展。 22

结论

用于 Java 服务器的 Jalapeo 的虚拟机是用 Java 语言编写的。传统上用本机方法支持的运行时服务主要地用 Java 代码实现。

Jalapeo 的对象布局支持单指令字段访问,对数组元素的三指令访问,硬件空指针检查和四指令虚方法调度。通过全局 JTOC 数组快速访问静态字段和方法也实现了。

Jalapeo 的线程通过虚拟处理器进行多路复用。线程切换是准抢先的。三个不同的锁定机制提供了轻量级同步,而不用操作系统支持。

Jalapeo 的内存管理子系统支持一系列内存管理器,每个内存管理器由一个并发对象分配器和一个并行的、类型确切的、停住一切的垃圾回收器组成。繁衍的和不繁衍的,拷贝的和非拷贝的管理器都被支持。增量及并发回收器正在研究之中。

Jalapeo 的三个可互操作的编译器提供了不同级别的动态优化,确保了实时的线程抢先,并且生成了支持异常处理、堆栈中的调用的位置和调试的表。

Jalapeo 的优化编译器为已被识别为频繁执行或计算密集的方法产生优质代码。需被再次编译的方法将根据运行时配置动态地选择。

我们已经证明了用 Java 语言为 Java 服务器构建虚拟机的可行性。我们尚未证明这样一个虚拟机能达到并保持世界级的性能。我们正在为此努力。

 

附录 A:MAGIC

为分配一个对象,Jalapeo 的内存管理器必须访问原始内存以获得要求大小的一块可用空间。它们“遍历”线程堆栈以识别堆栈帧中的对象引用。拷贝管理器在垃圾回收期间访问对象头以标记对象,访问原始内存以拷贝一个对象。异常处理要求非结构化的传送控制以转到适当的 catch块(“go to”在 Java 语言中是禁止的)。静态数据和方法通过一个专用的机器寄存器访问,不能从 Java 指令访问这个专用寄存器。输入和输出要求访问 Java 语言不知道的操作系统服务。线程切换依赖于接收到来自操作系统的周期性中断。Jalapeo 的锁定机制是用 PowerPC 指令实现的,这些指令无法表达成 Java 字节码。不打破 Java 的编程模型,这些操作都无法进行。

为实现 Jalapeo Java 代码,有必要增加 Java 的功能,以包含本机方法传统上要求的功能:

  • 调用操作系统服务
  • 使用特定于体系结构的机器指令
  • 访问机器寄存器和内存
  • 强制对象引用原始地址, 反之亦然
  • 把执行转到任意地址

Jalapeo 必须要有这些功能,但 Jalapeo 也必须防止用户应用程序能使用这些功能。

在专门的 MAGIC 类的帮助下,Jalapeo 的编译器支持这些违例。这个类的方法符合 Java 外部操作,Jalapeo 必须能够执行这些操作。这些方法的体是空的。Java 的源代码编译器能够编译它们。但是,Jalapeo 的编译器忽略这些结果字节码。而且,这些编译器识别出 MAGICR 类的名字并内联地插入必需的机器代码。为确认用户代码未违背 Java 的约束,当 Jalapeo 的编译器碰到调用一个 MAGIC 方法时,它们将验证正在编译的方法是 JVM 的一个已授权的部分。

需要使用 MAGIC 类的代码在这样做时需特别小心。将要讲述的规则就是一个原因。某些操作要格外小心。涉及原始地址的计算尤其微妙。MAGIC 方法 objectAsAddress 把对象引用转换成原始地址(一个整数)。例如,在进行动态链接时就需要这个功能。然而,它也是有问题的。Jalapeo 的拷贝内存管理器在移动所引用的对象时会更新对象引用,但原始地址却未被更新。在进行涉及原始地址的计算时,为避免垃圾回收的发生,必须很小心,以免拷贝回收器使这些地址无效。这通过调用一个能禁用垃圾回收的方法来避免。

已经禁用了垃圾回收的线程不能试图创建一个对象,因为如果内存不足,系统将会挂起。(注意,其它线程可以自由申请内存。如果无法得到内存,则这些线程被延迟,而且一旦垃圾回收被重新启用,就将开始进行一个回收。)这个约束有一些微妙的牵连。类不能被装入,因为对象是在类装入期间创建的。这意味着必须避免动态链接。类型的强制转型(和存储到对象数组)也不允许,因为这可能也要求类装入。类似地,如果线程试图进入一个共享对象的一个管程,而这个管程当前正被一个正在等待垃圾回收的线程占有,那么系统将陷入死锁。因此,线程在进行涉及原始地址的计算时,必须严格限制在 Java 功能的子集内。

就线程当它的垃圾回收被禁用时进行让出(显式地或隐式地)来说,也会有点问题。这样一个让出可能会任意地延迟所需的垃圾回收。当线程的垃圾回收被禁用时,隐式线程切换被延迟(而显式线程切换被禁止)。

Jalapeo 系统中大约有 650 个 Java 类,其中大约有 110 个访问了 MAGIC 类。其中只有 12 个类要求禁用垃圾回收。

 

附录 B:开始

一组相当坚实的服务 ― 一个类装入器,一个对象分配器,一个编译器 ― 在 JVM 能够装入正常操作所需的所有剩余服务之前就必须已经存在。用本机代码编写的 JVM 的初始服务,或者运行在另一台 JVM 上的 JVM,都从底层运行时例程可用。Jalapeo 不是用本机代码编写的,它没有底层运行时例程。因此,我们把基本的核心服务装配进一个可执行 引导映象,这个引导映象先于 JVM 运行。这个引导映象是 Jalapeo 虚拟机的一个快照,它被写入到一个文件中。随后,这个文件被装入内存并执行。

引导映象由一个名为 引导映象编写器(boot-image writer)的 Java 程序创建。引导映象编写器构造运行中的 Jalapeo 虚拟机的实体模型(mock-up),然后把它包装进引导映象。引导映象编写器是一个普通的 Java 程序,它可以在任何 JVM 上运行。运行引导映象编写器的 JVM 将被称为 JVM,而产生的结果 Jalapeo 虚拟机则称为 目标JVM。

引导映象编写器类似于一个交叉编译器和链接器:它把字节码编译成机器码并重写机器地址以把程序组件绑定进可运行映象。然而,由于 Jalapeo 的编译器、类装入器和运行时数据结构都是 Java 代码形式,而不似多数编译器,所以引导映象编写器也必须把“活动”对象绑定进引导映象。

引导映象编写器在源 JVM 中实例化 Java 对象,这些对象表示了目标 JVM。然后,它使用 Java 内置的反射功能把这些实体模型对象从源 JVM 的对象模型转换为 Jalapeo 的对象模型。引导映象编写器和这种自引用特征使得它相对简单 ― 它实际上只是一个对象模型转换器。

由于 Jalapeo 是一个 Java 程序,所以它的每个组件都是一个 Java 对象,而且,通过在 Jalapeo 的每个主子系统中执行专门的初始化方法,引导映象编写器能够构造其实体模型。定制的类装入器确保了执行这些代码所需的任何类都既装入到了源 JVM 中,也装入到了实体模型中。当装入一个类时,类的方法被编译(由运行在源 JVM 中的 Jalapeo 的编译器执行)并被包含进实体模型。

要成功实施把类同时装入源 JVM 和它的目标 JVM 的实体模型的策略,就需要一个完整的类列表。如果当 Jalapeo 开始运行时,核心运行时环境的一个方法引用了不在引导映象中的任何类,则将产生无穷递归的结果:运行时环境要求装入它自己的一部分以装入它自己的一部分 ― 等等。

通过仔细的计划和反复试验,我们解决了判断实体模型中最少需要哪些类以阻止这种情形的问题。Jalapeo 的所有核心类都以 VM 为前缀命令。这里是提供使虚拟机能执行编译、内存管理和动态类装入的充足机器所需的类。这个专门的前缀由 Jalapeo 的编译器识别并用于抑制正常的动态链接规则:编译器从不在有这个前缀的类的方法之间生成动态链接代码。小心地编写核心类以避免对 Java 库类的不必要使用。这些基础类 ― java.lang.Object、java.lang.Class、java.lang.String 和一些 I/O 类 ― 是绝不可以排除在外的。VM_ 类和这些基础 Java 类构成了我们认为有必要在引导映象中出现的类的启动集。

然后通过反复试验识别出另外一小部分依赖(例如,Integer、Float、Double 和各种数组和异常类)。我们编译一个引导映象并试图执行它。如果它在试图(递归地)装入类 X时崩溃了,那我们就把 X 添加到写入引导映象的类的列表,并反复进行这个过程。这个过程集中进行了少数重试,也不证明一旦 VM_ 类的实现隐定了,这会成为一个维护问题。

实体模型在完成之后被转换成引导映象。这包括查找实体模型中的所有对象,把它们转换成 Jalapeo 的对象格式并存储进 引导映象数组。运行中的 Jalapeo 虚拟机的所有组件都可以从一个 JTOC 数组中获得(请参阅静态字段和方法部分)。在实体模型中,JTOC 被编码成三个并行的数组:一个整数数组(用于原始值),一个对象实例数组(用于引用)和一个用于区分这两个数组的布尔数组。JTOC 数组的结构被递归地遍历,所碰到的值(引用的和原始的)被转换到引导映象数组。由于每个装入类的类型信息块(请参阅对象头部分)都从 JTOC 引用,所以所有必需的编译后的方法体都将被包含到引用映象中。

这个转换过程使用了映象。引导映象为实体模型中的每个对象获得 java.lang.Class 对象并在由 getFields 方法返回的字段上反复进行一些操作。对每一个字段,它从源对象抽取字段值,从对象的 Jalapeo 类描述中抽取目标字段偏移量。然后,它把位于从对象的索引偏移该偏移量的值写入引导映象。当碰到对象引用时,我们不能使用来自实体模型的任何值。通过用一个作为分配到的引导映象维护的散列表,实体模型中的引用被转换成引导映象地址。(包含引导映象中的所有引用的地址的数组可以被包含进引导映象,以支持引导映象的重定位。)

总的来说,引导映象编写器一个字段一个字段地拷贝 Java 对象,从实体模型到引导映象,同时从源 JVM 的对象模型转换到目标 JVM 的对象模型。有赖于 Java 的映象功能,我们碰上了一个麻烦:Sun 的 Java 开发包(Java Development Kit),版本 1.1.4 不允许对私有字段的反射访问。这在 Java 2 软件开发包(Java 2 Software Development Kit)中不是一个问题,这个开发包允许这样的访问。通过预处理类文件,关掉私有位,我们在更早的版本中就解决了这个问题。

除了能从 JTOC 数组中访问的对象外,引导映象中还需要另外两个对象:一个初始线程对象和一个“引导记录”。初始线程对象包含一个空堆栈,它已为在 Jalapeo 启动时运行 boot( ) 方法的第一条指令准备就绪;“引导记录”是引导映象和引导映象运行器(稍后论述)之间的接口。这个引导记录包含映象中的开始、结束和最后使用的地址,也包含用于启动 Jalapeo 的四个寄存器值,boot( ) 方法的地址和 AIX 系统调用的地址。当这些值被存储到引导映象数组时,这个引导记录被写到磁盘上。

一个称为 引导映象运行器的短小程序启动 Jalapeo 的运行。它把引导映象读进内存,把四个寄存器设置为指定值并转到 boot( ) 方法分支。引导映象是用 C(带有一些汇编程序以设置寄存器和执行最后的分支)写的,不是 Java 代码,所以 不需要在 JVM 上运行。

当 boot( ) 方法开始执行时,虚拟机处在一个脆弱状态:它能够运行机器指令的单个线程,但它还未创建支持它自己的执行所需的外部操作系统资源。引导映象无法创建这些操作系统资源,因为这些资源要引用外部状态,而这些状态将不存在,一直到引导映象执行。因此,Jalapeo 必须执行另外的初始化。

在引导期间,虚拟机初始化特定于硬件的地址(例如,它将最终在自己的堆栈上建立硬件监视页),打开与 Java 库的 System.in、System.out 和 System.error 流对象相对应的文件,分析命令行参数并创建与一个与当前执行环境相对应的 System.Properties 对象。然后,通过创建充当虚拟处理器的操作系统线程初始化多线程子系统,Java 线程在虚拟处理器上多路复用。最后,启用定时器中断以支持线程抢先,生成一个 Java 线程以运行命令行指定的应用程序。

Jalapeo 运行直到最后一个(非守护)Java 线程终止或调用了 System.exit( )。

 

参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • 请参阅 引用的参考书和注释
  • 阅读 IBM 系统杂志中对当前问题有兴趣的其它文章。
  • IBM 技术杂志Web 站点上查找其它的技术出版物。

 

作者简介

B. Alpern has co-authored this article  

 

  C. R. Attanasio has co-authored this article  

 

  J. J. Barton has co-authored this article  

 

  M. G. Burke has co-authored this article  

 

  P. Cheng has co-authored this article  

 

  J.-D. Choi has co-authored this article  

 

  A. Cocchi has co-authored this article  

 

  S. J. Fink has co-authored this article  

 

  D. Grove has co-authored this article  

 

  M. Hind has co-authored this article  

 

  S. F. Hummel has co-authored this article  

 

  D. Lieber has co-authored this article  

 

  V. Litvinov has co-authored this article  

 

  M. F. Mergen has co-authored this article  

 

  T. Ngo has co-authored this article  

 

  J. R. Russell has co-authored this article  

 

  V. Sarkar has co-authored this article  

 

  M. J. Serrano has co-authored this article  

 

  J. C. Shepherd has co-authored this article  

 

  S. E. Smith has co-authored this article  

 

  V. C. Sreedhar has co-authored this article  

 

  H. Srinivasan has co-authored this article  

 

  J. Whaley has co-authored this article  


推荐阅读
  • LeetCode笔记:剑指Offer 41. 数据流中的中位数(Java、堆、优先队列、知识点)
    本文介绍了LeetCode剑指Offer 41题的解题思路和代码实现,主要涉及了Java中的优先队列和堆排序的知识点。优先队列是Queue接口的实现,可以对其中的元素进行排序,采用小顶堆的方式进行排序。本文还介绍了Java中queue的offer、poll、add、remove、element、peek等方法的区别和用法。 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 利用Visual Basic开发SAP接口程序初探的方法与原理
    本文介绍了利用Visual Basic开发SAP接口程序的方法与原理,以及SAP R/3系统的特点和二次开发平台ABAP的使用。通过程序接口自动读取SAP R/3的数据表或视图,在外部进行处理和利用水晶报表等工具生成符合中国人习惯的报表样式。具体介绍了RFC调用的原理和模型,并强调本文主要不讨论SAP R/3函数的开发,而是针对使用SAP的公司的非ABAP开发人员提供了初步的接口程序开发指导。 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • 服务器上的操作系统有哪些,如何选择适合的操作系统?
    本文介绍了服务器上常见的操作系统,包括系统盘镜像、数据盘镜像和整机镜像的数量。同时,还介绍了共享镜像的限制和使用方法。此外,还提供了关于华为云服务的帮助中心,其中包括产品简介、价格说明、购买指南、用户指南、API参考、最佳实践、常见问题和视频帮助等技术文档。对于裸金属服务器的远程登录,本文介绍了使用密钥对登录的方法,并提供了部分操作系统配置示例。最后,还提到了SUSE云耀云服务器的特点和快速搭建方法。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
author-avatar
姚威阳_489
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有