欢迎光临BDM
一枚菜鸟码农的成仙之路

如何计算一个 Java 对象的内存?

huangluyu阅读(341)

之所以用这个标题来写这篇文章而不是【如何使用 JMap】、【如何分析 Dump】等,是因为我一开始确实只想知道该如何知道一个 Java 的内存,毕竟优化的前提永远是可度量的量化指标

前言

许多同为新手的小伙伴们肯定遇到过,在实现一个需求的时候,脑海中浮现出两种不同的实现方式,强迫症使你很想在【时间】和【空间】复杂度上进行衡量,像 LeetCode 和 CodeWar 一样,然后选择最佳的路线。但是,无论是【时间】还是【空间】基本都只能以自己微薄的数据结构知识进行估计,稍微灵活一点的就是用System.currentTimeMillis()放在两端比较差值来计算【时间】了。

LeetCode最希望见到的字符串:您的执行用时已经战胜 99% 的Java提交记录

今天正好我在写面试作品时也遇到了这个需求,而我实在不知道也记不清哪个类占据多少,什么double比较Double该如何选型等。

恰好强迫症如我一定要搞清楚哪个好,于是翻阅搜索引擎,一共得知了两种方法。第一种 Java 代码编写的自动化估算,通过反射获取类型,再根据类型进行估算,网上有很多类似的;另一种就是本文即将展示的,直接内存读取法,直接看到对象占据了 JVM 占据了服务器多少的空间。

建议有实力的大家自己手工实现一下第一种,可以建立对 Java 对象内存结构的清晰认知,避免像我一样不知所措。

工具链

获取对象内存使用的工具主要是 JMapJVisualVm,两款都是 JDK 自带的工具,在 jdk_path/bin 目录下。

配好了环境变量后,可以直接在命令行里使用上述命令。如果使用了 Windows 安装版,环境变量会自动配置。如果命令行里能使用 Java 而不能使用上述命令,检查一下环境变量配的是 JDK 的还是 JRE 的。

基本流程

  1. 首先通过 任务管理器 或者 ps / jps 等命令找到想导出的 Java 进程的 pid。

不知道如何找请看我的另一篇基础文章 解决端口占用及快速定位Java进程

  1. 通过 jmap 命令导出该 Java 进程的 heap dump 文件。
# 仅为示例。具体使用方式建议自行查看 jmap -h
jmap -dump:live,format=b,file=./heap.bin [pid]

这样,当前文件夹(指定了./)下就会生成heap.bin 文件。它里面存放了那一时刻内存的所有信息。

  1. 直接输入命令 jvisualvm 打开内置的 Visual Vm。

Visual Vm 自带图形化界面,有诸多丰富的功能。内置的是 Java 版本发行期的稳定版本,可去官网上下载最新版,暂时还不知道有什么区别。

打开后如下图所示:

在左上角选择【文件】-【装入】后,选择 heap 文件,便会出现上图中间的信息。左边区域代表着一些操作选项及本地运行的线程。右边是打开的搜索框。

对了,我们的 dump 的目标如下。依然不忘初心,我们只想获得 Double 和 double 在内存占用上的区别。

public class TestDouble {

    public static Double thePackingType = 1d;

    public static double theBasicType = 1d;

    public static void main(String[] args) {
        while (true) {
            // Don't die
        }
    }
}

选择到【类】之后,通过下方的搜索,输入 Double 可检索获得 java.lang.Double 的内存类别。然后双击点进去可以看到单个 Double 实例为 24 字节,如下图所示:

double 这种基础类型在 visualvm 里没找到,于是通过查询资料(记忆)获取到它是 8 字节

其实通过 Double 类的源码可以看到,Double 对象里都包含一个 double 类型的 Value,多出来的 16 字节是用于对象头的开销。对象头在 64 位系统下是 16 字节,32 位下是 8 字节。

三倍的内存差别,还是很明显。在 Double[] 和 double[] 选型上,能不用 Double[] 就不用。


参考文档:

Java BIO NIO AIO 简述

huangluyu阅读(1226)

