天空 Blond - java
type: channel
link: https://www.skyblond.info/tag/java/
items: 【歪门邪道】网站迁移总结
items: 【代码札记】使用JNA在Windows上创建并操作TUN设备
items: 【代码札记】在JVM上优雅的使用Lua脚本
items: 【歪门邪道】针对Minecraft的JVM调优
items: 【代码札记】X25519密钥交换算法的纯BouncyCastle实现
items: 【代码札记】使用Java ASM替换字符串常量
items: JVM深度学习实践概述
items: 【代码札记】排查ObjectOutputStream引起的内存泄漏
items: 【代码札记】卢恩符文编码 - Proof of concept
items: 【代码札记】 从零开始的Clojure环境搭建

【歪门邪道】网站迁移总结
type: item
date: 2023-04-01T14:59:37+08:00
description: 今天上午终于完成了网站的整体迁移，历时不到5天。不知道这期间有多少朋友注意到了迁移导致的服务中断呢？本文将讨论这次迁移的全过程，以及我的一些想法。迁移数据库在迁移公告这篇文章里我提到的这次迁移的主要目的：使用Linode的托管数据库试图切换到WordPress来避开Typecho部分不成熟的功能实际上在发出这篇公告之前，网站就已经迁移到Linode的托管数据库了，这个过程还是相对无痛的：将现有的数据库导出成sql格式连上Linode的托管数据库，导入sql文件修改Typecho设置，让Typecho使用新的数据库配置从原理上来说这个过程不应该遇到任何麻烦，因为数据库的内容是完全没变的，只是相当于换了个地方。但是托管数据库和自建数据库不同，自建数据库运行在127.0.0.1上，通常只需要设置好密码就行了，不需要传输层加密，类似于以前的HTTP。但是托管数据库不一样，数据库位于公网的其他位置，如果继续使用明文协议，很容易会受到中间人攻击，因此需要传输层加密，也就是使用SSL连接。但是目前主流的博客平台似乎都没有考虑到这一点。在安装过程中WordPress不支持设置SSL证书，但安装完成后可以通过设置一个变量来启用相应的支持。而Typecho就不一样——压根儿不支持。好在我看到GitHub上有人提了pr，我把对应部分的修改应用到1.2.0版本上，完美可用。截至1.2.1-rc版本发出时，这个pr还没有合并，但截至本文发布时这个pr已经并入主线，不知道能不能赶上1.2.1的发布。迁移完数据库，我注意到了公告中提到的this is incompatible with sql_mode=only_full_group_by，变萌生了迁移网站的想法。迁移服务器为了避免网站服务不受中断，所以我决定暂且将旧的网站搬迁到一个临时服务器上，以便我在这个服务器上尝试新的东西。这个过程也比较顺畅：开一台新的服务器，装好lnmp之类的软件使用rsync把网站复制过去，改好权限保持nginx配置不变，原封不动把网站配置和证书复制过去修改DNS记录等待旧的DNS记录失效后，验证网站是否还在如期工作我印象中这部分好像没有遇到太多问题。在等文件迁移的过程中，我发现Typecho的作者已经声称修复了那个聚合查询的问题，但我还是遇到了。本着科学的原则，我打算深入研究一下这个问题。我新装了一个干净的Typecho，建立了一个测试分类，又在这个分类里面创建了一个子分类。在旧的网站中，查看测试分类的页面会抛出聚合查询错误，而新装的typecho就没有这个问题。我很是费解，但是我又不会PHP，这怎么办呢？本着自己动手丰衣足食的原则，我一边查谷歌，一边用记事本改PHP文件，开启了Typecho的debug模式，找到了报错的点，尝试使用var_dump查看出错的查询，我发现出错的点在于查询中使用了无关于内容的表。在新的Typecho中，查询分类的select部分只选择了table.contents这个表的内容，这十分合理——分类页面会列出这个分类下的文章，而文章都存储在contents表里。但是旧的Typecho则直接变成了select *，而后面用到了join，就会导致这个查询不光select文章表的内容，还会select到后面join进来的表的内容，这就导致了聚合查询的错误。找到了问题所在，那我就要看看为什么这两个select的不一样。经过一番查找，最后发现是一个插件修改了select语句。这个插件年代久远，是统计文章阅读量的。工作原理就是访问文章的时候给数据库字段加1，查询文章的时候会查询数据，一并把阅读量作为结果返回。而又经过一番查找，我发现这个插件的作者后来提供了一个不需要数据库字段的实现：Typecho文章都可以设置自定义字段，如果把数据库字段改成文章的这个字段，岂不美哉？只可惜我目前使用的主题依赖这个插件，不知道以后作者会不会修改。不过就算作者修改了，这样统计阅读量也有问题：这种统计方法是建立在执行PHP代码的基础上实现的，如果使用了CDN，那么页面就会被缓存，下次请求的时候就不会再触发PHP代码了。这么看还是基于JS的解决方案比较好，但我对于阅读量也没什么执念，我只管写的开心，别人爱看不看。找到了问题所在，禁用了插件，问题解决，这下不用非得换WordPress了。评估新的博客系统当然，我还是想试试看WordPress的，于是就安装了。结果我大受震撼，不禁发表了如下感想：这一回，是我自己发昏，竟想着换到 WordPress 去了。我装好了 WordPress 一看，什么都要钱。好不容易看上一个主题，要价 59 美元我就不说了，好东西值好价，但是他妈的 59 美元竟然不支持 Dark mode？以及什么评论同步 IFTTT 和 Pushover 竟然也都是要钱的，我…

