每天都是面對對象編程,你真的瞭解你的對象嗎?

每天都是面對對象編程,你真的瞭解你的對象嗎?

 

Java是一種面向對象的編程語言,詳細本身對對象的理解是否只有一句話來描述:一切皆對象,new出來的對象都在堆上!等等,這不是2句話?不,後面這句只是我寫這篇文章的起因。初學Java你們都說new出來的對象都在堆上,對此深信不疑!可是後續愈加對這句話產生懷疑,想一想每一個類的toString方法都會new一個StringBuffer,這樣作堆內存豈不是增大一倍?For循環中建立對象爲何沒有堆溢出?建立的對象到底在堆中佔用多少內存?懷着以上疑問往下看,本篇文章做爲Java對象的綜合整理來描述何謂對象。java

 

Java中一切皆對象,對象的建立主要以下:程序員

People people = new People();

如今面試都是各類文字坑,例如:問這個對象是否在堆上分配內存?怎麼回答,是?不是?面試

這個問題,要根據上下文來回答,就是要根據這行代碼所處的環境來回答,何謂環境:運行環境JRE、書寫位置,不一樣環境結果不同。想知道結果,先Get到如下知識點:算法

逃逸分析是JDK6+版本後默認開啓的技術(如今都JDK15了,都是舊技術了==!),主要分析方法內部的局部變量的引用做用域,用於作後續優化。逃逸分析以後一個方法內的局部變量被分爲3類逃逸對象編程

  • 全局逃逸對象: 對外部而言,該對象能夠在類級別上直接訪問到(調用類獲取對象實例)
  • 參數逃逸對象:對外部而言,該對象能夠在方法級別上直接訪問到(調用方法獲取對象實例)
  • 未逃逸對象:對外部而言,該對象彷彿不存在同樣,不可嗅探

後續優化指的是對未逃逸的優化,主要分爲標量替換和鎖消除數組

標量替換:在Java中8種基本數據類型已是能夠直接分配空間的,不可再被細化,稱爲標準變量,簡稱標量。對象的引用是內存地址也不可再被細化,也能夠稱爲標量。而Java對象則是由多個標量聚合而來,稱爲聚合量。按照這種標準將Java對象的成員變量拆分替換爲標量的過程,稱爲標量替換。這個過程會致使對象的分配不必定在堆中,而是在棧上或者寄存器中。安全

鎖消除Java鎖是針對多線程而使用的,當在單線程環境下使用鎖後被JIT編譯器優化後就會移除掉鎖相關代碼,這個過程就是鎖消除(屬於優化,不影響對象)。多線程

指針壓縮:32位機器對象的引用指針使用32位表示,在64位使用64位表示,一樣的配置而內存佔用增多,這樣真的好嗎?JDK給出指針優化技術,將64位(8字節)指針引用(Refrence類型)壓縮爲32位(4字節)來節省內存空間。jvm

對象的逃逸

一個標準大小=32byte的Java對象(後面會寫如何計算)編程語言

class People {
    int i1;
    int i2;
    int i3;
    byte b1;
    byte b2;
    String str;
}

未逃逸對象

public class EscapeAnalysis {

    public static void main(String[] args) throws IOException {
        // 預估:在不發生GC狀況下32M內存
        for (int j = 0; j < 1024 * 1024; j++) {
            unMethodEscapeAnalysis();
        }
        // 阻塞線程,便於內存分析
        System.in.read();
    }

    /**
     * people對象引用做用域未超出方法做用域範圍
     */
    private static void unMethodEscapeAnalysis() {
        People people = new People();
        // do  something
    }
}

 

未開啓逃逸分析

啓動JVM參數

-server -Xss108k -Xmx1G -Xms1G -XX:+PrintGC -XX:-UseTLAB -XX:-DoEscapeAnalysis -XX:-EliminateAllocations

啓動控制檯:無輸出:未發生GC

 

堆內存查看

$ jps
3024 Jps
16436 EscapeAnalysis
24072 KotlinCompileDaemon

 

$ jmap -histo 16436

 num     #instances         #bytes  class name
