Java StackOverflowError 与递归及尾递归优化

本篇同样是阅读《100 Java Mistakes and How to Avoid Them》的一则笔记,只是火力全集中在 StackOverflowError 这个单一主题之上, 且主要与递归及尾递归优化相关。一提到 StackOverflow, 恐怕首先是想到那个代码搬运源网站 stackoverflow.com,其次才是代码执行过程中的 StackOverflowError 错误。

什么是 StackOverflow, 准确来说是指线程的栈内存不足,无法在栈中分配新的内存(或创建新的栈帧)。我们通常会把它与方法调用关联起来,因为一次方法调用会创建一个新的栈帧,分配在栈上的局部变量(包括基本类型与对象引用),和栈帧都要占用线程栈内存。而我们平时所说的方法调用栈,或出现异常时打印出的异常栈都是一个概念。

StackOverflowError 一般出现在方法调用太深(方法调方法),手动编写的方法调用一般不容易达到这个限制,所以它与递归调用关系最为密切,递归调用次数太多或甚至没有出口条件无限递归; 也可能是经过一番递归调用后,再正常调用后续方法时出现 StackOverflowError(因为前面的递归调用资源消耗的差不多)

何谓递归调用,简单的说就是方法自己调用自己,从而实现循环的效果,或使代码更精练,例如经典排序中的快速和归并排序就要用到递归。但循环与递归又有本质上的区别,循环不增加调用深度;递归却不同,递归分递进与回归两个过程,递进调用时每次调用前都需通过压栈来保留现场,逆向回归时再逐级恢复现场,保留现场的过程就要从线程栈中分配内存空间; 死循环可能不会造成程序异常,但死(无限)递归必定出现 StackOverflowError。 阅读全文 >>

《100 Java Mistakes and How to Avoid Them》笔记 3

本书的阅读又搁置了许久,虽然感觉 Manning 出版社的这一 100 Mistakes 系列从书的质量不是那么的高,但开了头还是继续从本书 40% 的位置往下。

开始要讲述到异常了,异常还是有必要认真对待的,比如

  1. Java 中很容易被 CheckedException 弄得代码不整洁
  2. 缺少必要的参数检查,不舍得抛出异常,视异常为 Bug
  3. 不明确出现异常时后续如何处理,
  4. 或者是捕获而隐藏了异常致使定位错误变得更难。

Java 的主要异常大分类是

Throwable
├── Error
└── Exception
          └── RuntimeException

NullPointerException, 这恐怕是一个最常见的异常,Java 对一个对象是否能为 null 值没什么约束,甚至用 null 来表示业务上的空。比如说方法的参数与返回值,Java 都可以是 null 值,而在 Kotlin 中非明确可为 null 的时不能为 null 阅读全文 >>

本地和 ECS 容器(EC2/Fargate) 如何处理 ENTRYPOINT

不觉一晃还是在五年前记录过一篇 Dockerfile 中命令的两种书写方式的区别,其中提到过 Dockerfile 中可选择用 ENTRYPOINT 或 CMD 来启动进程,并且在 ENTRYPOINT 和 CMD 都支持  exec, shell 和增强型 shell 方式。如果同时有 ENTRYPOINT 和  CMD(或 docker 运行时的 CMD), 则 CMD 将为 ENTRYPOINT 提供参数。

在原来那篇文中认为 shell 无法接收到 docker stop 或  docker -s SIGTERM 发来的信号,也许是随着 Docker 版本的变迁,Docker 变得越发聪明了起来,无论何种格式的 ENTRYPOINT, 都能够收到 SIGTERM 信号,比如在 Java 的 ShutdownHook 能捕捉到该信号,得以在进程停止之前作必要的清理工作。

进行本文相关研究的主因是部署在 ECS(Fargate) 中的 Java Web 服务,Task 总是因为 OutOfMemoryError 被杀掉,而在应用程序日志中却见不着半点线索说 JVM 的 OutOfMemoryError,即使后来给 Fargate 配上了 EFS, 加了 +XX:+HeapDumpOnOutOfMemoryError XX:HeapDumpPath=/efs JVM参数,在任务被 kill 时在 /efs 上从来就都没生成过内存映像文件。最后发现是因为 JVM 的 -Xmx 配置太高,留给 Fargate 容器的太少的缘故。 阅读全文 >>