什么是 Java I/O 系统?

Java 开发中,时常我们会用到除了 JVM 内存之外的数据,从数据库、文件或者网络中读取(通常情况下数据库也属于网络)。这些与外部数据源交互的方式,就是 I/O(In and Out)。Java 提供了一整套详尽而完善的 I/O API 来帮助开发者进行 I/O 操作,以传统的流式 API 为例,有如下的分类:

  • 读取、写入
  • 顺序、随机
  • 缓冲
  • 二进制、按字符、按行、按字
  • ……

Java I/O 系统进行了漫长的发展,都没有超出上述的需求,而是从使用方式和底层实现进行了优化和升级。Java I/O 系统总共经历了三个时代:

  • Java 1.0 的 BIO (Blocking I/O),同步阻塞 I/O
  • Java 1.4 的 NIO (New I/O),同步非阻塞 I/O
  • Java 1.7 的 AIO (Asynchronous I/O),异步非阻塞 I/O

BIO是传统的 Java 阻塞 I/O,速度最慢,以流的形式提供 API,功能强大而使用繁琐。NIO 将 I/O 交大部分的内容交由操作系统底层实现,所以速度快于 BIO,但 NIO 需手动实现同步非阻塞的轮询过程,使用起来同样麻烦。AIO 实现了异步 I/O,使用极其方便,但增加了多线程的开销。

在介绍这些 API 之前,我想先介绍一下其概念和原理,有助于更完美地运用 I/O 系统,减少使用过程中的误解和困惑。

什么是同步和异步、阻塞和非阻塞?

一个首当其冲的问题,到底什么是同步和异步、阻塞和非阻塞呢?这个问题曾经给初学的我带来了很大的困扰,究其原因,这类对立的描述在不同的语境下都有着略微不同的含义。尽管这些描述都包含着大部分的共同点,但那细微的区别,可能是引起困惑的主要原因,所以我们一定先把理解范畴局限于 Java 中。看了很多文章之后,我姑且总结如下:

同步:由调用方主动执行 I/O、数据获取和缓存

异步:由调用方执行 I/O,第三方帮调用方获取数据和缓存

阻塞:调用方会进行阻塞,直至 I/O 完成

非阻塞:调用方立即会获得返回,无论 I/O 是否完成

以我去饭店吃饭为例来表达这个差异:

同步:我去饭店吃饭,打饭得自己来

异步:我去饭店吃饭,我向服务员要饭,服务员把饭送到我手上

阻塞:饭店顾客太多,吃饭得站外面排队

非阻塞:饭店顾客太多,吃饭得先叫号,轮到我的号就可以进行吃饭

这里的就是调用方,打饭是 I/O 操作,服务员是第三方。两两结合后,应该是这样的:

同步阻塞:我去饭店吃饭,打饭得自己来,并且人多时需要排队

同步非阻塞:饭还没做好,于是我隔一会儿去看一下,饭做好了就打饭

异步非阻塞:饭还没做好,于是我告知服务员做好后送给我

异步阻塞在大部分语境下是不成立的,至少在 Java 语境下如此。Java 中,异步等同于另立了一个线程来执行任务,异步用来描述某个 API 时意味着该 API 一定能立即返回。但阻塞代表着当前线程执行某 API 时会遇到阻塞,直到完成所有工作才能返回,并继续执行。异步生成的线程哪怕遇到了阻塞,也无法对主线程造成影响,主线程无论如何不会因之而阻塞,所以异步阻塞是不存在的。

无论是同步、异步还是阻塞、非阻塞,他们都是在 API 上或者说高层次上的体现,而底层甚至到内核是如何运转都可能完全不同。不要太在乎他的称呼,只要弄清楚他的实现原理,便能灵活运用来实现我们的需求。

什么是 Java 中的 BIO NIO AIO?

首先看三张经典 UNIX I/O 模型的图片,Java 的三代 I/O 模型也是依赖于这个基础来实现。

Java BIO 底层模型

BIO

BIO 的流程中,首先线程调用 API 发起 I/O 请求,然后该线程进入 BLOCKING 状态,直至 I/O 操作全部完成。