【代码札记】使用JNA在Windows上创建并操作TUN设备
type: item
date: 2023-01-18T12:25:07+08:00
description: 虽然大家一致认为Java不适合做底层，但我觉得不适合不代表不能做。本篇文章比起札记，其实更像是一篇show off。背景最近我在看去中心系统相关的内容，看到了Yggdrasil，且不说它的原理如何，它在使用方式上就让我耳目一新。传统的overlay network都需要通过某种应用层的接口来让用户访问，比如Tor和I2P都需要浏览器支持，后者更是提供了诸如socks这种代理协议来提供访问，这就要求应用程序必须专门考虑并为此设计，才能获得一定程度的兼容性。对于不支持socks协议的程序，很遗憾，它没法接入I2P网络。Yggdrasil不同：它在运行时创建了一个TUN设备。TUN设备是一种虚拟网络适配器，类似TAP。TAP设备工作在二层，换言之它能够得到的是数据链路层的数据包（更细致地说，其实是媒体访问控制MAC包），而TUN设备则工作在三层，它能够得到的数据是IP包。Yggdrasil将网络中的每一个节点的公钥映射成IPv6地址中被废弃的一段0020::0/7，这段地址在可观的未来不会被使用（但Yggdrasil网络中节点的公钥是256位，而这个地址段只有121位，我还不太清楚他们是怎么解决地址冲突的），每个节点分配一个段内的IPv6地址，这样一来，只要在操作系统中访问这个段内的地址，不管你是ICMP还是TCP还是UDP，操作系统都封装成IP包，交给这个TUN设备。而Yggdrasil从这个TUN设备中获取封装好的IP号，看一下目的地地址，在网络内路由，到达对方节点后，直接通过TUN设备写入操作系统的IP栈，之后无论是什么协议，全部由操作系统负责处理和分发。多么优雅的解决方案啊！但是只有一个遗憾，这个软件是Go写的，而我是写Java的。我翻遍了Google和GitHub，也没有发现支持Java在Windows上创建和操作TUN设备的库。我很不爽，于是就自己写了一个。自己造轮子经过一番搜索，我发现了一个叫做wintun的C++库，这个库将win32 API的调用封装成了简单的函数，诸如WintunCreateAdapter这种。要知道，让Java调用C就已经很困难了，你需要处理好Java对象和C结构体之间的映射，要处理好指针、类型的问题。而微软的Win32为了让事情变得更加复杂（其实是历史遗留问题），他们使用了自己的一套类型系统，并且有者自己的一套API设计理念，用起来更加麻烦。看到有前人栽树，现在后人就得想怎么乘凉。好在Java调用C并不是什么稀奇事情：除了原生的JNI之外，我们还有JNA和JNR，还有未来的Project Panama。就目前而言，对于这个项目来讲，JNA是最好的选择。关于编写mapping的过程就不再赘述了，无非就是参照JNA文档，遇到问题了Google一下。不过其中有个小插曲，就是无论怎么调试，一开始得到的结果返回的都是null，我百思不得其解啊。最后无意中想到，这个玩意儿是不是需要管理员权限？但是IDEA并没有提供单独将程序作为管理员运行的选项，不得已，我只能将整个IDEA作为管理员运行。对于来路不明的软件，这个动作其实非常危险：不光是IDEA，由IDEA启动的Gradle也是作为管理员运行的，如果构建脚本里有一些恶意代码，使用管理员权限运行刚好给了他们可乘之机。不过考虑到这个是我自己写的软件，顶多蓝屏，能怎么样？编写好了Mapping，我发现一个问题：这个软件只能创建和操作TUN设备，但是没办法给TUN设备分配IP。虽然我可以在TUN设备中模拟DHCP包，但是这未免有些复杂且太不灵活了。查阅了一大堆文档，最后发现win32 API还是绕不开了。于是开始想办法编写相关的win32 API。具体内容可以参考这个项目的GitHub Repo。因为是C，免不了很多面向过程编程。与其一开始考虑怎么把这个东西做的尽可能OOP，我的建议是每次针对一个功能（例如列出IP，插入MIB ROW等），写一个demo，确保能用之后，再将这个功能封装成对象的成员方法。不然一边设计对象，一边考虑C的过程，实属给自己找不痛快。展示经过了一周的艰苦奋斗，这个库终于是成了。为了验证效果，我借助pcap4j实现了ICMP ECHO（也就是ping）响应。效果就是，运行程序，创建一个TUN设备，程序为这个TUN设备分配IP为0020::100/7，并开始处理IP包。这个时候在系统内ping任意一个同网段的ip，都可以得到相对应的ping响应。总体流程还是简单明了的：package info.skyblond.jna import com.sun.jna.platform.win32.Guid import info.skyblond.jna.wintun.* import org.pcap4j.packet.* import org.pca…

