日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

搞JAVA開(kāi)發(fā)的朋友,最怕的就是之一:JVM調(diào)優(yōu)。實(shí)話實(shí)說(shuō),在工作中用的不是很多,只有出現(xiàn)問(wèn)題了才會(huì)用到(也可以在項(xiàng)目發(fā)布時(shí)調(diào)整好相關(guān)參數(shù),避免線上出問(wèn)題)。但是在面試中,這一塊就是必須掌握的了,否則和HR聊薪水都會(huì)受到限制。

主要內(nèi)容:

  • 小白也能看懂的JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)詳解。
  • 垃圾回收,到底是怎么回收的?
  • JDK8為什么要引入元空間替換永久代?
  • CMS和G1垃圾收集器介紹
  • 常見(jiàn)JVM參數(shù)介紹
  • 堆空間如何設(shè)置?
  • 元空間如何設(shè)置?
  • 棧相關(guān)參數(shù)如何設(shè)置?
  • 日志參數(shù)如何設(shè)置?
  • 垃圾收集器如何配置?
  • CMS和G1參數(shù)設(shè)置介紹
  • 其他參數(shù)設(shè)置介紹
  • 調(diào)優(yōu)案例

適合人群:所有Java開(kāi)發(fā)人員,對(duì)JVM調(diào)優(yōu)感興趣的朋友

我們先從 JVM(Java Virtual machine)的基本知識(shí)點(diǎn)開(kāi)始聊。Java 中的一些代碼優(yōu)化技巧,和JVM的關(guān)系非常的大,比如逃逸分析對(duì)非捕獲型 Lambda 表達(dá)式的優(yōu)化。

在進(jìn)行這些優(yōu)化之前,你需要對(duì) JVM 的一些運(yùn)行原理有較深刻的認(rèn)識(shí),在優(yōu)化時(shí)才會(huì)有針對(duì)性的方向。

JVM 內(nèi)存區(qū)域劃分

學(xué)習(xí) JVM,內(nèi)存區(qū)域劃分是繞不過(guò)去的知識(shí)點(diǎn),這幾乎是面試必考的題目。如下圖所示,內(nèi)存區(qū)域劃分主要包括堆、Java 虛擬機(jī)棧、程序計(jì)數(shù)器、本地方法棧、元空間和直接內(nèi)存這五部分,我將逐一介紹。

程序員必備:JVM核心知識(shí)點(diǎn)總結(jié)

 

1.堆

如 JVM 內(nèi)存區(qū)域劃分圖所示,JVM 中占用內(nèi)存最大的區(qū)域,就是堆(Heap),我們平常編碼創(chuàng)建的對(duì)象,大多數(shù)是在這上面分配的,也是垃圾回收器回收的主要目標(biāo)區(qū)域。

2.Java 虛擬機(jī)棧

JVM 的解釋過(guò)程是基于棧的,程序的執(zhí)行過(guò)程也就是入棧出棧的過(guò)程,這也是 Java 虛擬機(jī)棧這個(gè)名稱的由來(lái)。

Java 虛擬機(jī)棧是和線程相關(guān)的。當(dāng)你啟動(dòng)一個(gè)新的線程,Java 就會(huì)為它分配一個(gè)虛擬機(jī)棧,之后所有這個(gè)線程的運(yùn)行,都會(huì)在棧里進(jìn)行。

Java 虛擬機(jī)棧,從方法入棧到具體的字節(jié)碼執(zhí)行,其實(shí)是一個(gè)雙層的棧結(jié)構(gòu),也就是棧里面還包含棧。

程序員必備:JVM核心知識(shí)點(diǎn)總結(jié)

 

如上圖,Java 虛擬機(jī)棧里的每一個(gè)元素,叫作棧幀。每一個(gè)棧幀,包含四個(gè)區(qū)域: 局部變量表 、操作數(shù)棧、動(dòng)態(tài)連接和返回地址。

其中,操作數(shù)棧就是具體的字節(jié)碼指令所操作的棧區(qū)域,考慮到下面這段代碼:

package com.tian.utils;

public class Test {
    public int test() {
        int a = 1;
        a++;
        return a;
    }
}

JVM 將會(huì)為 test 方法生成一個(gè)棧幀,然后入棧,等 test 方法執(zhí)行完畢,就會(huì)將對(duì)應(yīng)的棧幀彈出。在對(duì)變量 a 進(jìn)行加一操作的時(shí)候,就會(huì)對(duì)棧幀中的操作數(shù)棧運(yùn)用相關(guān)的字節(jié)碼指令。

我們對(duì)上面這個(gè)類進(jìn)行編譯成Test.class文件后,使用命令:

javap -verbose -c Test.class >test.txt

這樣就會(huì)把這個(gè)類的字節(jié)碼指令輸出到test.txt文件中:

Classfile /E:/workspace/other/hAppy-mall/target/classes/com/tian/utils/Test.class
  Last modified 2022-3-23; size 369 bytes
  MD5 checksum 58d655b96f21dd36600ad0a8df0efa70
  Compiled from "Test.java"
public class com.tian.utils.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#17         // java/lang/Object."<init>":()V
   #2 = Class              #18            // com/tian/utils/Test
   #3 = Class              #19            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lcom/tian/utils/Test;
  #11 = Utf8               test
  #12 = Utf8               ()I
  #13 = Utf8               a
  #14 = Utf8               I
  #15 = Utf8               SourceFile
  #16 = Utf8               Test.java
  #17 = NameAndType        #4:#5          // "<init>":()V
  #18 = Utf8               com/tian/utils/Test
  #19 = Utf8               java/lang/Object
{
  public com.tian.utils.Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/tian/utils/Test;

  public int test();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: iinc          1, 1
         5: iload_1
         6: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/tian/utils/Test;
            2       5     1     a   I
}
SourceFile: "Test.java"

上面這段字節(jié)碼代碼,可能很多人都看不懂,建議結(jié)合:

