Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

Monitoring Tools and Commands

Monitoring Tools and Commands

https://www.saashub.com/compare-visualvm-vs-jconsole

命令/工具 全称 作用
jps JVM Process Status Tool You use the jps command to list the instrumented JVMs on the target system.
显示正在运行的所有 HotSpot VM 进程。
jstat JVM Statistics Monitoring Tool You use the jstat command to monitor JVM statistics.
用于监视本地或远程 HotSpot VM 各方面的运行数据,例如类加载/卸载、运行时数据区、GC、JIT。
jconsole You use the jconsole command to start a graphical console to monitor and manage Java applications.
JDK 5 起免费提供。一款基于 JMX (Java Management Extensions) 的可视化监视、管理工具。它的主要功能是通过 JMX 的 MBean (Managed Bean) 对系统进行信息收集和参数动态调整。
VisualVM VisualVM is a visual tool integrating commandline JDK tools and lightweight profiling capabilities.

jps

jps 命令的功能与 ps 类似,用于列出正在运行的 JVM 进程状态。

常用参数:

  • -q 只输出 LVMID,省略主类的名称。
  • -l 输出主类的全名,如果进程执行的是 JAR 包,则输出 JAR 路径。
  • -m 输出虚拟机进程启动时传递给主类 main() 函数的参数。
  • -v 输出虚拟机进程启动时的 JVM 参数。

jstat

jstat 命令用于监视当前 JVM 的各种运行状态信息。在用户体验上也许不如 JMC、VisualVM 等可视化监控工具以图表形式展示那样直观,但在实际生产环境中不一定可以使用 GUI 图形界面,因此在没有 GUI、只提供命令行界面的服务器上,仍是运行期定位虚拟机性能问题的常用工具。

命令格式:

1
$ jstat options vmid [interval[s|ms] [count]]

常用参数:

  • options,要查询的虚拟机信息,主要分为三类:
    • 类加载
      • -class 监视类加载、卸载数量、总空间以及类加载所耗费的时间
    • 运行时数据区、GC
      • -gccapacity 查看 GC 情况和 JVM 各区的容量(字节)
      • -gc 查看 GC 情况和 JVM 各区的容量使用量(字节)
      • -gcutil 查看 GC 情况和 JVM 各区的使用率(%)
    • JIT
      • -compiler 输出即时编译器编译过的方法、耗时等信息
      • -printcompilation 输出已经被即时编译的方法
  • vmid,如果是本地虚拟机进程,VMID 与 LVMID 一致;如果是远程虚拟机进程,则 VMID 的格式为:[protocol:][//]lvmid[@hostname[:port]/servername]
  • interval,间隔时间,单位为秒或者毫秒
  • count,打印次数,如果缺省则打印无数次

示例展示:

此示例连接到 lvmid 21891,并以 250 毫秒的间隔获取 7 个样本,每 6 行显示一次标题([-h<lines>]),并显示由 -gcutil 选项指定的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ jstat -gcutil -h6 21891 250 7

S0 S1 E O P YGC YGCT FGC FGCT GCT

12.44 0.00 27.20 9.49 96.70 78 0.176 5 0.495 0.672

12.44 0.00 62.16 9.49 96.70 78 0.176 5 0.495 0.672

12.44 0.00 83.97 9.49 96.70 78 0.176 5 0.495 0.672

0.00 7.74 0.00 9.51 96.70 79 0.177 5 0.495 0.673

0.00 7.74 23.37 9.51 96.70 79 0.177 5 0.495 0.673

0.00 7.74 43.82 9.51 96.70 79 0.177 5 0.495 0.673

S0 S1 E O P YGC YGCT FGC FGCT GCT

0.00 7.74 58.11 9.51 96.71 79 0.177 5 0.495 0.673

该示例结果显示,对象首先都在 Eden 区中创建,在第 3 和第 4 个样本之间由于 Eden 区装满,发生了 Young GC, gc 耗时 0.001 秒,并将对象从 Eden 区(E)提升到 Old 区(O),导致 Old 区的使用率从 9.49% 增加到 9.51%。

-gcutil 选项每列说明:

列名 描述
S0 Survivor space 0 区占用率
S1 Survivor space 1 区占用率
E Eden space 区占用率
O Old space 区占用率
P Perm space 区占用率
列名 描述
YGC 从应用程序启动到采样时发生 Young GC 的次数,E 区满后触发
YGCT 从应用程序启动到采样时 Young GC 所用的时间(单位秒)
FGC 从应用程序启动到采样时发生 Full GC 的次数, O 区满后触发
FGCT 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT 从应用程序启动到采样时用于垃圾回收的总时间(单位秒)

VisualVM

VisualVM 脱胎于自 JDK 6 起免费提供的 jvisualvm,目前已经从 Oracle JDK 中分离出来,成为一个独立发展的开源项目:https://visualvm.github.io/

功能:

  • 监控进程的 CPU 使用率、内存(运行时数据区)、类加载、线程等统计;

  • 监控线程状态。如下图:

  • VisualVM 拥有丰富的插件扩展。例如:

    • Visual GC

      Integration of the Visual GC tool into VisualVM. Visual GC user interface is displayed for each local or remote application with performance counters available via jvmstat API.

      The Visual GC tool attaches to an instrumented HotSpot JVM and collects and graphically displays garbage collection, class loader, and HotSpot compiler performance data.

    • Threads Inspector

      Threads Inspector adds a new section to the Threads tab showing stack traces for the selected live threads.

    • TDA Plugin

      Thread Dump Analyzer is a GUI for analyzing thread dumps generated by the Java VM.

Troubleshooting Tools and Commands

Troubleshooting Tools and Commands

命令 全称 作用 备注
jinfo Configuration Info for Java You use the jinfo command to generate Java configuration information for a specified Java process.
实时显示或修改虚拟机配置信息。
在 JDK 9 中已集成到 jhsdb
jstack Stack Trace for Java You use the jstack command to print Java stack traces of Java threads for a specified Java process.
显示虚拟机当前时刻的线程快照(thread dump/javacore 文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成堆栈快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。
在 JDK 9 中已集成到 jhsdb
jmap Memory Map for Java You use the jmap command to print details of a specified process.
用于实时生成 JVM 堆内存转储快照(heap dump/hprof 文件),或查看堆内存信息。其它转储方法:
-XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpOnCtrlBreak
在 JDK 9 中已集成到 jhsdb
jhat JVM Heap Dump Browser 用于分析 heap dump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果。分析结果默认以包为单位进行分组显示,分析内存泄漏问题主要会使用到其中的 Heap Histogram(与 jmap -histo 功能一样)与 OQL 页签功能,前者可以找到内存中总容量最大的对象,后者是标准的对象查询语言,使用类似 SQL 的语法堆内存中的对象进行查询统计。 在 JDK 9 中已被 jhsdb 替代
jhsdb Java HotSpot Debugger 一个基于 Serviceability Agent 的 HotSpot 进程调试器。 自 JDK 9 起免费提供

jstack

jstack 命令用于 thread dump(生成虚拟机的线程堆栈快照),根据堆栈信息我们可以定位到具体代码,所以它在 JVM 性能调优中使用得非常多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ jstack 21090 > /tmp/threaddump
$ less /tmp/localfile

Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode):

"Attach Listener" daemon prio=10 tid=0x00007f67e03b4800 nid=0x7bb9 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"catalina-exec-8000" daemon prio=10 tid=0x00007f67ba4a0000 nid=0x795a waiting on condition [0x00007f6558c0a000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007886ab360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043)
at org.apache.http.pool.PoolEntryFuture.await(PoolEntryFuture.java:139)
at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:307)
at org.apache.http.pool.AbstractConnPool.access$000(AbstractConnPool.java:65)
at org.apache.http.pool.AbstractConnPool$2.getPoolEntry(AbstractConnPool.java:193)
at org.apache.http.pool.AbstractConnPool$2.getPoolEntry(AbstractConnPool.java:186)
at org.apache.http.pool.PoolEntryFuture.get(PoolEntryFuture.java:108)
at org.apache.http.impl.conn.PoolingClientConnectionManager.leaseConnection(PoolingClientConnectionManager.java:212)
at org.apache.http.impl.conn.PoolingClientConnectionManager$1.getConnection(PoolingClientConnectionManager.java:199)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:424)
at org.apache.http.impl.client.AbstractHttpClient.doExecute(AbstractHttpClient.java:884)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:107)
at com.xxx.xxx.xxx.HttpClientService.doPost(HttpClientService.java:103)
......