实际 I/O 流程中,JVM 通知内核进行 I/O,内核将数据放在了内核空间,然后 JVM 主动将内核空间的数据移至自己的内存中(事件机制或者轮询机制,这点我暂时没有深究),再改变线程的状态,使其继续运行。

Java NIO 底层模型

NIO

NIO 的流程中,首先线程调用 API 发起 I/O 请求后,需要不断轮询是否完成 I/O,轮询时能立即获得当前已传输的部分数据,并对之执行逻辑。

NIO 在 BIO 的基础上新增加了 Channel 机制,依赖于操作系统内核。在 BIO 中,由 JVM 主动将内核空间中的数据移植用户空间(JVM 进程)中,而 NIO 为内核自动将数据同步至用户空间(JVM 进程)。

NIO 新增 Zero 复制,由 JVM 调用内核的复制功能在内核空间进行复制,无需加载至用户空间中。

NIO 实现了虚拟内存映射,将内核空间地址和用户空间地址进行映射绑定,使得 DMA 硬件(只能访问物理内存地址)能直接对 JVM 内存进行访问。

Java AIO 底层模型

AIO

AIO 在 NIO 的 Channel 上,新增了异步的 API。线程发起异步 I/O 请求时,将指定回调的任务,该任务由两个方法组成:I/O 执行成功和 I/O 执行失败,当 I/O 完成后,则会依据结果来执行这两个方法中的一个。

实际上,AIO 会使用另一个线程来执行回调任务,故在普通场景中,由于线程上下文切换性能有所损耗反而使效率降低。

AIO 同样依赖于操作系统内核的异步机制来实现,实现了 Proactor 模式。Proactor 模式中,应用程序需要传递缓存区,也就是 NIO 中的 Buffer,但与 NIO 不同的是,该 Buffer 需要足够大来容纳所有数据。

Java NIO 多路复用模型

NIO3

NIO 多路复用模型是使用 Selector 来实现的,Selector 可以简单地理解为 Channel 的容器,并且提供了一些简单管理的 API。Selector 内部对 Channel 用 SelectionKey 进行了封装,一一对应,使 SelectionKey 能包含更多的信息,Selector 通过内部 SelectionKey 的轮询来获取 Channel 的状态来实现需求,但这一切需要手工编码来实现 。

很多人这种模型称之为异步阻塞模型,我并不认同,对于它的 API 来说,它是阻塞的;对于运行状态来说,它是同步的(运行在当前线程上)。但它通常使用方式是以另一个线程轮询开启 Selector 监听(谁也不会让一个专职阻塞的 I/O 运行在主线程上),所以他被理解为异步阻塞模型。

Java 中的 BIO NIO AIO 简单实践

BIO 读取文件

@Test
@DisplayName("Java BIO file read")
void bio() throws IOException {
    // 创建文件输入流【对接操作系统,获取共享锁】
    try(FileInputStream fileInputStream = new FileInputStream(filePath)) {
        // 新建输入接收区,用于缓冲,非必需
        byte[] fileBuff = new byte[fileInputStream.available()];
        // 文件读取到接收区
        int fileLength = fileInputStream.read(fileBuff);
        // 将 byte[] 转化为String,以供输出
        String fileStr = new String(fileBuff);
        // 打印输出
        System.out.println("文件长度:" + fileLength);
        System.out.println("文件内容:" + fileStr);
    }
}

BIO 的流式读取比起另外两种很大的特点是不需要缓冲区,当调用 read 方法时会直接返回一个字节或者字符的单个或者数组,这可以节省到内存,但也失去了缓冲区的灵活性。

由于阻塞的特性,BIO 在 Socket 中会阻塞端口,占用端口资源。

NIO 读取文件