程序員必備:JVM核心知識(shí)點(diǎn)總結(jié)

 

推薦博客(如果英語(yǔ)好的話,也可以去官網(wǎng)查看):

字節(jié)碼指令大全

然后,再結(jié)合我們上面的Java虛擬機(jī)棧這塊知識(shí),就能輕松閱讀了。

棧幀的創(chuàng)建是需要耗費(fèi)資源的,尤其是對(duì)于 Java 中常見(jiàn)的 getter、setter 方法來(lái)說(shuō),這些代碼通常只有一行,每次都創(chuàng)建棧幀的話就太浪費(fèi)了。

另外,Java 虛擬機(jī)棧對(duì)代碼的執(zhí)行,采用的是字節(jié)碼解釋的方式,考慮到下面這段代碼,變量 a 聲明之后,就再也不被使用,要是按照字節(jié)碼指令解釋執(zhí)行的話,就要做很多無(wú)用功。

另外,我們了解到垃圾回收器回收的目標(biāo)區(qū)域主要是堆,堆上創(chuàng)建的對(duì)象越多,GC 的壓力就越大。要是能把一些變量,直接在棧上分配,那 GC 的壓力就會(huì)小一些。

3.程序計(jì)數(shù)器

既然是線程,就要接受操作系統(tǒng)的調(diào)度,但總有時(shí)候,某些線程是獲取不到 CPU 時(shí)間片的,那么當(dāng)這個(gè)線程恢復(fù)執(zhí)行的時(shí)候,它是如何確保找到切換之前執(zhí)行的位置呢?這就是程序計(jì)數(shù)器的功能。

和 Java 虛擬機(jī)棧一樣,它也是線程私有的。程序計(jì)數(shù)器只需要記錄一個(gè)執(zhí)行位置就可以,所以不需要太大的空間。事實(shí)上,程序計(jì)數(shù)器是 JVM 規(guī)范中唯一沒(méi)有規(guī)定 OutOfMemoryError 情況的區(qū)域。

4.本地方法棧

與 Java 虛擬機(jī)棧類似,本地方法棧,是針對(duì) native 方法的。我們常用的 HotSpot,將 Java 虛擬機(jī)棧和本地方法棧合二為一,其實(shí)就是一個(gè)本地方法棧,大家注意規(guī)范里的這些差別就可以了。

5.元空間

元空間是一個(gè)容易引起混淆的區(qū)域,原因就在于它經(jīng)歷了多次迭代才成為現(xiàn)在的模樣。關(guān)于這部分區(qū)域,我們來(lái)講解兩個(gè)面試題,大家就明白了。

  • 元空間是在堆上嗎?

答案:元空間并不是在堆上分配的,而是在堆外空間進(jìn)行分配的,它的大小默認(rèn)沒(méi)有上限,我們常說(shuō)的方法區(qū),就在元空間中。

  • 字符串常量池在那個(gè)區(qū)域中?

答案:這個(gè)要看 JDK 版本。

在 JDK 1.8 之前,是沒(méi)有元空間這個(gè)概念的,當(dāng)時(shí)的方法區(qū)是放在一個(gè)叫作永久代的空間中。

而在 JDK 1.7 之前,字符串常量池也放在這個(gè)叫作永久帶的空間中。但在 JDK 1.7 版本,已經(jīng)將字符串常量池從永久帶移動(dòng)到了堆上。

所以,從 1.7 版本開(kāi)始,字符串常量池就一直存在于堆上。

  • 為什么使用元空間替換永久代?

表面上看是為了避免OOM異常。因?yàn)橥ǔJ褂肞ermSize和MaxPermSize設(shè)置永久代的大小就決定了永久代的上限,但是不是總能知道應(yīng)該設(shè)置為多大合適, 如果使用默認(rèn)值很容易遇到OOM錯(cuò)誤。當(dāng)使用元空間時(shí),可以加載多少類的元數(shù)據(jù)就不再由MaxPermSize控制, 而由系統(tǒng)的實(shí)際可用空間來(lái)控制啦。

6.直接內(nèi)存

直接內(nèi)存,指的是使用了 Java 的直接內(nèi)存 API,進(jìn)行操作的內(nèi)存。這部分內(nèi)存可以受到 JVM 的管控,比如 ByteBuffer 類所申請(qǐng)的內(nèi)存,就可以使用具體的參數(shù)進(jìn)行控制。

需要注意的是直接內(nèi)存和本地內(nèi)存不是一個(gè)概念。

  • 直接內(nèi)存比較專一,有具體的 API(這里指的是ByteBuffer),也可以使用 -XX:MaxDirectMemorySize 參數(shù)控制它的大??;
  • 本地內(nèi)存是一個(gè)統(tǒng)稱,比如使用 native 函數(shù)操作的內(nèi)存就是本地內(nèi)存,本地內(nèi)存的使用 JVM 是限制不住的,使用的時(shí)候一定要小心。

GC Roots

對(duì)象主要是在堆上分配的,我們可以把它想象成一個(gè)池子,對(duì)象不停地創(chuàng)建,后臺(tái)的垃圾回收進(jìn)程不斷地清理不再使用的對(duì)象。當(dāng)內(nèi)存回收的速度,趕不上對(duì)象創(chuàng)建的速度,這個(gè)對(duì)象池子就會(huì)產(chǎn)生溢出,也就是我們常說(shuō)的 OOM。

把不再使用的對(duì)象及時(shí)地從堆空間清理出去,是避免 OOM 有效的方法。那 JVM 是如何判斷哪些對(duì)象應(yīng)該被清理,哪些對(duì)象需要被繼續(xù)使用呢?

這里首先強(qiáng)調(diào)一個(gè)概念,這對(duì)理解垃圾回收的過(guò)程非常有幫助,面試時(shí)也能很好地展示自己。

垃圾回收,并不是找到不再使用的對(duì)象,然后將這些對(duì)象清除掉。它的過(guò)程正好相反,JVM 會(huì)找到正在使用的對(duì)象,對(duì)這些使用的對(duì)象進(jìn)行標(biāo)記和追溯,然后一股腦地把剩下的對(duì)象判定為垃圾,進(jìn)行清理。