由于导出的 threaddump 文件非常大,可以先统计下所有线程、或关注的线程分别处于什么状态:

1
2
3
4
5
6
7
8
$ grep /tmp/threaddump | awk '{print $2$3$4$5}' | sort | uniq -c | sort

39 RUNNABLE
21 TIMED_WAITING (onobjectmonitor)
6 TIMED_WAITING (parking)
51 TIMED_WAITING (sleeping)
3 WAITING (onobjectmonitor)
305 WAITING (parking)

发现有大量 WAITING (parking) 状态的线程。重新打开 threaddump 文件排查,根据堆栈可以定位到具体的问题代码,可以初步判断是 HTTP 连接耗尽资源导致的问题。

jmap

常用参数:

  • -dump 实时生成 JVM 堆内存转储快照(heap dump/hprof 文件)。格式为 -dump:[live,] format=b, file=<filename>,其中 live 子参数表示是否只 dump 出存活的对象。
  • -histo[:live] 显示堆中对象统计信息,包括类、实例数量、合计容量。
  • -heap 查看当前堆内存的详细信息,如 Heap ConfigurationHeap Usage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ jmap -heap 7059
Attaching to process ID 7059, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 7516192768 (7168.0MB)
NewSize = 5368709120 (5120.0MB)
MaxNewSize = 5368709120 (5120.0MB)
OldSize = 2147483648 (2048.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
capacity = 5249171456 (5006.0MB)
used = 1933544816 (1843.9720306396484MB)
free = 3315626640 (3162.0279693603516MB)
36.835238326800805% used
From Space:
capacity = 59768832 (57.0MB)
used = 44812496 (42.73652648925781MB)
free = 14956336 (14.263473510742188MB)
74.97636226185581% used
To Space:
capacity = 59768832 (57.0MB)
used = 0 (0.0MB)
free = 59768832 (57.0MB)
0.0% used
PS Old Generation
capacity = 2147483648 (2048.0MB)
used = 80178568 (76.46424102783203MB)
free = 2067305080 (1971.535758972168MB)
3.733605518937111% used

注意:

Heap Configuration Heap Usage
JDK 7 及以下版本 PermSizeMaxPermSize Heap 中包含 Perm Generation
JDK 8 及以上版本 MetaspaceSizeMaxMetaspaceSize Heap 不再包含 Perm Generation,取而代之的是在 Heap 之外有一块 Metaspace

可以进一步分析对象分布。

反汇编工具

大多数情况下,通过诸如javap等反编译工具来查看源码的字节码已经能够满足我们的日常需求,但是不排除在有些特定场景下,我们需要通过反汇编来查看相应的汇编指令。两个很好用的工具——HSDIS、JITWatch

工具 描述
HSDIS (HotSpot disassembler) 一款 HotSpot 虚拟机 JIT 编译代码的反汇编插件。
JITWatch 用于可视化分析。

https://zhuanlan.zhihu.com/p/158168592?from_voters_page=true

参考

《深入理解 Java 虚拟机》

https://docs.oracle.com/en/java/javase/11/tools/index.html

https://openjdk.java.net/tools/

JDK 内置实用工具:监视、故障排除

使用 VisualVM 进行性能分析及调优

JConsole、VisualVM 依赖的 JMX 技术到底是什么?

本文总结下垃圾收集涉及的一些重点:

gc_summary

基于分代收集算法的垃圾收集器组合,总结如下图,常用于 JDK 8 及之前的版本:

generational_collection

GC 选项配置

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

  • DefNew – single-threaded mark-copy stop-the-world garbage collector and is what is used to clean the Young generation(单线程 (single-threaded), 采用标记-复制 (mark-copy) 算法的,使整个 JVM 暂停运行 (stop-the-world) 的新生代 (Young generation) 垃圾收集器 (garbage collector))
Advanced Garbage Collection Options GC Configuration Description
-XX:+UseSerialGC Serial + Serial Old Enables the use of the serial garbage collector. This is generally the best choice for small and simple applications that do not require any special functionality from garbage collection.
By default, this option is disabled and the collector is chosen automatically based on the configuration of the machine and type of the JVM.
-XX:+UseParNewGC ParNew + SerialOld Enables the use of parallel threads for collection in the young generation.
By default, this option is disabled. It is automatically enabled when you set the -XX:+UseConcMarkSweepGC option. Using the -XX:+UseParNewGC option without the -XX:+UseConcMarkSweepGC option was deprecated in JDK 8.
Java 8 JEP 173: Retire Some Rarely-Used GC Combinations
Java 9 JEP 214: Remove GC Combinations Deprecated in JDK 8
-XX:+UseConcMarkSweepGC ParNew + CMS Enables the use of the CMS garbage collector for the old generation. Oracle recommends that you use the CMS garbage collector when application latency requirements cannot be met by the throughput (-XX:+UseParallelGC) garbage collector. The G1 garbage collector (-XX:+UseG1GC) is another alternative.
By default, this option is disabled and the collector is chosen automatically based on the configuration of the machine and type of the JVM.
When this option is enabled, the -XX:+UseParNewGC option is automatically set and you should not disable it, because the following combination of options has been deprecated in JDK 8: -XX:+UseConcMarkSweepGC -XX:-UseParNewGC. (Serial + CMS)
Java 9 JEP 291: Deprecate the Concurrent Mark Sweep (CMS) Garbage Collector
Java 14 JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
-XX:+UseParallelGC Parallel Scavenge + Parallel Old Enables the use of the parallel scavenge garbage collector (also known as the throughput collector) to improve the performance of your application by leveraging multiple processors.
By default, this option is disabled and the collector is chosen automatically based on the configuration of the machine and type of the JVM. If it is enabled, then the -XX:+UseParallelOldGC option is automatically enabled, unless you explicitly disable it.
Java 14 JEP 366: Deprecate the ParallelScavenge + SerialOld GC Combination
-XX:+UseParallelOldGC Parallel Scavenge + Parallel Old Enables the use of the parallel garbage collector for full GCs.
By default, this option is disabled. Enabling it automatically enables the -XX:+UseParallelGC option.
-XX:+UseG1GC Enables the use of the garbage-first (G1) garbage collector. It is a server-style garbage collector, targeted for multiprocessor machines with a large amount of RAM. It meets GC pause time goals with high probability, while maintaining good throughput. The G1 collector is recommended for applications requiring large heaps (sizes of around 6 GB or larger) with limited GC latency requirements (stable and predictable pause time below 0.5 seconds).
By default, this option is disabled and the collector is chosen automatically based on the configuration of the machine and type of the JVM.
-XX:UseZGC

G1

Garbage First Garbage Collector Tuning - Oracle

GC 日志

Java 9 JEP 158: Unified JVM Logging

Java 9 JEP 271: Unified GC Logging

在线 GC 日志分析工具:https://gceasy.io/

1
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:E:/logs/gc-default.log

参考

HotSpot Virtual Machine Garbage Collection Tuning Guide - Java 17

Available Collectors

  • Serial Collector
  • Parallel Collector
  • Garbage-First (G1) Garbage Collector
  • The Z Garbage Collector

https://www.baeldung.com/java-verbose-gc

《深入理解 Java 虚拟机》

CMS 和 G1 改用三色标记法,可达性分析到底做错了什么?

背景

最近为了做春节大型活动,研究了下性能压测和 JVM 调优,先来看一张 JVM 监控图(硬件:4 核 8G)。

JVM 监控

6 小时的吞吐量为:(21600s - Young GC 35s + Old GC 0s) / 21600s = 99.8%,总吞吐量还是不错的(吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC 时间)。如果虚拟机总共运行了 100 分钟,其中 GC 总耗时 1 分钟,那么吞吐量就是 99%。)。同时单次 Young GC 的平均耗时仅为 35s / 639 = 55 ms,停顿时间较短。