@Test
@DisplayName("Java NIO file read with ByteBuffer")
void nio() throws IOException {
    // 创建随机文件读写器,读+写模式,非流
    // 获取文件 Channel
    try (RandomAccessFile file = new RandomAccessFile(filePath, "rw");
         FileChannel inChannel = file.getChannel();){
        // 初始化 NIO 的 Buffer,此处为 Byte 类型的 Buffer
        ByteBuffer buf = ByteBuffer.allocate(48);
        // 定义数字用于接受每次读取的字节or字符数
        int bytesRead;
        // 读取到缓冲区,并判断读取长度
        while((bytesRead = inChannel.read(buf)) != -1) {
            System.out.println("本次循环读取字节数:" + bytesRead);
            // 将 Buffer 由写模式改为读模式
            buf.flip();
            // 判断是否存在待读取内容
            while (buf.hasRemaining()) {
                // 每次读一个字节/字符
                System.out.print((char) buf.get());
            }
            // 将 Buffer 由读模式改为写模式
            buf.clear();
        }
    }
}

NIO 通过创建 BIO 的输入输出类来获取 NIO 的 Channel,然后对 Channel 进行操作,Channel 依赖于 Buffer 缓冲区。

较之 BIO,NIO 必须指定 ByteBuffer 缓冲区,并且 NIO 需要复杂的轮询控制,并手动操作 ByteBuffer 的状态来使其正常运行。关于 Buffer 的学习可以参见 http://ifeve.com/buffers/。

文件操作在 Unix 下因其设计,无论如何都会被阻塞,无法实现真正的 NIO。

NIO 的 Selector 专用于网络 I/O ,无法适用于文件 I/O,实现方式可以理解为使用观察者模式维护了一个 Channel 的列表并且对其进行轮询,代码较多,这里暂且不贴。

AIO 读取文件

@Test
@DisplayName("Java AIO file read with ByteBuffer")
void aio() throws IOException, InterruptedException {
    // 获取文件 Channel,这里和 NIO 类似,都是面向 Channel 的,但 AIO 使用是异步 Channel
    // 创建 Channel 的过程中,文件
    AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(Paths.get(filePath));
    // 创建缓存区
    ByteBuffer byteBuffer = ByteBuffer.allocate(48);
    asynchronousFileChannel.read(byteBuffer, 0, "AIO Test", new CompletionHandler<Integer, String>() {
        // 读取成功
        @Override
        public void completed(Integer result, String attachment) {
            System.out.println(Thread.currentThread().getName() + " 读取成功!");
            System.out.println("本次读取字节数:" + result);
            // 将 Buffer 由写模式改为读模式
            byteBuffer.flip();
            // 获取数据,并打印
            byte[] data = new byte[byteBuffer.limit()];
            byteBuffer.get(data);
            System.out.println("本次读取内容:" + new String(data));
            // 将 Buffer 由读模式改为写模式
            byteBuffer.clear();
        }

        // 读取失败
        @Override
        public void failed(Throwable exc, String attachment) {
            System.out.println("read error");
        }
    });
}

AIO 较之 NIO 不需要控制轮询,代码非常清晰简洁。

使用 AIO 时一定注意 ByteBuffer 的大小一定要大于数据长度。在 NIO 中,哪怕 ByteBuffer 设置很小,也可以通过多次轮询把数据全部取出来,但是在 AIO 中,completed 方法只会执行以此,超出缓冲区大小的数据将会被遗弃。

【BDM-ORM】一款链式 SQL 查询的 SQL 拼接器

huangluyu阅读(1893)

最近正在开发的一个 ORM 模块,提供如下的链式 SQL 查询的方式:

List<BaseModel> sql = Table.select("person.*")
        .from(Table.select("name", "id")
                .from(Person.class), "person")
        .leftJoin(PersonInfo.class, "person.id", "pi.id", "pi")
        .where(Where.init("person.name", "=", "xiaoMing")
                .orWhere("person.id", "=", "1"))
        .andWhere("pi.password", "!=", null)
        .andWhere("person.id", "=",
                Table.select("id")
                        .from("person")
                        .where("person.id", "=", "1"))
        .get();

你会得到下列查询 SQL 产生的结果(MySQL)。

SELECT
    person.*