了解了這個(gè)概念,我們就可以看下一些基本的衍生分析:

  • GC 的速度,和堆內(nèi)存活對(duì)象的多少有關(guān),與堆內(nèi)所有對(duì)象的數(shù)量無(wú)關(guān);
  • GC 的速度與堆的大小無(wú)關(guān),32GB 的堆和 4GB 的堆,只要存活對(duì)象是一樣的,垃圾回收速度也會(huì)差不多;
  • 垃圾回收不必每次都把垃圾清理得干干凈凈,最重要的是不要把正在使用的對(duì)象判定為垃圾。

那么,如何找到這些存活對(duì)象,也就是哪些對(duì)象是正在被使用的,就成了問(wèn)題的核心。

大家可以想一下寫(xiě)代碼的時(shí)候,如果想要保證一個(gè) HashMap 能夠被持續(xù)使用,可以把它聲明成靜態(tài)變量,這樣就不會(huì)被垃圾回收器回收掉。我們把這些正在使用的引用的入口,叫作GC Roots。

這種使用 tracing 方式尋找存活對(duì)象的方法,還有一個(gè)好聽(tīng)的名字,叫作可達(dá)性分析法。

概括來(lái)講,GC Roots 包括:

  • Java 線程中,當(dāng)前所有正在被調(diào)用的方法的引用類型參數(shù)、局部變量、臨時(shí)值等。也就是與我們棧幀相關(guān)的各種引用;
  • 所有當(dāng)前被加載的 Java 類;
  • Java 類的引用類型靜態(tài)變量;
  • 運(yùn)行時(shí)常量池里的引用類型常量(String 或 Class 類型);
  • JVM 內(nèi)部數(shù)據(jù)結(jié)構(gòu)的一些引用,比如 sun.jvm.hotspot.memory.Universe 類;
  • 用于同步的監(jiān)控對(duì)象,比如調(diào)用了對(duì)象的 wait() 方法;
  • JNI handles,包括 global handles 和 local handles。

對(duì)于這個(gè)知識(shí)點(diǎn),不要死記硬背,可以對(duì)比著 JVM 內(nèi)存區(qū)域劃分那張圖去看,入口大約有三個(gè):線程、靜態(tài)變量和 JNI 引用。

強(qiáng)、軟、弱、虛引用

那么,通過(guò) GC Roots 能夠追溯到的對(duì)象,就一定不會(huì)被垃圾回收嗎?這要看情況。

Java 對(duì)象與對(duì)象之間的引用,存在著四種不同的引用級(jí)別,強(qiáng)度從高到低依次是:強(qiáng)引用、軟引用、弱引用、虛引用。

  • 強(qiáng)應(yīng)用 默認(rèn)的對(duì)象關(guān)系是強(qiáng)引用,也就是我們默認(rèn)的對(duì)象創(chuàng)建方式。這種引用屬于最普通最強(qiáng)硬的一種存在,只有在和 GC Roots 斷絕關(guān)系時(shí),才會(huì)被消滅掉。
  • 軟引用 用于維護(hù)一些可有可無(wú)的對(duì)象。在內(nèi)存足夠的時(shí)候,軟引用對(duì)象不會(huì)被回收;只有在內(nèi)存不足時(shí),系統(tǒng)則會(huì)回收軟引用對(duì)象;如果回收了軟引用對(duì)象之后,仍然沒(méi)有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。
  • 弱引用 級(jí)別就更低一些,當(dāng) JVM 進(jìn)行垃圾回收時(shí),無(wú)論內(nèi)存是否充足,都會(huì)回收被弱引用關(guān)聯(lián)的對(duì)象。軟引用和弱引用在堆內(nèi)緩存系統(tǒng)中使用非常頻繁,可以在內(nèi)存緊張時(shí)優(yōu)先被回收掉。
  • 虛引用 是一種形同虛設(shè)的引用,在現(xiàn)實(shí)場(chǎng)景中用得不是很多。這里有一個(gè)冷門(mén)的知識(shí)點(diǎn):Java 9.0 以后新加入了 Cleaner 類,用來(lái)替代 Object 類的 finalizer 方法,這就是虛引用的一種應(yīng)用場(chǎng)景。

分代垃圾回收

上面我們提到,垃圾回收的速度,是和存活的對(duì)象數(shù)量有關(guān)系的,如果這些對(duì)象太多,JVM 再做標(biāo)記和追溯的時(shí)候,就會(huì)很慢。

一般情況下,JVM 在做這些事情的時(shí)候,都會(huì)停止業(yè)務(wù)線程的所有工作,進(jìn)入 SafePoint 狀態(tài),這也就是我們通常說(shuō)的 Stop the World。所以,現(xiàn)在的垃圾回收器,有一個(gè)主要目標(biāo),就是減少 STW 的時(shí)間。

其中一種有效的方式,就是采用分代垃圾回收,減少單次回收區(qū)域的大小。這是因?yàn)?,大部分?duì)象,可以分為兩類:

  • 大部分對(duì)象的生命周期都很短
  • 其他對(duì)象則很可能會(huì)存活很長(zhǎng)時(shí)間

這個(gè)假設(shè)我們稱之為弱代假設(shè)(weak generational hypothesis)。

如下圖,分代垃圾回收器會(huì)在邏輯上,把堆空間分為兩部分:年輕代(Young generation)和老年代(Old generation)。

程序員必備:JVM核心知識(shí)點(diǎn)總結(jié)

 

1.年輕代

年輕代中又分為一個(gè)伊甸園空間(Eden),兩個(gè)幸存者空間(Survivor)。對(duì)象會(huì)首先在年輕代中的 Eden 區(qū)進(jìn)行分配,當(dāng) Eden 區(qū)分配滿的時(shí)候,就會(huì)觸發(fā)年輕代的 GC。

