JVM
JVM簡介
虛擬機:通過軟件模擬的具有完整硬件功能的、運行在一個完全隔離環境中的完整的計算機系統。
JVM:通過軟件模擬Java字節碼的指令集,JVM中只保留了PC寄存器
內存區域與內存溢出異常
1.運行時數據區域
線程私有區域
程序計數器、Java虛擬機棧、本地方法棧
線程私有:生命週期與具體線程相同,隨着線程的創建而創建,隨着線程銷燬,對應空間回收
線程共享區域
java堆、方法區、運行時常量池
1.1程序計數器
記錄程序當前執行地址
如果當前線程執行一個java方法,程序計數器記錄正在執行的虛擬機字節碼指令的地址;
如果當前線程執行一個native方法,計數器值爲空;
1.2Java虛擬機棧
java方法執行的內存模型,例如:方法執行->入棧(局部變量表相當於虛擬機棧的一部分)
每一個方法執行時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,每一個方法從調用到執行完成的過程中,對應一個棧幀在虛擬機棧中出棧入棧的過程;
局部變量表:8大基本數據類型、對象引用;局部變量表所需的內存在編譯期間完成
產生兩大異常:
(1)線程請求的棧深度大於虛擬機所允許的深度(-Xss設置棧容量),拋出StackOverFlowError異常
(2)虛擬機在動態擴展時無法申請到足夠的內存,拋出OutOfMemoryError(OOM)異常
1.3本地方法棧:
爲虛擬機使用的native方法服務;
在HotSpot虛擬機中,本地方法棧和虛擬機棧是同一塊內存區域
1.4Java堆
存放內容:對象實例
所有對象的實例以及數組都要在堆上分配
Java堆是垃圾回收器管理的主要區域,也可稱“GC”堆;
Java堆可以處於物理上不連續的內存空間中;
Java堆在主流的虛擬機上是可擴展的(-Xmx設置最大值,-Xms設置最小值)
產生異常:
如果在堆中沒有足夠的內存完成實例分配並且堆也無法再拓展時,拋出OOM異常
1.5方法區
存儲內容:已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯的代碼等數據
產生異常:
當方法區無法滿足內存分配需求時,拋出OOM異常
1.6運行時常量池
方法區的一部分
存儲內容:字面量、符號引用
字面量:字符串(JDK1.7後移動至堆中)、final常量、基本數據類型的值
符號引用:類和結構的完全限定名、字段的名稱和描述符、方法的名稱和描述符
2.Java堆溢出
2.1產生異常
即內存溢出異常(OOM),三個條件全部滿足,產生該異常
(1)不斷創建對象(2)保證GC Roots到對象之間有可達路徑(3)對象數量達到最大堆容量
2.2異常處理分析
(1)java堆內存溢出,異常信息提示(Java heap space)
(2)判斷是內存泄漏還是內存溢出
內存泄漏:泄露對象無法被GC
內存溢出:內存對象確實還應該活着
1.調大內存;2.檢查對象的生命週期是否過長
3.虛擬機棧和本地方法棧溢出
虛擬機棧的兩大異常
(1)線程請求的棧深度大於虛擬機所允許的深度(-Xss設置棧容量),拋出StackOverFlowError異常
(2)虛擬機在動態擴展時無法申請到足夠的內存,拋出OutOfMemoryError(OOM)異常
如果因爲多線程導致內存溢出問題,在不減少線程數的情況下,只能減少最大堆和減少棧容量的方法來換取更多線程
垃圾回收器與內存分配策略
1.判斷對象已"死"
Java堆中的所有對象實例,垃圾回收器對堆進行垃圾回收之前。首先會判斷對象是否存活
1.1引用計數法
使用分析:給對象加一個引用計數器,引用一次,計數器+1,當引用失效時,計數器-1;任何時刻計數器爲0的對象已"死"
不足:在主流的JVM中沒有選用引用計數器法來管理內存,最主要的原因是引用計數器法無法解決對象的循環引用問題
循環引用栗子:
public class Main{
public Object instance = null;
public static void testGC(){
Main testA = new Main();
Main testB = new Main();
//兩個對象互相引用
testA.instance = testB;
testB.instance = testA;
testA = null;
testB = null;
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
1.2可達性分析算法
核心思想:以"GC Roots"對象作爲起始點,從這些節點開始向下搜索,搜索走過的路徑稱爲"引用鏈",當一個對象到GC Roots沒有任何的引用鏈相連(從GC Roots到這個對象不可達)時,證明此對象是不可達的;
GC Roots對象包含:
(1)虛擬機棧(棧幀中的本地變量表)中引用的對象;
(2)方法區中類靜態屬性引用的對象;
(3)方法區中常量引用的對象;
(4)本地方法棧中(Native方法)引用的對象;
引用的擴充:
(1)強引用:new出來的,只要JVM中存在任何一個強引用,即便內存不夠用,也無法回收此對象
(2)軟引用:在內存溢出之前,把軟引用對象列入第二次回收範圍,若這次回收還沒有足夠的內存,拋出OOM異常(緩存) JDK1.2後提供了SoftReference類實現軟引用
(3)弱引用:不管當前內存是否夠用,只要gc開始,都會回收掉僅被弱引用指向的對象;
JDK1.2後提供了WeakReference類實現弱引用
(4)虛引用:與對象的存活週期無關;被虛引用指向的對象在被gc之前會發送一個系統通知;
JDK1.2後提供了PhantomReference類實現虛引用
對象的自我拯救
宣告對象的死亡經歷兩次標記
(1)對象在可達性分析後發現沒有與GC Roots相連接的引用鏈,被第一次標記並進行一次篩選;
篩選的條件:對象是否有必要執行finalize()方法
(2)當對象沒有覆蓋finalize()方法或者finalize()已被JVM調用過,則沒有必要執行,對象真正死亡;
如果一個對象在finalize()中成功拯救自己(只需要重新與引用鏈上的任何一個對象建立關聯即可),在第二次標記時就會被移出即將回收的集合
任何一個對象的finalize()方法只會被系統自動調用一次;
finalize並不是關鍵字,是Object類中的空方法,只由JVM調用一次;
2.回收方法區
收集內容:廢棄常量+無用的類
回收廢棄常量:與java堆回收對象類似,一個常量已進入常量池,但是沒有對象引用這個常量,如果此時發生GC並且有必要的化,這個常量會被清理出常量池
無用的類:(同時滿足以下三個條件)
(1)該類所有實例已被回收;
(2)加載該類的ClassLoader已被回收;
(3)該類對應的Class對象未被引用,無法通過反射訪問該類的方法;
JVM可以對無用的類進行回收,但不是必然,在大量使用反射、動態代理等場景都需要JVM具有類卸載的功能防止永久代溢出
3.垃圾回收算法
3.1標記-清除算法
算法思想:
(1)標記出所有需要回收的對象;
(2)標記後統一回收所有被標記的對象;
不 足:
(1)效率問題:標記和清除兩個過程的效率都不高;
(2)空間問題:標記清除後會產生大量不連續的碎片,空間碎片太多可能會導致在程序運行中分配對象時,無法找到足夠的連續內存而不得不觸發另一次垃圾回收;
3.2複製算法(新生代回收)
產生原因:解決標記-清除的效率問題
算法思想:將可用內存按容量劃分爲均等的兩塊,每次只使用其中的一塊,當這塊內存需要垃圾回收,將此區域存活的對象複製到另一塊,已經使用的那塊區域的內存一次清理;
優 點:每次只對整個區域的一半進行回收,內存分配不需要考慮內存碎片問題
新生代"朝生夕死",不需要按照1:1劃分內存,而是將內存劃分爲Eden空間和Survivor空間(Eden:Survivor = 8:2)(From:To = 1:1),每次使用Eden和其中的一塊Survivor(From或To),當回收時,將Eden和Survivor存活的對象一次性複製到另一塊Survivor空間,清理掉Eden區和使用的Survivor區;
當Survivor空間不夠時,需要依賴其他內存(老年代)進行分配擔保;
HotSpot默認Eden : Survivor = 8 : 2,即Eden : Survivor From : Survivor To = 8 : 1 : 1;
HotSpot複製算法流程:
(1)Eden區滿,觸發第一個Minor gc,把活着的對象拷貝到Survivor From區;
Eden區滿再次觸發Minor gc,會對Eden和From區進行垃圾回收,活着的對象拷貝至To區,Eden和From區清空;
(2)Eden區滿再次觸發Minor gc,會對Eden和To區進行垃圾回收,存活的對象拷貝至From區,Eden和To區清空;
(3)部分對象在From和To區域內複製來複制去,如此交換15次(JVM中MaxTenuringThreshold參數決定),最終還是存活,就存入老年代;
3.3標記-整理算法(老年代回收)
算法思想:
(1)標記出所有需要回收的對象;
(2)所有存活對象向一端移動,直接清理掉端邊界以外的內存;
3.4分代收集算法
複製算法(新生代回收算法)+ 標記整理算法(老年代回收算法)
一般java堆分爲新生代和老年代;
新生代中,每次垃圾回收都有大量對象死去,只有少量對象存活,它的額外空間分配擔保是老年代空間,因此使用複製算法;
老年代中,對象存活率高、沒有額外空間對它進行分配擔保,必須採用標記-清除/標記-整理算法;
Minor GC 和 Full GC的區別:
(1)Minor GC :又稱新生代GC,指的是發生在新生代的垃圾收集,因爲新生代中,java對象大多具備朝生夕死的特性,因此Minor GC(複製算法)非常頻繁,一般回收速度也比較快;
(2)Full GC:又稱老年代GC或Major GC,指的是發生在老年代的垃圾收集,出現了Major GC,經常會伴隨至少一次的Minor GC(並非絕對),Major GC的速度一般會比Minor GC慢10倍以上;
5.內存分配與回收策略
(1)對象優先在Eden分配;
(2)大對象直接進入老年代;
大對象:需要大量連續空間的Java對象;典型:長字符串/數組
目的:避免Eden區以及兩個Survivor區之間發生大量的內存複製(新生代採用複製算法來收集內存)
虛擬機提供參數:PretenureSizeThreshold參數,大於設置值的對象直接在老年代中分配
(3)長期存活的對象將進入老年代;
對象在Eden區出生,並且經過一次Minor GC後仍然存活,並且能被Survivor容納,將被移動至Survivor區,對象年齡設爲1,對象在Survivor中每經歷一次Minor GC並且存活,年齡+1,當年齡增加到一定程度(默認15),將其放入老年代中;
虛擬機提供參數:MaxTenuringThreshold參數,對象晉升到老年代的年齡閾值
(4)動態對象年齡判定;
如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於等於該年齡的對象直接進入老年代,無需到達閾值要求
(5)空間分配擔保;
在發生Minor GC之前,虛擬機會檢查老年代最大連續空間是否大於新生代所有對象的總空間
- 大於,Minor GC安全
- 小於,虛擬機查看HandlePromotionFailure設置值是否允許擔保失敗
- HandlePromotionFailure = true,繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小
- 大於,嘗試進行一次Minor GC,有風險
- 小於,進行Full GC
- HandlePromotionFailure = false,進行Full GC
- HandlePromotionFailure = true,繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小
常見JVM性能檢測與故障處理工具
1.JDK命令行工具
常用命令
命令全稱 | 全稱 | 用途 |
---|---|---|
jps | JVM Process Status Tool | 顯示指定系統內所有的HotSpot虛擬機進程 |
jstat | JVM Statistics Monitoring Tool | 用於收集HotSpot虛擬機各方面的運行數據 |
jinfo | Configuration Info for Java | 顯示虛擬機配置信息 |
jmap | Memory Map for Java | 生成虛擬機的內存轉儲快照,生成headdump文件 |
jhat | JVM Heap Dump Browser | 用於分析heapdump文件,它會建立一個HTTP/HTML服務器,讓用戶在瀏覽器上查看分析結果 |
jstack | 顯示虛擬機的線程快照 |
1.1jps-虛擬機進程狀態工具
使用頻率最高的JDK命令行工具
可以列出正在運行的虛擬機進程,並顯示虛擬機執行主類(main函數所在的類)名稱以及這次進程的本地虛擬機唯一ID
-q | 只輸出LVMID,省略主類的名稱 |
---|---|
-m | 輸出虛擬機進程啓動時傳遞給主類main()函數的參數 |
-l | 輸出主類的全名,如果進程執行的是jar,輸出jar路徑 |
-v | 輸出虛擬機進程啓動時JVM函數 |
1.2jstas-虛擬機統計信息監視工具
用於監控虛擬機各種運行狀態信息的命令行工具,可以顯示本地或遠程虛擬機中的類加載、內存、垃圾回收、JIT編譯等運行數據
JIT編譯:運行時需要代碼時,將 Microsoft 中間語言 (MSIL) 轉換爲機器碼的編譯。
1.3jinfo-Java配置信息工具
用於查看和調整虛擬機的配置參數
jinfo -flags 線程ID :查詢線程的參數
1.4jmap-Java內存映像工具
jmap的作用並不僅僅爲了獲取dump文件,它還可以查詢fifinalize執行隊列、Java堆和永久代的詳細信息,如空間使用率、當前使用的是哪種收集器等;
1.5jhat:虛擬機轉存儲快照分析工具
jhat命令搭配jmap命令使用,用於分析jmap生成的堆轉儲快照;
1.6jstack:Java堆棧跟蹤工具
jstack命令用於生成虛擬機當前時刻的線程快照;
線程快照:當前虛擬機內的每一條線程正在執行的方法堆棧的集合;
生成線程快照的作用是:可用於定位線程出現長時間停頓的原因,如線程間死鎖,死循環,請求外部資源導致的長時間等待等問題;
Java內存模型
1.主內存與工作內存
Java內存模型的主要目標:定義程序中各個變量的訪問規則;即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節
變量:實例字段、靜態字段、構成數組對象的元素,不包括局部變量和方法參數(因爲者兩者是線程私有的)
Java內存模型的規定:
(1)所有的變量都存儲在主內存中;
(2)每條線程有自己的工作內存,線程的工作內存中保存了該線程使用的變量的主內存副本拷貝;
(3)線程對變量的所有操作都必須在工作內存中進行,不能直接讀寫主內存中的變量;
(4)不同的線程之間無法直接訪問對方工作內存中的變量;
(5)線程間變量值的傳遞均需要通過主內存來完成;
併發程序三大問題:只有當以下三個特性同時滿足的程序纔是線程安全的
可見性:一個線程修改了共享變量的值,其他線程能夠立即得知此修改
原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
有序性:按照代碼順序依此執行
原子性經典問題
銀行賬戶轉賬問題:
比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。
試想一下,如果這2個操作不具備原子性,會造成什麼樣的後果。
假如從賬戶A減去1000元之後,操作突然中止。然後又從B取出了500元,取出500元之後,再執行往賬戶B加上1000元 的操作。這樣就會導致賬戶A雖然減去了1000元,但是賬戶B沒有收到這個轉過來的1000元。
可見性問題
//線程1執行
int i = 0;
i = 10;
//線程2執行
int j = i;
假若執行線程1的是CPU1,執行線程2的是CPU2。由上面的分析可知,當線程1執行 i =10這句時,會先把i的初始值加載到CPU1的高速緩存中,然後賦值爲10,那麼在CPU1的高速緩存當中i的值變爲10了,卻沒有立即寫入到主存當中。
此時線程2執行 j = i,它會先去主存讀取i的值並加載到CPU2的緩存當中,注意此時內存當中i的值還是0,那麼就會使得j的值爲0,而不是10.
2.volatile型變量的特殊規則
修飾變量
變量定義爲volatile後,具備的特性:
(1)保證此變量對所有線程的可見性;
可見性:當一個線程修改了這個變量的值,新值對於其他線程來說是立即得知的; 保證多線程訪問變量(volatile修飾),線程修改變量,其他線程立即可見 volatile變量在各個線程中是一致的,但是volatile變量的運算在併發下一樣是不安全的;原因:java中的運算非原子操作
(2)禁止指令重排;
a = 1;
volatile flag = 3;//相當於一個屏障
b = 2;
禁止指令重排的含義:
- 當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作肯定全部進行完畢,後面的的操作肯定未執行,前面操作的執行結果對後面的操作可見;
- 在進行指令優化時,不能將對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行;
栗子:
int x = 2;
int y = 0;
volatile flag = true;
x = 4;
y = 20;
分析:在進行指令重排的過程中,語句3不會放到語句1、2的前面,語句3也不會放到語句4、5的後面;
但是語句1和語句2的順序,語句4和語句5的順序是不保證順序執行的;
volatile關鍵字能保證,執行到語句3時,語句1、2肯定執行完畢;語句4、5肯定未執行;語句1、2執行的結果對語句3、4、5是可見的;
單例模式的Double Check
雙重檢驗鎖模式:會有兩次檢查instance == null,一次在同步塊外,一次在同步塊內
原因:因爲可能會有多個線程一起進入同步塊外的if,如果在同步塊內不進行二次檢驗會產生多個實例
public static Singleton getSingleton(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
instance = new Singleton();存在指令重排的操作;
該語句在JVM中的執行步驟:
(1)給instance分配內存;
(2)調用Singleton的構造函數來初始化成員變量instance;
(3)將instance對象指定分配的內存空間;
由於指令重排的情況:執行順序可能是1->2->3,1->3->2;
如果是第二種情況,則在3執行完畢,2未執行之前,被線程2搶佔,此時instance非null,但卻沒有被初始化,報錯;
解決:將instance聲明爲volatile即可;
class Singleton{
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getSingleton(){
if(instance == null){//檢查對象是否初始化
synchronized(Singleton.class){
if(instance == null){//確保多線程情況下對象只有一個
instance = new Singleton();
}
}
}
return instance;
}
}
第一層if判斷–判斷當前對象是否爲空
第二層if判斷–當線程1進去實例化對象後釋放鎖,線程2在synchronized語句上一行觀望,拿到鎖,進入同步代碼塊,此時如果沒有第二層if,它會選擇再次實例化對象,違反了單例模式只創建一個對象的目的。
syschronized加鎖–保證只有一個線程進入同步代碼塊
深淺拷貝
1.Cloneable接口:
Cloneable:CloneNotSupportedException
只有子類實現了Cloneable接口後纔可以使用Object類提供的clone方法。
2.Object類的clone()方法:
protected native Object clone() throws CloneNotSupportedException;
3.要想讓對象具有拷貝的功能
(1)就必須要實現Cloneable接口(只有接口名稱,稱爲標識接口),表示此類允許被克隆;
(2)並且在類中自定義clone()方法,在自定義方法中調用Object類提供的有繼承權限的clone()方法;
1.淺拷貝
對象值拷貝
對於淺拷貝而言,拷貝出來的對象仍然保留原對象的所有引用。
拷貝完成後,基本數據類型、String引用類型不保留,並不是所有的引用類型都保留;7
問題:牽一髮而動全身
只要任意一個拷貝對象(或原對象)中的引用發生改變,所有對象均會受到影響
class Teacher{
private String name;
private String direction;
public Teacher(String name, String direction) {
this.name = name;
this.direction = direction;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDirection() {
return direction;
}
public void setDirection(String direction) {
this.direction = direction;
}
@Override
public String toString() {
return "Teacher{" +
"name='" + name + '\'' +
", direction='" + direction + '\'' +
'}';
}
}
class Student implements Cloneable{
private String name;
private int age;
private Teacher teacher;
public Student(String name, int age, Teacher teacher) {
this.name = name;
this.age = age;
this.teacher = teacher;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Student student = null;
student = (Student) super.clone();
return student;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Teacher getTeacher() {
return teacher;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", teacher=" + teacher +
'}';
}
}
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
Teacher teacher = new Teacher("張老師","JavaTeacher");
Student student = new Student("張三",18,teacher);
Student cloneStudent = (Student) student.clone();
System.out.println("teacher:"+teacher.toString());
System.out.println("student:"+student.toString());
System.out.println("cloneStudent:"+cloneStudent.toString());
System.out.println("------------修改cloneStudent的值後-----------------");
cloneStudent.setAge(100);
cloneStudent.setName("李四");
cloneStudent.getTeacher().setName("錢老師");
cloneStudent.getTeacher().setDirection("C++Teacher");
System.out.println("teacher:"+teacher.toString());
System.out.println("student:"+student.toString());
System.out.println("cloneStudent:"+cloneStudent.toString());
}
}
輸出:
teacher:Teacher{name='張老師', direction='JavaTeacher'}
student:Student{name='張三', age=18, teacher=Teacher{name='張老師', direction='JavaTeacher'}}
cloneStudent:Student{name='張三', age=18, teacher=Teacher{name='張老師', direction='JavaTeacher'}}
------------修改cloneStudent的值後-----------------
teacher:Teacher{name='錢老師', direction='C++Teacher'}
student:Student{name='張三', age=18, teacher=Teacher{name='錢老師', direction='C++Teacher'}}
cloneStudent:Student{name='李四', age=100, teacher=Teacher{name='錢老師', direction='C++Teacher'}}
2.深拷貝
1.特點:修改任意一個對象,不會對其他對象產生影響。
2.實現方式:
(1)包含的其他類繼續實現Cloneable接口,並且調用clone方法(遞歸實現Clone)
(2)使用序列化
使用序列化進行深拷貝時,無須再實現Cloneable接口,只需要實現Serializable接口即可。
(1)通過內存進行序列化的讀取和寫入
(2)通過文件進行序列化的讀取和寫入
import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;
import java.io.*;
class Teacher implements Serializable{
private String name;
private String job;
public Teacher(String name, String job) {
this.name = name;
this.job = job;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
@Override
public String toString() {
return "Teacher{" +
"name='" + name + '\'' +
", job='" + job + '\'' +
'}';
}
}
class Student implements Serializable{
private String name;
private int age;
private Teacher teacher;
public Student(String name, int age, Teacher teacher) {
this.name = name;
this.age = age;
this.teacher = teacher;
}
public Student cloneObject() throws Exception{
ByteOutputStream bos = new ByteOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.getBytes());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Student) ois.readObject();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", teacher=" + teacher +
'}';
}
}
public class deapClone {
public static void main(String[] args) throws Exception {
Teacher teacher = new Teacher("張老師","JavaTeacher");
Student student = new Student("張三",18,teacher);
Student cloneStudent = (Student) student.cloneObject();
System.out.println("teacher:"+teacher.toString());
System.out.println("student:"+student.toString());
System.out.println("cloneStudent:"+cloneStudent.toString());
System.out.println("------------------修改cloneStudent的值後--------------------");
cloneStudent.setAge(100);
cloneStudent.setName("李四");
cloneStudent.getTeacher().setName("錢老師");
cloneStudent.getTeacher().setJob("C++Teacher");
System.out.println("teacher:"+teacher.toString());
System.out.println("student:"+student.toString());
System.out.println("cloneStudent:"+cloneStudent.toString());
}
}
輸出:
teacher:Teacher{name='張老師', job='JavaTeacher'}
student:Student{name='張三', age=18, teacher=Teacher{name='張老師', job='JavaTeacher'}}
cloneStudent:Student{name='張三', age=18, teacher=Teacher{name='張老師', job='JavaTeacher'}}
------------------修改cloneStudent的值後--------------------
teacher:Teacher{name='張老師', job='JavaTeacher'}
student:Student{name='張三', age=18, teacher=Teacher{name='張老師', job='JavaTeacher'}}
cloneStudent:Student{name='李四', age=100, teacher=Teacher{name='錢老師', job='C++Teacher'}}
序列化:將對象變爲二進制流
1.概念:將內存中的保存的對象變成二進制流進行輸出或者保存在文本中。
2.要想讓類支持序列化,必須實現Serializable接口(爲表示接口,沒有接口體)。
只有實現了Serializable接口的類才具備對象序列化的功能。
3.具體實現序列化與反序列化,需要使用io包中提供的兩個處理類:
3.1序列化類ObjectOutputStream:
writeObject(Object obj):將obj變爲二進制流輸出到目標終端
public ObjectOutputStream(OutputStream out):通過傳入參數選擇目標終端(傳入文件參數:FileOutputStream對象)
3.2反序列化類ObjectInputStream:
public final Object readObject():將二進制流反序列化爲對象
public ObjectInputStream(InputStream in):選擇反序列化的目標終端(也就是這個二進制流從哪來的)
4.transient關鍵字:
若希望類中的若干屬性不被序列化保存(反序列化時該屬性爲null),可以在屬性前添加transient關鍵字。