----------------------------------------------
   1:       1048576       33554432  cn.tinyice.demo.object.People
   2:          1547        1074176  [B
   3:          6723        1009904  [C
   4:          4374          69984  java.lang.String

 

此時堆中共建立了1024*1024個實例,每一個實例32byte,共32M內存

開啓逃逸分析

啓動JVM參數

-server -Xss108k -Xmx1G -Xms1G -XX:+PrintGC -XX:-UseTLAB -XX:-DoEscapeAnalysis -XX:-EliminateAllocations

啓動控制檯:無輸出:未發生GC

 

堆內存查看

$ jps
3840 Jps
24072 KotlinCompileDaemon
25272 EscapeAnalysis

 

$ jmap -histo 25272

 num     #instances         #bytes  class name
----------------------------------------------
   1:       1048576       33554432  cn.tinyice.demo.object.People
   2:          1547        1074176  [B
   3:          6721        1009840  [C
   4:          4372          69952  java.lang.String

此時與未開啓一致,仍然是在堆中建立了1024*1024個實例,每一個實例32byte,共32M內存

開啓逃逸分析和標量替換

啓動JVM參數

-server -Xss108k -Xmx1G -Xms1G -XX:+PrintGC -XX:-UseTLAB -XX:+DoEscapeAnalysis -XX:+EliminateAllocations

堆內存查看

$ jps
7828 Jps
21816 EscapeAnalysis
24072 KotlinCompileDaemon

 

$ jmap -histo 21816

 num     #instances         #bytes  class name
----------------------------------------------
   1:         92027        2944864  cn.tinyice.demo.object.People
   2:          1547        1074176  [B
   3:          6721        1009840  [C
   4:          4372          69952  java.lang.String

此時堆中僅建立了92027個實例,內存佔用少了11倍。

啓動控制檯:無輸出:未發生GC,說明實例的確未分配到堆中

 

 

未分配到堆中,是由於一部分分配到了棧中,這種未逃逸對象若是分配到棧上,則其生命週期隨棧一塊兒,使用完畢自動銷燬。下面爲java對象分配的具體細節。

 

對象的內存分配

 

實例分配原則
  1. 嘗試棧上分配
    • 基於逃逸分析和標量替換,將線程私有對象直接分配在棧上
    • 在函數調用完畢後自動銷燬對象,不須要GC回收
    • 棧空間很小,默認108K,不能分配大對象
  2. 嘗試TLAB
    • 判斷是否使用TLAB(Thread Local Allocation Buffer)技術
      • 虛擬機參數 -XX:+UseTLAB,-XX:-UseTLAB,默認開啓
      • 虛擬機參數-XX:TLABWasteTargetPercent 來設置TLAB佔用eEden空間百分比,默認1%
      • 虛擬機參數-XX:+PrintTLAB 打印TLAB的使用狀況
      • TLAB自己佔用eEden區空間,空間很小不能存放大對象,
      • 每一個線程在Java堆中預先分配了一小塊內存,當有對象建立請求內存分配時,就會在該塊內存上進行分配
      • 使用線程控制安全,不須要在Eden區經過同步控制進行內存分配
  3. 嘗試老年代分配(堆分配原則)
    • 若是能夠直接進入老年代,直接在老年代分配
  4. 以上都失敗時(注意分配對象時很容易觸發GC,堆分配原則)
    • 內存連續時:使用指針碰撞(Serial、ParNew等帶Compact過程的收集器)
      • 分配在堆的Eden區,該區域內存連續
      • 指針始終指向空閒區的起始位置。
      • 在新對象分配空間後,指針向後移動了該對象所佔空間的大小個單位,從而指向新的空閒區的起始位置
      • 對象分配過程當中使用了CAS加失敗重試的方式來保證線程安全(CAS即原子操做)
      • 若是成功:則進行對象頭信息設置
    • 內存不連續時:使用空閒列表(CMS這種基於Mark-Sweep算法的收集器)
      • 若是堆空間不是連續的,則JVM維護一張關係表,來使內存邏輯上連續從而達到對象分配的目
Image [13].png
堆分配原則:

  • 優先在Eden(伊甸園)區進行分配
    • 可經過-XX:SurvivorRation=8來肯定Eden與Survivor比例爲 8:1
    • 新生代存在2個Survivor區域(From和To),當新生代10份時,Survivor共佔2份,Eden佔8份
    • 新建對象會先在Eden中分配
      • 空間足夠時直接分配
      • 當Eden空間不足時
        • 將Eden內的對象進行一次Minor Gc 回收準備放入進入From類型的Survivor區
          • From類型的Survivor區
            • 空間足夠時,放置GC對象時將GC對象回收進來
            • 空間不足時,將GC對象直接放入老年代中
        • Minor GC後Eden空間仍然不足
          • 新建對象直接進入老年代
  • 長期存活的對象移交老年代(永久代)
    • 在Eden的對象通過一次Minor GC進入Survivo 區後,對象的對象頭信息年齡字段Age+1
    • Survivor區對象每通過一次Minor GC對象頭信息年齡字段Age+1
      • 會在From Survivor和ToSurvivor 區進行來回GC(複製算法)
    • 當對象的年齡達到必定值(默認15歲)時就會晉升到老年代
    • -XX:MaxTenuringThreshold=15設置分代年齡爲15
  • 大對象直接進入老年代(永久代)
    • 大對象爲佔用堆內大量連續空間的對象(數組類、字符串)
    • -XX:MaxTenuringThreshold=4M 能夠設置大於4M的對象直接進入老年代
  • 動態年齡判斷
    • GC回收對象時並不必定必須嚴格要求分代年齡進行晉升老年代
    • 當Survivor區的同年齡對象的總和大於Survivor空間1/2時
      • 年齡大於等於該年齡(相同年齡)的對象均可以直接進入老年代
  • 老年代對象分配使用空間分配擔保
    • 新生代全部對象大小小於老年代可用空間大小時,Minor GC是安全的
      • 至關於新生代全部對象均可以放到老年代裏面,於是不會出現溢出等現象
    • 相反,Minor GC是不安全的
      • 至關於新生代對象只能有一部分能夠放入老年代,另外一部分會由於空間不足而放入失敗
      • 安全措施-XX:HandlePromotionFailure=true,容許擔保失敗
      • 發生MinorGC以前,JVM會判斷以前每次晉升到老年代的平均大小是否大於老年代剩餘空間的大小
        • 若小於於而且容許擔保失敗則進行一次Minor GC
          • 對象GC預測平穩,不會發生大量對象忽然進入老年代致使其空間不足而溢出
        • 若小於而且不容許擔保失敗則進行一次full GC
          • 即便對象GC預測平穩,可是不保證不會激增,因此安全點仍是先去Full GC下
          • 回收全部區域,給老年代清理出更多空間
        • 若小於即便容許擔保失敗也進行一次full GC
          • 即Minor GC後的存活對象數量忽然暴增,即便容許擔保失敗可是仍是極大多是不安全的
          • 回收全部區域,給老年代清理出更多空間

對象實例組成

  • 對象頭

    • MarkWord(必須)
    • 類型指針:指向對象的類元數據(非必須)
    • 數組長度(數組類型對象纔有)
  • 實例數據

    • 對象的字段屬性,方法等,存儲在堆中
  • 數據填充

    • JVM要求java的對象佔的內存大小應該是8bit的倍數
    • 實例數據有可能不是8的倍數,須要使用0進行填充對齊
 
MarkWord結構
 
25Bit
 
4bit
1bit
2bit
 
鎖狀態
23bit
2bit
偏向鎖
鎖標誌
哈希碼
分代年齡
0
01
無鎖
指向鎖記錄的指針
00
輕量鎖
指向重量鎖的指針
10
重量鎖
11
GC標記
線程ID
時間戳
分代年齡
1
01
偏向鎖
 

對象的初始化

因爲對象初始化涉及到類加載,這裏很少描述

  • 分配到的空間設置爲0
  • 數據填充0,8字節對齊
  • 對象頭信息設置
  • 調用<init>進行初始化(類的實例化)

給個示例先體會下

public class ClinitObject {

    static ClinitObject clinitObject;

    static {
        b = 2;
        clinitObject = new ClinitObject();
        System.out.println(clinitObject.toString());
    }

    int a = 1;
    static int b;
    final static int c = b;
    final static String d = new String("d");
    String e = "e";
    String f = "f";

    public ClinitObject() {
        e = d;
        a = c;
    }

    @Override
    public String toString() {
        return "ClinitObject{" + "\n" +
                "\t" + "a=" + a + "\n" +
                "\t" + "b=" + b + "\n" +
                "\t" + "c=" + c + "\n" +
                "\t" + "d=" + d + "\n" +
                "\t" + "e=" + e + "\n" +
                "\t" + "f=" + f + "\n" +
                '}';
    }

    public static void main(String[] args) {
        System.out.println(clinitObject.toString());
    }
}

控制檯

ClinitObject{
	a=0
	b=2
	c=0
	d=null
	e=null
	f=f
}
ClinitObject{
	a=0
	b=2
	c=2
	d=d
	e=null
	f=f
}
 

對象的大小計算

  • 普通對象

    • 4或8字節(MarkWord)+4或8字節(klass Reference)+實例數據長度+ 0填充(Padding)
  • 數組對象

    • 4或8字節(MarkWord)+4或8字節(klass Reference)+4字節(ArrayLength)+實例數據長度+0填充(Padding)
  • 其它說明:

    • 對象頭(MarkWord)在32位JVM中爲4字節,在64位JVM中爲8字節
    • 爲了節約空間,使用了指針壓縮技術:
      • JDK6開始對類型指針(Reference)進行壓縮,壓縮前8字節,壓縮後4字節
        • 參數 -XX:+UseCompressedOops
      • JDK8開始新增元數據空間metaSpace,因而新增參數來控制指針壓縮:
        • -XX:+UseCompressedClassPointers(指針壓縮開關,堆內存>=32G時,自動關閉)
        • -XX:CompressedClassSpaceSize (Reference指向的類元數據空間大小,默認1G,上限32G)
    • 數據填充(Padding)爲保證對象大小爲8的整數倍的數據填充,使數據對齊
  • 經常使用數據類型大小

數據類型
佔用空間(byte)
byte
1
short
2
int
4
long
8
char
2
float
4
double
8
boolean
1或4 ,計算大小時爲1,判斷真假時爲4(底層爲int常量0,1)
Object(存儲的是引用指針)
由計算機位數和是否指針壓縮決定 4或8 字節

對象的定位

java源碼中調用對象在JVM中是經過虛擬機棧中本地變量標的reference來指向對象的引用來定位和訪問堆中的對象的,訪問方式存在主流的2種
  • 句柄訪問

    • jvm堆中單獨維護一張reference與對象實例數據(實例化數據)和對象類型數據(ClassFile數據)的關係表
    • 經過該關係表來查找到java實例對象
  • 直接訪問(Sun HotSpot 採用該方式)

    • reference直接指向了java堆中對象的實例數據(實例化數據),該實例對象的類型指針(Reference)指向類型數據(ClassFile數據)

 

指針壓縮示例

public class CompressedClassPointer {

    public static void main(String[] args) throws IOException {
        People people=new People();
        System.in.read();
    }
}

啓用指針壓縮(默認)

JVM參數

-server -XX:+UseCompressedOops -XX:+UseCompressedClassPointers -XX:CompressedClassSpaceSize=1G

堆內存查看

$ jps
11440
11744 RemoteMavenServer
14928 KotlinCompileDaemon
15540 Launcher
15908 Jps
9996 CompressedClassPointer

 

$ jmap.exe -histo 9996

 num     #instances         #bytes  class name
----------------------------------------------
... 
233:             1             32  cn.tinyice.demo.object.People

關閉指針壓縮

JVM參數

-server -XX:-UseCompressedOops

堆內存查看

$ jps
11440
11744 RemoteMavenServer
14928 KotlinCompileDaemon
8448 CompressedClassPointer

 

$ jmap.exe -histo 8448

 num     #instances         #bytes  class name
----------------------------------------------
...
254:             1             40  cn.tinyice.demo.object.People

 

 

示例解析

示例中開啓以後對象大小會減小8byte。而指針壓縮是8字節變4字節,按理說應該少4字節即32位,爲何這個樣子?

 

開啓壓縮指針時的對象大小計算

/**
 * Size(People) =
 * 8(mark word)+4(klass reference)+ 4(i1)+4(i2)+4(i2)+1(b1)+1(b2)+4(str reference) + 2(padding)
 * |----------------------------------- 30 byte ---------------------------------|----00-------/
 * |---------------------------------------- 32 byte ------------------------------------------/
 */

 

關閉壓縮指針時的對象大小計算

/**
 * Size(People) =
 * 8(mark word)+8(klass reference)+ 4(i1)+4(i2)+4(i2)+1(b1)+1(b2)+8(str reference) + 2(padding)
 * |----------------------------------- 38 byte ---------------------------------|----00-------/
 * |---------------------------------------- 40 byte ------------------------------------------/
 */

 

這裏就看到區別了,是數據填充形成的,java爲了便於數據管理,因而對象都是8字節對齊的,不足的使用0進行填充(padding)。

至於對象的實例化,會在寫類加載流程是再作描述。

 

原文地址: 程序員微錄  每天都是面對對象編程,你真的瞭解你的對象嗎?

相關文章
相關標籤/搜索