JEP解读与尝鲜系列1

Wesley13
• 阅读 652

涉及到的JEP:

Valhalla项目背景

最主要的一点就是,让Java适应现代硬件:在Java语言发布之初,一次内存访问和一次数字计算的消耗时间是差不多的,但是现在,一次内存访问耗时大概是一次数值计算的200~1000倍。从语言设计上来说,也就是间接访问带来的通过指针获取的需要操作的内存,对于整体性能影响很大。

Java是基于对象的语言,也就是说,Java是一种基于指针重间接引用的语言。这个基于指针的特性,给每个对象带来了唯一标识性。例如判断两个Object的==,其实判断的是两个对象的内存相对映射地址是否相同,尽管两个对象的field完全一样,他们的内存地址也不同。同时这个特性也给对象带来了多态性,易变性还有锁的特性。但是,并不是所有对象都需要这种特性。

由于指针与间接访问带来了性能瓶颈,Java准备对于不需要这种特性的对象移除这种特性。于是乎,Value type出现了。

Value Type

Value type用于表示纯数据集合。所有不需要的指针特性被移除。其实就是,Java的对象头被移除了。

来看一个例子:

final class Point {
  final int x;
  final int y;
}

这个在内存中的结构是:

JEP解读与尝鲜系列1

对于Value type

value class Point {
  int x;
  int y
}

这个在内存中的结构是:

JEP解读与尝鲜系列1  我们再来对比下数组的存储:

对于CommonObj[],只有引用是连续存储的,实际的值:

JEP解读与尝鲜系列1 对于Value types,数组存储可以扁平化,不用分散存储,采取真正的:

JEP解读与尝鲜系列1

这样,JVM不用再跑到堆上分配内存来存储这种对象,而是可以直接在栈上面分配。这样,Value types的表现,就和Java的原始类型int等就很像了。与原始类型不同的是,Value types可以有方法和fileds。

同时我们还希望能让它作为接口泛型。我们希望能有更广泛的接口泛型,无论是对象,还是Value types,还是原始类型(刚才已经说明了,利用原始类型性能更好),而不是封装的原始类型。这就引出了,Valhalla的另一个重要更新(针对泛型):Specialized Generics

Specialized Generics

从字面上理解,其实就是指泛型不止针对对象,也需要包含Value types,还有最重要的是原始类型例如int这些。

目前(JDK14之前的),泛型必须是一个对象类。针对原始类型,也必须使用原始类型的封装类,例如Integer之于int。这就违反了之前说的减少对象封装,使用原始类型。所以这个优化对于Value Types的实现也是必须的。

顺便一提,目前JDK框架的Java源码也有很多使用原始类型从而提高性能的地方,例如IntStream涉及到的所有int的操作函数,传参都是int,而不是Integer:

@FunctionalInterface
public interface IntUnaryOperator {
    int applyAsInt(int operand);
}

JDK 14 引入的关键字 inline:Inline Classes

这个Inline Classes实际上就是一种Value Types的实现。

我们首先回顾下,普通类对象的存储结构:

public static void main(String args) {
    CommonObj a = new CommonObj();
}

这段代码,会在栈上新建一个引用变量a, 在堆上面申请一块内存用于存储新建的CommonObj这个对象,对象包括,

  • 标记字
  • 指向class原数据的指针
  • 具体数据字段 如图所示: JEP解读与尝鲜系列1

Inline class 尝鲜

由于目前JDK 14 还没发布,我们只能通过目前开发版的OpenJDK进行尝鲜。可以通过这里下载全平台的OpenJDK project Valhalla尝鲜版:http://jdk.java.net/valhalla/

由于目前还没开发完,我们只能通过字节码去解读与原始类的不同。

目前,inline class的限制是:

  • 接口,注解和枚举不能成为inline class
  • 公共类,内部类,静态内部类,本地类可以作为inline class
  • inline class不能接受空值,需要有默认值
  • 可以声明内部类型,静态内部类性和本地类型
  • inline class 默认隐式final的,所以不能是abstract的
  • inline class 默认隐式继承java.lang.Object(就和enum, annotation还有interface一样)
  • inline class 可以实现普通的interface
  • inline class 的实例的所有field默认都是final的
  • nline class不能声明类型是自己这种类型的field
  • javac 编译的时候,自动给inline class 生成 hashCode(), equals(), and toString()方法
  • javac 编译的时候,会检查并禁止是否有对于inline class的 clone(), finalize(), wait(), 或者 notify()的调用

我们来声明一个类似于java.util.OptionalInt的类:

public inline class OptionalInt {
   private boolean isPresent;
   private int v;

   private OptionalInt(int val) {
       v = val;
       isPresent = true;
   }

   public static OptionalInt empty() {
       // New semantics for inline classes
       return OptionalInt.default;
   }

   public static OptionalInt of(int val) {
       return new OptionalInt(val);
   }

   public int getAsInt() {
       if (!isPresent)
           throw new NoSuchElementException("No value present");
       return v;
   }

   public boolean isPresent() {
       return isPresent;
   }

   public void ifPresent(IntConsumer consumer) {
       if (isPresent)
           consumer.accept(v);
   }

   public int orElse(int other) {
       return isPresent ? v : other;
   }

   @Override
   public String toString() {
       return isPresent
               ? String.format("OptionalInt[%s]", v)
               : "OptionalInt.empty";
   }
}