此時(shí),存活的對(duì)象會(huì)被移動(dòng)到其中一個(gè) Survivor 分區(qū)(以下簡(jiǎn)稱 from);年輕代再次發(fā)生垃圾回收,存活對(duì)象,包括 from 區(qū)中的存活對(duì)象,會(huì)被移動(dòng)到 to 區(qū)。所以,from 和 to 兩個(gè)區(qū)域,總有一個(gè)是空的。

Eden、from、to 的默認(rèn)比例是 8:1:1,所以只會(huì)造成 10% 的空間浪費(fèi)。這個(gè)比例,是由參數(shù) -XX:SurvivorRatio 進(jìn)行配置的(默認(rèn)為 8)。

2.老年代

對(duì)垃圾回收的優(yōu)化,就是要讓對(duì)象盡快在年輕代就回收掉,減少到老年代的對(duì)象。那么對(duì)象是如何進(jìn)入老年代的呢?它主要有以下四種方式。

  • 正常提升(Promotion)

上面提到了年輕代的垃圾回收,如果對(duì)象能夠熬過(guò)年輕代垃圾回收,它的年齡(age)就會(huì)加一,當(dāng)對(duì)象的年齡達(dá)到一定閾值,就會(huì)被移動(dòng)到老年代中。

  • 分配擔(dān)保

如果年輕代的空間不足,又有新的對(duì)象需要分配空間,就需要依賴其他內(nèi)存(這里是老年代)進(jìn)行分配擔(dān)保,對(duì)象將直接在老年代創(chuàng)建。

  • 大對(duì)象直接在老年代分配

超出某個(gè)閾值大小的對(duì)象,將直接在老年代分配,可以通過(guò)
-XX:PretenureSizeThreshold 配置這個(gè)閾值。

  • 動(dòng)態(tài)對(duì)象年齡判定

有的垃圾回收算法,并不要求 age 必須達(dá)到 15 才能晉升到老年代,它會(huì)使用一些動(dòng)態(tài)的計(jì)算方法。比如 G1,通過(guò) TargetSurvivorRatio 這個(gè)參數(shù),動(dòng)態(tài)更改對(duì)象提升的閾值。

老年代的空間一般比較大,回收的時(shí)間更長(zhǎng),當(dāng)老年代的空間被占滿了,將發(fā)生老年代垃圾回收。

目前,被廣泛使用的是 G1 垃圾回收器。G1 的目標(biāo)是用來(lái)干掉 CMS 的,它同樣有年輕代和老年代的概念。不過(guò),G1 把整個(gè)堆切成了很多份,把每一份當(dāng)作一個(gè)小目標(biāo),部分上目標(biāo)很容易達(dá)成。

程序員必備:JVM核心知識(shí)點(diǎn)總結(jié)

 

如上圖,G1 也是有 Eden 區(qū)和 Survivor 區(qū)的概念的,只不過(guò)它們?cè)趦?nèi)存上不是連續(xù)的,而是由一小份一小份組成的。G1 在進(jìn)行垃圾回收的時(shí)候,將會(huì)根據(jù)最大停頓時(shí)間(MaxGCPauseMillis)設(shè)置的值,動(dòng)態(tài)地選取部分小堆區(qū)進(jìn)行垃圾回收。

G1 的配置非常簡(jiǎn)單,我們只需要配置三個(gè)參數(shù),一般就可以獲取優(yōu)異的性能:

  • MaxGCPauseMillis 設(shè)置最大停頓的預(yù)定目標(biāo),G1 垃圾回收器會(huì)自動(dòng)調(diào)整,選取特定的小堆區(qū);
  • G1HeapRegionSize 設(shè)置小堆區(qū)的大??;
  • InitiatingHeapOccupancyPercent當(dāng)整個(gè)堆內(nèi)存使用達(dá)到一定比例(默認(rèn)是45%),并發(fā)標(biāo)記階段就會(huì)被啟動(dòng)。

逃逸分析

下面著重講解一下逃逸分析,這個(gè)知識(shí)點(diǎn)在面試的時(shí)候經(jīng)常會(huì)被問(wèn)到。

我們常說(shuō)的對(duì)象,除了基本數(shù)據(jù)類型,一定是在堆上分配的嗎?

答案是否定的,通過(guò)逃逸分析,JVM 能夠分析出一個(gè)新的對(duì)象的使用范圍,從而決定是否要將這個(gè)對(duì)象分配到堆上。逃逸分析現(xiàn)在是 JVM 的默認(rèn)行為,可以通過(guò)參數(shù) -XX:-DoEscapeAnalysis 關(guān)掉它。

那什么樣的對(duì)象算是逃逸的呢?可以看一下下面的兩種典型情況。

如代碼所示,對(duì)象被賦值給成員變量或者靜態(tài)變量,可能被外部使用,變量就發(fā)生了逃逸。

public class EscapeAttr {
    Object attr;
    public void test() {
        attr = new Object();
    }
}

再看下面這段代碼,對(duì)象通過(guò) return 語(yǔ)句返回。由于程序并不能確定這個(gè)對(duì)象后續(xù)會(huì)不會(huì)被使用,外部的線程能夠訪問(wèn)到這個(gè)結(jié)果,對(duì)象也發(fā)生了逃逸。

 public class EscapeReturn {
    Object attr;
    public Object test() {
        Object obj = new Object();
        return obj;
    }
}

那逃逸分析有什么好處呢? 1. 棧上分配

如果一個(gè)對(duì)象在子程序中被分配,指向該對(duì)象的指針永遠(yuǎn)不會(huì)逃逸,對(duì)象有可能會(huì)被優(yōu)化為棧分配。棧分配可以快速地在棧幀上創(chuàng)建和銷毀對(duì)象,不用再分配到堆空間,可以有效地減少 GC 的壓力。

2. 分離對(duì)象或標(biāo)量替換

但對(duì)象結(jié)構(gòu)通常都比較復(fù)雜,如何將對(duì)象保存在棧上呢?

