1. 程式人生 > >Java 9 揭祕(19. 平臺和JVM日誌)

Java 9 揭祕(19. 平臺和JVM日誌)

Tips
做一個終身學習的人。

Java 9

在這章中,主要介紹以下內容:

  • 新的平臺日誌(logging)API
  • JVM日誌的命令列選項

JDK 9已經對平臺類(JDK類)和JVM元件的日誌系統進行了大整。 有一個新的API可以指定所選擇的日誌框架作為從平臺類記錄訊息的日誌後端。 還有一個新的命令列選項,可以從所有JVM元件訪問訊息。 在本章中,詳細介紹兩個記錄工具。

一. 平臺日誌API

Java SE 9添加了一個平臺日誌API,可用於指定可由Java平臺類(JDK中的類)記錄訊息的記錄器(Logger),例如Log4j,SLF4J或自定義記錄器。 有關這個API的幾點要注意。 該API旨在由JDK中的類使用,而不是應用程式類。 因此,不應該使用此API來記錄應用程式訊息。 需要使用Log4j等日誌框架來記錄應用程式訊息。 API不允許以程式設計方式配置記錄器。 API由以下內容組成:

  • 一個服務介面java.lang.System.LoggerFinder,它是一個抽象的靜態類
  • 一個介面java.lang.System.Logger,它提供了日誌API
  • java.lang.System類中的過載方法getLogger()返回一個System.Logger

配置平臺記錄器的細節取決於要使用的記錄器。 例如,如果使用Log4j,則需要單獨配置Log4j框架,並配置平臺記錄器。 配置平臺記錄器需要執行以下步驟:

  • 建立一個實現System.Logger介面的類。
  • System.LoggerFinder服務介面建立一個實現。
  • 在模組宣告中指定實現。

在這裡配置Log4j 2.0為平臺記錄器。 配置和使用Log4j是一個廣泛的話題。 這裡僅涵蓋配置平臺記錄器所需的Log4i的詳細資訊。

Tips
如果不配置自定義平臺記錄器,JDK將使用System.LoggerFinder的預設實現,它在java.logging模組存在時使用java.util.logging作為後端框架。 它返回一個將日誌訊息路由到java.util.logging.Logger的記錄器例項。 否則,如果不存在java.logging模組,則預設實現將返回一個簡單的記錄器例項,它將INFO級別及以上的日誌訊息傳遞到控制檯(System.err)。

1. 設定Log4j類庫

需要下載Log4j 2.0庫,以便用在本節中的示例中。可以從https://logging.apache.org/log4j/2.0/download.html

下載Log4J 2.0類庫。 解壓縮下載的檔案並將以下兩個JAR複製到C:\Java8Revealed\extlib目錄。 如果將它們複製到另一個目錄,請務必更換路徑。

  • log4j-api-2.8.jar
  • log4j-core-2.8.jar

如果下載不同版本的Log4j,則這些JAR檔名中的版本可能不同。 在這個例子中,使用這些JAR作為自動模組。 自動模組名稱將從JAR檔名派生,它們是log4j.api和log4j.core。

2. 設定NetBeans專案

在NetBeans中建立了名為com.jdojo.logger的Java專案。 上一節討論的兩個Log4j JAR被新增到專案的模組路徑中,如下圖所示。 要將這些JAR新增到NetBeans中的模組路徑,需要從擴充套件選單中選擇“新增JAR/資料夾”選項以新增到模組路徑。

模組路徑

3. 定義一個模組

此示例的所有類和資源將位於com.jdojo.logger的模組中,其宣告如下所示。

// module-info.java
module com.jdojo.logger {    
    requires log4j.api;
    requires log4j.core;
    exports com.jdojo.logger;
    provides java.lang.System.LoggerFinder
        with com.jdojo.logger.Log4jLoggerFinder;
}

前兩個requires語句對Log4j JAR的依賴關係,這是這種情況下的自動模組。exports語句匯出此模組的com.jdojo.logger包中的所有型別。 provides語句對於設定平臺記錄器很重要。 它宣告提供了com.jdojo.logger.Log4jLoggerFinder類作為服務介面java.lang.System.LoggerFinder的實現。 很快就會建立這個類。 com.jdojo.logger模組的模組圖如下所示。

模組圖

注意模組圖中的迴圈依賴關係和未命名的模組。 它們是因為在本模組宣告中使用的自動模組。com.jdojo.logger模組讀取兩個自動模組。 每個自動模組讀取所有其他模組,可以在從log4j.code和log4j.api模組中的箭頭中看到所有其他模組。 即使在模組圖的顯示中,此示例中沒有任何未命名的模組。 在這個例子中,未命名的模組將不包含任何型別。 未命名的模組出現在圖表中,因為自動模組讀取所有其他模組,包括未命名的模組。

4. 新增Log4J配置檔案