编译后,我们反编译一下代码,查看下,发现:

public final value class OptionalInt {
 private final boolean isPresent;

 private final int v;

class 变成 value class修饰,同时,按照之前的约束,这里多了final修饰符。同时,所有的field也多了final修饰。

然后是构造器部分:

public static OptionalInt empty();
    Code:
       0: defaultvalue  #1                  // class OptionalInt
       3: areturn

  public static OptionalInt of(int);
    Code:
       0: iload_0
       1: invokestatic  #11                 // Method "<init>":(I)OptionalInt;
       4: areturn

  private static OptionalInt OptionalInt(int);
    Code:
       0: defaultvalue  #1                  // class OptionalInt
       3: astore_1
       4: iload_0
       5: aload_1
       6: swap
       7: withfield     #3                  // Field v:I
      10: astore_1
      11: iconst_1
      12: aload_1
      13: swap
      14: withfield     #7                  // Field isPresent:Z
      17: astore_1
      18: aload_1
      19: areturn

我们来看java.util.OptionalIntof方法对应的字节码:

public static OptionalInt of(int);
    Code:
       0: new           #5  // class OptionalInt
       3: dup
       4: iload_0
       5: invokespecial #6  // Method "<init>":(I)V
       8 setfield
       9: areturn

我们发现,对于inline class,没有new也没有serfield这两个字节码操作。而是用defaultvaluewithfield代替。因为字段都是final的,没必要保留引用,所以用withfield

Inline class 和原始类堆栈内存占用大小对比

首先编写测试代码,下面的OptionalInt在两次测试中,分别是刚刚自定义的Inline class,还有java.util.OptionalInt

public static void main(String[] args) {
        int MAX = 100_000_000;
        OptionalInt[] opts = new OptionalInt[MAX];
        for (int i=0; i < MAX; i++) {
            opts[i] = OptionalInt.of(i);
            opts[++i] = OptionalInt.empty();
        }
        long total = 0;
        for (int i=0; i < MAX; i++) {
            OptionalInt oi = opts[i];
            total += oi.orElse(0);
        }
        try {
            Thread.sleep(60_000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("Total: "+ total);
    }

运用jmap命令查看:

jmap -histo:live

对于Inline class:

num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:             1      800000016  [OptionalInt;
   2:          1687          97048  [B (java.base@14-internal)
   3:           543          70448  java.lang.Class (java.base@14-internal)
   4:          1619          51808  java.util.HashMap$Node (java.base@14-internal)
   5:           452          44600  [Ljava.lang.Object; (java.base@14-internal)
   6:          1603          38472  java.lang.String (java.base@14-internal)
   7:             9          33632  [C (java.base@14-internal)

大概占用了8*100_000_000这么多字节的内存,剩下的16字节是数组头,这也符合之前提到的Value Type的特性。

对于java.util.OptionalInt:

 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:      50000001     1200000024  java.util.OptionalInt
   2:             1      400000016  [Ljava.util.OptionalInt;
   3:          1719          98600  [B
   4:           540          65400  java.lang.Class
   5:          1634          52288  java.util.HashMap$Node
   6:           446          42840  [Ljava.lang.Object;
   7:          1636          39264  java.lang.String

大概多了400MB的空间,并且多了50000000个对象。并且根据之前的描述,内存分配并不是在一起连续的,发生垃圾回收的时候,降低了扫描效率。

利用JMH测试下性能

import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public class MyBenchmark {

    @Benchmark
    public long timeInlineOptionalInt() {
        int MAX = 100_000_000;
        infoq.OptionalInt[] opts = new infoq
.
OptionalInt[MAX];
        for (int i=0; i < MAX; i++) {
            opts[i] = OptionalInt.of(i);
            opts[++i] = OptionalInt.empty();
        }
        long total = 0;
        for (int i=0; i < MAX; i++) {
            infoq.OptionalInt oi = opts[i];
            total += oi.orElse(0);
        }

        return total;
    }

    @Benchmark
    public long timeJavaUtilOptionalInt() {
        int MAX = 100_000_000;
        java.util.OptionalInt[] opts = new java
.
util
.
OptionalInt[MAX];
        for (int i=0; i < MAX; i++) {
            opts[i] = java.util.OptionalInt.of(i);
            opts[++i] = java.util.OptionalInt.empty();
        }
        long total = 0;
        for (int i=0; i < MAX; i++) {
            java.util.OptionalInt oi = opts[i];
            total += oi.orElse(0);
        }

        return total;
    }
}

结果:

Benchmark                             Mode  Cnt  Score   Error  Units
MyBenchmark.timeInlineOptionalInt    thrpt   25  5.155 ± 0.057  ops/s
MyBenchmark.timeJavaUtilOptionalInt  thrpt   25  0.589 ± 0.029  ops/s

可以看出,Inline class的效率,远大于普通原始类。

参考:https://www.infoq.com/articles/inline-classes-java/?itm_campaign=rightbar_v2&itm_source=infoq&itm_medium=articles_link&itm_content=link_text

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
5个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这