这都Java15了,Java7特性还没整明白?

  • 「MoreThanJava」 宣扬的是 「学习,不止 CODE」,本系列 Java 基础教程是本身在结合各方面的知识以后,对 Java 基础的一个总回顾,旨在 「帮助新朋友快速高质量的学习」
  • 固然 不论新老朋友 我相信您均可以 从中获益。若是以为 「不错」 的朋友,欢迎 「关注 + 留言 + 分享」,文末有完整的获取连接,您的支持是我前进的最大的动力!

特性总览

如下是 Java 7 中引入的部分新特性,关于 Java 7 更详细的介绍可参考官方文档html

  • java.langjava

    • Java 7 多线程下自定义类加载器的优化
  • Java 语言特性git

    • 改进的类型推断;
    • 使用 try-with-resources 进行自动资源管理
    • switch 支持 String
    • catch 多个异常;
    • 数字格式加强(容许数字字面量下划线分割);
    • 二进制字面量;
    • 加强的文件系统;
    • Fork/Join 框架;
  • Java 虚拟机 (JVM)程序员

    • 提供新的 G1 收集器;
    • 增强对动态调用的支持;
    • 新增分层编译支持;
    • 压缩 Oops;
    • 其余优化;
  • 其余;github

多线程下自定义类加载器的优化

在 Java 7 以前,某些状况下的自定义类加载器容易出现死锁问题。下面👇来简单分析演示一下官方给的例子 (下面用中文伪代码还原了一下)算法

// 类的继承状况:
class A extends B
class C extends D

// 类加载器:
Custom Classloader CL1:
    直接加载类 A
    委托 CL2 加载类 B
Custom Classloader CL2:
    直接加载类 C
    委托 CL1 加载类 D
    
// 多线程下的状况:
Thread 1:
    使用 CL1 加载类 A
    → 定义类 A 的时候会触发 loadClass(B),这时会尝试 锁住🔐 CL2    
Thread 2:
    使用 CL2 加载类 C
    → 定义 C 的时候会触发 loadClass(D),这时会尝试 锁住🔐 CL1
➡️ 形成 死锁☠️

形成死锁的重要缘由出在 JDK 默认的 java.lang.ClassLoader.loadClass() 方法上:api

JDK 7 和 JDK 6 loadClass 方法的对比

能够看到,JDK 6 及以前的 loadClass()synchronized 关键字是加在方法级别的,那么这就意味加载类时获取到的是一个 ClassLoader 级别的锁。数组

咱们来描述一下死锁产生的状况:缓存

文字版的描述以下:服务器

  • 线程1:CL1 去 loadClass(A) 获取到了 CL1 对象锁,由于 A 继承了类 B,defineClass(A) 会触发 loadClass(B),尝试获取 CL2 对象锁;
  • 线程2:CL2 去 loadClass(C) 获取到了 CL2 对象锁,由于 C 继承了类 D,defineClass(C) 会触发 loadClass(D),尝试获取 CL1 对象锁
  • 线程1 尝试获取 CL2 对象锁的时候,CL2 对象锁已经被 线程2 拿到了,那么 线程1 等待 线程2 释放 CL2 对象锁。
  • 线程2 尝试获取 CL1 对像锁的时候,CL1 对像锁已经被 线程1 拿到了,那么 线程2 等待 线程1 释放 CL1 对像锁。
  • 而后两个线程一直在互相等中…从而产生了死锁现象...

究其缘由就是由于 ClassLoader 的锁太粗粒度了。在 Java 7 中,在使用具备并行功能的类加载器的时候,将专门用一个带有 类加载器和类名称组合的对象 用于进行同步操做。(感兴趣能够看一下 loadClass() 内部的 getClassLoadingLock(name) 方法)

Java 7 以后,以前线程死锁的状况将不存在:

线程1:
  使用CL1加载类A(锁定CL1 + A)
    defineClass A触发
      loadClass B(锁定CL2 + B)

线程2:
  使用CL2加载类C(锁定CL2 + C)
    defineClass C触发
      loadClass D(锁定CL1 + D)

改进的类型推断

在 Java 7 以前,使用泛型时,您必须为变量类型及其实际类型提供类型参数:

Map<String, List<String>> map = new HashMap<String, List<String>>();

在 Java 7 以后,编译器能够经过识别空白菱形推断出在声明在左侧定义的类型:

Map<String, List<String>> map = new HashMap<>();

自动资源管理