如果还需要进一步优化,思路如下:

  • 合理调整 Old Gen 与 Young Gen 大小比例,以减少 Young GC 次数,但单次 GC 耗时可能会相应延长,具体需测试。
  • 更换垃圾收集器,并对垃圾收集器参数进行调优。

下面介绍一些基础知识。

运行时数据区

手绘的运行时数据区如下:

jvm

JVM 定义了在程序执行期间使用的各种运行时数据区:

  • 其中一些数据区是在 JVM 启动时创建、仅在 JVM 退出时才被销毁。
  • 另外一些数据区是随每个线程创建及销毁。

java Command

java [options] classname [args]

java [options] -jar filename [args]

  • options: Command-line options separated by spaces.
  • classname: The name of the class to be launched.
  • filename: The name of the Java Archive (JAR) file to be called. Used only with the -jar option.
  • args: The arguments passed to the main() method separated by spaces.

java 命令用于启动 Java 应用程序。它通过启动 JRE,加载指定类并调用其 main() 方法来实现启动。main() 方法声明如下:

1
public static void main(String[] args)

java 命令支持以下几类选项:

说明:

  • 所有 JVM 实现都需要保证支持标准选项。标准选项用于执行常见操作,例如检查 JRE 版本、设置类路径、启用详细输出等。
  • 非标准选项是针对 Java HotSpot VM 的通用选项,因此不能保证所有 JVM 实现都能支持,并且随时可能改变。非标组选项以 -X 开头。
  • 高级选项不建议随意使用。这些是开发人员用于调整 Java HotSpot VM 特定区域的选项。这些区域通常具有特定的系统要求,并且可能需要对系统配置参数的访问权限。这些选项也不能保证所有 JVM 实现都能支持,并且随时可能改变。高级选项以 -XX 开头。

想跟踪最新版本中被弃用或删除的选项,参考已废弃与已移除的选项(JDK 8)。

布尔类型的选项无需参数,格式如下:

  • -XX:+OptionName 用于启用 默认情况下禁用的功能;
  • -XX:-OptionName 用于禁用 默认情况下启用的功能。

对于需要参数的选项,每个选项的确切语法有所差异:

  • 参数可以用空格、冒号(:)或等号(=)与选项名分开,或者参数可以直接跟在选项后面,具体参考文档。

如果需要指定字节大小,可以使用以下几种格式:

  • no suffix
  • k or K for kilobytes (KB)
  • m or M for megabytes (MB)
  • g or G for gigabytes (GB)

例如:

  • 大小为 8 GB,参数可以设为 8g, 8192m, 8388608k, 8589934592
  • 大小为 1.5 GB,参数不能设为 1.5g,可以设为 1536m
  • 如果需要指定百分比,使用 0 到 1 之间的数字(例如, 0.25 for 25%)。

JVM Stack

-Xsssize

Sets the thread stack size (in bytes). Append the letter k or K to indicate KB, m or M to indicate MB, g or G to indicate GB. The default value depends on the platform:

  • Linux/ARM (32-bit): 320 KB
  • Linux/i386 (32-bit): 320 KB
  • Linux/x64 (64-bit): 1024 KB
  • OS X (64-bit): 1024 KB
  • Oracle Solaris/i386 (32-bit): 320 KB
  • Oracle Solaris/x64 (64-bit): 1024 KB

The following examples set the thread stack size to 1024 KB in different units:

1
2
3
-Xss1m
-Xss1024k
-Xss1048576

This option is equivalent to -XX:ThreadStackSize.

Heap

参数 描述
-Xms-XX:InitialHeapSize
-Xmx-XX:MaxHeapSize
设置 Heap 堆区的初始值和最大值,Server 端 JVM 建议将 -Xms-Xmx 设为相同值。
参数 描述
-Xmn 设置 Heap 堆内 Young Generation,而 Old Generation 等于:堆区减去 -Xmn
设置 -Xmn 等同于设置了相同的 Young Generation 初始值 -XX:NewSize 和最大值 -XX:MaxNewSize
参数 描述
-XX:NewRatio Sets the ratio between young and old generation sizes. By default, this option is set to 2 (Young Gen can get up to 1/3 (Y=H/(R+1)) of the Heap).
-XX:SurvivorRatio Sets the ratio between eden and survivor space sizes. By default, this option is set to 8 (S0/S1 can get up to 1/10 (S=Y/(R+2)) of Young Gen).

-Xms-Xmx

-Xmssize

Sets the initial size (in bytes) of the heap. This value must be a multiple of 1024 and greater than 1 MB. Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, g or G to indicate gigabytes.

The following examples show how to set the size of allocated memory to 6 MB using various units:

1
2
3
-Xms6291456
-Xms6144k
-Xms6m