JIT 可以將對(duì)象打散,全部替換為一個(gè)個(gè)小的局部變量,這個(gè)打散的過(guò)程,就叫作標(biāo)量替換(標(biāo)量就是不能被進(jìn)一步分割的變量,比如 int、long 等基本類型)。也就是說(shuō),標(biāo)量替換后的對(duì)象,全部變成了局部變量,可以方便地進(jìn)行棧上分配,而無(wú)須改動(dòng)其他的代碼。

從上面的描述我們可以看到,并不是所有的對(duì)象或者數(shù)組,都會(huì)在堆上分配。由于JIT的存在,如果發(fā)現(xiàn)某些對(duì)象沒(méi)有逃逸出方法,那么就有可能被優(yōu)化成棧分配。

3.同步消除

如果一個(gè)對(duì)象被發(fā)現(xiàn)只能從一個(gè)線程被訪問(wèn)到,那么對(duì)于這個(gè)對(duì)象的操作可以不考慮同步。

注意這是針對(duì) synchronized 來(lái)說(shuō)的,JUC 中的 Lock 并不能被消除。

要開(kāi)啟同步消除,需要加上-XX:+EliminateLocks參數(shù)。由于這個(gè)參數(shù)依賴逃逸分析,所以同時(shí)要打開(kāi)-XX:+DoEscapeAnalysis 選項(xiàng)。

JVM 常見(jiàn)優(yōu)化參數(shù)

現(xiàn)在大家用得最多的 Java 版本是 Java 8,如果你的公司比較保守,那么使用較多的垃圾回收器就是 CMS 。但 CMS 已經(jīng)在 Java 14 中被正式廢除,隨著 ZGC 的誕生和 G1 的穩(wěn)定,CMS 終將成為過(guò)去式。

Java 9 之后,Java 版本已經(jīng)進(jìn)入了快速發(fā)布階段,大約是每半年發(fā)布一次,Java 8 和 Java 11 是目前支持的 LTS 版本。

由于 JVM 一直處在變化之中,所以一些參數(shù)的配置并不總是有效的。有時(shí)候你加入一個(gè)參數(shù),“感覺(jué)上”運(yùn)行速度加快了,但通過(guò) -XX:+PrintFlagsFinal來(lái)查看,卻發(fā)現(xiàn)這個(gè)參數(shù)默認(rèn)就是這樣了。

所以,在不同的 JVM 版本上,不同的垃圾回收器上,要先看一下這個(gè)參數(shù)默認(rèn)是什么,不要輕信別人的建議,命令行示例如下:

java -XX:+PrintFlagsFinal -XX:+UseG1GC 2>&1 | grep UseAdaptiveSizePolicy

還有一個(gè)與之類似的參數(shù)叫作PrintCommandLineFlags,通過(guò)它,你能夠查看當(dāng)前所使用的垃圾回收器和一些默認(rèn)的值。

可以看到下面的 JVM 默認(rèn)使用的就是并行收集器:

# java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=127905216 -XX:MaxHeapSize=2046483456 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
openjdk version "1.8.0_41"
OpenJDK Runtime Environment (build 1.8.0_41-b04)
OpenJDK 64-Bit Server VM (build 25.40-b25, mixed mode)

下面我大致羅列一些JVM調(diào)優(yōu)參數(shù):

堆相關(guān)參數(shù)

-Xms10g :JVM啟動(dòng)時(shí)申請(qǐng)的初始堆內(nèi)存值
-Xmx20G :JVM可申請(qǐng)的最大Heap值
-Xmn3g : 新生代大小,一般設(shè)置為堆空間的1/3 1/4左右,新生代大則老年代小
-Xss :Java每個(gè)線程的Stack大小
-XX:PermSize :持久代(方法區(qū))的初始內(nèi)存大小
-XX:MaxPermSize : 持久代(方法區(qū))的最大內(nèi)存大小
-XX:SurvivorRatio : 設(shè)置新生代eden空間和from/to空間的比例關(guān)系,關(guān)系(eden/from=eden/to)
-XX:NewRatio : 設(shè)置新生代和老年代的比例老年代/新生代

日志相關(guān)參數(shù)

-XX:+PrintGC :打印GC日志
-XX:+PrintGCDetailsGC :時(shí)的詳細(xì)堆信息
-XX:+PrintHeapAtGC :打印GC前后的堆信息
-XX:+PrintGCTimeStamps :輸出GC發(fā)生時(shí)間,輸出的時(shí)間為虛擬機(jī)啟動(dòng)的偏移量
-XX:+PrintGCApplicationConcurrentTime :輸出應(yīng)用程序執(zhí)行時(shí)間
-XX:+PrintGCApplicationStoppedTime :輸出應(yīng)用程序由于GC產(chǎn)生停頓的時(shí)間
-XX:+PrintRefrenceGC :輸出軟引用、弱引用、虛引用和Finalize隊(duì)列
-XX:+HeapDumpOnOutOfMemoryError :產(chǎn)生OOM時(shí)可以在內(nèi)存溢出時(shí)導(dǎo)出整個(gè)堆信息
-XX:HeapDumpPath :導(dǎo)出堆文件存放路徑
-XX:+TraceClassLoading :跟蹤類加載信息
-XX:+TraceClassUnloading :跟:蹤類卸載信息
-XX:PrintClassHitogram :查看系統(tǒng)中的類的分布情況(占用空間最多、實(shí)例數(shù)量空間大小)
-XX:+PrintVMOptions :打印虛擬機(jī)接收到的命令行顯示參數(shù)
-XX:+PrintCommandLineFlags :打印虛擬機(jī)的顯式和隱式參數(shù)
-XX:+PrintFlagsFinal :打印虛擬機(jī)的所有系統(tǒng)參數(shù)

GC相關(guān)參數(shù)

