這都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,與您共同成長!

很是感謝各位人才能 看到這裏,若是以爲本篇文章寫得不錯,以爲 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!

創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!

相關文章
相關標籤/搜索