设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

似乎我对Java存在误解–Part 1

2016-12-21 20:07| 发布者: joejoe0332| 查看: 1050| 评论: 0|原作者: Robbie_Zhu, leoxu, Tocy, Viyi, Tony, 一刀, 花间_拾零, 白羊沈歌, 无若, snake_007, sword_87|来自: oschina

摘要: 我不是最狂热的 Java 粉,虽然也会在某些需要的情况下用到 Java 代码,但仍然不太习惯。和其它不怎么使用 Java 的人一样,我觉得 Java 有些刻板:它占内存且运行慢,虽然在编程界口碑不错,但也并不是那么好用。 ...

我不是最狂热的 Java 粉,虽然也会在某些需要的情况下用到 Java 代码,但仍然不太习惯。和其它不怎么使用 Java 的人一样,我觉得 Java 有些刻板:它占内存且运行慢,虽然在编程界口碑不错,但也并不是那么好用。

我因为精心解析 PHP 这篇文章与大家结缘。在我写出这篇文章之前,我认为 PHP 挺难摸透的,为此遭受了不少挫折。而对于 Java,我在试图了解它的同时,会觉得它……无趣。

换个角度说。PHP 流行是因为它有一个独特的优势:它抛弃了 Web 开发中一些赘余的步骤。在这一点上,Java 没有能与之相比的显著优势,但它一定有它自己擅长的领域,否则 Oracle 就不会吹嘘有9万亿设备都在运行 Java了;Java 也不会有胆量支持如此大量的大型网站;占据着主导地位的智能手机操作系统也不会建立在 Java 之上。我可不会想要一个运行 JVM 的烤面包机。(它会比想像的工作得好,但需要花点时间等它预热。)

所以也许 — 也许 — 我误解了 Java。也许我对它的看法存在偏见。也许吧。

或许我应该重新审视自己对 Java 先入为主的观念。只要你愿意,也应该消除成见。

介绍一下,我涉猎了许多种语言,但我主要是做 Python,我喜欢它的很多设计模式。Python 有一些概念与 Java 相同,这也是一个很有趣的比较点。

Java 很慢

我觉得 Java 内存大运行慢。这句话一直出现在我有脑袋里。每当我想到 Java 的时候,就会想起在桌面应用中遇到的一些阻碍。最臭名昭著的可能是 Azureus —— Java 写的 BitTorrent 客户端,在十多年前大家用它来下载 Ubuntu 的发行版。但它只能用来下载文件,而且还出人意料的慢。所以当 µTorrent 出现的时候,大家都换过去了。µTorrent 是 C++ 写的,更快更高效。

这都不重要。我在那些年唯一使用的 BT 客户端是 Deluge,它是用 Python 写的。在所有测试中,Python 都没有 Java 快,但我用 Deluge 一直没问题。有时候使用 JVM,是因为某些场景下它真的很快。那什么时候我又会觉得它慢呢?

我们都知道,现在要做一项真实的测试是很困难的;有许多因素影响着速度,一个程序与自身进行比较都不全面,更不要说比较两个完全不同的软件。某个机制陷入困境了吗?GC 发生得不是时候?某种语言在进行特定任务的时候会更好一些?这些因素通常不可控,即使我能控制,测量对速度的感知仍然是棘手的事情。那么,除了这些,我能做的最好的事情就只有思考。

慢的程序,慢的语言

我怀疑我在这里有点偏见。如果程序慢,我们会责怪程序——但是如果 Java 程序慢我们就会责怪 Java。在这点上对 Java 特别的不利。由于 Swing 太显眼了 ,很容易注意到 GUI 是用 Java 写的,但是很难确定是不是 C++ 或其他类似的写的。

以我的经验来看,这在博弈圈特别明显,在那里你可以从从来没有写过一行代码的人身上发现对博弈执行的深入了解。 Minecraft 缓慢是因为它是用 Java 写的,你看,然而 Starbound 慢是因为开发者没有“优化”它,不管是什么没有优化。