If you do not set this option, then the initial size will be set as the sum of the sizes allocated for the old generation and the young generation.

The -Xms option is equivalent to -XX:InitialHeapSize.

-Xmxsize

Specifies the maximum size (in bytes) of the memory allocation pool in bytes. This value must be a multiple of 1024 and greater than 2 MB. Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, g or G to indicate gigabytes. The default value is chosen at runtime based on system configuration.

For server deployments, -Xms and -Xmx are often set to the same value. See the section “Ergonomics” in Java SE HotSpot Virtual Machine Garbage Collection Tuning Guide at http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html.

The following examples show how to set the maximum allowed size of allocated memory to 80 MB using various units:

1
2
3
-Xmx83886080
-Xmx81920k
-Xmx80m

The -Xmx option is equivalent to -XX:MaxHeapSize.

-Xmn

-Xmnsize

Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery).

The young generation region of the heap is used for new objects. GC is performed in this region more often than in other regions.

  • If the size is too small, then a lot of minor garbage collections will be performed.
  • If the size is too large, then only full garbage collections will be performed, which can take a long time to complete.

⚠️ Oracle recommends that you keep the size for the young generation between a half and a quarter of the overall heap size.

Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, g or G to indicate gigabytes. The following examples show how to set the initial and maximum size of young generation to 256 MB using various units:

1
2
3
-Xmn256m
-Xmn262144k
-Xmn268435456

Instead of the -Xmn option to set both the initial and maximum size of the heap for the young generation, you can use -XX:NewSize to set the initial size and -XX:MaxNewSize to set the maximum size.

-XX:NewRatio

-XX:NewRatio=ratio

Sets the ratio between young and old generation sizes. By default, this option is set to 2.

The NewRatio is the ratio of old generation to young generation (e.g. value 2 means max size of old will be twice the max size of young, i.e. young can get up to 1/3 of the heap).

1
Y=H/(R+1)

设置 Young Generation 和 Old Generation 的比值,例如该值默认为 2,则表示 Young Generation 和 Old Generation 比值为1:2。

-XX:SurvivorRatio

-XX:SurvivorRatio=ratio

Sets the ratio between eden and survivor space sizes. By default, this option is set to 8.

The following formula can be used to calculate the initial size of survivor space (S) based on the size of the young generation (Y), and the initial survivor space ratio (R):

1
S=Y/(R+2)

The 2 in the equation denotes two survivor spaces. The larger the value specified as the initial survivor space ratio, the smaller the initial survivor space size.

By default, the initial survivor space ratio is set to 8. If the default value for the young generation space size is used (2 MB), the initial size of the survivor space will be 0.2 MB.

Method Area

PermGen

JDK 7 及以下版本:

参数 描述
-XX:PermSize Perm 的初始值
-XX:MaxPermSize Perm 的最大值

JVM 的永久代(PermGen)主要用于存放 Class 的 meta-data,Class 在被 Loader 加载时就会被放到 PermGen space,GC 在主程序运行期间不会对该区进行清理,默认是 64M 大小,当程序需要加载的对象比较多时,超过 64M 就会报这部分内存溢出了,需要加大内存分配。

Metaspace

JDK 8 及以上版本,永久代(PermGen)的概念被废弃掉了,参考 JEP 122: Remove the Permanent Generation

The proposed implementation will allocate class meta-data in native memory and move interned Strings and class static variables to the Java heap.

Hotspot will explicitly allocate and free the native memory for the class meta-data. Allocation of new class meta-data would be limited by the amount of available native memory rather than fixed by the value of -XX:MaxPermSize, whether the default or specified on the command line.

取而代之的是一个称为 Metaspace 的存储空间。Metaspace 使用的是本地内存,而不是堆内存,也就是说在默认情况下 Metaspace 的大小只与本地内存大小有关。可以通过以下的几个参数对 Metaspace 进行控制:

参数 描述
-XX:MetaspaceSize Metaspace 的初始值
-XX:MaxMetaspaceSize Metaspace 的最大值

Direct Memory

-XX:MaxDirectMemorySize=size

Sets the maximum total size (in bytes) of the New I/O (the java.nio package) direct-buffer allocations. Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, g or G to indicate gigabytes. By default, the size is set to 0, meaning that the JVM chooses the size for NIO direct-buffer allocations automatically.

The following examples illustrate how to set the NIO size to 1024 KB in different units:

1
2
3
-XX:MaxDirectMemorySize=1m
-XX:MaxDirectMemorySize=1024k
-XX:MaxDirectMemorySize=1048576

异常

java.lang.StackOverflowError

java.lang.StackOverflowError:线程栈溢出,要么是方法调用层次过多(比如存在无限递归调用):

StackOverflow

要么是线程栈太小,可调整 -Xss 参数增加线程栈大小。

java.lang.OutOfMemoryError: Java heap space

java.lang.OutOfMemoryError: Java heap space:这种是堆内存不够,一个原因是真不够,另一个原因是程序中有死循环,例如:

OutOfMemory

如果是堆内存不足,可调整 -Xms-Xmx,或者新老生代的比例。

java.lang.OutOfMemoryError: PermGen space

java.lang.OutOfMemoryError: PermGen space:这种是P区内存不够,可调整:-XX:PermSize-XX:MaxPermSize

参考

Java 虚拟机规范(Java SE 8 版 - 中文版)

Java 虚拟机规范(Java SE 8 版 - 英文版)

JEP 122: Remove the Permanent Generation - Release on JDK 8

Command Line Options - JDK 8 HotSpot VM

写博客难免会引用图片资源,这里提供一种不用图床解决图片资源上传的思路:将图片资源作为源文件一并上传仓库。

新建 img 目录

首先,在 hexo 博客 source 目录下新建 img 目录,即:hexo/source/img

然后,在文章的图片引用处使用该路径即可,例如:![example](/img/example.png)

最后,hexo g 构建出 ./public 目录,发现 img 在该目录之中。hexo s 启动服务后,确认能够成功引用图片。

解决 Typora 实时预览

通过上述方法能够解决部署后图片引用问题,但带来一个新的问题就是 Typora 无法实时预览。解决办法:在文章顶部加上 typora-root-url: ..

hexo_with_img

可以将该路径加入到 hexo 模板之中,这样每次 hexo n 新建文稿都会带上该配置:

hexo_with_img_2

配置如下:

1
2
3
4
5
6
7
---
title: {{ title }}
date: {{ date }}
updated: {{ updated }}
tags:
typora-root-url: ..
---

数据结构

Strings

单个 批量
设值 SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]
SETNX key value
SETEX key seconds value
PSETEX key milliseconds value
GETSET key value
SETRANGE key offset value
MSET key value [key value …]
MSETNX key value [key value …]
取值 GET key
GETDEL key
GETRANGE key start end
STRLEN key
MGET key [key …]
原子递增、递减 INCR key
INCRBY key increment
INCRBYFLOAT key increment
DECR key
DECRBY key decrement
追加 APPEND key value
位操作 SETBIT key offset value
GETBIT key offset
BITCOUNT key [start end]
BITOP operation destkey key [key …]
BITFIELD
BITPOS key bit [start] [end]

