1.简介
Oracle甲骨文公司于2015年1月15日发布了新一版JDK8,新版本加入了许多新的特性。这些特性带来了一些改变,可以很大方便Java程序的编写。新特性主要涉及:对于JDK7中Fork/Join并行处理的升级;支持Lambda表达式;添加了Stream API;对于注解的拓展,加入了类型注解、重复注解;在G1回收器中支持字符串去重;内存空间中删除了永久代,引入了元空间。
2.新特性
2.1 Fork/Join
JDK7引入了轻量级的数据并行fork/join框架,但用户必须实现自己的算法以完成简单任务/普通任务,在JDK8中提供了并行数组排序的标准实现。相比JDK7,JDK8中fork/join运行效率更高。2.1.1 并行API
通过这个接口可以对数组通过并行处理的方式进行排序int[] numbers = {2,1,3};
Arrays.parallelSort(numbers);
2.1.2 Fork/Join基准测试 JDK7 vs JDK8
通过Java Microbenchmark Harness project写一个基准测试来对比Java最近的两个版本JDK7&JDK8,可以发现Fork/Join运行效率在单线程环境下没有明显区别,在多线程环境下,运行JDK8的Fork/Join效率明显提高。2.2 Lambda
JDK8中引入了函数式编程Lambda表达式,这是Java第一次引入函数式编程的相关内容。对于Java中的函数式接口java.lang.Runnable、java.util.Comparator等,都可以使用Lambda表达式来简化代码。
以Runnable接口为例,创建一个传统线程代码:
public void runThread() {
new Thread(new Runnable() {
public void run() {
System.out.println("Run!");
}
}).start();
}
使用Lambda表达式后,创建一个线程代码:
public void runThreadUseLambda() {
new Thread(() -> {
System.out.println("Run!");
}).start();
}
Lambda表达式的声明方式比较简单,由形式参数和方法体两部分组成,中间通过“->”分隔。形式参数可以不需要包含类型的声明,可以进行
自动推断。方法体可以是简单的表达式或代码块。
比如把一个整数列表按照降序排列可以用下面的代码来简洁实现:
Collections.sort(list, (x, y) -> y - x);
2.3 Stream API
JDK8引入了全新的Stream API。这里的Stream和I/O流不同,它更像具有Iterable的集合类,但行为和集合类有有所不同。使用JDK8中java.util.stream下的API无需编写一行多线程代码,就可以很方便地写出高性能的并发程序。
Stream就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,像水流过,不可往复。与迭代器不同的是,Stream可以并行化操作,迭代器只能命令式地、串行化操作。当使用串行化方式遍历时,每个item读完后再读下一个。而使用并行化方式遍历时,数据会被分成多个段,其中每个都在不同的线程中处理,然后将结果一起输出。
2.3.1 流的构造与转换
流的常用构造方式
// 1. Individual values
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();
流转换为其他数据结构
// 1. Array
String[] strArray1 = stream.toArray(String[]::new);
// 2. Collection
List<String> list1 = stream.collect(Collectors.toList());
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream.collect(Collectors.toSet());
Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));
// 3. String
String str = stream.collect(Collectors.joining()).toString();
2.3.2 流的操作
常用数据结构转为流后,就可以进行操作。流的操作通常分为以下三类:
- Intermediate:
map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
- Terminal:
forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
- Short-circuiting:
anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
从上述三个类别中分别选取一种操作来进行分析。
map
通过map操作把List<String>中String都转为大写
List<String> output = wordList.stream().
map(String::toUpperCase).
collect(Collectors.toList());
forEach
forEach 方法接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。
// Java 8
roster.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.forEach(p -> System.out.println(p.getName()));
// Pre-Java 8
for (Person p : roster) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
}
limit/skip
limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素(它是由一个叫 subStream 的方法改名而来)。
public void testLimitAndSkip() {
List<Person> persons = new ArrayList();
for (int i = 1; i <= 10000; i++) {
Person person = new Person(i, "name" + i);
persons.add(person);
}
List<String> personList2 = persons.stream().
map(Person::getName).limit(10).skip(3).collect(Collectors.toList());
System.out.println(personList2);
}
private class Person {
public int no;
private String name;
public Person (int no, String name) {
this.no = no;
this.name = name;
}
public String getName() {
System.out.println(name);
return name;
}
}
输出结果为:
name1
name2
name3
name4
name5
name6
name7
name8
name9
name10
[name4, name5, name6, name7, name8, name9, name10]
2.4 注解
2.4.1. 类型注解
JDK8的类型注解拓展了注解的使用范围。在之前版本,注解只能在声明的地方使用,现在几乎可以为任何东西添加注解。通过新增的元素类型ElementType.TYPE_USE和ElementType.TYPE_PARAMETER可以用来描述注解使用的场合。ElementType.TYPE_USE表示该注解能写在使用类型的任何语句中,ElementType.TYPE_PARAMETER表示注解能使用在类型变量的声明语句中。通过对类型注解的支持,增强了程序的健壮性。在程序中,可以定义一个非空注解,注释变量,如果变量没有判空,调用方法,可以通过静态检测程序工具发现。类型注解也还可以有其它的应用。2.4.2. 重复注解
在之前版本,同一注解在同一地方只能使用一次。在JDK8中,引入了重复注解机制,这样相同注解在同一地方可以使用多次,但重复注解必须使用@Repeatable注解。2.5 字符串去重(String deduplication)
通过JVM内存工具可以观测到,在JDK8之前,String都是占用内存空间较大的,如果对String进行优化,那么JVM的内存使用效率可以得到提高。在JDK8中加入了对String存储的优化,如果GC使用G1回收器,那么就能够使用这一特性。
从JDK7 update 6开始,每个String底层都有自己私有的char[],这样允许jvm做一些优化。在JDK8 update 20中正式引入了字符串去重,字符串去重会在垃圾回收执行时运行,计算字符串的char[]的hash值,如果之前有相同的hash值,那么用之前的char[]替换现有的。字符串一般在jvm中占用的空间是比较大的,这一特性对jvm内存空间的利用提升是巨大的。
不开启字符串去重 配置jvm参数-Xmx256m -XX:+UseG1GC,运行下列代码
public class LotsOfStrings {代码会在30次循环之后因OutOfMemoryError异常而结束运行
private static final LinkedList<String> LOTS_OF_STRINGS = new LinkedList<>();
public static void main(String[] args) throws Exception {
int iteration = 0;
while (true) {
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 1000; j++) {
LOTS_OF_STRINGS.add(new String("String " + j));
}
}
iteration++;
System.out.println("Survived Iteration: " + iteration);
Thread.sleep(100);
}
}
}
开启字符串去重 配置jvm参数-Xmx256m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics,开启字符串去重,可以发现程序可以多运行一段时间。
2.6 元空间(Metaspace)
元空间是java内存模型中一个极大的升级,从JDK8起,取消了持久代,使用元空间,从而引起了垃圾回收算法等的调整。
为什么移除持久代
- 持久代的大小是在启动时固定好的——很难进行调优。-XX:MaxPermSize,设置成多少好呢?
- HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,同时它对应用不透明,且是非强类型的,难以跟踪调试,还需要存储元数据的元数据信息(meta-metadata)。
元空间的优点
- 简化Full GC:每一个回收器有专门的元数据迭代器。
- 可以在GC不进行暂停的情况下并发地释放类数据。
- 使得原来受限于持久代的一些改进未来有可能实现。
- 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
- 每个加载器有专门的存储空间。
- 只进行线性分配。
- 不会单独回收某个类
- 省掉了GC扫描及压缩的时间
- 元空间里的对象的位置是固定的
- 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
3.版本升级注意事项
3.1.第三方库以来老版本JDK
java的语言特性是向上兼容的,但也存在第三方库依赖老版本的jdk,而不与JDK8兼容的情况,这个时候需要考虑兼容性。最好先在开发测试环境先升级JDK8,如果没有出现兼容性问题,再升级线上版本。3.2.JVM参数设置Metaspace
在JDK8上原有的JVM参数“-XX:MaxPermSize=128m”会无效,需要设置新的参数“-XX:MaxMetaspaceSize=128m”替换。4.总结
JDK8是一次较大的升级,从语言层面升级了并行框架、引入了Lambda表达式、Stream API、升级注解,在虚拟机层面,升级了字符串存储的处理方式,JVM内存的管理结构。现在JDK8版本在社区已经趋于稳定,喜欢这些新特性的,可以考虑把java项目升级到JDK8。