Vagrant 没了 VirtualBox 的话可以配 Qemu

开源虚拟机软件 VirtualBox 从当初不可一世的 Sun 易手到 Oracle 之间,变得不那么被许多公司信任了。之前一直是用 Vagrant 搭配 VirtualBox 在 Mac 下使用 Linux 虚拟机,因为不需要用到 Linux 桌面,用 Vagrant 操作虚拟机非常方便。但现在不得不要考虑别的方式,那就是 Vagrant + Qemu。Vagrant 的默认 Provider 是 VirtualBox, 我们搜索 Vagrant Box https://app.vagrantup.com/boxes/search 的时候,注意到  Vagrant 其实支持的 Provider 是一个很长的列表:

aws, cloudstack, digitalocean, docker, google, hyperv, libvirt, lxc, openstack, parallels, qemu, rackspace, softlayer, veertu, virtubalbox, vmware, vmware_desktop, vmware_fusion, vmware_ovf, vmware_workstation, vsphere, xenserver

有些尚未听说过,还有一些虽说支持,但别人提供的相应的 box 不多,Qemu 还算不错的 virtualbox 的替代品。Qemu 也是一款开源的虚拟机软件,它支持 Linux, macOS 和 Windows  平台,但它本身未提供友好的 UI 工具,所以创建一个 Qemu 虚拟机的命令会让人畏而却步。在 macOS 下有一个 Qemu 的 UI 工具 UTM 阅读全文 >>

Docker 容器中使用 Docker - DinD 和 DooD

突然间研究这个来的缘由是正在从 Jenkins 往 Harness 的过度, 而完全用命令来构建 Docker 镜像变得不一样了。在 Jenkins 中 Agent 本身也是一个 Docker Daemon, 所以 Docker 命令执行无障碍,而 Harness 的所谓的 Agent 就是一个个的运行在 Kubernetes 中的 Docker Container (Pod) 了,这其中没有 Docker Daemon, 又不能连接到 Kubernetes 本身的 Docker Daemon。另外 CloudBees CI/CD 的运行环境与 Harness 类似,也是运行在 Kubernetes 中的 Pod。

因此可能要使用某个 Docker 容器来作为 Docker Daemon, 所以牵连出对此的研究,相应的方案有 Docker in Docker(DinD) 和  Docker outside of Docker(DooD)。

对容器中启动 Docker Daemon 的探索

在知晓 DinD 和 DooD 这两个概念本人还试图构建过一个 Docker 镜像,试图用一个 Docker 容器既作 Docker Daemon 又作为 Docker 客户端。在容器中 Docker 安装成功,但无法在容器中启动 Docker Daemon。比如用下面的 Dockerfile 阅读全文 >>

理解 JUnit, JaCoCo 到 SonarQube 的过程及 Maven 配置

Java 项目需要产生单元测试及代码覆盖率的话一直都是走的 JUnit 单元测试,JaCoCo 基于测试产生测试覆盖率,然后送到 SonarQube 去展示这条路子。当然 SonarQube 还可以帮我们进行代码的静态分析。但对其中的具体使用及过程知晓的并不深,基本就是在 pom.xml 中依葫芦画瓢。本文稍加深入的理解每一步的功效与配置,以 Maven 管理的 Java 项目为例,JUnit 采用是众多旧项目仍然无法摆脱的 JUnit 4。

示例项目名称为 JaCoCoSonar, 创建一个 Calc 类,其中有 int add(int op1, int op2) 方法,为其写一个单元测试 CalcTest

单元测试实际是被 maven-surefire-plugin 插件执行的

现在开始第一步,执行 mvn test 看会发生什么,执行过程中控制台显示 阅读全文 >>

《100 Java Mistakes and How to Avoid Them》笔记 2

继续阅读本书,编程语言处理数值都有可能出现问题,如溢出,整数的最大最小值不对称,Double.NaN 等。

