2019年1月

Java Mission Control (JMC) and Java Flight Recorder (JFR)

JMC 是个图形化工具, 监控JVM 以及操作系统的一些指标; 它可以直接连JVM 去采集, 也可以读存档的JFR文件;
JFR 对于java应用程序进行诊断和profiling的工具, 它集成在HotSpot JVM 里面, 并且有很小的性能影响. 基于一组事件去采集,记录;可以通过JMC图形化界面控制JFR 事件,也可以通过jcmd 命令行来执行;

  1. 默认开启的JFR事件有小于1%的性能影响;
  2. 事件类型: 内存, 线程, I/O, code (编译, hot package, hot class),系统;

-- 基于事件长度的event, 记录事件长度, 你可以设置超过每个长度的才记录;
-- 瞬时事件
-- 采样(sampling) 事件, 你可以配置采样频率;

  1. 每个事件有 事件名称, 时间戳, 和payload 组成;
  2. 通过各种纬度的事件, 你可以构建运行时系统状态;
  3. 数据流: JFR collects data from the JVM (through internal APIs) and from the Java application (through the JFR APIs). This data is stored in small thread-local buffers that are flushed to a global in-memory buffer. Data in the global in-memory buffer is then written to disk.
  4. JFR 架构

-- JFR 运行时引擎, 它产生事件到buffer,并可选持久化到磁盘
-- JFR plugin 在JMC里面分析JFR事件

  1. JFR 默认是disabled, 并且在HotSpot JVM里面是商业软件, 所以要在启动时通过2个flags 来启动它

-- java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder MyApp
-- java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:FlightRecorderOptions=defaultrecording=true MyApp
-- for OpenJDK, 要使用JDK 11, 这篇文章介绍如何在 openJDK 使用 JFR https://dzone.com/articles/using-java-flight-recorder-with-openjdk-11-1

  1. 启动app时, 可以通过 -XX:+FlightRecorderOptions=string 来设置参数, 多个参数用逗号隔开, 可选参数如下

    name=name
    The name used to identify the recording.
    defaultrecording=<true|false>
    Whether to start the recording initially. The default value is false; for reactive analysis, this should be set to true.
    settings=path
    Name of the file containing the JFR settings (see next section).
    delay=time
    The amount of time (e.g., 30s, 1h) before the recording should start.
    duration=time
    The amount of time to make the recording.
    filename=path
    Name of the file to write the recording to.
    compress=<true|false>
    Whether to compress (with gzip) the recording; the default is false.
    maxage=time
    Maximum time to keep recorded data in the circular buffer.
    maxsize=size
    Maximum size (e.g., 1024K, 1M) of the recording’s circular buffer.

  2. 上面参数可以在启动时传入, 但是更灵活的方式是, 通过jcmd 运行时传入, 如:

jcmd process_id JFR.start [options_list] //start 时候可以选择模版 settings=
jcmd process_id JFR.dump [options_list] //dump 一个连续的recording
jcmd process_id JFR.check [verbose] //check 当前的recording
jcmd process_id JFR.stop [options_list]

  1. 选取JFR 事件

JFR 事件是基于模版的, HotSpot 默认有2个模版: default template & a profile template;
这些event template 都是xml 文件, 2个默认的模版在$JAVA_HOME/ jre/lib/jfr 目录, 用户自定义的在 $USER_HOME/.jmc/ 目录;

Java SimpleDateFormat 的格式化字符串里的Y

2017年元旦假期三天, 程序员小吴没有出去玩. 早上10点多了, 还在家睡懒觉. 突然电话响起,吵醒了半睡半醒的他. 他心想是推销还是贷款专员? 休假都被叫醒, 很是生气. 拿起手机, 发现竟然是公司打来的电话,顿时感觉不妙. 接通电话, 那头说有个紧急的site issue, 可能跟小吴的代码相关, 让他紧急上线看一下. 小吴一下子清醒了很多, 连忙登录VPN, 检查最新邮件, 确实发现有个问题, 跟自己几个月前上线的代码有关系.

这段代码巨简单, 就是取服务器当前的时间, 然后格式化一下, 显示在页面上. 同时,页面上有些数据统计信息, 会通过Ajax请求的方式去慢慢取,然后展示在页面. 去取的时候, 把本来展示在页面的这个格式化后的日期, 发给后台服务器. 问题就出现在, 本来这天是2017年12月31日, 可是页面上却显示的是 2018/12/31, 默认加了1年, 非常的奇怪.