如下顯示了log4j2.xml的配置檔案,它位於NetBeans專案原始碼的根目錄下。 換句話說,log4j2.xml檔案放在一個未命名的包中。 此配置使Log4j將訊息記錄在當前目錄下的logs/platform.log檔案中。 有關此配置的更多資訊,請參閱Log4j文件。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="error">
    <Appenders>
        <File name="JdojoLogFile" fileName="logs/platform.log">
            <PatternLayout>
                <Pattern>%d %p %c [%t] %m%n</Pattern>
            </PatternLayout>
        </File>
        <Async name="Async">
            <AppenderRef ref="JdojoLogFile"/>
        </Async>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Async"/>
        </Root>
    </Loggers>
</Configuration>

5. 建立系統記錄器

需要建立一個系統記錄器,它是一個實現System.Logger介面的類。 該介面包含以下方法:

String getName()
boolean isLoggable(System.Logger.Level level)
default void log(System.Logger.Level level, Object obj)
default void log(System.Logger.Level level, String msg)
default void log(System.Logger.Level level, String format, Object... params)
default void log(System.Logger.Level level, String msg, Throwable thrown)
default void log(System.Logger.Level level, Supplier<String> msgSupplier)
default void log(System.Logger.Level level, Supplier<String> msgSupplier, Throwable thrown)
void log(System.Logger.Level level, ResourceBundle bundle, String format, Object... params)
void log(System.Logger.Level level, ResourceBundle bundle, String msg, Throwable thrown)

需要在System.Logger介面中提供四種抽象方法的實現。 getName()方法返回記錄器的名稱。 可以任何你想要的名字。 如果記錄器可以記錄指定級別的訊息,則isLoggable()方法返回true。 log()方法的兩個版本方法用於記錄訊息,他們被其他預設log()方法所呼叫。

System.Logger.Level列舉定義要記錄的訊息級別的常量。 級別具有名稱和嚴重程度。 級別值為ALLTRACEDEBUGINFOWARNINGERROROFF,按照嚴重程度增加。 可以使用其getName()getSeverity()方法獲取級別的名稱和嚴重程度。

下面包含一個Log4jLogger類的程式碼,它實現了System.Logger介面。

// Log4jLogger.java
package com.jdojo.logger;
import java.util.ResourceBundle;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4jLogger implements System.Logger {
    // The backend logger. Our logger will delegate all loggings
    // to this backend logger, which is Log4j
    private final Logger logger = LogManager.getLogger();
    @Override
    public String getName() {
        return "Log4jLogger";
    }
    @Override
    public boolean isLoggable(Level level) {
        // Get the log4j level from System.Logger.Level
        org.apache.logging.log4j.Level log4jLevel = toLog4jLevel(level);
        // Check if log4j can handle this level of logging and return the result
        return logger.isEnabled(log4jLevel);
    }
    @Override
    public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) {                              
        logger.log(toLog4jLevel(level), msg, thrown);
    }
    @Override
    public void log(Level level, ResourceBundle bundle, String format, Object... params) {        
        logger.printf(toLog4jLevel(level), format, params);
    }
    private static org.apache.logging.log4j.Level toLog4jLevel(Level level) {        
          switch (level) {
              case ALL:
                  return org.apache.logging.log4j.Level.ALL;
              case DEBUG:
                  return org.apache.logging.log4j.Level.DEBUG;
              case ERROR:
                  return org.apache.logging.log4j.Level.ERROR;
              case INFO:
                  return org.apache.logging.log4j.Level.INFO;
              case OFF:
                  return org.apache.logging.log4j.Level.OFF;
              case TRACE:
                  return org.apache.logging.log4j.Level.TRACE;
              case WARNING:
                  return org.apache.logging.log4j.Level.WARN;
              default:
                  throw new RuntimeException("Unknown Level: " + level);
          }        
    }
}

Log4jLogger的例項用作平臺記錄器來記錄平臺類的訊息。它將所有日誌記錄工作委託給一個後端記錄器,這是基礎的Log4j。 logger例項變數儲存對Log4j Logger例項的引用。

