一、創(chuàng)建對象過程
1、類加載檢測
虛擬機遇到一條new指令時,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程。
new指令對應(yīng)語言層面講是,new關(guān)鍵字、對象克隆、對象序列化等
2、分配內(nèi)存
確保類加載完成之后,接下來jvm就需要為新生對象分配內(nèi)存。對象所需內(nèi)存的大小其實在類加載完成之后就可以確定了,然后就是從堆空間中劃分出一塊確定大小的內(nèi)存給該對象使用
對象內(nèi)存分配涉及到兩個問題:如何劃分內(nèi)存和如何控制并發(fā)分配內(nèi)存
劃分內(nèi)存的方法:
- 指針碰撞(Bump the Pointer)(默認用指針碰撞):在堆中分配內(nèi)存中,有一個指針作為對象已分配和為分配內(nèi)存分界點,當對象分配進來,指針往未分配內(nèi)存區(qū)域挪動該對象相同大小的距離,這個過程叫指針碰撞
- 空閑列表:(Free List):在堆中分配對象中,如果堆內(nèi)存的對象已分配區(qū)和未分配區(qū)特別凌亂,不是那么規(guī)整,這時候只能在jvm內(nèi)部維護一個列表去記錄哪些是內(nèi)存區(qū)域是可用的,當有對象分配進來就更新這個列表
解決并發(fā)問題的方法:
- CAS(Compare And Swap):jvm內(nèi)部采用CAS自旋來保證對象一定會被分配到內(nèi)存
- 本地線程分配緩沖(Thread Local Allocation Buffer, TLAB):每個線程在堆中預(yù)先分配一小塊內(nèi)存。通過-XX:+/-UseTLAB參數(shù)來設(shè)定虛擬機是否使用TLAB(JVM默認開啟-XX:+UseTLAB),-XX:TLABSize指定TLAB大小
3、初始化
內(nèi)存分配完成之后,虛擬機需要將分配到內(nèi)存空間的對象成員變量都初始化為零值(不包括對象頭),如果是TLAB,這一工作過程也可以提前至TLAB分配時進行。這一步操作保證了對象的實例字段在java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值
4、設(shè)置對象頭
初始化零值后,jvm要對對象進行必要的設(shè)置,例如這個對象是哪個類的實例、如何才能找類的元數(shù)據(jù)信息、對象的哈希碼、對象的gc分帶年齡等信息。這些信息放在對象的對象頭Object Header之中
在HotSpot虛擬機中,對象在內(nèi)存中存儲的布局可分為三個區(qū)域:對象頭(Header)、示例數(shù)據(jù)(Instance Data)和對齊填充
- 對象頭:比如hash碼,對象所屬分帶年齡、對象鎖、鎖狀態(tài)標志、偏向鎖(線程)ID、偏向時間,數(shù)組長度(數(shù)組對象才有)等
- 實例數(shù)據(jù):存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息
- 對其填充:由于Hotspot虛擬機的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說就是任何對象的大小都必須是8字節(jié)的整數(shù)倍。對象頭已被精心設(shè)計成正好是8的整數(shù)倍,因此,如果是對象實例數(shù)據(jù)沒有對齊的話,就需要通過對齊填充來補全
找了一張對象頭的圖片,
5、執(zhí)行<init>方法
執(zhí)行<init>方法,即對象按照程序的意愿進行初始化,就是為屬性賦程序員指定值,然后執(zhí)行構(gòu)造方法。
6、對象的指針壓縮
引入查看對象大小的包:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
/**
* 查看對象大小
*/
public class ObjectSizeTest {
public static void main(String[] args) {
// object對象16B
// 對象頭
// mark word 8B
// Klass Pointer 4B
// 不是數(shù)組 數(shù)組長度沒有
// 實例數(shù)據(jù) 沒有成員變量
// 對其填充 要求整個對象大小是8的整數(shù)倍 補上4B
ClassLayout layout = ClassLayout.parseInstance(new Object());
System.out.println(layout.toPrintable());
System.out.println();
// object對象16B
// 對象頭
// mark word 8B
// Klass Pointer 4B
// 數(shù)組長度占4B
// 實例數(shù)據(jù) 沒有成員變量
// 對其填充 要求整個對象大小是8的整數(shù)倍 不需要填充
ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
System.out.println(layout1.toPrintable());
System.out.println();
ClassLayout layout2 = ClassLayout.parseInstance(new A());
System.out.println(layout2.toPrintable());
}
// -XX:+UseCompressedOops 默認開啟的壓縮所有指針
// -XX:+UseCompressedClassPointers 默認開啟的壓縮對象頭里的類型指針Klass Pointer
// Oops : Ordinary Object Pointers
public static class A {
// 對象頭
// mark word 8B
// Klass Pointer 4B 如果關(guān)閉壓縮-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,則占用8B
int id; // 4B
String name; // 對象在堆中的指針4B 如果關(guān)閉指針壓縮-XX:-UseCompressedOops,則占用8B
byte b; // 1B 這個有個內(nèi)部的填充3B
Object o; // 對象在堆中的指針4B 如果關(guān)閉指針壓縮-XX:-UseCompressedOops,則占用8B
// 需要對齊填充4B
// A對象大小為32B
}
}
打印結(jié)果:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 0 int [I.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
com.gaorufeng.jvm.ObjectSizeTest$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 61 cc 00 f8 (01100001 11001100 00000000 11111000) (-134165407)
12 4 int A.id 0
16 1 byte A.b 0
17 3 (alignment/padding gap)
20 4 java.lang.String A.name null
24 4 java.lang.Object A.o null
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
?什么是java對象的指針壓縮?
- jdk1.6 update14開始,在64bit操作系統(tǒng)中,jvm支持指針壓縮
- jvm配置參數(shù):UseCompressedOops,compressed--壓縮、oop(ordinary object pointer)--對象指針
- 啟動指針壓縮:-XX:+UseCompressedOops(默認開啟),禁止指針壓縮:-XX:UseCompressedOops
為什么要進行指針壓縮?
節(jié)省內(nèi)存空間,減少總線尋址性能上的消耗,堆內(nèi)存小于4G時,不需要啟用指針壓縮;堆內(nèi)存小于4G時,不需要啟用指針壓縮
二、對象的內(nèi)存分配
1、棧上分配
棧上分配是為了減少gc的次數(shù),提高jvm性能,每次棧幀入棧時創(chuàng)建的對象,出棧就可以釋放內(nèi)存,對象自然就被清理了
需要開啟兩個參數(shù)來確定對象是否能在棧上分配
- 對象逃逸分析:當一個對象在棧幀方法中,分析出沒有被外部對象引用到時,確定有可能在棧上分配,有引用到那就不能在棧上分配,逃逸分析參數(shù)(-XX:+DoEscapeAnalysis),jdk7之后默認開啟,關(guān)閉逃逸分析參數(shù)(-XX:-DoEscapeAnalysis)
舉個例子:
public User test1() {
User user = new User();
return user;
}
public void test2() {
User user = new User();
}
?test1()方法的對象的作用范圍是不確定的,test2()方法可以確定user對象作用范圍只在當前棧幀方法,不會逃逸出當前方法,可以在棧上進行分配
- 標量替換:棧幀內(nèi)存區(qū)域的可用內(nèi)存不一定能夠存放一整個對象且隨機分布,開啟標量替換參數(shù)(-XX:+EliminateAllocations),jdk7之后默認開啟
- 標量與聚合量:標量即不可被進一步分解的量,而Java的基本數(shù)據(jù)類型就是標量(如int、long等基本數(shù)據(jù)類型以及reference類型等),標量的對立就是可以被進一步分解的量,而這種量稱之為聚合量,而在Java中對象就是可以被進一步分解的聚合量
棧上分配依賴于逃逸分析和標量替換
棧上分配示例:
?
/**
* 把堆空間設(shè)小一點 如果發(fā)生gc那就是分配在堆中,如果沒有g(shù)c就證明在棧上分配
* 使用如下參數(shù)不會發(fā)生GC
* -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
* 使用如下參數(shù)都會發(fā)生大量GC
* -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
* -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
*/
public class AllotOnStack {
public static void allocation() {
User user = new User();
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
allocation();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
2、堆中Eden區(qū)分配
大多數(shù)情況下,對象在新生代中Eden區(qū)分配。當Eden區(qū)沒有足夠內(nèi)存空間,虛擬機發(fā)起一次minor gc
- minor gc/young gc:新生代垃圾對象的占用內(nèi)存回收
- major gc/full gc:回收整個堆和方法區(qū)的垃圾對象占用的內(nèi)存,比minor gc慢10倍以上
新生代和老年代比例1:2,eden區(qū)和s0、s1區(qū)比例8:1:1
大部分對象都分配eden區(qū),eden區(qū)放滿了之后觸發(fā)minor gc,存活的對象放到survivor區(qū)中空的區(qū)域,下次觸發(fā)minor gc,繼續(xù)eden區(qū)和非空的survivor區(qū)的非垃圾對象往空的survivor區(qū)里面放
jvm默認有個參數(shù)-XX:+UseAdaptiveSizePolicy(默認開啟),會導(dǎo)致這個8:1:1比例自動變化,如果不想這個比例有變化可以設(shè)置參數(shù)-XX:-UseAdaptiveSizePolicy
示例:
/**
* eden區(qū)大概默認65M
* 添加運行JVM參數(shù): -XX:+PrintGCDetails
*/
public class GCTest {
public static void main(String[] args) {
byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/;
allocation1 = new byte[62000*1024];
//allocation2 = new byte[8000*1024];
/*allocation3 = new byte[1000*1024];
allocation4 = new byte[1000*1024];
allocation5 = new byte[1000*1024];
allocation6 = new byte[1000*1024];*/
}
}
運行結(jié)果:
Heap
PSYoungGen total 75776K, used 65024K [0x000000076bc00000, 0x0000000771080000, 0x00000007c0000000)
eden space 65024K, 100% used [0x000000076bc00000,0x000000076fb80000,0x000000076fb80000)
from space 10752K, 0% used [0x0000000770600000,0x0000000770600000,0x0000000771080000)
to space 10752K, 0% used [0x000000076fb80000,0x000000076fb80000,0x0000000770600000)
ParOldGen total 173568K, used 0K [0x00000006c3400000, 0x00000006cdd80000, 0x000000076bc00000)
object space 173568K, 0% used [0x00000006c3400000,0x00000006c3400000,0x00000006cdd80000)
Metaspace used 2644K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K
?可以看到eden區(qū)被全部占用,繼續(xù)分配觸發(fā)minor gc,由于被引用是非垃圾對象,survivor區(qū)放不下,放到老年代
/**
* eden區(qū)大概默認65M
* 添加運行JVM參數(shù): -XX:+PrintGCDetails
*/
public class GCTest {
public static void main(String[] args) {
byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/;
allocation1 = new byte[62000*1024];
allocation2 = new byte[8000*1024];
/*allocation3 = new byte[1000*1024];
allocation4 = new byte[1000*1024];
allocation5 = new byte[1000*1024];
allocation6 = new byte[1000*1024];*/
}
}
打印結(jié)果:
[GC (Allocation Failure) [PSYoungGen: 64601K->632K(75776K)] 64601K->62640K(249344K), 0.0270385 secs] [Times: user=0.09 sys=0.02, real=0.03 secs]
Heap
PSYoungGen total 75776K, used 9282K [0x000000076bc00000, 0x0000000775000000, 0x00000007c0000000)
eden space 65024K, 13% used [0x000000076bc00000,0x000000076c472a78,0x000000076fb80000)
from space 10752K, 5% used [0x000000076fb80000,0x000000076fc1e030,0x0000000770600000)
to space 10752K, 0% used [0x0000000774580000,0x0000000774580000,0x0000000775000000)
ParOldGen total 173568K, used 62008K [0x00000006c3400000, 0x00000006cdd80000, 0x000000076bc00000)
object space 173568K, 35% used [0x00000006c3400000,0x00000006c708e010,0x00000006cdd80000)
Metaspace used 2644K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K
后面有對象來會繼續(xù)分配在eden區(qū)
/**
* eden區(qū)大概默認65M
* 添加運行JVM參數(shù): -XX:+PrintGCDetails
*/
public class GCTest {
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6;
allocation1 = new byte[62000*1024];
allocation2 = new byte[8000*1024];
allocation3 = new byte[1000*1024];
allocation4 = new byte[1000*1024];
allocation5 = new byte[1000*1024];
allocation6 = new byte[1000*1024];
}
}
打印結(jié)果:
[GC (Allocation Failure) [PSYoungGen: 64601K->696K(75776K)] 64601K->62704K(249344K), 0.0411987 secs] [Times: user=0.06 sys=0.03, real=0.04 secs]
Heap
PSYoungGen total 75776K, used 13621K [0x000000076bc00000, 0x0000000775000000, 0x00000007c0000000)
eden space 65024K, 19% used [0x000000076bc00000,0x000000076c89f3d8,0x000000076fb80000)
from space 10752K, 6% used [0x000000076fb80000,0x000000076fc2e030,0x0000000770600000)
to space 10752K, 0% used [0x0000000774580000,0x0000000774580000,0x0000000775000000)
ParOldGen total 173568K, used 62008K [0x00000006c3400000, 0x00000006cdd80000, 0x000000076bc00000)
object space 173568K, 35% used [0x00000006c3400000,0x00000006c708e010,0x00000006cdd80000)
Metaspace used 2644K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K
3、大對象直接進入老年代
大對象就是需要大量連續(xù)內(nèi)存空間的對象(比如數(shù)組、字符串)。jvm參數(shù)-XX:PretenureSizeThreshold可以設(shè)置大對象的大小,如果對象超過設(shè)置大小會直接進入老年代,不會進入年輕代,這個參數(shù)只在Serial和ParNew兩個收集器下有效
比如設(shè)置jvm參數(shù):-XX:PretenureSizeThreshold=1000000 (單位是字節(jié)) -XX:+UseSerialGC,再執(zhí)行下上面的第一個程序就會發(fā)現(xiàn)大對象直接入老年代
/**
* eden區(qū)大概默認65M
* 添加運行JVM參數(shù): -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000000 -XX:+UseSerialGC
*/
public class GCTest {
public static void main(String[] args) {
// 6M的對象 設(shè)置1M就是大對象-XX:PretenureSizeThreshold=1000000
byte[] allocation1 = allocation1 = new byte[6000*1024];
}
}
打印結(jié)果(eden區(qū)默認會占用一定空間):
Heap
def new generation total 78016K, used 4162K [0x00000006c3400000, 0x00000006c88a0000, 0x0000000717800000)
eden space 69376K, 6% used [0x00000006c3400000, 0x00000006c3810bd0, 0x00000006c77c0000)
from space 8640K, 0% used [0x00000006c77c0000, 0x00000006c77c0000, 0x00000006c8030000)
to space 8640K, 0% used [0x00000006c8030000, 0x00000006c8030000, 0x00000006c88a0000)
tenured generation total 173440K, used 6000K [0x0000000717800000, 0x0000000722160000, 0x00000007c0000000)
the space 173440K, 3% used [0x0000000717800000, 0x0000000717ddc010, 0x0000000717ddc200, 0x0000000722160000)
Metaspace used 2644K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 280K, capacity 386K, committed 512K, reserved 1048576K
為什么要這樣?
年輕代的minor gc是比較頻繁的,復(fù)制耗費性能
4、長期存活的對象進入老年代
對象的對象頭分帶年齡占4個字節(jié)(1111),換算成十進制就是15,所以分帶年齡不可能超過15.如果對象在eden區(qū)分配并且已經(jīng)經(jīng)歷過一次minor gc后仍然能夠存活,并且能被survivor區(qū)所容納,該對象將被移動到survivor區(qū),并將對象的年齡設(shè)為1。對象在survivor區(qū)每經(jīng)歷一次minor gc,年齡就會加1,當年齡增加到一定程度(默認15,CMS收集器默認6,不同的垃圾收集器會略微有點不同),就會被晉升到老年代中,對象晉升到老年代的年齡閾值,可以通過參數(shù)-XX:MaxTenuringThreshold來設(shè)置
5、對象動態(tài)年齡判斷機制
當前對象的survivor區(qū)域里(其中一塊區(qū)域,放對象的那快s區(qū)),一批對象的總大小大于這塊survivor區(qū)域的50%(-XX:TargetSurvivorRatio可以指定),那么此時大于等于這批對象年齡最大值的對象,就可以直接進入老年代了,例如survivor區(qū)域里現(xiàn)在有一批對象,年齡1+年齡2+年齡n的多個年齡對象總和超過survivor區(qū)域的50%,此時就會把n(含)以上的對象都放入老年代。這個規(guī)則其實是希望那些可能是長期存活的對象,盡早進入老年代。對象動態(tài)年齡判斷機制一般是在minor gc之后觸發(fā)
6、老年帶空間分配擔保機制
年輕代每次minor gc之前jvm都會計算下老年代剩余可用空間,如果這個可用空間小于年輕代里現(xiàn)有的所有對象大小之和(包括垃圾對象),就會看一個-XX:-HandlePromotionFailure(jdk1.8默認就設(shè)置了)的參數(shù)是否設(shè)置了,如果有這個參數(shù),就會看看老年代的可用內(nèi)存大小,是否大于之前每一次minor gc后進入老年代的對象的平均大小。如果上一步結(jié)果是小于或者之前說的參數(shù)沒有設(shè)置,那么就會觸發(fā)一次full gc,對老年代和年輕代一起回收一次垃圾,如果回收完還是沒有足夠空間存放新的對象就會發(fā)生OOM,如果minor gc之后剩余存活的需要挪到老年代的對象大小還是大于老年代可用空間,那么也會觸發(fā)full gc,full gc完之后如果還沒有空間放minor gc之后存活的對象,也會發(fā)生OOM
三、內(nèi)存回收
1、引用計數(shù)法
此算法就是當一個對象被引用一次計數(shù)器就會加1,引用為0,則認為是可回收垃圾;唯一的問題就是如果是循環(huán)引用,那就永遠不可能為0,這樣會導(dǎo)致內(nèi)存泄露
2、可達性分析算法
從GC Roots(線程棧的本地變量、靜態(tài)變量、本地方法棧的變量等)開始往下找,找被GC Roots引用的對象,這些對象都是非垃圾對象
3、常見引用類型
- 強引用:普通變量的引用
public static User user = new User();
- 軟引用:一般情況下不會被回收,當發(fā)生gc釋放不出來空間了就會被回收;比如對象頻繁的創(chuàng)建但又不是特別重要的對象,可用用與大屏展示,可有可無的對象
public static SoftReference<User> user = new SoftReference<User>(new User());
- 弱引用:弱引用跟沒引用一樣,會直接被回收,很少用
public static WeakReference user = new WeakReference(new User());
- 虛引用:也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系,幾乎不用
4、finalize()方法自救
每個對象都會執(zhí)行一次finalize()方法,在回收之前都會進行標記
- 第一次標記:查看是否有重寫finalize()方法,如果沒有就直接被回收掉了
- 第二次標記:執(zhí)行finalize()方法,這個時候如果為了不讓gc回收,可以讓對象與GC Roots關(guān)聯(lián)(這樣每次創(chuàng)建這個對象都會被自救,內(nèi)存泄漏,遲早會出現(xiàn)OOM)
5、方法區(qū)判斷無用的類
- 該類在堆中實例對象已被回收
- 加載該類的ClassLoader已被回收
- 該類的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法
6、內(nèi)存泄漏(會出現(xiàn)OOM)
程序內(nèi)部采用了靜態(tài)map緩存數(shù)據(jù),由于這個對象是靜態(tài)的,所以一直在old區(qū),隨著數(shù)據(jù)不斷增加,map占用內(nèi)存更大,會觸發(fā)full gc,但是清理之后map還是被引用,清理不掉,頻繁full耗費cpu和內(nèi)存(可以采用Ehcache對象淘汰結(jié)構(gòu)、LRU)文章來源:http://www.zghlxwxcb.cn/news/detail-533817.html
7、內(nèi)存溢出(會出現(xiàn)OOM)
新建的對象過大,導(dǎo)致內(nèi)存被占滿了文章來源地址http://www.zghlxwxcb.cn/news/detail-533817.html
到了這里,關(guān)于jvm對象創(chuàng)建和內(nèi)存分配優(yōu)化的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!