FROM (
    SELECT
        name, id
    FROM person
) person
LEFT JOIN person_info pi ON person.id = pi.id
WHERE (
    person.name = 'xiaoMing'
    AND person.id = '1'
)
AND pi.password IS NOT NULL
AND person.id = (
    SELECT
        id
    FROM person
    WHERE person.id = '1'
)

更多信息移步至 http://git.huangluyu.com/huangluyu/bdmorm 进行查看

JRebel配置教程(二):Tomcat独立部署

huangluyu阅读(2006)

上一篇JRebel配置教程(一):注册及IDE配置(Eclipse)已经详细介绍了 JRebel 的免费注册及在 Eclipse 下的配置方法,下载插件加上简单配置即可搞定,享受快速地本地无缝编程体验,IDEA 的配置不再赘述。但在项目上线部署时,仍然有复杂的操作及漫长的等待验证时间。

今天我们来总结一下,如何使 JRebel 不依赖 IDE,独立配置在服务器上,本教程最后更新时间 2018 年 3 月 8 日。

运行环境

  • Tomcat 5.X ~ 8.X
  • JRebel
  • Linux 64 位

下载及解压 JRebel

首先从官网上下载好安装包,使用解压至服务器中存放 JRebel 的目录,Tomcat 的版本为 5.X 及 6.X 时 JRebel 的路径中不能有空格

以 CentOS 6.9 为例:

  1. 首先从官网下载好 ZIP 安装包,安装包名如jrebel-x.x.x-nosetup.zip所示,其中x.x.x为下载的版本号。
  2. 通过 FTP 工具将jrebel-x.x.x-nosetup.zip上传至服务器的~目录,也可以选取其他目录。
  3. 使用 CentOS 自带的unzip命令解压 JRebel zip 文件,如下文所示:
# 命令为:unzip JRebel-zip-路径 -d JRebel存放文件夹
unzip ~/jrebel-x.x.x-nosetup.zip -d /usr/local/JRebel

这里使用 CentOS 默认的软件文件夹/usr/local来存放 JRebel,同样也可以使用其他目录,但记得修改上述命令对应的目录路径。

激活 JRebel

执行 bin 目录下的activate.sh+激活码进行激活,如下文所示,没有激活码请查看上一篇文章进行申请。

# 命令为:%JREBEL_HOME%/bin/activate.sh 激活码
/usr/local/JRebel/bin/activate.sh rO0ABXNyAChjb20uemVyb3R1cm5hcm91bmQubGljZW5zaW5n
LlVzZXJMaWNlbnNlAAAAAAAAAAECAANMAAdkYXRhTWFwdAAPTGphdmEvdXRpbC9NYXA7WwAHbGljZW
5zZXQAAltCWwAJc2lnbmF0dXJlcQB+AAJ4cHB1cgACW0Ks8xf4BghU4AIAAHhwAAACZqztAAVzcgAR
amF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAA
AAAAAYdwgAAAAgAAAAFHQAA3VpZHQAJDljNzMwZDgzLTdkOTItNDQzMC05MWFlLTgyMGE1NTRjYmM2
NnQACGooF9BAJqZHyD+D76s=

成功后会显示JRebel successfully activated!

配置 Tomcat

官网推荐以继承脚本的方式在启动 Tomcat 前动态添加启动项来启动JRebel。

首先在$TOMCAT_HOME/bin下新建catalina-jrebel.sh,然后将下列代码粘贴进去保存退出。

#!/bin/bash
export REBEL_HOME=JRebel root folder.
export JAVA_OPTS="-agentpath:$REBEL_HOME/lib/libjrebel64.so $JAVA_OPTS"
`dirname $0`/catalina.sh $@

这样可以无需备份及修改默认的catalina.sh

然后在bin目录下运行./catalina-jrebel.sh run即可享受JRebel服务。

参考资料

  1. Running JRebel with command line

【2018.07.05已不可用】JRebel配置教程(一):免费注册及IDE配置(Eclipse)

huangluyu阅读(1982)

前言