由于 Java 学了 C,也用 0 开始的数字来表示 8 进制数,如 037, 010 分别是十进制的 31 和 8,这与现实不相符。因为如果你在纸上写下 037, 010, 几乎所有人(除了某些程序员)都会认为它们就是十进制的 37 和 10。但是 Java 表示 2 进制, 16 进制的方式没有问题的,如 0b10, 0x37。IntelliJ IDEA 看到使用 0 开头的 8 进制数会不建议那么使用. 8 进制数字的范围是 0~8, 所以 09 是错误的, 但是 Java 编译器似乎对此很陌生.

int a = 09;

IntelliJ IDEA 会提示 Integer number too large, 编译器提示说 java: ';' expected, 有点驴唇不对马嘴.

现在几乎没有必要使用 0 开始的 8 进制数的方式, 或许还有用的就是表示 Unix 下文件权限, 如

int fileMode = 0644

所以任何时候看到 0 开头的数字都必须仔细检视, 基本可以禁止使用这种方式 阅读全文 >>

《100 Java Mistakes and How to Avoid Them》笔记 1

这几日在阅读 Manning 出版社的 《100 Java Mistakes and How to Avoid Them》, 其中列举的确实是一些容易带入到代码中的错误,不少还是通过代码 Review 或单元测试很难发现的问题。也有些看似很弱智,却可能是隐匿许久的定时炸弹,只等某一特定条件出现时即爆。

阅读的同时简单的作了笔记及少许联想,所以内容有些杂乱无条理。最前面介绍了一些静态代码分析工具,也有两个动态分析工具。本书目前还是 Manning 的 MEAP 体验版,未正式发售。一共讲了 100 个常见错误如何避免(例如,怎么用最新 Java(Java 9 -- Java 21) 语法, API 来改进),以及用静态分析工具,单元测试及早发现。

这是读完了 1/4 数量的记录,笔记开始 阅读全文 >>

Lombok @With 的纯弊端及如何避免

由于是第一篇写关于 Lombok 的日志,所以有些不情愿去开门见山直接触及 @With, 而要先提一提本人对 Lombok 的接触过程。

两三年之前写 Java 代码一直都是全手工打造。一个数据类,所有必须的 setter/getter, toString, hashcode() 等全体现在源代码中,当然是在 IDE 中自动生成的。听说过 Lombok,但觉得它用了会影响到对源代码的阅读,因为造成代码的行为与源代码所展示的不一致,还可能依赖于特定的 IDE 或构建工具插件,所以一直未真正应用。

然而现代语言一直在避免不断书写象 JavaBean 里那一大片样本代码,同时解决试图提高覆盖率写出毫无意义单元测试的烦恼。比如 Scala 发展出了 case class, Kotlin 的 data class, Python 的 @dataclass,还有 JDK 14 引入的及至 JDK 16 转正的  record, 都是为了自动生成 Java 类的 setter/getter/toSring/hashcode/equals 等方法。 所以源代码中看不到实际可调用方法不该再是问题,况且在 JDK 5 加入的 enum 类型本质上也是在源代码的背后生成了一系列的方法和类型声明的。 阅读全文 >>

postgres in (?,?) 和 =any(?) 用法/性能对比

刚刚回顾了一下 JDBC 操作 SQL Server 时如何传入列表参数,即如何给 in (?) 条件直接传入一个列表参数,然而本质上是不支持,最终不得不展开为 in (?, ?,...?) 针对每个元素单独设置参数,不定长的参数对于重用已编译 PreparedStatement 语句的帮助不大。

那么 JDBC 操作 PostgreSQL 是何种状态呢?展开为多个参数当然是有效的。继续尝试 Spring 提供的 NamedParameterJdbcTemplate 的操作方式

String query = "select * from users where id in (:ids)";
Map<String, Object> parameters = new HashMap<>();
parameters.put("ids", IntStream.rangeClosed(1, 5).boxed().collect(toList()));
List<Map<String, Object>> maps = namedParameterJdbcTemplate.queryForList(query, parameters);

执行后查看到实际执行的语句是

select * from users where id in (?, ?, ?, ?, ?)

阅读全文 >>