【代码札记】在JVM上优雅的使用Lua脚本
type: item
date: 2022-08-04T16:13:50+08:00
description: 本文将记述如何在JVM上使用Lua脚本。众所周知，JVM上运行的都是字节码，而要得到字节码，就离不开编译。而脚本的目的就在于不进行事先编译的情况下提供一个灵活的控制系统。谈及脚本语言，JavaScript和Python算是两个比较火的编程语言了，但是我认为他们都太复杂了，不够优雅。我心目中比较理想的脚本语言是Lua，它比较简单，而且能够面向特定领域。如果说Python是为了解决任何可以通过编程解决的问题而设计的，那么Lua可能就是为了解决某个特定领域中的问而设计的。例如大名鼎鼎的Cheat Engine，他就支持用户使用Lua脚本来操作程序的内存，或者将Lua脚本编译成机器指令插入到源程序中，这可比写汇编容易不少。除了Lua，似乎也可以为某一个特定领域创建一门语言，来自JetBrains的MSP工具就是为此而生。但是为此你需要自定义一套语法规则，相当于为了你的领域而单独创造一门编程语言，即便它非常简单。这对我来说有些因小失大了——引入脚本最初是为了简化开发，同时提供足够的灵活性，而为了达到这个目的要设计一个全新的语言，我想这对开发者、对用户的心智负担都不小。本文将记述我在JVM平台上使用Lua脚本的过程。LuaKLuaK是隶属于KorGE的Lua库，其旨在将LuaJ迁移到Kotlin上。其中KorGE是一个基于Kotlin的游戏引擎。但这个库看起来还在开发中，暂时没有可以使用的文档与制品。我个人比较期待这个库，但是目前还没有完工，只能寻求别的代替品了。Luaj经过一番搜索，我找到了一个有年头的库，LuaJ，有年头到什么程度呢？就是最后一次更新的时候（2019年3月，3.0.2发布），社区开发者没能联系上原作者，进而无法为Maven仓库上的制品进行更新。于是使用时只得将库下载到硬盘上进行使用。这让我想起了早年间我学习Java，还不会用maven、gradle这些工具时，就从网络上下载依赖的jar，让IDEA帮我管理依赖。总之，通过类似implementation(files("lib/luaj-jse-3.0.2.jar"))的语句就可以让gradle使用本地的jar依赖了。这个库在功能上还是比较丰富的：例如执行Lua脚本（使用库的API），提供脚本执行引擎（使用Java Scripting API），Lua解释器（将Lua脚本处理为语法树，使用Java访问语法树），Lua编译器（将Lua脚本编译成LuaVM的字节码），Lua到Java编译器（将LuaVM的字节码翻译成JVM字节码）。而以上所有内容都跟C代码没有任何关系——纯Java实现。当然了，本文的目标很简单：让Java能够为Lua脚本提供新的能力，并且在Lua脚本中调用Java提供的能力。经过一番查阅，LuaJ为扩展提供了较为方便的接口，其中比较重要的就是TwoArgFunction。从名字来看，这个抽象类似乎是描述了一个具有两个参数的函数，但LuaJ将其用作加载库的方式。具体来说，当我们在Lua脚本中require 'MyCustomLib'时，LuaJ会搜索classpath中所有实现了LuaFunction、并且具有默认构造函数（即无参数的构造函数）的类，如果找到了name为MyCustomLib，就把这个类当作一个函数来执行，其中第一个参数是模块名（modname，也就是MyCustomLib），第二个参数就是调用者的环境。通过操作环境可以在环境中注册库，之后的代码便可以调用我们的库了。需要注意的是，前文说的name并不是传统意义上的类名，而是Class.java中getName()返回的类名。因此对于info.skyblond.demo.lua.luaj包中的MyCustomLib类，他的name是info.skyblond.demo.lua.luaj.MyCustomLib。如果仅用MyCustomLib，LuaJ是找不到对应类的。这里我编写了一个简单的例子，展示了LuaJ提供的无参函数、单参函数、双参函数、三参函数和可变参函数：package info.skyblond.demo.lua.luaj import org.luaj.vm2.LuaFunction import org.luaj.vm2.LuaTable import org.luaj.vm2.LuaValue import org.luaj.vm2.Varargs import org.luaj.vm2.lib.* import java.math.BigDecimal import java.math.BigInteger import java.time.Duration class MyCustomLib : TwoArgFunction() { /** * Will be called…

【歪门邪道】针对Minecraft的JVM调优
type: item
date: 2022-06-13T00:57:38+08:00
description: 大学四年过得真快，转眼间就毕业了。趁着做完毕设、毕业之前，和同学搞了个Minecraft服务器。然而2核4G对于140多个Fabric模组来说还是太多了。本文记述我如何对JVM调优。前言Java和JVM一直是一个很庞大的系统。Java语言在JVM的基础上隐藏了很多细节，从而让程序员更关注功能而非性能。而JVM的作用则是对程序员编写的代码进行优化，因此JVM中引入了垃圾回收、即时编译等一系列先进而复杂的子系统。这种复杂度也使得JVM的性能并不像C++、Go或者Rust这样值观：你以为一段循环即可测量某个操作的性能，实际上这个操作可能随着循环的进行被即时编译机制优化。总之，本文希望以一种尽量量化的方式说明如何对JVM进行调优，从而避免「摆弄开关」和「按照坊间传闻」调优。本文的测试环境是一个4核心8GB内存的虚拟机，安装了openSUSE Leap 15.4，Linux内核5.14.21-150400.22-default，带有2GB交换空间。测试程序为MCPLUS整合包（版本1.18.1 Beta4），额外加入了Chunky用于生成区块。选择最佳JVM这里准备了5个候选JDK，三个HotSpot，一个OpenJ9，还有一个来自Azul的Zing：OpenJDK（Java 17.0.2+8 Linux/x64 Hotspot）Adoptium Temurin（原AdoptOpenJDK，Java 17.0.3+7 Linux/x64 Hotspot）IBM Semeru Runtime Open Edition（Java 17.0.3+7 Linux/x64 OpenJ9 0.32.0）Azul zulu jdk（Java 17.0.3+7 Linux/x64 Hotspot）Azul zing（Java 17.0.3+7 Linux/x64 Tiered VM Zing22.05.0.0+2）测试时使用的世界Seed为-3566672805144844787。测试流程为：删除logs和worlds文件夹启动服务端线程设置Chunky在主世界，以（0，0）为中心，半径为500生成区块（总计4225个区块）完成后停止服务端保存logs文件夹测量途中除了收集Minecraft服务端日志（Chunky日志）外，还收集JVM的垃圾回收日志。测试时使用如下脚本：#! /bin/bash JVM_OPENJDK=openjdk-17.0.2 JVM_TEMURIN=temurin-jdk-17.0.3+7 JVM_SEMERU=ibm-semeru-open-jdk-17.0.3+7 JVM_ZULU=zulu17.34.19-ca-jdk17.0.3-linux_x64 JVM_ZING=zing22.05.0.0-3-jdk17.0.3-linux_x64 sudo rm -rf /home/skyblond/jdks/test/logs* for JVM_NAME in $JVM_OPENJDK $JVM_TEMURIN $JVM_SEMERU $JVM_ZULU $JVM_ZING; do echo '========================================' echo "Start testing $JVM_NAME" echo '========================================' sudo rm -rf /home/skyblond/jdks/test/world mkdir logs sudo nice -n -20 /home/skyblond/jdks/$JVM_NAME/bin/java \ -Xmx6G -Xms6G -Xlog:gc:logs/gc.log -Xlog:gc* \ -jar fabric-server-launch.jar nogui | tee logs/full.log chmod 777 -R /home/skyblond/jdks/test/logs mv -v /home/skyblond/jdks/test/logs /home/skyblond/jdks/test/logs-$JVM_NAME echo '========================================' echo "Finish testing $JVM_NAME" echo '========================================' done该脚本定义了5个JVM的路径，然后再循环中依次使用这些JVM启动MC服务端，并在服务端退出后保存日志。其中启动MC服务端的时候，设置堆大小为6GB内存，开启垃圾回收日志并存储于logs中…

【代码札记】X25519密钥交换算法的纯BouncyCastle实现
type: item
date: 2022-05-10T13:58:07+08:00
description: 整个人类社会都在因为疫情开倒车：各国政府出于好意或恶意，无不以疫情为名试图拿走我们曾经有的权力。虽然我个人对此无能为力，但我认为，个体还是应当掌握技术以保护自己的。所以本文来介绍一下如何使用X25519密钥交换算法实现加密通信。当然了，由于我并不是信安专业的，对于密码学这块并没有那么熟，所以这里只是简单的说一说原理。若是哪里说错了，还请读者友善的在评论区中提出。加密的两大阵营首先我们来说说加密算法的两大阵营吧，这是从密钥的结构来说的。对于我们常规使用的加密算法，例如打压缩包的时候，我们可以选择对压缩包加密。这种加密就是对称加密，因为加密解密使用同一个密钥，加密和解密互为逆过程，是对称的。这种加密算法的好处是速度比较快，但坏处是，如果你想把加密后的数据分享给别人，那么你就必须要将密码也一起分享给别人。如果途中这个密码泄露了，那么这个加密就形同虚设了。在实践中，常用的加密算法有AES系列，ChaCha系列等，本文使用我较为习惯的AES-256-GCM。而另一个阵营就是非对称加密，相比对称加密，这种加密算法的加密过程和解密过程是不同的。这种加密算法的钥匙有两种：私钥和公钥。它们成对出现，私钥是必须保护好的秘密数据，而公钥则由私钥计算得出，与私钥对应，但不会泄露私钥，因此可以随意公开出去。当别人想要给你发送数据时，对方首先需要使用你的公钥加密数据，而你接收到数据后，使用自己的私钥解密数据。这样一来，你的秘密私钥压根不需要发送出去，就大大减少了私钥泄露的可能性。但是非对称加密有一个问题：性能非常慢。于是人们想出了一个好办法：先用非对称加密算法传输/协商一个比较短的、用于对称加密的密钥，再使用这个密钥进行对称加密。由于一开始的非对称加密不需要泄露私钥，是安全的，从而保证交换的加密密钥也是安全的，再使用这个密钥进行对称加密，通信的内容还是安全的，而且性能更高！非对称加密的两大阵营在说密钥交换之前，这里还想横插一杠，介绍一下非对称加密的两大主要阵营。它们分别是基于素数的RSA，和基于椭圆曲线的ECC。RSA是一个非常老牌的非对称加密算法了，到现在（2022年）我的网站证书使用的仍然是RSA算法。RSA算法的基本原理就是利用大整数难以在多项式时间内被分解出因子来保证安全，因此，只要RSA密钥的长度足够长，别人就拿你没办法（目前推荐使用2048位以上的长度，我个人倾向于4096位）。并且在对抗量子攻击上（使用量子计算机破解密钥），RSA做得更好（可能是因为量子计算机也对着素数和分解因子发愁）。不过从另一个角度来说，现阶段而言，量子计算机只有国家或商业资助的大学和研究所才有，并且用起来也比较繁琐（IBM曾经提供过免费的量子计算试用，但我不会用）。国家或商业巨人来搞我，我配吗？我不配。所以抗量子攻击并不在我的主要威胁模型里。而另一个阵营则是ECC，椭圆曲线加密算法。这种算法根据选择的曲线不同，各有各的特点。本文所使用的25519曲线，是由著名密码学家Daniel J. Bernstein设计的，据说设计的初衷是因为由美国国家机构设计的曲线在参数选取上不透明，可能存在后门，因此这位密码学家一气之下自己设计了一条公开透明而且还性能好的25519曲线。只不过这条曲线用起来比RSA稍微麻烦一些。对于加密和签名，RSA可以使用一套密钥，而25519则不行，基于25519的密钥交换算法称为X25519，而使用25519曲线的签名算法称为Ed25519，二者密钥长度相同，但要求却不同：Ed25519密钥来者不拒，只要够长就行，但X25519对密钥的特定位有要求，并且公钥的计算方式与Ed25519不同，使得二者无法相互替代。也就是说，如果同时需要加密和签名的话，使用25519曲线需要有两套私钥和公钥，但好在本文只用加密。本文青睐ECC的另一个原因是，不考虑量子攻击的情况下，ECC的密钥长度通常比RSA的短，并能够提供相似的安全性。例如25519曲线使用256位的密钥（不可能更改）提供的安全强度（Security bits）接近128位（实际为126位），而要达到这种强度，RSA需要使用3072位；而448曲线使用448位密钥就可以提供224位的安全强度，而要想达到此强度，RSA的密钥长度需要达到一万多位（7680位RSA密钥的强度时192位，15360位RSA密钥的强度是256位）。因此当你想要交换公钥时，你肯定想要尽快结束这个过程，否则一旦被其他人注意到，交换过程就可能会受阻，因此考虑同等安全性，ECC的密钥更短，交换更快，更适合实际使用。密钥交换算法关于密钥交换算法，最简单的可以从Diffie-Hellman算法说起，该算法基于RSA密钥，利用素数的整数模n乘法群及其原根实现。所谓“整数模n乘法群”，就是一堆数（a,b,c,d...），他们与n互质，并且在模n后…

【代码札记】使用Java ASM替换字符串常量
type: item
date: 2021-08-10T19:52:51+08:00
description: 最近在用Java写智能合约，其中有一个静态字段是合约的管理员地址，这个地址用于鉴权，确保只有该地址能够对合约进行管理操作（升级、销毁等）。但是为了做到自动化测试，这个管理员地址在develop分支上是私链的，而实际部署的时候得换成公链上的另一个地址。前几天升级合约的时候忘记替换地址，直接把私链地址部署到了公开的测试网了。好在是测试网，问题不大，这要是主网就别玩了，重开吧。为了杜绝这样的后顾之忧，本文决定探索一种方法，能够一份代码按照使用情况填入不同的地址来生成合约，避免将地址写死在代码里产生问题。背景具体来说，这个区块链是Neo N3区块链，其中Java的语言支持由Neow3j这个库提供。这个库允许开发者使用任意JVM语言开发合约，并根据最终编译的Java字节码生成Neo的可执行合约。因此上文提到的合约就是使用Java开发的，没有用Kotlin的原因就是怕翻车。典型的合约管理员地址是将其作为一个静态字段实现的：public class TestContract { static Hash160 CONTRACT_OWNER = addressToScriptHash("NR9pwfToFHHYti4RY1dLP3wGr4waSGn7NH"); }其中NR9pwfToFHHYti4RY1dLP3wGr4waSGn7NH是用于测试的地址，其私钥随着代码一并公开到GitHub仓库，显然这种公开的钱包地址并不适宜作为公链的管理地址使用，但对于自动化测试来说，让程序获取到地址的私钥并进行自动调用，这可比手工测试轻松多了。其中的addressToScriptHash函数是Neow3j编译器提供的一个本地方法，它没有具体实现，而是在编译为合约的时候，由编译器将里面的字符串解读为NeoVM能够理解的钱包地址。这种方法最直观，官方的教程也是这样写的。看起来没什么问题，但涉及到实际使用和开发，就如同我所说的，自动化测试要求密钥开放，而实际部署要求密钥必须保密，如果在开发和部署两个分支上频繁修改这个地址，不美观且麻烦，最重要的是一旦忘记，会导致很大的安全隐患。另一个解决方法是在合约中获取当前网络的MagicNumber来判断网络类型，由合约自动决定使用哪一个地址。由于每个网络的魔数都是固定的，并且公共测试网和主网的数字都是固定不变的，这种方法相对可行，但额外的代码会给执行阶段带来额外的手续费（NeoN3的智能合约是按指令收费的），并且同时写两个地址，并不美观，也不优雅。解决为了美观优雅，我决定探索ObjectWeb ASM库，这个库用于直接操作底层的Java字节码。谈及Java，我的印象总是诸如“抽象”、“不关心底层硬件”、“面向Java虚拟机”之类的，所以我们在编写Java的时候只需要关注逻辑上的实现，而不必关心这套代码是否会在不同架构的CPU上有不同的行为。而能够支持这种功能得，正是Java虚拟机和Java字节码的配合，所有运行在Java虚拟机上的语言，包括Java、Kotlin，最终都要编译成Class文件。而这个Class文件中就包含了即将执行的Java字节码，可以理解为面向Java虚拟机的汇编语言。字节码初探以如下类为例：package info.skyblond.jvm.asm; public class ASMTargetClass { static Pair<String, String> target = genPair("Something", "Nothing"); String local = "local"; static String staticDirect = "staticDirect"; private static Pair<String, String> genPair(String s1, String s2) { return new Pair<>(s1, s2); } }其中的Pair<>是这个类：package info.skyblond.jvm.asm; public class Pair <T, U>{ private final T first; private final U second; public Pair(T first, U second) { this.first = first; this.second = second; } public T getFirst() { return first; } public U getSecond() { return second; } }编译ASMTargetClass会得到文件ASMTargetClass.class，和其他可执行文件一样，我们可以直接用16进制编辑器打开它看内容，这种方法可以但是并不推荐。在Linux下我们会使用…

JVM深度学习实践概述
type: item
date: 2020-11-06T16:54:00+08:00
description: 10月用一整月写完了一篇论文，因此也没能兑现月更的承诺，实属丢人现眼。本文将总结10月以及最近这几天的心得与体会，并希望能够给更多遭遇同样境况的人一些方向。序尽管现在在深度学习方面最火热的编程语言是Python，但作为高贵的Java的程序员，使用全动态的Python语言有害于我的纯粹性。因此在前些日子赶论文的时候，一大部分时间都花在了使用JVM进行深度学习上。从一开始的无数次Python入门失败，到今天能够写成文章总结JVM上进行深度学习的经验，我认为这比写论文更有意思，毕竟Java天下第一，在应用领域我还是最支持JVM。 警告 本文对Python持有强烈的偏见，如有不适请及时关闭网页。 引言尽管序和引言在中文颇有重复的嫌疑，但即便如此我还是要将他们分开。在本节我将试图阐述为什么Python做机器学习很火，而Java上的深度学习一直不温不火。这里需要强调的是本文所说的深度学习，是通常需要显卡进行训练的任务，而不是传统的机器学习。在传统机器学习领域，我认为Java的Weka库和Oracle的tribuo都已经做的非常优秀了。关于Python在机器学习领域的火爆，网络上有那么几种说法，我听到的一种说法是CPython对于C系的库调用起来很方便，因为机器学习库底层都是C/C++写的，比较快，而Java通过JavaCPP或者JNI调用普遍比较慢。但是我觉得这种说法并不稳妥，我认为更实际的原因可能是Python好学，毕竟深度学习的核心在于数学，而搞数学的人在以前能写个Matlab就算不错了，而Python又没有什么严格的工程结构，王八蛋都能学会，故因为便于入门，许多搞理论研究的人使用Python构建他们通往实践的路。另一方面Java则没有这么好入门，各种工程设计的模式以及强类型的束缚，以及JVM的深不可测，使得一些Java程序员写了十几年代码也不敢说自己真正了解Java，因此Java才在深度学习领域不温不火吧。另外值得一说的是Python本身只是一个编程语言规范，而本文所指的Python实际上是其基于C/C++的实现，即CPython，在以前也曾经有过JVM实现的Python，但是其版本止步于2.7，而后来Oracle的GraaleVM也试图打通Python与JVM的隔阂，但就现阶段来说GraalVM上通过Python调用C的API还仍然在开发中。Support for more extension modules is high priority for us. We are actively building out our support for the Python C API to make extensions such as NumPy, SciPy, Scikit-learn, Pandas, Tensorflow and the like work fully. This work means that some other extensions might also already work, but we're not actively testing other extensions right now and cannot promise anything. Note that to try other extensions on this implementation, you have to download, build, and install them manually for now.支持更过拓展模块是我们最先要做的。我们正在努力开发Python的C语言API，使得诸如NumPy、SciPy、Scikit-learn、Pandas、Tensorflow之类的框架完全正常工作。这些工作意味着其他一些扩展也可能已经起作用，但是我们现在不积极测试这些扩展，也无法做出任何保证或承诺。 请注意，要尝试此实现的其他扩展，您现在必须手动下载，构建和安装它们。来源：Github - GraalVM翻译过来就是GraalVM的Python支持在做，也许能用，但是不稳定。尽管现阶段没有任何应用的可能（我不希望在代码中引入任何不稳定性），但是我还是很看好GraalVM，除了Python，它还有一个Native Image，能够将Java等其支持的语言编译为静态镜像，十分值得期待。但话说回来，DDL不等人，我们没法等着GraalVM开发完毕再去使用Java调用Python的代码搞深度学习。因此本文将就现阶段可用的，且好用的、符合Java程序员习惯的一些深度学习框架进行介绍与对比。最后还需要提一点，Python上面的深度学习库依赖于其底层的C++库，原则上可以通过JNI调用他们去做JVM的深度学习，但是很显然这有不是很优雅，…

【代码札记】排查ObjectOutputStream引起的内存泄漏
type: item
date: 2020-09-28T20:42:00+08:00
description: 今天在利用Java处理语料的时候发现随着时间的推移，Java进程需要的内存越来越多，虽然过程中有往下掉的环节，但是总体而言还是增长的趋势。由此我推断是发生了内存泄漏。本文将记述排查及解决的过程。运行的程序本身代码很简单：import ch.qos.logback.classic.Level import info.skyblond.fiona.helper.HanLPHelper import org.slf4j.LoggerFactory import java.io.File import java.io.ObjectOutputStream import java.nio.charset.StandardCharsets fun main(args: Array<String>) { val corpus = File("").bufferedReader(StandardCharsets.UTF_8) val structuralizedSentence = File("") val objectOutputStream = ObjectOutputStream(structuralizedSentence.outputStream()) corpus.lineSequence().shuffled().map { // 分词 }.map { sentence -> // 其他处理 }.forEachIndexed { index, it -> objectOutputStream.writeObject(it) if (index % 1000 == 0) { objectOutputStream.flush() System.gc() } } objectOutputStream.close() }可以看出来基本上就是对语料库进行处理，处理的过程中并没有将循环内的变量存储到循环外部，内部变量也没有和外部长生命周期的变量交互的机会。而循环内部的变量与外部唯一交互的机会就是最后forEachIndexed中利用ObjectOutputStream写入文件的过程。为了科学严谨，我注释了与ObjectOutputStream相关的语句，只在循环内部执行同样的操作，唯独不写入文件。利用Jconsole查看内存发现并没有内存泄漏。因此根据控制变量的原理，可以确定就是ObjectOutputStream导致了这次内存泄漏。确定了问题的位置，之后就好办了。原本为了方便存储处理结果，Json显得十分冗余，而使用其他编码方式又相当麻烦，于是我直接用ObjectOutputStream存入文件，十分优雅。可结果没想到这东西不仅没优雅成，反倒还给我弄了个内存泄漏出来，但即便如此，Java仍然比Python优秀十万倍。经过网上的一番搜索（关键字ObjectOutputStream memory leak），总结起来是这样的：ObjectOutputStream在工作的时候会对已经发送（调用writeObject(obj)的obj）对象记录在一个表中（引用形式），为了节省带宽和内存，如果之后发送过同样的数据，就可以直接告诉接收端使用之前发送过的内容，从而避免发送重复的数据。在搜索的过程中还看到有人提到writeUnshared函数，通过说明来看应该是无论之前是否发送过同样的数据，经由这个函数发送的数据总是会被传输。看起来是解决了内存泄漏的问题，但是在发送的时候还是会记录被发送对象的引用，从而导致GC认为这不是带回收的对象，引起内存泄漏。这个问题真正的解法在于经常调用reset方法，这个方法会将对应的ObjectOutputStream重置，效果就是和新new出来的对象一样。这样之前持有的发送过的对象的引用就会被清零，同时也会在流上输出一个reset标志，告诉（未来的）接收者对方也要清空这个引用表。这样之前没能被GC收集并清理的对象就可以成功的为后来者腾出内存了。而关于writeUnshared，我认为不必要，出于两方面考虑：一方面是默认的机制可以节省带宽，本文中我要写入文件，这样可以避免在文件中写入相同的数据，能够减小文件大小；另一方面考虑如果要常规调用reset()，那么调用这个还是writeObject()我觉得可能区别不大。因此我选择看上去更优雅的writeObject()。于是对forEachIndexed块修改如下：.forEachIndexed { index, it -> objectOutputStream.writeObject(it) if (index % 10000 == 0) { objectOutputStream.flush() objectOutputStream.reset() System.gc() } }…

【代码札记】卢恩符文编码 - Proof of concept
type: item
date: 2020-03-02T07:23:00+08:00
description: 最近在学习Clojure，我认为学习一门编程语言，只看书不实践是不行的。故决定使用Clojure写一个类似Base64的卢恩符文编码。这里使用Elder Futhark字母表，关于此字母表的更多信息还请移步维基百科。这篇文章是Proof of Concept性质的，本文将使用Java/Kotlin编写相同功能的代码，以确定具体的实现细节和编码方式。原理Base64关于Base64详细的编解码方式，请移步维基百科。简单来说就是将要编码的字节按照顺序摆好，然后选定6位为一组算出来对应的数字，再以预先选定好的方式将数字替换成字母。解码时反向操作，通过字母找到对应的数字，按顺序将数字翻译回二进制，按顺序放好便是原来的被编码字节。卢恩符文同样按照Base64的思路，将字母换成卢恩符文，并修改一下位数，可以达到目的。这里选用的Elder Futhark字母表具有如下字母：ᚠᚢᚦᚨᚱᚲᚷᚹᚺᚾᛁᛃᛇᛈᛉᛊᛏᛒᛖᛗᛚᛝᛟᛞ对应Unicode编码：\u16A0 \u16A2 \u16A6 \u16A8 \u16B1 \u16B2\u16B7 \u16B9 \u16BA \u16BE \u16C1 \u16C3\u16C7 \u16C8 \u16C9 \u16CA \u16CF \u16D2\u16D6 \u16D7 \u16DA \u16DD \u16DF \u16DE一共是24个字母。它还具有三种标点：᛫᛬᛭对应Unicode编码：\u16EB \u16EC \u16ED第一个标点是空格，即单词间的分隔符。中间的是句号，即句子间的分隔符。最后一个不知道，我也没能查到相关的说明。 以下方式只是我选定的一个方式，不唯一，而且很灵活 卢恩符文与Base64首先不一样之处在于字符数量，Base64选了64个字符作为编码字符，他正好是2的6次幂，刚好可以用6个二进制位来表示，这也是为什么要把字节摆好，每六个位一组翻译成数字的原因。而卢恩符文只有24个，加上标点符号也才27个，正好卡在5位和6位二进制数能表达的范围之间，很尴尬，因此这套系统涉及非二进制运算，所以远不及Base64这些利用二进制位操作的方式高效迅速。当然，我写这个东西就是奔着好玩去的，能用就行。如果换一个角度想，这实际上就是进制转换的问题。Base64是利用64进制表示数据，那么卢恩符文也可以用24进制来表示数据。对于每一块数据（可以是任意字节），按照字节顺序放好，翻译成数的形式，再按照数制转换的方式操作即可。得到数字之后按照表进行翻译便可得到卢恩符文编码。解码时则反向操作。以上便是基本原理，当实际实现时还需要有些调整或技巧。下面将分解代码进行介绍。代码分解定义常量首先是定义字母表：val rawElderFutharkCharacters = charArrayOf( 'ᚠ', 'ᚢ', 'ᚦ', 'ᚨ', 'ᚱ', 'ᚲ', 'ᚷ', 'ᚹ', 'ᚺ', 'ᚾ', 'ᛁ', 'ᛃ', 'ᛇ', 'ᛈ', 'ᛉ', 'ᛊ', 'ᛏ', 'ᛒ', 'ᛖ', 'ᛗ', 'ᛚ', 'ᛝ', 'ᛟ', 'ᛞ' ).sortedArray()这里要保证一定是个有序数组，其中原数组的最后两个字母是字母表正确的顺序，但是Unicode编码却是相反的。因为后面解码时要用二分查找确定字符在数组中的位置，所以一定要保证被查找的数组是有序的，否则将不能正确解码。然后是定义块长度。这里我选了三种不同的长度，实际上大家可以随意选择，只要电脑能跑得动就可以。const val minBlockSize = 128 const val middleBlockSize = 256 const val maxBlockSize = 512然后是一些趁手的工具函数。fun randomBlockSize(): Int { return when (Random.nextInt(0, 3)) { 0 -> minBlockSize 1 -> middleBlockSize 2 -> maxBlockSize else -> throw IllegalStateException() } } fun getSpace(blockSize: Int): Char { return when (blockSize) { minBlockSize -> '᛫' // \u16EB middleBlockSize -> '᛬' // \u16EC maxBlockSize -> '᛭'// \u16ED else -> throw IllegalArgumentException() } } fun getBlockSize(c: Char): Int { return when (c) { '᛫' -> minBlockSize /…

【代码札记】 从零开始的Clojure环境搭建
type: item
date: 2020-02-15T00:36:05+08:00
description: 我现在日常开发的主力语言是Java和Kotlin，其中后者可能占比更加多一些，如果可以的话我尽可能的会选择Kotlin来做我熟悉的事情，但是另一种情况，比如尝试我以前未曾涉足过的非Kotlin原生框架（例如DeepLearning4J），我就比较偏好于使用Java，因为通常情况下我可以从框架的文档处很方便的复制粘贴来一些代码跑跑看。今天闲来无事，决定试试看基于JVM的其他语言。本文将记述我今天搭建Clojure运行和编译环境的一些事宜。杂谈在函数式这方面，JVM上Scala勉强算吧，比较纯粹的就是Eta-lang和Clojure了，前者类似Haskell，后者类似Lisp。我是比较偏好Haskell的，但是前者实在是太新了，而且有风评传它对Java原生的调用不是很美，而且为了贴近GHC又不得不做出一些牺牲，但最终又不是原生的GHC。等于它处在了一个JVM和GHC之间很尴尬的位置。不过目前他们成立了一个公司来开发Eta-lang，我决定在等几个版本，万一就好了呢。而且说起Haskell，我是真喜欢Haskell的写法和理念，不用担心括号叠罗汉，Monad也很酷（虽然我不会用吧，可我乐意学），但是迫使我不得不弃坑（我试图入门过Haskell，有博文为证）的一个最主要的原因就是Haskell的包管理工具stack，遇到一个要求了不同版本GHC的依赖，就要下载一整套GHC，真的用起来的时候代码还没写多少，光是引用了几个依赖，一个200GB的分区就红了，我这笔记本真是撑不住啊。而且看了好久，我发现社区可能还想把这个当作一个特性，遂不得已弃坑了。如果Eta-lang能做好，它是基于JVM的，虽然也兼容与Haskell互相调用，但是我认为这样子就完全可以避开stack来节省我的硬盘空间，毕竟我认为Java没有什么做不到的，编程思想不一样可以自己写接口包装一下嘛。对于Lisp的印象，我觉得就是一个远古魔法。我确实没有深入了解过Lisp，听闻远古Lisp支持的运算/函数/关键字屈指可数，但是能达到有限状态机那种境界，我觉得是真厉害。但是现在的Lisp，真就是括号叠罗汉。同样是一个2加2，在Haskell里：2 + 2加号是中缀函数，可以加括号变为普通函数调用，可是Lisp就不行：(+ 2 2)非得用括号包起来才是一个表达式，不然编译器就骂街。且还没有中缀函数的概念。我可能还是不懂Lisp的精髓吧。我买了一本书叫「Living Clojure」，我决定深入的学一学，没事儿用这个做点儿小玩意儿也是好的嘛。但前提还是得先把环境搭起来。客观条件我的电脑是Win10，常用的IDE就是Intellij家的IDEA，因为在读大学，所以可以申请到免费的教育版许可。关于Clojure的IDE，我决定在IDEA的基础上安装插件。我选择的插件是Cursive，他们非常人性的提供了几种不同的许可：个人许可 - 99刀一年企业许可 - 不知道多少钱一年，我没细看非商业许可 - 免费我选的是后者，因为我还到敢在商业项目中使用Clojure的程度，而且我也保证我不会用非商业许可写用来赚钱的代码。所以我申请的最后一种，有效期是六个月，可以随时申请新的。【警告】请务必根据你的工作性质购买合适的许可。这个插件也是别人日夜操劳写出来的，大家都不容易，花点钱支持正版没毛病。坑其实安装过程挺简单的，直接装上插件输入密钥就可以了。但是我装完了建立好工程准备进REPL耍一耍的时候，IDEA突然提示我：Dependency resolution error Error synchronising learning:0.1.0-SNAPSHOT: Failed to collect dependencies at org.clojure:clojure:jar:1.10.0我：？？？后来我发现这个插件是用的Maven获取的jar文件，m2文件夹中有对应的文件夹了，可是文件夹里还是空的。在其他项目中我的Maven好好的，没有出现过类似的情况。经过一顿排查，确定是网络的问题。遂一通更改settings.xml，又是挂代理又是换Repo的，折腾一溜够。最终在代理软件中发现端倪：unknown Socks version: 67我：？？？我明明用的http协议啊？最终发现IDEA的设置里是Socks5协议，而IDEA在调用Maven下载Jar文件时默认把代理设置传给了Maven，后者似乎是不认socks5的样子，反正就是没谈好，导致网络始终不通。最终在IDEA中改回Http代理，再刷新工程就完美解决问题了。至此Cursive插件的全部功能都能得以正常运作，等书到了就可以学起来了。-全文完-