所有开发过大型 Java 系统的开发人员一定会诟病其令人发指的启动部署速度,诚然,部署时提笔练字、浏览论坛、抽烟喝水或者摸鱼放羊等操作已经是业界共识,但针对一些需要不断调试的敏感模块,还是太让人难过了。本文作者在一次又一次等待 Tomcat 部署期间,找到了一款 Eclipse 热部署软件——JRebel,减少了成吨的开发时间,下面将介绍一下此款插件的安装及部署。

 

安装与注册

JRebel 安装方式支持直接下载安装、内嵌市场安装以及输入软件源安装,本文介绍最方便快捷的内嵌市场安装,其他方式百度均有大量资料。JRebel 为收费软件,个人版485美元一年,但其下网站MyJRebel 推出了个人社交版,只需要将其绑定上 Twitter 或者 Facebook 账号即可免费获取。

 

使用 Eclipse 自带的应用市场安装 JRebel

  1. 打开应用市场

打开 Eclipse,选择菜单 Help,选择选项 Eclipse Marketplace,如图1所示。

图 1 打开 Eclipse 应用市场

  1. 安装 JRebel

打开标签页 Search,在红框1 Find 处输入 JRebel,点击红框2处的 install,开始安装。

图 2 搜索 JRebel,点击 install

  1. 安装完成,等待激活

一路点击 Next,注意,其中在第二个 Next 的时候,取消 Maven Plugin 的勾选,仅安装前两个,直至安装完成,重启 Eclipse,开始下一步。

 

在 MyJRebel 上注册社区版

  1. 注册MyJRebel

访问MyJRebel注册页面,输入邮箱及密码,点击 Next ,如图3所示。

图 3 注册MyJRebel

  1. 获取激活码

一路点击 Next,完成注册。注册后登录,在首页进行手机号以及社交账号的绑定,当然,社交账号绑定这一个操作就得八仙过海了。完成后点击左菜单栏 Install And Activate,复制图4红框2处激活码。

图 4 获取 JRebel 激活码

  1. 激活 JRebel

回到JRebel安装结束的激活页面,选择 I already have a license,在图5红框处粘贴激活码,点击右下角 Active,即可安装成功。

图 5 激活License

 

部署与运行

至此,JRebel 已经安装完成。关于 JRebel 热部署的实现机制,已有大量的资料,故此处不再赘述,但正式使用 JRebel 之前,仍然需要几步准备工作。

 

增大Java栈内存空间

Java 默认栈内存非常小,我在开发公司的系统时,系统里大量 jar 包都将在启动时消耗栈内存空间,而垃圾回收机制并不会对启动过程中的栈内存生效。加之 JRebel 也需要增加20%栈内存使用的情况下,使用公司系统默认的本地开发配置参数,必然会报出OutOfMemoryError: PermGen space的错误。下面将介绍 JVM 启动参数设置的方法,该设置会覆盖所有其他地方的同名参数设置。

  1. 打开设置菜单

打开菜单栏 Window > Preference,选择 Java > Install JREs,点选右边区域中 Tomcat 使用的 JRE,然后点击 Edit,如图6所示。

图 6 JRE设置

  1. 添加启动参数

在红框1处添加启动参数,至少一项 -XX:MaxPermSize=1024m,如果仍然有栈内存超出的错误,继续增大该数值。点击 Finish,完成设置。

图 7 添加JVM启动参数

 

简单配置JRebel运行

关于 JRebel 初始化设置,官方教程有更详细清晰的介绍。注意第三步中,使用 Eclipse 开发是 Run via IDE,然后继续执行教程步骤即可。

 

后记

至此,JRebel 已经完全设置完毕。在 Eclipse 里开启 Tomcat,将看到 Console页面下,伴随着 Tomcat 的启动 JRebel 疯狂操作的 Log。微略增加启动时间后,与之俱来的是无尽的便捷。项目中修改Java文件后,点击Ctrl + s保存,JRebel将自动完成重新部署该class的操作,仅在修改了框架启动过程中需要加载的配置文件的情况下,才需要重新启动应用。

如果安装过程中遇到了问题,则回顾一下上文中加粗的部分,那里每一个字都是前人踩过的坑。