在 Java 7 以前,咱们必须使用 finally 块来清理资源,但防止系统崩坏的清理资源的操做并非强制性的。在 Java 7 中,咱们无需显式的资源清理,它容许咱们使用 try-with-resrouces 语句来借由 JVM 自动完成清理工做。

Java 7 以前:

BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader(path));
    return br.readLine();
} catch (Exception e) {
    log.error("BufferedReader Exception", e);
} finally {
    if (br != null) {
        try {
            br.close();
        } catch (Exception e) {
            log.error("BufferedReader close Exception", e);
        }
    }
}

Java 7 及以后的写法:

try (BufferedReader br = new BufferedReader(new FileReader(path)) {
    return br.readLine();
} catch (Exception e) {
    log.error("BufferedReader Exception", e);
}

switch 支持 String

switch 在 Java 7 中可以接受 String 类型的参数,实例以下:

String s = ...
switch(s) {
case "condition1":
    processCondition1(s);
    break;
case "condition2":
    processCondition2(s);
    break;
default:
    processDefault(s);
    break;
}

catch 多个异常

自Java 7开始,catch 中能够一次性捕捉多个异常作统一处理。示例以下:

public void handle() {
    ExceptionThrower thrower = new ExceptionThrower();
    try {
        thrower.manyExceptions();
    } catch (ExceptionA | ExceptionB ab) {
        System.out.println(ab.getClass());
    } catch (ExceptionC c) {
        System.out.println(c.getClass());
    }
}

请注意:若是 catch 块处理多个异常类型,则 catch 参数隐式为 final 类型,这意味着,您不能在 catch 块中为其分配任何值。

数字格式加强

为了解决长数字可读性很差的问题,在 Java 7 中支持了使用下划线分割的数字表达形式:

/**
 * Supported in int
 * */
int improvedInt = 10_00_000;
/**
 * Supported in float
 * */
float improvedFloat = 10_00_000f;
/**
 * Supported in long
 * */
float improvedLong = 10_00_000l;
/**
 * Supported in double
 * */
float improvedDouble = 10_00_000;

二进制字面量

在 Java 7 中,您可使用整型类型 (byteshortintlong) 并加上前缀 0b (或 0B) 来建立二进制字面量。这在 Java 7 以前,您只能使用八进制值 (前缀为 0) 或十六进制值 (前缀为 0x 或者 0X) 来建立:

int sameVarOne = 0b01010000101;
int sameVarTwo = 0B01_010_000_101;
byte byteVar = (byte) 0b01010000101;
short shortVar = (short) 0b01010000101

加强的文件系统

Java 7 推出了全新的NIO 2.0 API以此改变针对文件管理的不便,使得在java.nio.file包下使用PathPathsFilesWatchServiceFileSystem等经常使用类型能够很好的简化开发人员对文件管理的编码工做。

1 - Path 接口 和 Paths 类

Path接口的某些功能其实能够和java.io包下的File类等价,固然这些功能仅限于只读操做。在实际开发过程当中,开发人员能够联用Path接口和Paths类,从而获取文件的一系列上下文信息。

  • int getNameCount(): 获取当前文件节点数
  • Path getFileName(): 获取当前文件名称
  • Path getRoot(): 获取当前文件根目录
  • Path getParent(): 获取当前文件上级关联目录

联用Path接口和Paths类型获取文件信息:

Path path = Paths.get("G:/test/test.xml");
System.out.println("文件节点数:" + path.getNameCount());
System.out.println("文件名称:" + path.getFileName());
System.out.println("文件根目录:" + path.getRoot());
System.out.println("文件上级关联目录:" + path.getParent());

2 - Files 类

联用Path接口和Paths类能够很方便的访问到目标文件的上下文信息。固然这些操做全都是只读的,若是开发人员想对文件进行其它非只读操做,好比文件的建立、修改、删除等操做,则可使用Files类型进行操做。

Files类型经常使用方法以下:

  • Path createFile(): 在指定的目标目录建立新文件
  • void delete(): 删除指定目标路径的文件或文件夹
  • Path copy(): 将指定目标路径的文件拷贝到另外一个文件中
  • Path move(): 将指定目标路径的文件转移到其余路径下,并删除源文件

使用Files类型复制、粘贴文件示例:

Files.copy(Paths.get("/test/src.xml"), Paths.get("/test/target.xml"));

使用 Files 类型来管理文件,相对于传统的 I/O 方式来讲更加方便和简单。由于具体的操做实现将所有移交给 NIO 2.0 API,开发人员则无需关注。

3 - WatchService

Java 7 还为开发人员提供了一套全新的文件系统功能,那就是文件监测。 在此或许有不少朋友并不知晓文件监测有何意义及目,那么请你们回想下调试成热发布功能后的 Web 容器。当项目迭代后并从新部署时,开发人员无需对其进行手动重启,由于 Web 容器一旦监测到文件发生改变后,便会自动去适应这些“变化”并从新进行内部装载。Web 容器的热发布功能一样也是基于文件监测功能,因此不得不认可,文件监测功能的出现对于 Java 文件系统来讲是具备重大意义的。

文件监测是基于事件驱动的,事件触发是做为监测的先决条件。开发人员可使用java.nio.file包下的StandardWatchEventKinds类型提供的3种字面常量来定义监测事件类型,值得注意的是监测事件须要和WatchService实例一块儿进行注册。

StandardWatchEventKinds类型提供的监测事件:

  • ENTRY_CREATE:文件或文件夹新建事件;
  • ENTRY_DELETE:文件或文件夹删除事件;
  • ENTRY_MODIFY:文件或文件夹粘贴事件;

使用WatchService类实现文件监控完整示例:

public static void testWatch() {
    /* 监控目标路径 */
    Path path = Paths.get("G:/");
    try {
        /* 建立文件监控对象. */
        WatchService watchService = FileSystems.getDefault().newWatchService();

        /* 注册文件监控的全部事件类型. */
        path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE,
                StandardWatchEventKinds.ENTRY_MODIFY);

        /* 循环监测文件. */
        while (true) {
            WatchKey watchKey = watchService.take();

            /* 迭代触发事件的全部文件 */
            for (WatchEvent<?> event : watchKey.pollEvents()) {
                System.out.println(event.context().toString() + " 事件类型:" + event.kind());
            }

            if (!watchKey.reset()) {
                return;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

经过上述程序示例咱们能够看出,使用WatchService接口进行文件监控很是简单和方便。首先咱们须要定义好目标监控路径,而后调用FileSystems类型的newWatchService()方法建立WatchService对象。接下来咱们还需使用Path接口的register()方法注册WatchService实例及监控事件。当这些基础做业层所有准备好后,咱们再编写外围实时监测循环。最后迭代WatchKey来获取全部触发监控事件的文件便可。

Fork/ Join 框架

1 - 什么是 Fork/ Join 框架

Java 7 提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每一个小任务结果后获得大任务结果的框架。好比咱们要计算 1 + 2 + .....+ 10000,就能够分割成 10 个子任务,让每一个子任务分别对 1000 个数进行运算,最终汇总这 10 个子任务的结果。

Fork/Join 的运行流程图以下:

2 - 工做窃取算法

工做窃取 (work-stealing) 算法是指某个线程从其余队列里窃取任务来执行。核心思想是:本身的活干完了去看看别人有没有没有干完的活儿,若是有就拿过来帮他干。

工做窃取的运行流程图以下:

工做窃取算法的优势是充分利用线程进行并行计算,并减小了线程间的竞争,其缺点是在某些状况下仍是存在竞争,好比双端队列里只有一个任务时。而且消耗了更多的系统资源,好比建立多个线程和多个双端队列。

3 - 简单示例

让咱们经过一个简单的需求来使用下Fork/Join框架,需求是:计算1 + 2 + 3 + 4的结果。

使用Fork/Join框架首先要考虑到的是如何分割任务,若是咱们但愿每一个子任务最多执行两个数的相加,那么咱们设置分割的阈值是2,因为是4个数字相加,因此Fork/Join框架会把这个任务fork成两个子任务,子任务一负责计算1 + 2,子任务二负责计算3 + 4,而后再join两个子任务的结果。

由于是有结果的任务,因此必须继承RecursiveTask,实现代码以下:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

/**
 * CountTask.
 *
 * @author blinkfox on 2018-01-03.
 * @originalRef http://blinkfox.com/2018/11/12/hou-duan/java/java7-xin-te-xing-ji-shi-yong/#toc-heading-5
 */
public class CountTask extends RecursiveTask<Integer> {

    /** 阈值. */
    public static final int THRESHOLD = 2;

    /** 计算的开始值. */
    private int start;

    /** 计算的结束值. */
    private int end;

    /**
     * 构造方法.
     *
     * @param start 计算的开始值
     * @param end 计算的结束值
     */
    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    /**
     * 执行计算的方法.
     *
     * @return int型结果
     */
    @Override
    protected Integer compute() {
        int sum = 0;

        // 若是任务足够小就计算任务.
        if ((end - start) <= THRESHOLD) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // 若是任务大于阈值,就分裂成两个子任务来计算.
            int middle = (start + end) / 2;
            CountTask leftTask = new CountTask(start, middle);
            CountTask rightTask = new CountTask(middle + 1, end);

            // 等待子任务执行完,并获得结果,再合并执行结果.
            leftTask.fork();
            rightTask.fork();
            sum = leftTask.join() + rightTask.join();
        }
        return sum;
    }

    /**
     * main方法.
     *
     * @param args 数组参数
     */
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ForkJoinPool fkPool = new ForkJoinPool();
        CountTask task = new CountTask(1, 4);
        Future<Integer> result = fkPool.submit(task);
        System.out.println("result:" + result.get());
    }

}

虚拟机加强

Oracle 官网介绍:https://docs.oracle.com/javase/7/docs/technotes/guides/vm/enhancements-7.html

1 - 提供新的 G1 收集器

Java 7 引入了一个被称为 Garbage-First (G1) 的垃圾收集器。G1 是服务器式的垃圾收集器 (设计初衷是尽可能缩短处理超大堆——大于 4GB——时产生的停顿),适用于具备大内存多处理器的计算机。

与以前收集器不一样的是 G1 没有使用 Java 7 以前连续的内存模型:

而是将整个 堆空间 划分为了多个大小相等的独立区域 (Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔阂了,它们都是一部分 (能够不连续) Region的集合:

G1 彻底能够预测停顿时间,而且能够为内存密集型应用程序提供更高的吞吐量。

⚠️ 对于 G1 和垃圾收集器不熟悉的同窗赶忙来这里补课啦!!!

2 - 增强对动态调用的支持

Java 7 以前字节码指令集中,四条方法调用指令 (invokevirtualinvokespeicialinvokestaticinvokeinterface) 的第一个参数都是 被调用方法的符号引用,但动态类型的语言只有在 运行期 才能肯定接受的参数类型。这样,在 Java 虚拟机上实现的动态类型语言就不得不使用“曲线救国”的方式 (如编译时留个占位符类型,运行时动态生成字节码实现具体类型到占位符类型的适配) 来实现,这样势必让动态类型语言实现的复杂度增长,也可能带来额外的性能或者内存开销。

为了从 JVM 底层解决这个问题 (早在 1997 年出版的《Java 虚拟机规范》初版中就规划了这样一个愿景:“在将来,咱们会对 Java 虚拟机进行适当的扩展,以便更好的支持其余语言运行于 Java 虚拟机之上”), Java 7 新引入了 invokedynamic 指令以及 java.lang.invoke 包。

想进一步了解能够阅读:

3 - 分层编译

Java 7 中引入的 分层编译 为服务器 VM 带来了客户端通常的启动速度。一般,服务器 VM 使用 解释器 来收集有关「提供给 编译器 的方法」的分析信息。在分层模式中,除了 解释器 以外,客户端编译器 还用于生成方法的编译版本,这些方法收集关于自身的分析信息。因为编译后的代码比 解释器 要快得多,程序在分析阶段执行时会有更好的性能。在许多状况下,能够实现比客户机 VM 更快的启动,由于服务器编译器生成的最终代码可能在应用程序初始化的早期阶段就已经可用了。分层模式还能够得到比常规服务器 VM 更好的峰值性能,由于更快的分析阶段容许更长的分析周期,这可能产生更好的优化。(ps: 官方文档如是说...)

支持 32 位和 64 位模式,以及压缩 Oops。在 java 命令中使用 -XX:+TieredCompilation 标志来启用分层编译。

(ps: 这在 Java 8 是默认开启的)

4 - 压缩 Oops (CompressOops)

HotSpot JVM 使用名为 oopsOrdinary Object Pointers 的数据结构来表示对象。这些 oops 等同于本地C指针。 instanceOops 是一种特殊的 oop,表示 Java 中的对象实例。

32 位的系统中,对象头指针占 4 字节,只能引用 4 GB 的内存,在 64 位系统中,对象头指针占 8 字节。更大的指针尺寸带来了问题:

  1. 更容易 GC,由于占用空间更大了;
  2. 下降了 CPU 缓存命中率,由于一条 cache line 中能存放的指针数变少了;

为了可以保持 32 位的性能,oop 必须保留 32 位。那么,如何用 32oop 来引用更大的堆内存呢?答案是——压缩指针 (CompressedOops)。JVM 被设计为硬件友好,对象都是按照 8 字节对齐填充的,这意味着使用指针时的偏移量只会是 8 的倍数,而不会是下面中的 1-7,只会是 0 或者 8

mem:  | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
        ^                               ^

这就容许了咱们再也不保留全部的引用,而是每隔 8 个字节保存一个引用:

mem:  | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
        ^                               ^
        |    ___________________________|
        |   |
heap: | 0 | 1 |

CompressedOops,可让跑在 64 位平台下的 JVM,不须要由于更宽的寻址,而付出 Heap 容量损失的代价 (其中还涉及零基压缩优化——Zero-Based Compressed OOPs 技术)。 不过它的实现方式是在机器码中植入压缩与解压指令,可能会给 JVM 增长额外的开销。

想要了解更多戳这里:

其余优化

将 interned 字符串移出 perm gen

在 JDK 7 中,interned 字符串再也不在 Java 堆的永久生成中分配,而是在 Java 堆的主要部分 (称为年轻代和年老代) 中分配,与应用程序建立的其余对象一块儿分配。这一更改将致使驻留在主 Java 堆中的数据更多,而驻留在永久生成中的数据更少,所以可能须要调整堆大小。因为这一变化,大多数应用程序在堆使用方面只会看到相对较小的差别,但加载许多类或大量使用 String.intern() 方法的较大应用程序将看到更显著的差别。

(ps: String.intern() 方法是运行期扩展方法区常量池的一种手段)

NUMA 收集器加强

Java 7 对 Parallel Scavenger 垃圾收集器进行了扩展,以利用具备 NUMA (非统一内存访问) 体系结构的计算机的优点。大多数现代计算机都基于 NUMA 架构,在这种架构中,访问内存的不一样部分须要花费不一样的时间。一般,系统中的每一个处理器都具备提供低访问延迟和高带宽的本地内存,以及访问速度至关慢的远程内存。

在 Java HotSpot 虚拟机中,已实现了 NUMA 感知的分配器,以利用此类系统并为 Java 应用程序提供自动内存放置优化。分配器控制堆的年轻代的 eden 空间,在其中建立大多数新对象。分配器将空间划分为多个区域,每一个区域都放置在特定节点的内存中。分配器基于如下假设:分配对象的线程将最有可能使用该对象。为了确保最快地访问新对象,分配器将其放置在分配线程本地的区域中。能够动态调整区域的大小,以反映在不一样节点上运行的应用程序线程的分配率。这甚至能够提升单线程应用程序的性能。另外,年轻一代,老一代和永久一代的“从”和“到”幸存者空间为其打开了页面交错。这样能够确保全部线程平均平均具备对这些空间的相等的访问延迟。

版本号大于 50 的类文件必须使用 typechecker 进行验证

从 Java 6 开始,Oracle 的编译器使用 StackMapTable 制做类文件。基本思想是,编译器能够显式指定对象的类型,而不是让运行时执行此操做。这样能够在运行时提供极小的加速,以换取编译期间的一些额外时间和已编译的类文件 (前面提到的 StackMapTable) 中的某些复杂性。

做为一项实验功能,Java 6 编译器默认未启用它。 若是不存在 StackMapTable,则运行时默认会验证对象类型自己。

版本号为 51 的类文件 (也就是 Java 7 的类文件) 是使用类型检查验证程序专门验证的,所以,方法在适当时必须具备 StackMapTable 属性。对于版本 50 的类文件,若是文件中的堆栈映射丢失或不正确,则 HotSpot JVM 将故障转移到类型推断验证程序。对于版本为 51 (JDK 7 默认版本) 的类文件,不会发生此故障转移行为。

参考资料

  1. Oracle 官方文档 - https://www.oracle.com/java/technologies/javase/jdk7-relnotes.html
  2. 闪烁之狐 - Java7新特性及使用 - http://blinkfox.com/2018/11/12/hou-duan/java/java7-xin-te-xing-ji-shi-yong/#toc-heading-5
  3. JVM - 指针压缩 - https://chanjarster.github.io/post/jvm/oop-compress/
  • 本文已收录至个人 Github 程序员成长系列 【More Than Java】,学习,不止 Code,欢迎 star:https://github.com/wmyskxz/MoreThanJava
  • 我的公众号 :wmyskxz,我的独立域名博客:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长!

很是感谢各位人才能 看到这里,若是以为本篇文章写得不错,以为 「我没有三颗心脏」有点东西 的话,求点赞,求关注,求分享,求留言!

创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见!