小吴首先把线上已部署的代码的commit id 找到,然后把这个commit拉下来, 仔细检查了一番, 并没有发现什么情况. 这段代码已经在生产环境运行了8个多月, 之后一直没有人改过. 之前的测试以及线上从来没出现过问题, 真是奇了怪了. 小吴只好本地debug了一下, 发现本地能完美重现... 用当天的日期格式化一把, 格式化之后一直是2018年12月31日. 之前从来没有问题, 难道这天是啥好日子? 经过一番修改测试, 终于发现了问题:

public static void main(String[] args) {
    Calendar cal = Calendar.getInstance();
    cal.set(2017, 11, 29);
    SimpleDateFormat sdf0 = new SimpleDateFormat("YYYY/MM/dd");
    SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy/MM/dd");
    Date date = null;
    for (int i = 0; i < 4; i++) {
        date = cal.getTime();
        System.out.println(sdf0.format(date));
        System.out.println(sdf1.format(date));
        cal.add(Calendar.DATE, 1);
    }
}

运行结果:

2017/12/29
2017/12/29
2017/12/30
2017/12/30
2018/12/31
2017/12/31
2018/01/01
2018/01/01

从上面代码片段及运行结果看, 只有2017年12月31日出问题了. 问题出现在格式化的时候, 使用是大写的Y还是小写的y. 那么根据官方文档, 差别是什么呢?

y: Year Year 如: 1996; 96
Y: Week year Year 如: 2009; 09

一个是year, 一个 week year. year 很好理解, 就是我们所说的年, 那么 week year 呢? week year的计算涉及到2个东西, 一个是: 一周的第一天是从周一开始算还是从周日开始算, 有的人/地方从周日开始算, 有的从周一开始算. 第二个是, 我们经常听说这是今年第几周, 试想如果一个week跨过1年, 那么这周算到那年里?
JDK 文档举的例子是: 1998年1月1日是周四.
如果我们设定一周的第一天是周一, 那么这周属于1998年的就有4天(周4,5,6,7), 属于1997年的只有3天, 那么这周算在1998年,这是1998年的第一周. 那么1997年12月29, 30, 31日的 week year 就是1998, 而不是1997了.
如果我们设定一周的第一天是周日, 那么这周属于1998年的就有3天(周4,5,6), 属于1997年的就有4天了. 那么这周算在1997年,这是1997年的最后一周,1998年的第一周从1月4号开始, 那么1998年,1月1,2.3号的 week year 就是1997了.

FirstDayOfWeek 是根据JDK所在的系统设置, 有个默认设置的.

所以这个问题, 只要改回正确的yyyy问题就解决了. 另外还发现有人误以为这是JDK的bug, 去提了bug: https://bugs.openjdk.java.net/browse/JDK-8194625

参考:
https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
https://docs.oracle.com/javase/9/docs/api/java/util/GregorianCalendar.html#week_year

<Exploring ES6> 读书笔记 - Variables and scoping

  1. var is function-scoped, let is block-scoped; //for (const x of ['a', 'b']) {} is ok
  2. let/const 有emporal dead zone (TDZ), 其实这和Java生命使用变量一直, 未声明前不可用. 但var声明但变量一旦进入这个变量声明但function, 即使还没到声明这行代码, 但是这个变量已经有了, 并且赋值为 undefined;

    let tmp = true;
    if (true) { // enter new scope, TDZ starts

     // Uninitialized binding for `tmp` is created
     console.log(tmp); // ReferenceError
    
     let tmp; // TDZ ends, `tmp` is initialized with `undefined`
     console.log(tmp); // undefined
    
     tmp = 123;
     console.log(tmp); // 123

    }
    console.log(tmp); // true
    例子2

    if (true) { // enter new scope, TDZ starts

     const func = function () {
         console.log(myVar); // OK!
     };
    
     // Here we are within the TDZ and
     // accessing `myVar` would cause a `ReferenceError`
    
     let myVar = 3; // TDZ ends
     func(); // called outside TDZ

    }

  3. always use const, let, 避免 var

<Exploring ES6> 读书笔记 - New number and Math features

  1. New integer literals, 直接可以二进制, 八进制, 十六进制了

    0xFF // ES5: hexadecimal
    255
    0b11 // ES6: binary
    3
    0o10 // ES6: octal
    8
  2. New Number properties

    Number.isInteger(num)
    Number.isSafeInteger(number)
    Number.MIN_SAFE_INTEGER
    Number.MAX_SAFE_INTEGER
    Number.isNaN(num)
    Number.isFinite
    Number.parseFloat
    Number.parseInt

  3. New Math methods

    Math.sign(-8) // -1
    Math.trunc(3.1)
    Math.log10(100)