简述PHP与JAVA的主要区别

huangluyu阅读(1612)

PHP与JAVA的主要区别

本总结中前后几点之间通常存在因果或者关联关系,因为想要描述语言之间的差距,总是由点概面的。希望能通过关键性质的几点,来描绘出PHP和JAVA这两种工具间基本的区别。

该总结内容结合自身经验得出,深度及广度取决于作者的水平,望能海涵!

1. 数据类型

  • PHP为弱类型语言,JAVA为强类型语言。
  • PHP所有数据类型采用同一种数据结构,该结构体中包含了“数据”,“数据的长度”,“数据引用”的信息。其中“数据”底层实现为一个联合体,包含了整形,长整型,字符串,对象,哈希表几种类型。字符串类型为一个结构体。

2. 应用场景

  • PHP为web开发而生,只能用于web开发。
  • PHP进行web开发时不依赖任何框架,但运用上框架也能达到JAVA框架的很多优点,诸如MVC,面向切面,依赖注入。
  • PHP内置模板引擎,JAVA需要依赖其他类库。

3. 语言类型

  • PHP为脚本语言,在执行时编译,JAVA为半编译语言,在执行前编译。
  • PHP实时热部署,部署速度取决于存储介质读写速度。
  • PHP底层为C语言,所有类库都是C实现,而JAVA是JAR包,仍然是JAVA。

4. 多线程

  • PHP每一个请求都是一个进程,JAVA每一个请求都是一个线程。
  • PHP能完美利用多核性能。

5. 数据库连接

  • PHP天然不支持连接池,除非运用到常驻内存以及进程间通讯等相关技术。
  • PHP数据库连接底层为C,效率更高,能弥补一定的IO成本。

6. 垃圾回收

  • PHP的运行时GC机制为当该数据没有被引用时回收,该标识符存储在数据本身之中,参考1.数据类型。
  • PHP在请求结束时释放该进程所占的空间,常驻内存空间基本为0。
  • PHP使用命令能随时随地释放对象所占的空间,JAVA即使使用命令也必须等待垃圾回收。

7. 面向对象

  • PHP支持面向过程和面向对象两种方式,执行HelloWorld只需要一条语句。
  • 基于PHP是弱类型语言,PHP的对象也是一种结构非常松散的对象,更像是静态方法的集合加上数据的集合。

8. 面向接口

  • PHP的面向接口因为弱类型而非常不严谨,无法定义接口的返回类型和参数类型,导致面向接口被PHP社区默认为保留特性。

9. 语言特性

  • PHP拥有文件包含- 机制,使用语句引入另一个文件之后可以识别并包含引入其中的内容。
  • PHP拥有析构函数,可以在对象释放时(被引用为0之前)执行。
  • PHP不支持重载,可使用可变参数的函数曲线救国。
  • PHP默认方法及变量的权限是public。

后记

这篇对比文章写于大半年前刚刚由 PHP 转 Java 的时候,从那之后就投身入 Java 的怀抱,很少继续想过这个问题,今天面试很意外地被问到了这个问题,一时间竟然有点语无伦次。要放在半年前,这个问题我或许有清晰的答案,也能侃侃而谈谁是世界上最好的语言。但对于现在的我来说,再度思考这个问题,却有着许多不同的角度。

语言终归都是一门工具,对于普通开发者来说,工具只要能知道如何使用,方便使用,便能成为开发者实现自身价值的利器。放到这个问题来说,这篇文章里列出的语言特性所展现的差异,仿佛就无足轻重了。对于一个架构设计者来说,或许市场上是否已有成型的技术生态,是否流通着丰富的技术从业者,更影响对技术选型的判断。对于开发者来说,是否能快速掌握这门工具的使用,是否有足够的途径来支撑学习的过程,可能更为关键。

我们无法否认 PHP 简单易上手、快速构建应用的优点,也无法忽略 Java 在大型企业级服务构建中几十年来积累的丰富的经验,还是得参照各方面的因素进行抉择。

人生苦短,我选 PHP Python

联系我们GitHub