使用场景:

  • WEB 集群下的 Session 共享:

    1
    $ SET key value
  • 分布式锁:

    1
    2
    3
    4
    5
    6
    -- 返回 1 表示加锁成功,0 表示加锁失败
    $ SET key value NX
    $ SETNX key value

    -- 解锁
    $ DEL key
  • 全局计数器:

    1
    2
    $ INCR key
    $ DECR key
  • 分布式流水号:

    1
    2
    $ INCR key
    $ INCRBY key

分布式流水号 Java 伪代码如下,单机一次性取 1000 个 ID,以降低网络开销和 Redis 负载:

1
2
3
4
5
6
7
8
9
10
11
12
13
private int id;
private int maxId;
private int INCR_BY = 1000;

public synchronized int nextId() {
if (id == 0 || id == maxId) {
maxId = eval(incrby id INCR_BY);
id = maxId - INCR_BY + 1;
return id;
} else {
return id++;
}
}

Hashes

散列表,一种通过散列函数计算对应数组下标,并通过下标随机访问数据时,时间复杂度为 O(1) 的特性快速定位数据的数据结构。Redis 散列表使用这种数据结构来快速获取指定 field。

单个 批量
设置 field 的 value HSET key field value [field value …]
HSETNX key field value
HMSET key field value [field value …]
获取 field 的 value HGET key field
HSCAN key cursor [MATCH pattern] [COUNT count]
HSTRLEN key field
HMGET key field [field …]
获取所有 fields HKEYS key
获取所有 values HVALS key
获取所有 fields 和 values HGETALL key
获取 field 的个数 HLEN key
判断 field 是否存在 HEXISTS key field
删除 field HDEL key field [field …]
原子递增、递减指定 field HINCRBY key field increment
HINCRBYFLOAT key field increment

Lists

双端队列。

redis_lists

队列操作
入队 LPUSH key element [element …]
LPUSHX key element [element …]
RPUSH key element [element …]
RPUSHX key element [element …]
出队 LPOP key RPOP key
阻塞出队 BLPOP key [key …] timeout BRPOP key [key …] timeout
插队 LINSERT key BEFORE|AFTER pivot element
获取指定索引的元素 LINDEX key index
获取指定范围的元素 LRANGE key start stop
获取列表长度 LLEN key
覆盖元素 LSET key index element
移除元素 LREM key count element
移除指定范围的元素 LTRIM key start stop
非阻塞 阻塞
出队并重新入队另一个队列 RPOPLPUSH source destination BRPOPLPUSH source destination timeout

使用场景:

  • Stack (FILO): LPUSH + LPOP
  • Queue (FIFO): LPUSH + RPOP,实现简单的消息队列
  • Unbounded Blocking Queue (FIFO): LPUSH + BRPOP

Sets

无序集合(散列表实现)。

集合操作:

集合操作 命令
添加元素(去重) SADD key member [member …]
移除元素 SREM key member [member …]
判断指定元素是否存在 SISMEMBER key member
获取元素个数 SCARD key
增量式遍历集合元素 SSCAN key cursor [MATCH pattern] [COUNT count]
获取所有元素 SMEMBERS key
获取指定个数的随机元素 SRANDMEMBER key [count]
移除指定个数的随机元素,并返回 SPOP key [count]

集合运算:

集合运算 数学符号 命令
移动指定元素到另一个集合 SMOVE source destination member
求交集(Intersection) SINTER key [key …]
求交集(Intersection),并保存结果 SINTERSTORE destination key [key …]
求并集(Union) SUNION key [key …]
求并集(Union),并保存结果 SUNIONSTORE destination key [key …]
求差集(Difference) SDIFF key [key …]
求差集(Difference),并保存结果 SDIFFSTORE destination key [key …]

使用场景:

  • 抢红包、抽奖、秒杀 —— 本质上都是同一类问题,解决思路类似。为了减少对临界资源的竞争,避免使用各种锁进行并发控制,可以预先对临界资源进行拆分,以提升性能:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 预拆红包,放入集合
    $ SADD key 子红包ID1 [子红包ID2 …]
    # 查看所有红包
    $ SMEMBERS key
    # 随机抽取红包
    $ SPOP key [count]

    # 预先将奖品放入奖池
    $ SADD key member [member …]
    # 查看所有奖品
    $ SMEMBERS key
    # 随机抽奖(只抽一次)
    $ SRANDMEMBER key [count]

    # 登记参与抽奖的候选人
    $ SADD key member [member …]
    # 查看所有候选人
    $ SMEMBERS key
    # 随机抽取一二三等奖的获得者
    $ SPOP key [count]
  • 社交应用的关注模型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 我关注的人
    $ SMEMBERS key

    # 求共同关注
    $ SINTER key [key ...]

    # 我关注的人也关注 ta
    foreach(member in 我_关注的人) {
    # 我每个关注的人,他们关注的人中,是否有 ta
    $ SISMEMBER member_关注的人 ta
    }

    # 我可能认识的人
    foreach(member in 我_关注的人) {
    # 我每个关注的人,他们关注的人中,有我还没关注的
    $ SDIFF member_关注的人 我_关注的人
    }
  • 商品筛选

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 1、分类筛选维度,每个维度都为一个集合
    # 2、将商品按维度加入所属集合
    # 3、多选筛选条件时,求交集
    $ SINTER key [key ...]

    # 4、根据交集 member,获取商品详情(O(1) 时间复杂度)
    foreach member {
    # 每个 field 为商品属性
    $ HGETALL member
    }

Sorted Sets

有序集合(复合数据结构实现:散列表+跳表)。

使用场景:

  • Top K(例如排行榜)。实现思路:利用集合的三大特性之一——互异性,进行去重,相同元素只进行计数,形成一个二元组集合(key 为元素,value 为计数)。最后按计数结果对集合进行倒序排序,取前 N 个元素。

其它命令

命令
删除 key DEL
设值 key 的过期时间(秒) EXPIRE

参考

https://redis.io/commands

Hexo 博客使用好多年了,总结下日常使用的一些内容。

Hexo 博客搭建

安装配置

1
2
3
4
5
6
7
8
9
10
11
# -g 参数全局安装 Hexo 命令行工具,安装后才可以使用下述 hexo 命令
$ npm install -g hexo-cli

# 初始化本地仓库及 hexo 文件,适用于第一次使用
$ hexo init

# hexo 基础配置、主题、插件配置等等,详细配置参考官网
$ vim _config.yml

# 根据 package.json 的声明(hexo 版本及 dependencies 版本)安装所需依赖到当前目录 node_modules
$ npm install

主题

NexT

https://github.com/next-theme/hexo-theme-next

https://theme-next.js.org/pisces/

https://hexo-next.readthedocs.io/zh_CN/latest/

常用命令

依赖安装完毕,开始使用 hexo,常用命令如下:

Hexo 常用命令

自动化构建 & 部署