还有一个可能的因素,但我不确定这是否真实:在我的印象中许多开发者使用 C++ 是因为他们想用,而用 Java 是因为他们不得不用?我看到过许多人使用 C++ 代码都没有什么特别的理由,但是许多使用 Java 代码的人都会说“我使用 Rails 编写,当它太慢时就移植”(或者“它是为 Android 开发的,因此我不得不使用 Java”)。换句话来说就是,Java 似乎是专门用来加速那些已经太而慢的代码的,因此让 Java 程序来处理非常繁重的任务是合理的。这样来看,这不怪 Java 慢,使用 Java 写的往往是一些慢的程序。这其实是一个自我选择的问题。

与此同时,许多 C++ 写的是纯粹出于跟风。或者更确切地说,有些程序员只想着获得最大性能而不管他们是否需要。因为他们认为冒虚假的段错误和内存错误的风险来反对“裸机”是完全值得的。因此那些为了使用 C++ 获得高性能而使用 C++ 的做法未必合理。

问题是不是出在 JIT 上面?

我原本试图将运行缓慢的问题归结到 JIT 的热启动这个上面,但又不确定是不是因为它。因为用 PyPy 比较多,所以我已经开始更加频繁的对 JIT 的热启动进行体验。PyPy 是一个 JIT 的 Python 实现 (使用其自身来编写,并因此而得名), 而阻碍它被更大范围应用的一大障碍就是启动要花费好几秒的热身时间。 Python 程序在使用 PyPy 运行时会有明显的延迟,而使用原装 Python 则基本上都是立即就执行了 — 而由于 Python 普遍被用于命令行工具,这样的延迟会对相当多的程序带来影响。

不过对于那些要运行超过半分钟或者更久的程序来说,PyPy 的速度显然快得多。我期望 JVM 会有一个比相对而言比较年轻 PyPy 更加先进的 JIT,那样的话热身时间一定会更短。我怀疑 Java 之所以会有一个运行缓慢的名声,只是因为它在开始运行的 20 或者 30 多秒时间里比较慢而已。

我也曾观察到使用了 LÖVE 的 LuaJIT 的游戏引擎, JIT 启动会实时地发生。如果你在屏幕上将帧率显示出来的话,会发现它会在几秒钟之内从一开始的 10 左右呈斜坡趋势上升到 60。此后的运行就正常了。这里的减速耗时还是相当短的,而且并没有让我觉得引擎或者 JIT 运行缓慢。

作为玩具的 Java

我敢打赌如果要追溯 Java 发展,那一定有很多要说。一些 C++ 开发者当然会把 Java 当做 C++ 的再创造,但它比 C++ 要慢... 原来的 Sun JVM 好几年都没有一个 JIT,因此早起的 Java 很像现在的 Python,是以字节码解释的方式运行的 — 不过那会儿是运行在要比在慢得多的硬件上。

从文化观念的角度来看这也是有意义的。Java 是作为一种“严肃的”语言而出现 — 例如它是 C++ 的一个竞争者 — 而当时唯一“严肃的”语言就是 C++。任何一种就像是解释型的语言在很大程度上都会被视作一个玩具。如果将“人们可以拿来用于进行严肃开发的语言”范围从 C++ 延伸到 Java 的话, 那么 Java 理所当然就像一个笨重的野兽了。自动垃圾收集? 这是什么?小孩子玩意儿么? 我甚至认为这连语言都区分不了。

快进二十年,我的操作系统有一半是用 JavaScript 写的。我甚至曾用 Perl(yolo) 写过一个桌面应用。难道相比之下 Java 会更好?

Java 很臃肿

我不喜欢 “臃肿”这个词。因为它代表不了任何优势,换句话说,它表示的性能是消极的,也是我们不在意的。

曾经有人对我说他们不用 GIMP 来艺术创作,因为它跟臃肿;还有人说他们不喜欢简单的 Paint.NET,因为它缺乏一个明确的功能特性,但是 GIMP 包含该特性。

为了更准确地说明这个问题,下面用一些例子来说明“臃肿”的含义。

Java 程序占用大量内存

一提起 java 的内存,我就会想起我在 TECHCOMPANY 工作时候的一个故事。那时候我负责我们的构建系统,使用 Closure 编译器,这是一个 Java 编写的 JavaScript-to-JavaScript 的"编译器"。