現在正在處理兩個日誌API,一個由System.Logger定義,另一個由Log4j定義。它們使用不同的日誌級別,它們由兩種不同的型別表示:System.Logger.Levelorg.apache.logging.log4j.Level。要記錄訊息,JDK類將將System.Logger.Level傳遞給System.Logger介面的log()方法,該方法又需要將級別對映到Log4j級別。 toLog4jLevel()方法執行此對映。它收到一個System.Logger.Level並返回一個相應的org.apache.logging.log4j.Level。`isLoggable()方法將系統級別對映到Log4j級別,如果啟用日誌記錄功能,則查詢Log4j。可以使用其配置檔案配置Log4j來啟用任何級別的日誌記錄。

在本例中保持執行的兩個log()方法邏輯簡單。它們只是把他們的工作委託給Log4j。這些方法不使用ResourceBundle引數。如果要在記錄之前本地化訊息,則需要使用它。

現在已經寫了平臺記錄器的主要邏輯,但還沒有準備好測試。需要更多的工作才能看到它的實際效果。

6. 建立日誌查詢器(Finder)

Java執行時需要找到平臺記錄器。 它使用服務定位器模式來查詢它。 在模組宣告中回想一下以下語句。

provides java.lang.System.LoggerFinder
        with com.jdojo.logger.Log4jLoggerFinder;

在本節中,建立了Log4jLoggerFinder實現類,它實現服務介面System.LoggerFinder。 記住,服務介面不需要是Java介面。 它可以是一個抽象類。 在這種情況下,System.LoggerFinder是一個抽象類,Log4jLoggerFinder類將繼承System.LoggerFinder類。 下面包含Log4jLoggerFinder類的程式碼,它作為服務介面 System.LoggerFinder的實現。

// Log4jLoggerFinder.java
package com.jdojo.logger;
import java.lang.System.LoggerFinder;
public class Log4jLoggerFinder extends LoggerFinder {
    // A logger to be used as a platform logger
    private final Log4jLogger logger = new Log4jLogger();
    @Override
    public System.Logger getLogger(String name, Module module) {        
        System.out.printf("Log4jLoggerFinder.getLogger(): " +
                          "[name=%s, module=%s]%n", name, module.getName());
        // Use the same logger for all modules        
        return logger;
    }
}

Log4jLoggerFinder類建立了Log4jLogger類的例項,並將其引用儲存在logger的例項變數中。 當JDK要求記錄器時,getLogger()方法返回相同的記錄器。 getLogger()方法中的名稱和模組引數是請求的記錄器和請求者的模組的名稱。 例如,當java.util.Currency類需要記錄訊息時,它會請求一個名java.util.Currency的記錄器,並且請求者模組是java.base模組。 如果要為每個模組使用單獨的記錄器,可以根據模組引數返回不同的記錄器。 此示例為所有模組返回相同的記錄器,因此所有訊息都將記錄到同一個位置。 在getLogger()方法中留下了System.out.println()語句,因此可以在執行此示例時看到名稱和模組引數的值。

7. 測試平臺記錄器

下面包含PlatformLoggerTest類的程式碼,用於測試平臺記錄器。 你可能得到不同的輸出。

// PlatformLoggerTest.java
package com.jdojo.logger;
import java.lang.System.Logger;
import static java.lang.System.Logger.Level.TRACE;
import static java.lang.System.Logger.Level.ERROR;
import static java.lang.System.Logger.Level.INFO;
import java.util.Currency;
import java.util.Set;
public class PlatformLoggerTest {    
    public static void main(final String... args) {
        // Let us load all currencies  
        Set<Currency> c = Currency.getAvailableCurrencies();
        System.out.println("# of currencies: " + c.size());
        // Let us test the platform logger by logging a few messages
        Logger logger = System.getLogger("Log4jLogger");
        logger.log(TRACE, "Entering application.");
        logger.log(ERROR, "An unknown error occurred.");
        logger.log(INFO, "FYI");
        logger.log(TRACE, "Exiting application.");
    }
}

輸出結果為:

# of currencies: 225
Log4jLoggerFinder.getLogger(): [name=javax.management.mbeanserver, module=java.management]
Log4jLoggerFinder.getLogger(): [name=javax.management.misc, module=java.management]
Log4jLoggerFinder.getLogger(): [name=Log4jLogger, module=com.jdojo.logger]

main()方法嘗試獲取可用的貨幣符號列表並列印貨幣符號數量。 這樣做的目的是什麼? 稍後解釋其目的。 現在,它只是java.util.Currency類中的一個方法呼叫。

即使不應該使用記錄器來記錄應用程式的訊息,但可以這樣做進行測試。 可以使用System.getLogger()方法獲取平臺記錄器的引用,並開始記錄訊息。main()方法中的以下程式碼段執行此操作。

Logger logger = System.getLogger("Log4jLogger");
logger.log(TRACE, "Entering application.");
logger.log(ERROR, "An unknown error occurred.");
logger.log(INFO, "FYI");
logger.log(TRACE, "Exiting application.");

Tips
在JDK 9中,System類包含兩個可用於獲取平臺日誌錄引用的靜態方法。 方法是getLogger(String name)getLogger(String name, ResourceBundle bundle)。 兩種方法都返回System.Logger介面的例項。

輸出中不顯示四條訊息。 他們去哪兒了? 回想一下,配置了Log4j將訊息記錄到當前目錄下logs/platform.log的檔案中。 當前目錄取決於如何執行PlatformLoggerTest類。 如果從NetBeans執行它,專案的目錄C:\Java9Revealed\com.jdojo.logger是當前目錄。 如果從命令提示符執行它,則可以控制當前目錄。 假設從NetBeans內部執行此類,將在C:\Java9Revealed\com.jdojo.logger\logs\platform.log中找到一個檔案。 其內容如下所示。

2017-02-09 09:58:34,644 ERROR com.jdojo.logger.Log4jLogger [main] An unknown error occurred.
2017-02-09 09:58:34,646 INFO com.jdojo.logger.Log4jLogger [main] FYI

每次執行PlatformLoggerTest類時,Log4j都會將訊息附加到logs\platform.log檔案中。 可以在執行程式之前刪除日誌檔案的內容,也可以刪除日誌檔案,每次執行程式時都會重新建立該檔案。

日誌檔案指示只記錄了兩個訊息ERROR和INFO,丟棄了TRACE訊息。 這與Log4j配置檔案中的記錄級別設定有關,如下所示。 已在記錄器中啟用INFO級別日誌記錄:

<Loggers>
    <Root level="info">
        <AppenderRef ref="Async"/>
    </Root>
</Loggers>

每個記錄器級別都有一個嚴重程度,它是一個整數。 如果啟用具有級別x的記錄器,則會記錄其級別嚴重程度大於或等於x的所有訊息。 下表顯示了由System.Logger.Level列舉及其相關嚴重程度定義的所有級別的名稱。 請注意,在示例中,正在使用Log4j記錄器級別,而不是System.Logger.Level列舉定義的級別。 但是,Log4j定義的級別的相對值與下表所示的順序相同。

Name Severity
ALL Integer.MIN_VALUE
TRACE 400
DEBUG 500
INFO 800
WARNING 900
ERROR 1000
OFF Integer.MAX_VALUE

如果啟用記錄器的級別INFO,記錄器將記錄在INFO,WARNING和ERROR級別上的所有訊息。 如果要在各級記錄訊息,可以使用TRACE或ALL作為Log4j配置檔案中的級別值。

請注意,輸出中平臺類通過呼叫Log4jLoggerFinder類的getLogger()方法請求記錄器三次。前兩次,請求由javax.management模組進行。第三個請求出現在輸出中,因為從PlatformLoggerTest類的main()方法請求了一個記錄器。

在日誌中看到自己的訊息,但沒有從JDK類記錄的任何訊息。相信你好奇地看到日誌中的JDK類的訊息。如何知道將訊息記錄到平臺記錄器的JDK類的名稱以及如何使其記錄訊息?沒有簡單直接的方式來知道這一點。檢視JDK的原始碼,並找到用於記錄平臺訊息的sun.util.logging.PlatformLogger類的引用,發現javax.management模組記錄了TRACE級別的訊息。要檢視這些訊息,需要設定Log4j記錄器的級別來跟蹤並重新執行PlatformLoggerTest類。這在日誌檔案中記錄大量訊息。

讓我們回到在PlatformLoggerTest類中使用Currency類。 用它來顯示JDK類記錄的訊息,在這種情況下,這是java.util.Currency類。 當請求所有貨幣的列表時,JDK會讀取其自身的貨幣列表(JDK內建的貨幣列表),以及位於JAVA_HOME\lib目錄中的自定義currency.properties檔案。 在這種情況下,正在使用JDK執行此示例,因此JAVA_HOME是指JDK_HOME。 建立一個文字檔案,該檔案的路徑將是JDK_HOME\lib\currency.properties。 請注意,該檔案只包含一個單詞,即ABadCurrencyFile。 可以使用任何一個單詞。

Currency類嘗試將currency.properties檔案載入為Java屬性檔案,該檔案應包含j鍵值對。 此檔案不是有效的屬性檔案。 當Currency類嘗試載入它時,將丟擲異常,並且該類向平臺記錄器記錄錯誤訊息。 現在,知道建立了無效的貨幣檔案,因此可以通過JDK類檢視平臺記錄器。

再次執行PlatformLoggerTest類,它提供以下輸出:

Log4jLoggerFinder.getLogger(): [name=javax.management.mbeanserver, module=java.management]
Log4jLoggerFinder.getLogger(): [name=javax.management.misc, module=java.management]
Log4jLoggerFinder.getLogger(): [name=java.util.Currency, module=java.base]
# of currencies: 225
Log4jLoggerFinder.getLogger(): [name=Log4jLogger, module=com.jdojo.logger]

輸出表明java.base模組請求了java.util.Currency的平臺記錄器。 這是因為使用的無效的貨幣檔案。 日誌檔案的內容如下所示,它顯示了Currency類中記錄的訊息。

2017-02-09 10:45:52,413 INFO com.jdojo.logger.Log4jLogger [main] currency.properties entry for ABADCURRENCYFILE is ignored because of the invalid country code.
2017-02-09 10:45:52,420 ERROR com.jdojo.logger.Log4jLogger [main] An unknown error occurred.
2017-02-09 10:45:52,420 INFO com.jdojo.logger.Log4jLogger [main] FYI

8. 進一步的工作

展示了使用Log4j 2.0作為後端記錄器配置平臺記錄器的示例。在生產環境中可以使用此示例之前,還有很多工作要做。其中一個改進是記錄正在記錄訊息的類的名稱。在上面的例子中,發現所有記錄的訊息使用相同的類名稱,即com.jdojo.logger.Log4jLogger作為記錄器的類,這是不正確的。從com.jdojo.logger.PlatformLoggerTest類中記錄了兩條訊息,並從java.util.Currency類中記錄了一條訊息。怎麼解決這個問題?

先來嘗試理解這個問題。 Logger的類名由Log4j決定。它只是檢視其log()方法的呼叫者,並將該類用作記錄訊息的那個類。在之前的例子中,兩個log()方法呼叫Log4j的log()方法來委託日誌記錄工作。 Log4j將com.jdojo.logger.Log4jLogger類視為訊息的記錄器,並將其名稱作為記錄訊息中的記錄器類。

這裡有兩種方法來解決它:

  • Log4jLogger類中,使用使用新增到JDK 9的棧遍歷API自己格式化訊息。棧遍歷API提供呼叫者的類名和其他詳細資訊。這將更改Log4j配置檔案中的模式佈局,因此Log4j不會在訊息中確定幷包含記錄器的類名稱。
  • 可以等待下一個版本的Log4j,這可能會支援開箱即用的JDK 9平臺記錄器。

二. 統一JVM日誌

JDK 9添加了一個新的命令列選項-Xlog,它可以訪問從JVM的所有元件記錄的所有訊息的單點訪問。 該選項的使用語法有點複雜。 先解釋一下已記錄訊息的詳細資訊。

Tips
可以使用-Xlog:help選項與java命令列印-Xlog選項的描述。 該描述包含具有示例的所有選項的語法和值。

當JVM記錄訊息或當正在尋找JVM記錄的訊息時,請記住以下幾點:

  • JVM需要確定訊息所屬的主題(或JVM元件)。 例如,如果訊息與垃圾回收有關,那麼訊息應該被標記為這樣。 訊息可能屬於多個主題。 例如,訊息可能屬於垃圾回收和堆管理。 因此,訊息可以具有與其相關聯的多個標籤((tag)。

像任何其他日誌記錄工具一樣,JVM日誌可能會發生在不同的級別,如資訊,警告等。

應該能夠使用附加的上下文資訊(如當前日期和時間,執行緒記錄訊息,訊息使用的標籤等)來裝飾已記錄的訊息。

資訊應該在哪裡記錄? 他們應該記錄到stdout,stderr,還是一個或多個檔案? 是否可以指定日誌檔案的選項策略,例如檔名,大小和檔案輪換策略。

如果瞭解了這些要點,現在是學習用於描述JVM日誌記錄的以下術語的時候了:

  • Tag(標籤)
  • Level(級別)
  • Decoration(裝飾)
  • Output(輸出)

以下是執行com.jdojo.Welcome類的示例,它使用標準輸出上的嚴格級別為trace或以上的gc標籤(tag)記錄所有訊息,其級別,時間和標籤裝飾。

C:\Java9revealed> java -Xlog:gc=trace:stdout:level,time,tags
--module-path com.jdojo.intro\dist
--module com.jdojo.intro/com.jdojo.intro.Welcome

輸出結果為:

[2017-02-10T12:50:11.412-0600][trace][gc] MarkStackSize: 4096k  MarkStackSizeMax: 16384k
[2017-02-10T12:50:11.427-0600][debug][gc] ConcGCThreads: 1
[2017-02-10T12:50:11.432-0600][debug][gc] ParallelGCThreads: 4
[2017-02-10T12:50:11.433-0600][debug][gc] Initialize mark stack with 4096 chunks, maximum 16384
[2017-02-10T12:50:11.436-0600][info ][gc] Using G1
Welcome to the Module System.
Module Name: com.jdojo.intro

1. 訊息標籤

每個日誌的訊息與一個或多個稱為標籤集的標籤相關聯。以下是所有可用標籤的列表。此列表將來可能會更改。要獲取支援的標籤列表,使用-Xlog:help選項與java命令。

add, age, alloc, aot, annotation, arguments, attach, barrier, biasedlocking, blocks, bot, breakpoint, census, class, classhisto, cleanup, compaction, constraints, constantpool, coops, cpu, cset, data, defaultmethods, dump, ergo, exceptions, exit, fingerprint, freelist, gc, hashtables, heap, humongous, ihop, iklass, init, itables, jni, jvmti, liveness, load, loader, logging, mark, marking, methodcomparator, metadata, metaspace, mmu, modules, monitorinflation, monitormismatch, nmethod, normalize, objecttagging, obsolete, oopmap, os, pagesize, patch, path, phases, plab, promotion, preorder, protectiondomain, ref, redefine, refine, region, remset, purge, resolve, safepoint, scavenge, scrub, stacktrace, stackwalk, start, startuptime, state, stats, stringdedup, stringtable, stackmap, subclass, survivor, sweep, task, thread, tlab, time, timer, update, unload, verification, verify, vmoperation, vtables, workgang, jfr, system, parser, bytecode, setting, event

如果有興趣記錄垃圾收集和啟動的訊息,則可以使用帶有-Xlog選項的gcstartuptime標籤。列表中的大多數標籤都有深奧的名稱,實際上它們適用於在JVM上工作的開發人員,而不是應用程式開發人員。

Tips
可以使用-Xlog選項使用all這個特別標籤,通知JVM記錄所有訊息,而不考慮與它們相關聯的標記。 標籤的預設值就是all。

2. 訊息級別

級別是根據訊息的嚴重程度確定要記錄的訊息日誌的嚴重性級別。 級別具有以下嚴重性順序的值:trace,debug,info,warning和error。 如果為嚴重級別S啟用日誌記錄,則將記錄嚴重級別為S和更大的所有訊息。 例如,如果在info級別啟用日誌記錄,將記錄info,warning和error級別的所有訊息。

Tips
可以使用-Xlog選項命名的特殊嚴重成都級別off來禁用所有級別的日誌記錄。 級別的預設值為info。

3. 訊息裝飾

記錄JVM訊息之前,可以增加其他資訊。 這些額外的資訊被稱為裝飾,它們是前面的訊息。 每個裝飾都在括號內——“[]”。 下表含所有具有長名稱和短名稱的裝飾的列表。長名稱或短名稱與-Xlog選項 一起使用。

長名字 短名字 描述
hostname hn 計算機名稱
level l 訊息的嚴重程度
pid p 程序識別符號
tags tg 與訊息相關聯的所有標籤
tid ti 執行緒識別符號
time t 當前時間和日期為ISO-8601格式(例如:2017-02-10T18:42:58.418 + 0000)
timemillis tm 當前時間以毫秒為單位,與System.currentTimeMillis()生成的值相同
timenanos tn 當前時間以納秒為單位,與System.nanoTime()生成的值相同
uptime u 自JVM啟動以來執行的時間,以秒和毫秒為單位(例如9.219s)
uptimemillis um 自JVM啟動以來執行的毫秒數
uptimenanos un 自JVM啟動以來執行的納秒數
utctime utc UTC格式的當前時間和日期(例如:2017-02-10T12:42:58.418-0600)

Tips
可以使用none的特殊裝飾與-Xlog選項關閉裝飾。 裝飾的預設值為uptime,level和tag。

4. 訊息輸出裝飾

可以指定JVM日誌傳送的三個目的地:

  • stdout
  • stderr
  • file=

使用stdout和stderr值分別在標準輸出和標準錯誤上列印JVM日誌。 預設的輸出目標是stdout。

使用檔案值指定文字檔名將日誌傳送到文字檔案。 可以在檔名中使用%p%t,這將分別擴充套件到JVM的PID和啟動時間。 例如,如果使用-Xlog選項指定file=jvm%p_%t.log作為輸出目標,則對於每個JVM執行,訊息將被記錄到名稱如下所示的檔案中:

  • jvm2348_2017-02-10_13-26-05.log
  • jvm7292_2017-02-10_13-26-06.log

每次啟動JVM時,將建立一個類似於此列表中顯示的日誌檔案。 這裡,2348和7292是兩個執行的JVM的PID。

Tips
缺少stdout和stderr作為輸出目的地表示輸出目的地是一個文字檔案。 代替使用file=jvm.log,可以簡單地使用jvm.log作為輸出目的地。

可以指定用於將輸出傳送到文字檔案的其他選項:

  • filecount=
  • filesize=

這些選項用於控制每個日誌檔案的最大大小和最大日誌檔案數。 考慮以下選項:

file=jvm.log::filesize=1M,filecount= 3

注意使用兩個連續的冒號(::)。 此選項使用jvm.log作為日誌檔案。 日誌檔案的最大大小為1M,日誌檔案的最大計數為3。它將建立四個日誌檔案:jvm.log,jvm.log.0,jvm.log.1和jvm.log0.2。 當前訊息被記錄到jvm.log檔案,當前檔案中記錄的訊息超過1MB時,其他三個檔案輪換。 可以使用K指定千位元組的檔案大小,M表示兆位元組。 如果在不包含K或M字尾的情況下指定檔案大小,則該選項假定為位元組。

5. -Xlog語法

以下是使用-Xlog選項的語法:

-Xlog[:<contents>][:[<output>][:[<decorators>][:<output-options>]]]

-Xlog使用的選項用冒號(:)分隔。 所有選項都是可選的。 如果缺少-Xlog中的前一部分,則必須對該部分使用冒號。 例如,-Xlog :: stderr表示所有部分都是預設值,除了指定為stderr的<output>部分。

-Xlog的最簡單的使用方法如下,將所有JVM訊息記錄到標準輸出:

java -Xlog --module-path com.jdojo.intro\dist --module com.jdojo.intro/com.jdojo.intro.Welcome

有兩個特殊的-XLog選項:helpdisable,可以用作-Xlog-Xlog:help列印-Xlog的幫助,-Xlog:disable禁用所有JVM日誌。 你可能會認為,不是使用-Xlog:disable,你根本不會使用-Xlog選項。 你是對的。 但是,由於不同的原因存在disable選項。 -Xlog選項可以使用相同的命令多次使用。 如果多次出現-Xlog包含相同型別的設定,則最後一個-Xlog的設定將生效。 因此,可以指定-Xlog:disable作為第一個選項,並指定另一個-Xlog開啟特定型別的日誌記錄。 這樣,首先關閉所有預設值,然後指定你感興趣的選項。

<contents>部分指定要記錄的訊息的標籤和嚴重程度級別。 其語法如下:

tag1[+tag2...][*][=level][,...]

<contents>部分中的“+”表示邏輯AND。例如,gc + exit表示記錄其標籤集完全包含兩個tag——gcexit的所有訊息。標籤名末尾的“*”用作萬用字元,這意味著“至少”。例如,gc *表示記錄標籤集至少包含gc的所有訊息,它將使用標籤集[gc][gc,exit][gc,remset,exit]等記錄訊息。如果使用gc + exit *,表示記錄包含至少包含gcexit標籤的標籤集的所有訊息,它將使用標籤集[gc,exit][gc,remset,exit]等記錄訊息。可以指定嚴重程度每個要記錄的標籤名稱的級別。例如,gc = trace記錄所有帶有標籤集的訊息,其中只包含嚴重級別為trace或更高的gc。可以指定以逗號分隔的多個條件。例如,gc = trace,heap = error將使用gc標籤集在trace或更高級別或使用heap標籤集在錯誤級別記錄所有訊息。

執行這些命令時可能會得到不同的輸出。以下命令將gcstartuptime指定為標籤,將其他設定保留為預設值:

C:\Java9Revealed>java -Xlog:gc,startuptime --module-path com.jdojo.intro\dist
--module com.jdojo.intro/com.jdojo.intro.Welcome

輸出結果為;

[0.017s][info][startuptime] StubRoutines generation 1, 0.0002258 secs
[0.022s][info][gc         ] Using G1
[0.022s][info][startuptime] Genesis, 0.0045639 secs
...

使用-Xlog-Xlog:all=info:stdout:uptime,level,tags相同。 它記錄嚴重級別info或更高級別的所有資訊,幷包括帶有裝飾器的uptimeleveltag。 以下命令顯示如何使用預設設定獲取JVM日誌。

C:\Java9Revealed>java -Xlog --module-path com.jdojo.intro\dist
--module com.jdojo.intro/com.jdojo.intro.Welcome

顯示部分輸出:

[0.015s][info][os] SafePoint Polling address: 0x000001195fae0000
[0.015s][info][os] Memory Serialize Page address: 0x000001195fdb0000
[0.018s][info][biasedlocking] Aligned thread 0x000001195fb37f40 to 0x000001195fb38000
[0.019s][info][class,path   ] bootstrap loader class path=C:\java9\lib\modules
[0.019s][info][class,path   ] classpath:
[0.020s][info][class,path   ] opened: C:\java9\lib\modules
[0.020s][info][class,load   ] opened: C:\java9\lib\modules
[0.027s][info][os,thread    ] Thread is alive (tid: 17724).
[0.027s][info][os,thread    ] Thread is alive (tid: 6436).
[0.033s][info][gc           ] Using G1
[0.034s][info][startuptime  ] Genesis, 0.0083975 secs
[0.038s][info][class,load   ] java.lang.Object source: jrt:/java.base
[0.226s][info][os,thread               ] Thread finished (tid: 7584).
[0.226s][info][gc,heap,exit            ] Heap
[0.226s][info][gc,heap,exit            ]  Metaspace       used 6398K, capacity 6510K,
[0.226s][info][safepoint,cleanup       ] mark nmethods, 0.0000057 secs
[0.226s][info][os,thread               ] Thread finished (tid: 3660).
...

以下命令將所有具有至少gc嚴重級別debug或更高標記的訊息記錄到當前目錄中具有時間裝飾器的gc.log的檔案。 請注意,該命令在標準輸出上列印兩行訊息,這些訊息來自Welcome類的main()方法。

C:\java9revealed>java -Xlog:gc*=trace:file=gc.log:time --module-path com.jdojo.intro\dist
--module com.jdojo.intro/com.jdojo.intro.Welcome

但是,顯示了gc.log檔案的部分輸出,而不是標準輸出上列印的內容。

[2017-02-11T08:40:23.942-0600]   Maximum heap size 2113804288
[2017-02-11T08:40:23.942-0600]   Initial heap size 132112768
[2017-02-11T08:40:23.942-0600]   Minimum heap size 6815736
[2017-02-11T08:40:23.942-0600] MarkStackSize: 4096k  MarkStackSizeMax: 16384k
[2017-02-11T08:40:23.966-0600] Heap region size: 1M
[2017-02-11T08:40:23.966-0600] WorkerManager::add_workers() : created_workers: 4
[2017-02-11T08:40:23.966-0600] Initialize Card Live Data
[2017-02-11T08:40:23.966-0600] ParallelGCThreads: 4
[2017-02-11T08:40:23.966-0600] WorkerManager::add_workers() : created_workers: 1
...

以下命令記錄與上一個命令相同的訊息,除了它記錄沒有任何裝飾的訊息:

C:\java9revealed>java -Xlog:gc*=trace:file=gc.log:none --module-path com.jdojo.intro\dist
--module com.jdojo.intro/com.jdojo.intro.Welcome

輸出的部分訊息為:

Maximum heap size 2113804288
   Initial heap size 132112768
   Minimum heap size 6815736
MarkStackSize: 4096k  MarkStackSizeMax: 16384k
Heap region size: 1M
WorkerManager::add_workers() : created_workers: 4
Initialize Card Live Data
ParallelGCThreads: 4
WorkerManager::add_workers() : created_workers: 1
...

以下命令記錄與上一個命令相同的訊息,除了它使用有10個檔案的輪換檔案集,大小為5MB,基本名稱為gc.log:

C:\Java9Revealed>java -Xlog:gc*=trace:file=gc.log:none:filesize=5m,filecount=10
--module-path com.jdojo.intro\dist --module com.jdojo.intro/com.jdojo.intro.Welcome

以下命令記錄包含嚴重級別為debug或更高版本的gc標籤的所有訊息。 它關閉所有包含exit標籤的訊息。 它不會記錄包含gcexit標籤的訊息。 訊息以預設裝飾輸出在stdout上。

C:\Java9Revealed>java -Xlog:gc*=debug,exit*=off --module-path com.jdojo.intro\dist
--module com.jdojo.intro/com.jdojo.intro.Welcome

以下顯示部分輸出。

[0.015s][info][gc,heap] Heap region size: 1M
[0.015s][debug][gc,heap] Minimum heap 8388608  Initial heap 132120576  Maximum heap 2113929216
[0.015s][debug][gc,ergo,refine] Initial Refinement Zones: green: 4, yellow: 12, red: 20, min yellow size: 8
[0.016s][debug][gc,marking,start] Initialize Card Live Data
[0.016s][debug][gc,marking      ] Initialize Card Live Data 0.024ms
[0.016s][debug][gc              ] ConcGCThreads: 1
[0.018s][debug][gc,ihop         ] Target occupancy update: old: 0B, new: 132120576B
[0.019s][info ][gc              ] Using G1
[0.182s][debug][gc,metaspace,freelist]    space @ 0x000001e7dbeb8260 704K,  99% used [0x000001e7fe880000, 0x000001e7fedf8400, 0x000001e7fee00000, 0x000001e7ff080000)
[0.191s][debug][gc,refine            ] Stopping 0
...

以下命令記錄startuptime標籤的資訊,同時使用了hostnameuptimeleveltag的裝飾器。 所有其他設定都保留為預設設定。 它在info級別或更高級別記錄訊息,並將它們記錄到stdout。 注意在命令中使用兩個連續的冒號(::)。 它們是必需的,因為沒有指定輸出目的地。

C:\Java9Revealed>java -Xlog:startuptime::hostname,uptime,level,tags
--module-path com.jdojo.intro\dist --module com.jdojo.intro/com.jdojo.intro.Welcome

輸出資訊為:

[0.015s][kishori][info][startuptime] StubRoutines generation 1, 0.0002574 secs
[0.019s][kishori][info][startuptime] Genesis, 0.0038339 secs
[0.019s][kishori][info][startuptime] TemplateTable initialization, 0.0000081 secs
[0.020s][kishori][info][startuptime] Interpreter generation, 0.0010698 secs
[0.032s][kishori][info][startuptime] StubRoutines generation 2, 0.0001518 secs
[0.032s][kishori][info][startuptime] MethodHandles adapters generation, 0.0000229 secs
[0.033s][kishori][info][startuptime] Start VMThread, 0.0001491 secs
[0.055s][kishori][info][startuptime] Initialize java.lang classes, 0.0224295 secs
[0.058s][kishori][info][startuptime] Initialize java.lang.invoke classes, 0.0015945 secs
[0.162s][kishori][info][startuptime] Create VM, 0.1550707 secs
Welcome to the Module System.
Module Name: com.jdojo.intro

三. 總結

JDK 9已經對平臺類(JDK類)和JVM元件的日誌系統進行了大修。有一個新的API可以指定所選擇的日誌記錄框架作為從平臺類記錄訊息的日誌後端。還有一個新的命令列選項,可讓從所有JVM元件訪問訊息。

平臺日誌API允許指定將由所有平臺類用於記錄其訊息的自定義記錄器。可以使用現有的日誌記錄框架,如Log4j作為記錄器。該API由java.lang.System.LoggerFinder類和java.lang.System.Logger介面組成。

System.Logger介面的例項代表平臺記錄器。 System.LogFinder類是一個服務介面。需要為此服務介面提供一個實現,該介面返回System.Logger介面的例項。可以使用java.lang.System類中的getLogger()方法獲取System.Logger。應用程式中的一個模組必須包含一個表示System.LogFinder服務介面實現的provides語句。否則,將使用預設記錄器。

JDK 9允許使用-Xlog的單個選項從所有元件記錄所有JVM訊息。該選項允許指定訊息的型別,訊息的嚴重程度級別,日誌目標,記錄訊息的裝飾和日誌檔案屬性。訊息由一組標籤標識。System.Logger.Level列舉的常量指定訊息的嚴重程度級別。日誌目標可以是stdout,stderr或一個檔案。