推荐使用 GitHub Actions,简单、免费,而 Travis CI 收费了。

GitHub Actions

为了方便随时随地可以编写博客,搭建好的本地仓库及其源文件一般会推送到 GitHub 远程仓库中保管,并自动构建 & 部署到 GitHub Pages 服务。步骤如下:

  1. 创建 GitHub Pages 服务所需的仓库 yourname.github.io,注意该仓库必须是 public 权限。
  2. 创建 .github/workflows/pages.yml 配置文件。
  3. 推送源文件至该仓库。

参考:https://hexo.io/docs/github-pages

Travis CI

完成上面两步就可以开始创作了。但毕竟命令还是有些繁琐,因此可以利用持续集成服务代替人工来做重复的事情。引入 Travis CI 后,整体流程如下:

GitHub Pages with CI

从上述流程来看,作者只需要完成创作并推送即可,其它构建、部署的事则由 Travis CI 来完成,非常简单。

下面来看下如何配置:

GitHub 创建 access token

登录 GitHub - Settings - Developer Settings 选项,找到 Personal access tokens 页面,创建个人 access token,创建时权限 repo 权限和 user:email 权限。

Travis CI 仓库配置

  1. 使用 GitHub 账户登录 Travis CI 官网并进行 OAuth 授权
  2. 同步仓库一,过程中会在 GitHub 账户下安装 Travis CI 的 GitHub App,用于触发持续集成
  3. 为仓库一设置环境变量:
    • GH_TOKEN 值为 GitHub access token
    • GH_REF 值为 GitHub 仓库二地址

创建 .travis.yml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 设置语言
language: node_js
# 设置相应的版本,可以指定版本 10,或者使用稳定版 stable
node_js: stable
# 设置只监听哪个分支
branches:
only:
- master
# 缓存 node_modules 目录,可以节省持续集成的时间
cache:
directories:
- node_modules
before_install:
- npm install -g hexo-cli
install:
- npm install
script:
- hexo clean
- hexo g
after_script:
- cd ./public
- git init
- git config user.name "yourname" # 修改name
- git config user.email "youremail" # 修改email
- git add .
- git commit -m "Travis CI Auto Builder"
- git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:master

自动构建

创作并推送到仓库一即可,其它构建、部署的事都由 Travis CI 来完成,过程如下:

Travis CI

流是从支持数据处理操作的源生成的元素序列,源可以是数组、集合、文件、函数。流不是集合元素,它不是数据结构并不保存数据,它的主要目的在于计算。

本文总结下 Stream API:

java.util.stream

创建流

数组

通过 Arrays.stream 方法生成流,并且该方法生成的流是数值流(即 IntStream 而不是 Stream<Integer>)。使用数值流可以避免计算过程中的拆箱装箱,提高性能。Stream API 提供了 mapToIntmapToDoublemapToLong 三种方式将对象流(Stream<T>)转换成对应的数值流,同时提供了 boxed 方法将数值流转换为对象流:

1
2
3
4
5
6
7
8
int[] intArr = new int[]{1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(intArr);

long[] longArr = new long[]{1L, 2L, 3L, 4L, 5L};
LongStream longStream = Arrays.stream(longArr);

double[] doubleArr = new double[]{1.0, 2.0, 3.0, 4.0, 5.0};
DoubleStream doubleStream = Arrays.stream(doubleArr);

集合

通过集合生成,最常用的一种:

1
2
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = integerList.stream();

文件

通过文件生成,得到的每个流是给定文件中的每一行:

1
2
3
4
5
6
// [1, 2, 3, 4, 5]
Stream<String> stream1 = Files.lines(Paths.get("E:\\data.txt"), Charset.defaultCharset());

// [1, 2, 3, 4, 5]
BufferedReader reader = new BufferedReader(new FileReader("E:\\data.txt"));
Stream<String> stream2 = reader.lines();

函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// []
Stream<Integer> stream3 = Stream.empty();

// [1, 2, 3, 4, 5]
Stream<Integer> stream4 = Stream.of(1, 2, 3, 4, 5);

// iterate 方法接受两个参数,第一个为初始化值,第二个为进行的函数操作,因为 iterate 生成的流为无限流,因此通过 limit 方法对流进行了截断,只生成 5 个偶数
// [0, 2, 4, 6, 8]
Stream<Integer> stream5 = Stream.iterate(0, n -> n + 2).limit(5);

// generate 方法接受一个参数,方法参数类型为 Supplier<T> ,由它为流提供值。generate 生成的流也是无限流,因此通过 limit 对流进行了截断
// [0.0819448251044178, 0.9273399484995596, 0.3050941986467305, 0.824966110053092, 0.6101914799225238]
Stream<Double> stream6 = Stream.generate(Math::random).limit(5);

// 使用 builder 模式创建流
// [1, 2]
Stream<Integer> stream8 = Stream.<Integer>builder().add(1).add(2).build();

// 使用 concat 方法拼接两个流
// [1, 2, 3, 4, 5]
Stream<Integer> stream7 = Stream.concat(Stream.of(1, 2), Stream.of(3, 4, 5));

中间操作

一个流可以后面跟随零个或多个中间操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的,仅仅调用到这类方法,并没有真正开始流的遍历,真正的遍历需等到终结操作。

终止操作

一个流有且只能有一个终结操作,当这个操作执行后,流就被关闭,无法再被操作了。

转换为数组(toArray)

1
2
3
Object[] objects = Stream.of(1, 2, 3, 4, 5).toArray();
Integer[] integers = Stream.of(1, 2, 3, 4, 5).toArray(Integer[]::new);
int[] arr = IntStream.of(1, 2, 3, 4, 5).toArray();

转换为列表(toList)

1
2
3
4
5
6
7
8
9
10
11
// Java 8, modifiable List
List<String> result = list.stream()
.collect(Collectors.toList());

// Java 10, unmodifiable List
List<String> result = list.stream()
.collect(Collectors.toUnmodifiableList());

// Java 16, unmodifiable List
List<String> result = list.stream()
.toList();

转换为键值对(toMap)

测试数据如下:

1
2
3
List<Pair<String, Integer>> peoples = Arrays.asList(Pair.of("Lucy", 10),
Pair.of("Lucy", 30),
Pair.of("Peter", 18));

需求:按 key 分组,key 冲突则保留 value 最大的。

1
2
3
4
5
6
7
8
// 演示 `mergeFunction`
// {Peter=(Peter,18), Lucy=(Lucy,30)}
Map<String, Pair<String, Integer>> map = peoples.stream()
.collect(Collectors.toMap(
Pair::getKey,
Function.identity(),
(people1, people2) -> people1.getValue() > people2.getValue() ? people1 : people2)
);

分组统计(groupingBy)

需求:按 key 分组统计总个数、总和、平均数、最大值、最小值。

方式一,各项单独统计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// {Lucy=2, Peter=1}
Map<String, Long> counting = peoples.stream()
.collect(Collectors.groupingBy(Pair::getKey, Collectors.counting()));
// {Lucy=40, Peter=18}
Map<String, Integer> summing = peoples.stream()
.collect(Collectors.groupingBy(Pair::getKey, Collectors.summingInt(Pair::getValue)));
// {Lucy=20.0, Peter=18.0}
Map<String, Double> averaging = peoples.stream()
.collect(Collectors.groupingBy(Pair::getKey, Collectors.averagingDouble(Pair::getValue)));
// {Lucy=Optional[(Lucy,30)], Peter=Optional[(Peter,18)]}
Map<String, Optional<Pair<String, Integer>>> max = peoples.stream()
.collect(Collectors.groupingBy(Pair::getKey, Collectors.maxBy(Comparator.comparing(Pair::getValue))));
// {Lucy=Optional[(Lucy,10)], Peter=Optional[(Peter,18)]}
Map<String, Optional<Pair<String, Integer>>> min = peoples.stream()
.collect(Collectors.groupingBy(Pair::getKey, Collectors.minBy(Comparator.comparing(Pair::getValue))));

方式二,汇总统计:

1
2
3
4
5
6
// {
// Lucy=IntSummaryStatistics{count=2, sum=40, min=10, average=20.000000, max=30},
// Peter=IntSummaryStatistics{count=1, sum=18, min=18, average=18.000000, max=18}
// }
Map<String, IntSummaryStatistics> summary = peoples.stream()
.collect(Collectors.groupingBy(Pair::getKey, Collectors.summarizingInt(Pair::getValue)));

参考:https://www.baeldung.com/java-groupingby-collector

常见问题

获取列表索引

forEach 方法入参缺少列表索引,无法实现某些特殊需求。

解决方案一,通过 IntStream 获取索引 index:

1
2
IntStream.range(0, elements.size())
.forEach(index -> downloadFile(elements.get(index), index));

解决方案二,自定义工具类通过 BiConsumer 传参,获取索引 index 和元素 element:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class IterateUtil {
public static <E> void forEach(
Iterable<? extends E> elements, BiConsumer<Integer, ? super E> action) {
Objects.requireNonNull(elements);
Objects.requireNonNull(action);

int index = 0;
for (E element : elements) {
action.accept(index++, element);
}
}
}

// 使用方式
IterateUtil.forEach(
elements,
(index, element) -> downloadFile(element, index)
);

异常处理

Exceptions in Java 8 Lambda Expressions

Stream 中异常处理的四种方式

参考

https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html

https://docs.oracle.com/javase/tutorial/collections/streams/index.html

https://github.com/amaembo/streamex

https://www.baeldung.com/category/java/tag/java-streams/

用了 Stream API 之后,代码反而越写越丑?——写出具有可维护性的 Stream API 代码

IntelliJ IDEA 如何优雅的调试 Java Stream 操作?

Java 8 引入了 Optional 类用于解决臭名昭著的空指针异常。它本质上是一个可以为 null 的容器对象,并提供了很多有用的方法,以函数式编程的风格简化 null 处理。

Optional 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
String nullStr = null;

// 初始化 Optional
Optional<String> a = Optional.of("a");
Optional<String> b = Optional.empty();
Optional<String> c = Optional.ofNullable(nullStr);

String s0 = a.get(); // a
String s1 = b.orElse("other"); // other
String s2 = b.orElseGet(this::someExpensiveOperation); // 方法引用的返回值
String s3 = b.orElseGet(() -> someExpensiveOperation()); // lambda 表达式的返回值
String s4 = b.orElseThrow(IllegalArgumentException::new); // 抛异常
String s5 = b.orElseThrow(() -> new IllegalArgumentException("非法参数")); // 抛异常

boolean isPresent = a.isPresent(); // true
a.ifPresent(System.out::println); // a
a.filter(String::isEmpty).ifPresent(System.out::println); // 条件不匹配,无打印
a.map(String::toUpperCase).ifPresent(System.out::println); // 映射为大写字母 A 并打印

上述示例中,Optional 几个关键方法主要使用到这几个函数式接口:

  • orElseGet 使用到: java.util.function.Supplier
  • orElseThrow 使用到: java.util.function.Supplier
  • ifPresent 使用到:java.util.function.Consumer
  • filter 使用到:java.util.function.Predicate
  • map 使用到:java.util.function.Function

Optional 几个关键方法的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public final class Optional<T> {

/**
* Return the value if present, otherwise invoke {@code other} and return
* the result of that invocation.
*/
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}

/**
* Return the contained value, if present, otherwise throw an exception
* to be created by the provided supplier.
*/
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}

/**
* If a value is present, invoke the specified consumer with the value,
* otherwise do nothing.
*/
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}

/**
* If a value is present, and the value matches the given predicate,
* return an {@code Optional} describing the value, otherwise return an
* empty {@code Optional}.
*
*/
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}

/**
* If a value is present, apply the provided mapping function to it,
* and if the result is non-null, return an {@code Optional} describing the
* result. Otherwise return an empty {@code Optional}.
*/
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
}

Lambda 表达式

Lambda 表达式总结:

lambda

Java 8 为函数式编程新增的重点 API:

api

函数式接口

函数式接口是只有一个抽象方法的接口,作为 Lambda 表达式和方法引用的目标类型

JDK 8 新增了 9 组共 43 个通用型函数式接口,位于 java.util.function 包下,用来支持 Java 的函数式编程。接口如此之多的原因有二:

  • 为了支持不同的参数个数。如 UnaryOperator<T> 仅支持一个参数,而 BinaryOperator<T> 支持两个参数。这一点从接口命名及函数签名也能看出:

    • Unary 一元
    • Binary 二元
    • Ternary 三元
    • Quaternary 四元
    • ……
  • 泛型不支持原始数据类型。而在面对大数据量的流式 API 运算时,为了解决包装类在自动拆装箱的性能消耗,引入了 intlongDouble 原始数据类型的函数式接口。

    千万不要用带包装类型的基础函数接口来代替基本类型的函数接口。虽然可行,但它破坏了第 61 条的规则“基本类型优于装箱基本类型”。使用装箱基本类型进行批量操作处理,最终会导致致命的性能问题。——《Effective Java》

这些接口统计如下:

接口 函数签名 范例 范例 基本类型特化
Predicate<T> boolean test(T t) String::isEmpty 符合某个条件吗? IntPredicate
LongPredicate
DoublePredicate
BiPredicate<T, U> boolean test(T t, U u)
Supplier<T> T get() Instant::now 无参的工厂方法 BooleanSupplier
IntSupplier
LongSupplier
DoubleSupplier
Consumer<T> void accept(T t) System.out::println 输出一个值 IntConsumer
LongConsumer
DoubleConsumer
BiConsumer<T, U> void accept(T t, U u) ObjIntConsumer<T>
ObjLongConsumer<T>
ObjDoubleConsumer<T>
Function<T, R> R apply(T t) Arrays::asList 类型转换 IntFunction<R>
IntToLongFunction
IntToDoubleFunction
LongFunction<R>
LongToIntFunction
LongToDoubleFunction
DoubleFunction<R>
DoubleToIntFunction
DoubleToLongFunction
ToIntFunction<T>
ToLongFunction<T>
ToDoubleFunction<T>
BiFunction<T, U, R> R apply(T t, U u) ToIntBiFunction<T, U>
ToLongBiFunction<T, U>
ToDoubleBiFunction<T, U>
UnaryOperator<T> T apply(T t) String::toUpperCase 格式转换 IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator
BinaryOperator<T> T apply(T t1, T t2) BigInteger::add 求两个数的加减乘除 IntBinaryOperator
LongBinaryOperator
DoubleBinaryOperator