-XX:+UseSerialGC :新生代、老年代使用串行收集器
-XX:SurvivorRatio :設(shè)置eden區(qū)和survivor區(qū)大小的比例
-XX:PretenureSizeThreshold,:當(dāng)對(duì)象大小超過(guò)此值時(shí),直接分配到老年代
-XX:MaxTenuringThreshold :設(shè)置對(duì)象進(jìn)入老年代的最大年齡
-XX:+UseParNewGC :新生代使用并行收集器
-XX:+UseParallelOldGC :老年代使用并行回收收集器
-XX:+ParallelGCThreads :設(shè)置垃圾回收線程數(shù),一般最好與CPU數(shù)量相當(dāng),默認(rèn)情況下,當(dāng)CPU數(shù)量小于8個(gè)時(shí),ParallelGCThreads的值相當(dāng)于CPU數(shù)量,當(dāng)CPU數(shù)量大于8個(gè)時(shí),ParallelGCThreads的值等于3+((5*CPU_COUNT)/8
-XX:MaxGCPauseMillis :設(shè)置最大垃圾收集停頓時(shí)間
-XX:GCTimeRatio :設(shè)置吞吐量大小,它的值是一個(gè)0~100之間的整數(shù),假設(shè)值為n,那么系統(tǒng)將花費(fèi)不超過(guò)1/(1+n)的時(shí)間用于垃圾收集
-XX:+UseAdaptiveSizePolicy :打開(kāi)自適應(yīng)GC策略,JVM對(duì)新生代的大小、eden和survivior的比例、晉升老年代對(duì)象年齡等參數(shù)自動(dòng)調(diào)整
-XX:+UseConcMarkSweepGC :?jiǎn)⒂肅MS
-XX:ParallelCMSThreads :設(shè)置CMS線程數(shù)量
-XX:CMSInitiatingOccupancyFraction :默認(rèn)68當(dāng)老年代的空間超過(guò)68%時(shí)會(huì)執(zhí)行一次CMS回收
-XX:UseCMSCompactAtFullCollection :設(shè)置CMS結(jié)束后是否需要進(jìn)行一次內(nèi)存空間整理
-XX:CMSFullGCsBeforeCompaction :進(jìn)行多少次CMS后進(jìn)行內(nèi)存空間壓縮
-XX:+CMSClassUnloadingEnabled :允許對(duì)類元數(shù)據(jù)區(qū)進(jìn)行回收
-XX:CMSInitiatingPermOccupancyFraction :當(dāng)永久區(qū)占用率達(dá)到此值時(shí)進(jìn)行CMS回收(須激活CMSClassUnloadingEnabled)
-XX:UseCMSInitiatingOccupancyOnly:只要達(dá)到閾值時(shí)進(jìn)行CMS回收
-XX:+UseG1GC :使用G1
-XX:MaxGCPauseMillis :最大垃圾收集停頓時(shí)間
-XX:GCPauseIntervalMillis :最大停頓間隔時(shí)間

JVM 的參數(shù)配置繁多,但大多數(shù)不需要我們?nèi)リP(guān)心。

調(diào)優(yōu)案例

下面,我們通過(guò)對(duì) ES 服務(wù)的 JVM 參數(shù)分析,來(lái)看一下常見(jiàn)的優(yōu)化點(diǎn)。

ElasticSearch(簡(jiǎn)稱 ES)是一個(gè)高性能的開(kāi)源分布式搜索引擎。ES 是基于 Java 語(yǔ)言開(kāi)發(fā)的,既然是Java開(kāi)發(fā),那肯定會(huì)涉及到JVM調(diào)優(yōu)了,在它的 conf 目錄下,有一個(gè)叫作jvm.options的文件,JVM 的配置就放在這里。

堆空間的配置

下面是 ES 對(duì)于堆空間大小的配置。

-Xms1g
-Xmx1g

JVM 中空間最大的一塊就是堆,垃圾回收也主要是針對(duì)這塊區(qū)域。通過(guò) Xmx 可指定堆的最大值,通過(guò) Xms 可指定堆的初始大小。我們通常把這兩個(gè)參數(shù),設(shè)置成一樣大小的,可避免堆空間在動(dòng)態(tài)擴(kuò)容時(shí)的時(shí)間開(kāi)銷。

在配置文件中還有AlwaysPreTouch這個(gè)參數(shù)。

-XX:+AlwaysPreTouch

其實(shí),通過(guò) Xmx 指定了的堆內(nèi)存,只有在 JVM 真正使用的時(shí)候,才會(huì)進(jìn)行分配。這個(gè)參數(shù),在 JVM 啟動(dòng)的時(shí)候,就把它所有的內(nèi)存在操作系統(tǒng)分配了。在堆比較大的時(shí)候,會(huì)加大啟動(dòng)時(shí)間,但它能夠減少內(nèi)存動(dòng)態(tài)分配的性能損耗,提高運(yùn)行時(shí)的速度。

如下圖,JVM 的內(nèi)存,分為堆和堆外內(nèi)存,其中堆的大小可以通過(guò) Xmx 和 Xms 來(lái)配置。

程序員必備:JVM核心知識(shí)點(diǎn)總結(jié)

 

但我們?cè)谂渲?ES 的堆內(nèi)存時(shí),通常把堆的初始化大小,設(shè)置成物理內(nèi)存的一半。這是因?yàn)?ES 是存儲(chǔ)類型的服務(wù),我們需要預(yù)留一半的內(nèi)存給文件緩存 ,等下次用到相同的文件時(shí),就不用與磁盤(pán)進(jìn)行頻繁的交互。這一塊區(qū)域一般叫作 PageCache,占用的空間很大。

對(duì)于計(jì)算型節(jié)點(diǎn)來(lái)說(shuō),比如我們普通的 Web 服務(wù),通常會(huì)把堆內(nèi)存設(shè)置為物理內(nèi)存的 2/3,剩下的 1/3 就是給堆外內(nèi)存使用的。