这个构建系统运行 Closure 时使用了参数 java -Xmx1024M,将最大堆内存设置成 1GB,但是手动使用 Closure 的时候,我经常会忘记这个奇怪的 -Xmx 参数,然后 JVM 马上就会挂掉。后来我发现了有个 _JAVA_OPTIONS 的环境变量可以保存这些参数,不用我每次去记它。但是即使我加上了,这个现象还是会出现,最后聪明的我仔细研究后找到原因。

我们使用的是共享的开发机器,每台 64G 的内存,并且设置了 ulimit -v 为 5GB 大小,防止任何进程分配的内存超过这个大小。默认情况下,JVM 在启动的时候会分配机器物理内存的1/4,很明显这个内存分配超过了 5GB 然后失败了,这时候 JVM 就立刻关闭了。(文档建议堆内存默认至少 1GB,但是这种说法可能不准确

这个问题其实不能怪 JVM,Linux 会尽可能的将机器内存分配给进程使用,只要不用完所有的机器内存,一切都工作的很好。JVM 利用这个好处提前分配一个超大内存块,从而避免了后面多次分配工作(耗时操作)。ulimit 可防止某个进程随意占用大量内存的进程而导致机器挂掉,但是它设定的这个限制值没有任何意义(悲哀,ulimit -m 原本是用来限制实际内存使用的,从 Linux 2.4 之后就没实际意义了)

在我脑海中,有这么一个印象"一次 Java 需要 16GB 内存"。这样容易让人误解的就是 Java 完全就是个内存黑洞啊,其实这种说法是有失偏颇的。就算 Java 本身好像需要这么多内存,但是如果我想想之前的事,就不会再这么认为了。

我能想到的绝大部分性能原因是:当 Java 程序变得庞大的时候会被谴责,但是 C++ 程序变得巨大的时候却不会被谴责;似乎复杂的任务用 Java 来实现就会挂掉;C++ 开发者鄙视垃圾回收和对象开销。

对于单个应用程序来讲,速度已经很难来量化,因而程序大小变成决定因素。每次运行同样类型的程序,同样的值占用的内存是一样的。例如,我们知道一个 CPython 对象占用16字节——一个类型指针和一个引用计数。我不是很确定 Java 中的对象占用——实际的内存使用取决于虚拟机的实现细节,并且 Java 中有很多虚拟机可以使用。(我只知道我所熟悉的 CPython)。但是我严重怀疑 JVM 会比 Python 用的内存更多,不过很多时候它需要的内存要少很多——Python 没有原始数据类型。所以, Java 的表现不应该比 Python 还要差,我并不认为 Python 是内存黑洞,因此我敢肯定 Java 也不是。

伪科学

我们大胆的推测,如果内存大小更能说明问题,那么让我们来做一些测试。这里我们简单地来与几个任意选择的桌面应用程初始内存的使用量进行对比,可能缺乏科学依据。

  • XMind (Java), 一个思维导图器: 416 MB

  • yEd (Java), 图形编辑器(如流程图所示,不是y =x²): 372 MB

  • jDiskReport (Java), 一个磁盘空间上报的东西: 228 MB

  • muCommander (Java), 一个MC风格的文件浏览器: 183 MB

  • FreeMind (Java), 另一个思维导图器: 181 MB

  • SLADE (C++/wxWidgets), Doom地图编辑器: 93 MB

  • GIMP (C++/GTK), 图像编辑器: 88 MB

  • LMMS (C++/Qt), 一个数字音频工作站: 75 MB

  • Deluge (Python/GTK), 一个BitTorrent客户端: 85 MB

(顺便说一下,所有的 Java 程序启动至少有 11GB 虚拟空间 --- 除了 FreeMind,因为它带有一个添加 -Xmx的shell 脚本。即使一个简单的 hello-world Java 程序都占用了 10.5 GB 的空间,所以默认初始堆的空间看起来仍然很大,我有 32 GB 的物理内存,所以一个四分之一将是 8 GB;其他的 10.5+ GB 的值我不知道来自哪里)。

即使了解到有许多因素正式针对 Java,我也没想到会有那么多反对意见。GTK 和 Qt 或许可以同其它进程共享一个库,Python UI 甚至都会对这些库进行封装并将它的许多数据放到 C 的层面来进行存储;Swing 几乎整个都立足于 Java 之中,而且只能在打开的进程中使用到。当然 Java 拥有一整个运行时 (似乎得用掉至少 26 MB 的空间), 而 C++ 的 “运行时”则要微观不少。Python 也有一个运行时, 不过 Deluge 比其它的要简单许多; 它只不过是我手上的一个非 C++ 类的东西而已。而 XMind 以及 yEd 则因为某些方面的原因,并没有什么代表性。

Java (就像 Python 一样) 因为反射和栈跟踪方面的缘故,也需要记住许多由遗留的 C++程序所产生的调试信息。垃圾收集往往会为了速度而牺牲备用内存;我并不了解 JVM GC 的运行细节, 但见过 GC 计算开销占到 100% 之多, 因此如果这会是一个重要因素的话,我并不会感到惊讶。我想说的是 GC 也会有很难解决的碎片问题。

Java 的字符串比较浪费

我怀疑另外一个罪魁祸首就是 Java 的字符串类型。Java 字符串是“Unicode”的, 采用的是 1990 年代定义的“Unicode”。Unicode 曾承诺它永远不会超过 65,536 个字符,因此两个字节就足够把任何东西都表示出来。Windows 的 AP,至少有一个 GUI 的工具包,Java,JavaScript (有可能是从 Java 抄过来的),以及其它语言似乎都对这上了心,傻乎乎地将字符串声明为全都使用俩个字节的字符。字符编码从此一劳永逸。

终于 Unicode 入“坑”,又承诺说它永远不会超过 1,114,111 个字符, 如此三个字节就足够可以表示所有东西, 但没有谁有三个字节的整型类型, 因此就四舍五入到了四。现在的 Java 就处在了一个非常尴尬的位置。ASCII 文本所占空间是其实际需要的两倍。而没有另外一个编码机制的话,Java 字符串就不能将所有的 Unicode 字符表示出来, 所以哪怕错误很多,他也要写文本处理代码,来假定每一个字符都是一个实际的字符。这听起来挺糟糕的。

字符串应该是 GUI 内存使用的大头, 因为有太多的标签和工具提示要显示, 这里面的门路可以说千差万别。我知道 XML 和配置文件在 Java 里也相当流行,而这些东西会很自然地产生大量的字符串。我发现 Java 9 正准备采取一种更加紧凑的方案,在这个方案中 ASCII 字符串的每一个字符底下都明显只会使用一个字节。Python 在几个发布版之前就切换到了一种类似的方案,而相比俩字节的构建版本,Django (web) 应用程序的内存使用降低了 40% 之多。Java 在重文本的程序中也可能会有类似的提升;一份关于对服务器影响的基准测试的报告指出,其发现堆的利用效率有 21% 的提升。

最后

而我希望 Java 远比希望的更紧凑,但这样的希望并没有发生。  228 MB 还不算糟糕,但 416 MB 就不那么合适了。我有次让 yEd 跑了一整晚,它没有打开任何其他进程就有了 854 MB 内存,当时它只是处于第三方程序之中,然后开了几个大型的画布,而这就让浏览器有了几百个选项卡页面,这让我惊讶不已,但又不知道该做些什么或者找谁帮忙。我还想说说非常有意思的一点,FreeMind 和 XMind 分别对应的是最好用和最不好用的,虽然它们用相同语言编写的类型相同的软件,但没想到差别那么大。

公平来说,如果没有刻意去观察,我真不知道 Java 的内存使用是什么情况。虽然我有 32 GB 的 RAM,但我的整个基于 Java 的手机还不到 2GB(不过这好像刻意调试)。也许我考虑的时候忽略了某些因素。因为虽然我认为 Java 大而慢,但我得承认它的应用范围要比 C++ 更加广泛,而其中的原因我也说不上来。

Java 程序占据大量文件系统空间

对此表示不置可否的同意,在此期间我也关注过周围,看其他人是否也对此表示认同。

因为我不知道我的观点是否合理。

Arch Linux 的安装包仓库很方便, 它列出了每个安装包安装后的大小。 我一直使用的 Closure Compiler 是19.7 MB。你认为这很大吗?答案一般是肯定的。 与之最相似的是 Babel,它编译了最新 ECMAScript 以便更适应浏览器的项目,但它只有 7.0 MB 的大小。而 Closure Compiler 能实现 Babel 的大部分功能,它更像是个传统的编译器,所以内存也相应的大一些,这都不让我感到惊讶,我惊讶的是 Closure Compiler 是 Babel 的两倍大。而 GCC 是116.1 MB。

都说通过大小来衡量软件的好坏并不实际,也极不科学。 但现在我对这一观点深表怀疑。例如,早期的桌面软件都颇具吸引力,但即使是 FreeMind 和 XMind —— 两个功能几乎一样的 java 程序 —— 安装后的大小也有差别,分别是 27 MB 和 138 MB,而它们的性能也的确有差别。

我现在还想不出一个好理由来说服自己 java 内存大是件好事。我在想,是否因为 java 应用程序自流行起就相对庞大 —— 那时很多人仍然在用拨号的方式上网,所以内存大的特点就一直保留到了现在。

在我停止对 Closure Compiler 找茬之前,我想对其他的“文件系统占用空间”做个解释——源文件的放置。大量的 Closure 源文件位于一个5级深的文件夹中,这看起来有点极端。而那个文件夹包含347个 .java 文件,多而拥挤。合理的做法是将这些文件分组归类到各自的类别中,但是他们....并没有这么做.

也许 Closure 是不喜欢走寻常路吧。GitHub 告诉我 star 最多的 Java 项目 elasticsearch实际的代码也隐藏在5级文件夹之下。 虽然每个目录似乎包含十个文件,但我没有看到任何目录有三百之多,当然,我没有每个文件都打开看。这些项目的源代码多得让人畏惧。相比之下大多数的 Python 项目就比较紧凑:较少的深度,较少的文件分布。

我怀疑这归结于 Java 的一个重要的属性:一个单独的文件只能包含一个(公共)类。就像我理解的那样,Java 没有输出的概念;如果你有一个文件 foo/Bar.class,可以被导入的唯一类是一个叫做 Bar 的公共类。不管你想怎样安排的代码,不管你的类是多大或多小,不管你有多少类,你需要为每个公共类拆分文件。规则是不同的,但是在某种程度上,让我有点想起了 Perl。哇,这说起来有点奇怪。

这和 Python 比起来,Python 有相当类似的包层级结构。文件 foo/bar.py 可以作为点路径 foo.bar 被导入,它的内容可以被指向 foo.bar.Bar. 。但是 foo.bar 将会是一个中间命名空间,一个包含了所有在文件中已经定义模块对象。导入看起来和 Java 的类似,但是文件是自由的。一个单独的文件可以包含许多小类,自由的函数,公共的管道,以及任何你想要的。Python 文件包含全部的想法,而 Java 文件只能是任何你认为应该是一个“类”。

我有一段很艰难的时期,你只要设想一下在一个分类极有限的文件中写个大项目,就能明白我的感受了。创建一个新类(而不是附加在一个现有的类上)就不可避免要涉及一些冲突;再创建一个全新的文件就必然会受到限制。我当然想要尽可能少的类,所以,这可能是为什么我会把 Java 与又大又长的类联系在一起的原因。每个类包含的东西越多,那总体上要用到的类就越少——这样好像就解决了分类有限的问题!

说来也奇怪,Java 有一个可以模拟 Python 的模块,但我从未见它被使用过。import static 支持直接导入嵌套类,所以一个文件可以包含一个大的公共类与多个静态公共类。顶层类是一个挂名的容器,类似于 Python 模块,其静态子结点的任何组合均可以被其他代码导入。这看起来不同寻常,Java 开发人员之间产生的混乱可能会降低它带来的优势。

再看看 elasticsearch,包结构不同于我们之前项目,这样的目录安排类似于 Python 项目。看看这个 分析  目录 ,大多数文件的样板类都不会比顶部的授权许可长度更长。这个单包有很多小 class;因为语言的需要,他们分布在多个文件上。这样说吧,elasticsearch 是一个相当简洁的工程。我甚至找到了一些目录,它们只有一个单一的文件或类,最终应该是模仿了 Python 模块的包目录。

从发展的角度来看,文件的扩张似乎有点讨厌,但这却是一个相当纠结的语言基本定律。对不起,Java,在这一轮你输了。

Java 拥有庞大的标准库

每个人都会有这样的抱怨吗?我不太确定。它与终端用户对软件“膨胀”的使用相似,因此值得一看。

标准库的权衡比较棘手。C 的微小标准库意味着许多项目会重新发明相同的轮子,它的好处是保持了语言本身简单性和可移植性。另一方面,Python 的大量的标准库非常方便,但已经累积了很多奇怪的垃圾代码,像诸多 IRIX 模块等。我偏好那种可以很好增强核心语言功能的标准库,虽然我愈加发现,一个坚实的包管理器可以使用厨房水槽装运语言本身更好。

目前,我还想不到更好的方法来衡量标准库的大小,只能衡量磁盘上的大小。如下是我安装在我的机器上的一些语言的运行时的内存,以及我用来测量它们的 zsh glob。直觉告诉我:Java、.NET 和 Python 有相当大的标准库,Rust 和 Ruby 有中等大小的标准库;而 Perl 非常原始。 C 的话是什么库都没有。

  • Mono 4.6: 43 MB —/usr/lib/mono/4.5/**/*.dll

  • OpenJDK 7: 33 MB —/usr/lib/jvm/java-7-openjdk/jre/lib/rt.jar

  • CPython 3.5: 27 MB —/usr/lib/python3.5/**/*.py~*site-packages*

  • Perl 5.24: 17 MB —/usr/share/perl5/core_perl/**/*.p[ml]

  • Ruby 2.3: 8.9 MB —/usr/lib/ruby/2.3.0/**/*.rb

  • Rust 1.12: 4.9 MB —/usr/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-*.so

  • C: 2.0 MB —/usr/lib/libc-2.24.so

好吧。 这让我有点惊讶。

不可否认,这是一个可怕的比较。 Rust 的标准库直接被编译成机器语言(使其内存更小)。我认为, Mono 和 OpenJDK 也是这样被编译成字节码的。 CPython,Perl 和 Ruby 库是未压缩的源代码(使内存更大),但是排除了一些二进制组件(使它们内存更小),不过它又包括大量的内联文档(使它们内存更大) ,幸运的是它由更高级的语言编码的(使它们内存更小)。

Perl 的地位使我更加怀疑这种方法。 我可以真正地得出结论是,Java 的标准库比 Python 的更大,看起来的确是这样——Java 和 Python 同样带有两倍的 GUI 包。 我在这里没有看到任何明显的迹象——Java 的标准库大的无法处理。

目前的评分

Java 是传说中的笨重的野兽吗? 结果不好说。我未能找到合适的理由来解释为什么 Java 会被指责特别慢,尤其是在如今这个越来越依赖于 JavaScript 的世界里,我接触 Java 的情况比较少。我认为 Java 是第一个广泛使用的、依赖于 VM 的语言,虽然它也因此受到大量抨击,但它依然保持自我。

另外,我也没法很好的解释,为什么 Java 更加消耗内存,或者说,为什么它在桌面系统上运行比移动端上糟糕得多。当然,这有可能是我有限数据点的问题,但也有可能是因为 Java 真的是一个内存“贪吃鬼”,如果真是这样,就能解释为什么诸多桌面软件都不采用 Java 编写了。我很少关注 Java 在手机上的运行状况,因为我并不负责这一部分的内容。

我是一个程序员,但我不是一个 Java 程序员,所以通常会从终端用户的角度看问题——这可能就导致了对 Java 不公平看法。 Java 只是想提醒桌面用户自己在做什么事情,虽然这样的提示比较恼人,比如在启动时运行了一个快速启动服务或 bug 更新。我说过,Swing 很显眼,我用过的程序已经证实了这一看法:他们使用错误的颜色和字体,Java 默认的像素字体渲染不好,经常会产生一些奇怪的效果,如在 Linux 上模仿 Windows 95 文件选择器对话框。每次我使用 Swing 程序都会抱怨——我认为用“笨重”形容 Java 挺合适的。

不过 Java 在桌面上的应用正逐渐变小,并且正在从浏览器中消失,所以也许我这个终端用户的观点并不重要。在服务端 Java 仍然十分流行,它几乎完全支持 Android,这其中一定包含了许多不错的性能。现在我对 Java 仍然持怀疑态度,也保持着好奇心。在第二部分中,我将从开发的角度出发,试图了解 Java。


酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部