以上接口都标注了 @FunctionalInterface。这是 Java 8 为函数式接口引入的一个新注解,有两个目的:

  • 告诉这个接口及其文档的读者,这个接口是针对 Lambda 设计的;
  • 用于编译级错误检查。加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。该注解会强制 javac 检查一个接口是否符合函数式接口的标准。如果该注释添加给一个枚举类型、类或另一个注解,或者接口包含不止一个抽象方法javac 就会报错。重构代码时,使用它能很容易发现问题,因此建议必须始终用 @FunctionalInterface 注解对自己编写的函数式接口进行标注。

此外,函数式接口允许:

  • 函数式接口里允许定义默认方法,因为默认方法不是抽象方法,其有一个默认实现,所以是符合函数式接口的定义的。
  • 函数式接口里允许定义静态方法,因为静态方法不能是抽象方法,是一个已经实现了的方法,所以是符合函数式接口的定义的。
  • 函数式接口里允许定义 java.lang.Object 里的 public 方法。

方法引用

方法引用通过方法的名字来指向一个方法,可以使语言的构造更紧凑简洁,进一步减少冗余代码,尤其是 Lambda 表达式。

方法引用使用一对冒号 ::

下面对比下方法引用简化 Lambda 表达式的例子:

方法引用类型 方法引用范例 Lambda 表达式
静态 Integer::parseInt str -> Integer.parseInt(str)
有限制 Instant.now()::isAfter Instant then = Instant.now();
t -> then.isAfter(t)
无限制 String::toLowerCase str -> str.toLowerCase()
类构造器 TreeMap<K, V>::new () -> new TreeMap<K, V>
数组构造器 int[]::new len -> new int[len]

例子

这里对比 JavaScript 和 Java 两门语言的例子,方便对比学习。

JavaScript 箭头函数

在 JavaScript 语言中,函数是一等公民(参考为何钟爱一等公民知乎)。ES6 新特性允许使用“箭头”(=>)定义函数,语法简洁,使用如下:

没有参数,需要空括号:

1
2
3
4
5
6
7
var f = () => 5;

// 等同于
var f = function () { return 5 };

// 5
var result = f();

一个参数,无需括号:

1
2
3
4
5
6
7
var f = v => v;

// 等同于
var f = function (v) { return v; };

// 10
var result = f(10);

多个参数,需要括号:

1
2
3
4
5
6
7
var sum = (num1, num2) => num1 + num2;

// 等同于
var sum = function(num1, num2) { return num1 + num2; };

// 20
var result = sum(10, 10);

箭头函数的一个用处是简化回调函数

例子 1:

1
2
3
4
5
6
7
// 正常函数写法
[1, 2, 3].forEach(function (x) {
console.log(x);
})

// 箭头函数写法,结果 1 2 3
[1, 2, 3].forEach(x => console.log(x));

例子 2:

1
2
3
4
5
6
7
// 正常函数写法
var result = [1, 2, 3].map(function (x) {
return x * x;
});

// 箭头函数写法,结果 [1, 4, 9]
var result = [1, 2, 3].map(x => x * x);

例子 3:

1
2
3
4
5
6
7
// 正常函数写法
var result = [2, 3, 1].sort(function (a, b) {
return a - b;
});

// 箭头函数写法,结果 [1, 2, 3]
var result = [2, 3, 1].sort((a, b) => a - b);

例子 4:

1
2
3
4
5
6
7
// 正常函数写法
var result = [1, 2, 3].filter(function (x) {
return x > 1;
});

// 箭头函数写法,结果 [2, 3]
var result = [1, 2, 3].filter(x => x > 1);

Java Lambda 表达式

然而在 Java 语言中,函数并非一等公民。但可以利用 Lambda 表达式 + 函数式接口来模拟 JavaScript 类似的语法,对比如下:

没有参数,需要空括号:

1
2
3
4
IntSupplier f = () -> 5;

// 5
int result = f.getAsInt();

一个参数,无需括号:

1
2
3
4
ToIntFunction<Integer> f = i -> i;

// 10
int result = f.applyAsInt(10);

多个参数,需要括号:

1
2
3
4
5
6
7
IntBinaryOperator sum = (num1, num2) -> num1 + num2;

// 使用方法引用进一步简化语法
// IntBinaryOperator sum = Integer::sum;

// 20
int result = sum.applyAsInt(10, 10);

Lambda 表达式同样可以简化回调函数

例子 1:

1
2
3
4
5
// 1 2 3
IntStream.of(1, 2, 3).forEach(x -> System.out.println(x));

// 使用方法引用进一步简化语法
// IntStream.of(1, 2, 3).forEach(System.out::println);

例子 2:

1
2
// [1, 4, 9]
int[] result = IntStream.of(1, 2, 3).map(x -> x * x).toArray();

例子 3:

1
2
3
4
5
6
7
8
9
10
11
// [1, 2, 3]
int[] result = Stream.of(2, 3, 1)
.sorted((a, b) -> a - b)
.mapToInt(Integer::intValue)
.toArray();

// 使用方法引用进一步简化语法
// int[] result = Stream.of(2, 3, 1)
// .sorted(Comparator.naturalOrder())
// .mapToInt(Integer::intValue)
// .toArray();

例子 4:

1
2
// [2, 3]
int[] result = IntStream.of(1, 2, 3).filter(x -> x > 1).toArray();

使用场景

  • Java 8 引入了 Optional 类用于解决臭名昭著的空指针异常。它本质上是一个可以为 null 的容器对象,并提供了很多有用的方法,以函数式编程的风格简化 null 处理。
  • Stream API 是一种基于函数式编程的模型,用于增强集合处理。

参考

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

https://docs.oracle.com/javase/tutorial/collections/streams/index.html

《Effective Java 第三版》:

  • 第 42 条:Lambda 优先于匿名类
  • 第 43 条:方法引用优先于 Lambda
  • 第 44 条:坚持使用标准的函数式接口(包括基本数据类型的函数式接口)
  • 第 45 条:谨慎使用 Stream(必要时也需要使用 Iterator 外部迭代器)
  • 第 46 条:优先选择 Stream 中无副作用的函数(使用收集器 Collectors 而不是 forEach
  • 第 47 条:Stream 要优先用 Collection 作为返回类型
  • 第 48 条:谨慎使用 Stream 并行

《Java 8 函数式编程》

《Java 8 实战》

万字长文详解 Java lambda 表达式 | 阿里技术

Java8 Lambda 实现源码解析 | 阿里技术