我們這張圖,對(duì)堆外內(nèi)存進(jìn)行了非常細(xì)致的劃分,解釋如下:

  • 元空間 參數(shù) -XX:MaxMetaspaceSize 和 -XX:MetaspaceSize,分別指定了元空間的最大內(nèi)存和初始化內(nèi)存。因?yàn)樵臻g默認(rèn)是沒(méi)有上限的,所以極端情況下,元空間會(huì)一直擠占操作系統(tǒng)剩余內(nèi)存。
  • JIT 編譯后代碼存放 -XX:ReservedCodeCacheSize。JIT 是 JVM 一個(gè)非常重要的特性,CodeCahe 存放的,就是即時(shí)編譯器所生成的二進(jìn)制代碼。另外,JNI 的代碼也是放在這里的。
  • 本地內(nèi)存 本地內(nèi)存是一些其他 attch 在 JVM 進(jìn)程上的內(nèi)存區(qū)域的統(tǒng)稱。比如網(wǎng)絡(luò)連接占用的內(nèi)存、線程創(chuàng)建占用的內(nèi)存等。在高并發(fā)應(yīng)用下,由于連接和線程都比較多,這部分內(nèi)存累加起來(lái)還是比較可觀的。
  • 直接內(nèi)存 這里要著重提一下直接內(nèi)存,因?yàn)樗潜镜貎?nèi)存中唯一可以使用參數(shù)來(lái)限制大小的區(qū)域。使用參數(shù) -XX:MaxDirectMemorySize,即可設(shè)定 ByteBuffer 類所申請(qǐng)的內(nèi)存上限。
  • JNI 內(nèi)存 上面談到 CodeCache 存放的 JNI 代碼,JNI 內(nèi)存就是指的這部分代碼所 malloc 的具體內(nèi)存。很可惜的是,這部分內(nèi)存的使用 JVM 是無(wú)法控制的,它依賴于具體的 JNI 代碼實(shí)現(xiàn)。

日志參數(shù)配置

下面是 ES 的日志參數(shù)配置,由于 Java 8 和 Java 9 的參數(shù)配置已經(jīng)完全不一樣了,ES 在這里也分了兩份。

Java8參數(shù)設(shè)置:

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-Xloggc:logs/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=32
-XX:GCLogFileSize=64m

Java9參數(shù)設(shè)置:

-Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m

下面解釋一下這些參數(shù)的意義,以 Java 8 為例。

  • PrintGCDetails 打印詳細(xì) GC 日志。
  • PrintGCDateStamps 打印當(dāng)前系統(tǒng)時(shí)間,更加可讀;與之對(duì)應(yīng)的是 PrintGCTimeStamps,打印的是 JVM 啟動(dòng)后的相對(duì)時(shí)間,可讀性較差。
  • PrintTenuringDistribution 打印對(duì)象年齡分布,對(duì)調(diào)優(yōu) MaxTenuringThreshold 參數(shù)幫助很大。
  • PrintGCApplicationStoppedTime 打印 STW 時(shí)間
  • 下面幾個(gè)日志參數(shù)是配置了類似于 Logback 的滾動(dòng)日志,比較簡(jiǎn)單,不再詳細(xì)介紹

從 Java 9 開(kāi)始,JVM 移除了 40 多個(gè) GC 日志相關(guān)的參數(shù),具體參見(jiàn) JEP 158。所以這部分的日志配置有很大的變化,GC 日志的打印方式,已經(jīng)完全不一樣了,比以前的日志參數(shù)規(guī)整了許多。

參數(shù)如下所示:

-Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m

再來(lái)看下 ES 在異常情況下的配置參數(shù):

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=data
-XX:ErrorFile=logs/hs_err_pid%p.log


HeapDumpOnOutOfMemoryError、HeapDumpPath、ErrorFile是每個(gè) Java 應(yīng)用都應(yīng)該配置的參數(shù)。正常情況下,我們通過(guò) jmap 獲取應(yīng)用程序的堆信息;異常情況下,比如發(fā)生了 OOM,通過(guò)這三個(gè)配置參數(shù),即可在發(fā)生OOM的時(shí)候,自動(dòng) dump 一份堆信息到指定的目錄中。

拿到了這份 dump 信息,我們就可以使用 MAT 等工具詳細(xì)分析,找到具體的 OOM 原因。

垃圾回收器配置

ES 默認(rèn)使用 CMS 垃圾回收器,它有以下三行主要的配置。

-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

下面介紹一下這兩個(gè)參數(shù):

  • UseConcMarkSweepGC,表示年輕代使用 ParNew,老年代的用 CMS 垃圾回收器
  • -XX:CMSInitiatingOccupancyFraction 由于 CMS 在執(zhí)行過(guò)程中,用戶線程還需要運(yùn)行,那就需要保證有充足的內(nèi)存空間供用戶使用。如果等到老年代空間快滿了,再開(kāi)啟這個(gè)回收過(guò)程,用戶線程可能會(huì)產(chǎn)生“Concurrent Mode Failure”的錯(cuò)誤,這時(shí)會(huì)臨時(shí)啟用 Serial Old 收集器來(lái)重新進(jìn)行老年代的垃圾收集,這樣停頓時(shí)間就很長(zhǎng)了(STW)。

這部分空間預(yù)留,一般在 30% 左右即可,那么能用的大概只有 70%。參數(shù)
-XX:CMSInitiatingOccupancyFraction 用來(lái)配置這個(gè)比例,但它首先必須配置 -XX:+UseCMSInitiatingOccupancyOnly 參數(shù)。

另外,對(duì)于 CMS 垃圾回收器,常用的還有下面的配置參數(shù):

  • -XX:ExplicitGCInvokesConcurrent 當(dāng)代碼里顯示的調(diào)用了 System.gc(),實(shí)際上是想讓回收器進(jìn)行FullGC,如果發(fā)生這種情況,則使用這個(gè)參數(shù)開(kāi)始并行 FullGC。建議加上。
  • -XX:CMSFullGCsBeforeCompaction 默認(rèn)為 0,就是每次FullGC都對(duì)老年代進(jìn)行碎片整理壓縮,建議保持默認(rèn)。
  • -XX:CMSScavengeBeforeRemark 開(kāi)啟或關(guān)閉在 CMS 重新標(biāo)記階段之前的清除(YGC)嘗試??梢越档?remark 時(shí)間,建議加上。
  • -XX:+ParallelRefProcEnabled 可以用來(lái)并行處理 Reference,以加快處理速度,縮短耗時(shí)。

CMS 垃圾回收器,已經(jīng)在 Java14 中被移除,由于它的 GC 時(shí)間不可控,有條件應(yīng)該盡量避免使用。

針對(duì) Java10(普通 Java 應(yīng)用在 Java 8 中即可開(kāi)啟 G1),ES 可采用 G1 垃圾回收器。

G1垃圾收集器,它可以通過(guò)配置參數(shù) MaxGCPauseMillis,指定一個(gè)期望的停頓時(shí)間,使用相對(duì)比較簡(jiǎn)單。

下面是主要的配置參數(shù):

  • -XX:MaxGCPauseMillis 設(shè)置目標(biāo)停頓時(shí)間,G1 會(huì)盡力達(dá)成。
  • -XX:G1HeapRegionSize 設(shè)置小堆區(qū)大小。這個(gè)值為 2 的次冪,不要太大,也不要太小。如果是在不知道如何設(shè)置,保持默認(rèn)。
  • -XX:InitiatingHeapOccupancyPercent 當(dāng)整個(gè)堆內(nèi)存使用達(dá)到一定比例(默認(rèn)是45%),并發(fā)標(biāo)記階段就會(huì)被啟動(dòng)。
  • -XX:ConcGCThreads 并發(fā)垃圾收集器使用的線程數(shù)量。默認(rèn)值隨 JVM 運(yùn)行的平臺(tái)不同而不同。不建議修改。

JVM 支持非常多的垃圾回收器,下面是最常用的幾個(gè),以及配置參數(shù):

  • -XX:+UseSerialGC 年輕代和老年代都用串行收集器
  • -XX:+UseParallelGC 年輕代使用 ParallerGC,老年代使用 Serial Old
  • -XX:+UseParallelOldGC 新生代和老年代都使用并行收集器
  • -XX:+UseG1GC 使用 G1 垃圾回收器
  • -XX:+UseZGC 使用 ZGC 垃圾回收器

額外配置

我們?cè)賮?lái)看下幾個(gè)額外的配置。

-Xss1m

-Xss設(shè)置每個(gè) Java 虛擬機(jī)棧的容量為 1MB。這個(gè)參數(shù)和 -XX:ThreadStackSize是一樣的,默認(rèn)就是 1MB。

-XX:-OmitStackTraceInFastThrow

把 - 換成 +,可以減少異常棧的輸出,進(jìn)行合并。雖然會(huì)對(duì)調(diào)試有一定的困擾,但能在發(fā)生異常時(shí)顯著增加性能。隨之而來(lái)的就是異常信息不好排查,ES 為了找問(wèn)題方便,就把錯(cuò)誤合并給關(guān)掉了。

-Djava.awt.headless=true

Headless 模式是系統(tǒng)的一種配置模式,在該模式下,系統(tǒng)缺少了顯示設(shè)備、鍵盤(pán)或鼠標(biāo)。在服務(wù)器上一般是沒(méi)這些設(shè)備的,這個(gè)參數(shù)是告訴虛擬機(jī)使用軟件去模擬這些設(shè)備。

-Djava.locale.providers=COMPAT
-Dfile.encoding=UTF-8
-Des.networkaddress.cache.ttl=60
-Des.networkaddress.cache.negative.ttl=10
-Dio.netty.noUnsafe=true
-Dio.netty.noKeySetOptimization=true
-Dio.netty.recycler.maxCapacityPerThread=0
-Dlog4j.shutdownHookEnabled=false
-Dlog4j2.disable.jmx=true
-Djava.io.tmpdir=${ES_TMPDIR}
-Djna.nosys=true

上面這些參數(shù),通過(guò) -D 參數(shù),在啟動(dòng)一個(gè) Java 程序時(shí),設(shè)置系統(tǒng)屬性值,也就是在 System 類中通過(guò)getProperties()得到的一串系統(tǒng)屬性。

這部分自定義性比較強(qiáng),不做過(guò)多介紹。

其他調(diào)優(yōu)

以上就是 ES 默認(rèn)的 JVM 參數(shù)配置,大多數(shù)還是比較基礎(chǔ)的。在平常的應(yīng)用服務(wù)中,我們希望得到更細(xì)粒度的控制,其中比較常用的就是調(diào)整各個(gè)分代之間的比例。

  • -Xmn 年輕代大小,默認(rèn)年輕代占堆大小的 1/3。高并發(fā)快消亡場(chǎng)景可適當(dāng)加大這個(gè)區(qū)域,對(duì)半或者更多都是可以的。但是在 G1 下,就不用再設(shè)置這個(gè)值了,它會(huì)自動(dòng)調(diào)整;
  • -XX:SurvivorRatio 默認(rèn)值為 8,表示伊甸區(qū)和幸存區(qū)的比例;
  • -XX:MaxTenuringThreshold 這個(gè)值在 CMS 下默認(rèn)為 6,G1 下默認(rèn)為 15。這個(gè)值和我們前面提到的對(duì)象提升有關(guān),改動(dòng)效果會(huì)比較明顯。對(duì)象的年齡分布可以使用-XX:+PrintTenuringDistribution打印,如果后面幾代的大小總是差不多,證明過(guò)了某個(gè)年齡后的對(duì)象總能晉升到老年代,就可以把晉升閾值設(shè)的小一些;
  • PretenureSizeThreshold 超過(guò)一定大小的對(duì)象,將直接在老年代分配,不過(guò)這個(gè)參數(shù)用得不是很多。

分享到:
標(biāo)簽:JVM
用戶無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定