一、java?面向過程、面向對象的定義區別?面向對象特徵及理解。
java:
一種跨平臺性的面嚮對象語言,它的跨平臺性表現在JVM實現了一次編譯、到處運行。
一次編譯:
將java代碼通過編譯器轉換成字節碼文件(.class文件)
到處運行:
相同的字節碼在不同的操作系統內部的JVM解析出來的結果是一致的。
面向過程:
核心是過程兩個字,過程即解決問題的步驟,從上往下步步求精,最重要的是模塊化的思想。
面向對象:
面向對象離不開面向過程。例如造車,面向對象則是研究車這個對象是由什麼組成的,需要的哪些小零件對象,強調的是對象組成,而每一個小零件的製作過程對應的就是一個個過程要經過一道道工序才能做出來,強調的是面向過程。
面向對象的特徵:
封裝:
把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
{高內聚就是類的內部數據操作細節自己完成,不允許外部干涉;低耦合是僅暴露少量的方法給外部使用,儘量方便外部調用。}
實現-通過訪問修飾符來控制訪問的權限,java有4種訪問修飾符,public、private、protected、default
修飾符 | 同一個類 | 同一個包 | 子類 | 所有類 |
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
1.public表示可以被該項目的所有包中的所有類訪問
2.protected表示可以被同一個包的類以及其他包中的子類訪問
3.default表示沒有修飾符修飾,只有同一個包的類能訪問
4.private 表示私有,只有自己類能訪問
class A{
public String name; //所有類、包均可訪問該屬性,通過類名調用
private int grade; //必須創建該類,通過類自己調用,其他類不可訪問,也不可調用
protected double height; //所有子類,共同的包下均可調用,其他不可訪問
default String place; //默認情況下調用
}
繼承:
可以讓某個類型的對象獲得另一個類型的對象的屬性的方法。
它可以使用現有類的所有功能,並在無需重新編寫原來的類的情況下對這些功能進行擴展。 通過繼承創建的新類稱爲“子類”或“派生類”,被繼承的類稱爲“基類”、“父類”或“超類”。
子類繼承父類,可以得到父類的全部屬性和方法 (除了父類的構造方法),但不見得可以直接訪問(比如,父類私有的屬性和方法)。
繼承概念的實現方式有二類:實現繼承與接口繼承。實現繼承是指直接使用父類的屬性和方法而無需額外編碼的能力;接口繼承是指僅使用屬性和方法的名稱、但是子類必須提供實現的能力;
講到繼承,不得不提一下抽象類與接口,面試中常被問到,抽象類與接口的區別,這裏就提前總結一下吧 區別
參數 | 抽象類 | 接口 |
默認的方法實現 | 有默認方法的實現,抽象方法可有可無 | java8之後接口有了默認方法的實現,java8之前沒有 |
實現 | extend,可繼承實體類(必須有明確的構造方法),只能單繼承 | implement,多重繼承(一個類可實現多個接口) |
構造器 | 可有構造方法 | 不可有構造方法 |
與正常java類的區別 | 除了不能實例化,其他差不多一樣 | 完全不同的類型 |
訪問修飾符 | public、protected、default | 默認public,不可以用其他的 |
main方法 | 可有,可運行 | 不可有 |
多繼承 | 繼承一個類實現多個接口 | 可以繼承多個接口 |
速度 | 比接口快 | 尋找在類中實現的方法 |
添加新方法 | 提供默認的實現 | 必須改變實現接口的類 |
// 抽象類與實體類
package packa;
import packb.C;
import packb.D;
public class A implements C,D{
private String aa;
public A() { //空構造
}
public A(String aa) {
this.aa = aa;
}
public void aaa() {
System.out.println("實體類的方法。。。。");
}
@Override
public void dd() {
System.out.println("實體類實現D接口的方法");
}
@Override
public void cc() {
System.out.println("實體類實現C接口的方法");
}
}
package packa;
import packb.C;
import packb.D;
public abstract class B extends A implements C,D{
private String bb; //抽象類的成員變量
public B(String aa) { //構造方法
super(aa);
}
public abstract void bbb(); //抽象方法
public void bbbb() { //默認方法實現
System.out.println("抽象類B的非抽象方法");
}
public void cc() {
System.out.println("實現的C接口");
}
public void dd() {
System.out.println("實現的D接口");
}
}
package packb;
public interface C {
public static final double PI = 3.14;
public void cc();
}
package packb;
public interface D extends C{
public static final double PI = 3.22;
// @Override
// public void cc();
public void dd();
}
package packb;
public class E implements C,D{ //多重實現接口
@Override
public void dd() {
System.out.println("實現的C接口");
}
@Override
public void cc() {
System.out.println("實現的D接口");
}
}
多態:
一個引用變量到底會指向哪個類的實例對象,該引用變量發出的方法調用到底是哪個類中實現的方法,必須在由程序運行期間才能決定。
實現多態的方式:繼承和接口
1. 多態是方法的多態,不是屬性(變量)的多態(多態與屬性無關)。
2. 多態的存在要有3個必要條件:繼承,方法重寫,父類引用指向子類對象。
3. 父類引用指向子類對象後,用該父類引用調用子類重寫的方法,此時多態就出現了。
總結:所謂封裝,就是通過定義類並且給類的屬性和方法加上訪問控制來抽象事物的本質特性。所謂繼承,就是代碼重用,而多態,就是一個行爲(方法)在不同對象上的多種表現形式。
package packs;
import packa.A;
import packa.B;
import packb.C;
public class theme {
public static void main(String[] args) {
C c = new A(); // 多態的提現,父類引用指向子類對象
c.cc();
}
}
說完面向對象的三大特徵,有必要提一下最常說的重載與重寫;
重載:
發生在同一個類中,方法名必須相同,參數類型不同、個數不同、順序不同,方法返回值和訪問修飾符可以不同。
重寫:
發生在子類中,是子類對父類的允許訪問的方法的實現過程進行重新編寫,方法名、參數列表必須相同,返回值範圍小於等於父類,拋出的異常範圍小於等於父類,訪問修飾符範圍大於等於父類。另外,私有方法(private修飾)不可被重寫。一句話總結,方法提供的行爲改變,方法的外貌沒有改變
package packa;
public class A{ //重載
private String aa;
private int aaa;
private double aaaa;
private char a;
public void aq(String as) {
}
public int aq(int ass,double asp) {
return aaa;
}
public String aq(int psa,double spa,char ac) {
return aa;
}
public double aq(String as,double asp,int psa,char spp) {
return aaaa;
}
}
package packa;
public class A{ //重寫
public void aq() {
System.out.println("父類方法");
}
}
package packa;
public class Aa extends A {
@Override
public void aq() {
System.out.println("子類方法");
}
}
二、有關JDK、JRE、JVM
我們剛開始看到這三個東西,跟入門小白一樣,恐怕只知道JDK包含JRE,JRE也包含JVM, 之前就曾有面試官問過這樣的問題,問之前一臉懵逼,問之後無所不ji。。。開玩笑而已,入行之初,接觸的就是這個東西。
看到這張JDK體系結構圖,估計很多人是懵逼的,一開始看這種圖,都是暈 的,多看幾遍,說不定可以看出點什麼道道。
JDK :Java SE Development Kit,java開發工具包,它提供了編譯、運行Java程序所需的各種工具和資源,包括Java編譯器、Java運行時環境,以及常用的Java基礎的類庫等。
JRE: Java Runtime Environment,顧名思義,java運行時環境,主要包含jvm 的標準實現和 Java 的一些基本類庫。它相對於 jvm 來說,多出來的是一部分的 Java 類庫。
JVM:Java Virtual Machine,我們常提到的java虛擬機,它是Java程序跨平臺的關鍵部分,當使用Java編譯器編譯Java程序時,生成的是與平臺無關的字節碼,這些字節碼只面向JVM。不同平臺的JVM都是不同的,但它們都提供了相同的接口。
三、JVM淺學
程序運行時,虛擬機類加載子系統會load程序的.class文件到虛擬機內存裏面,並由執行引擎去執行程序,準確地說,jvm的組成分爲3大塊,類裝載子系統、運行時數據區、執行引擎。我們所說的運行時數據區只是程序運行時,JVM工作的一方面。
類的生命週期:加載、連接、初始化。
加載:
讀取.class文件,從磁盤讀進內存區,通過全限定名獲取這個類的二進制流。
連接:
<1>驗證:包含源文件格式的驗證。驗證class文件是不是符合JVM獨特的可執行文件的格式要求。
<2>準備:各類的靜態變量分配內存,並賦予虛擬機默認的一個初始值值。
<3>解析:類裝載器裝載一個類,和它其它引用的類。
初始化:
爲類的靜態變量賦予真正的初始值,並將執行靜態代碼塊。
如何確定一個類是唯一的?
通過全限定名來加載一個類,確定類的唯一。。。通過類加載器也可以。
類加載器包含啓動類加載器、擴展類加載器、系統類加載器(應用類加載器)。
類加載機制
全盤負責委託機制:
當一個classloader加載一個類的時候,除非顯示的使用另一個loader,該類所依賴的和引用的類也由這個Classloder載入。
雙親委派機制:
先委託父類加載器尋找目標類,找不到的情況下下載自己的路徑中查找並載入目標類。【加載一個類時,首先會先檢查是否已經被加載過了,如沒有加載則調用父加載器的loadClass()方法,若父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父類加載失敗,則拋出ClassNotFoundException異常,在調用自己的findClass()方法進行加載。】
一層一層向上委託,當一個類收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父類去完成,每一個層次類加載都是如此,因此所有的加載請求都應該傳送到啓動類加載器中,只有當父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑裏找不到這個所需要加載的類),子類加載器纔會嘗試自己去加載。
當一個HelloWorld.class文件被加載時.不考慮自定義類加載器,首先會在AppClassLoader中檢查是否已加載過,如果有就無需再加載.如果沒有,那麼會拿到父加載器,然後調用父加載器的loadClass().父類中同樣會先檢查自己是否已經加載過,如果沒有再往上.直到到達BootstrapClassLoader之前,都是沒有哪個加載器自己選擇加載的.如果父加載器無法加載,會下沉到子加載器去加載,一直到最底層,如果沒有任何加載器能加載,就會拋出ClassNotFoundException.
優點
1:安全,可避免用戶自己編寫的類動態替換Java的核心類,如java.lang.String
2:避免全限定命名的類重複加載(使用了findLoadClass()判斷當前類是否已加載)
這種設計有個好處是,如果有人想替換系統級別的類:String.在雙親委派機制下這些系統類已經被Bootstrap classLoader加載過了,不會再去加載,從一定程度上防止了危險代碼的植入.
打破雙親委派機制則不僅要繼承ClassLoader類,還要重寫loadClass和findClass方法 。
關於Tomcat的類加載機制:
Tomcat的類加載機制並不遵循父委派模型。下圖是Tomcat類加載器的結構:
1.CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,從而實現了公有類庫的共用。
2.CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對方相互隔離。
3.WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。
4.JasperLoader的加載範圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是爲了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並通過再建立一個新的Jsp類加載器來實現JSP文件的HotSwap功能。
Tomcat違背了父委派模型
tomcat違背了父委派模型,因爲雙親代理機制也就是父委派模型要求除了頂層啓動類加載器之外,其餘的類加載都應該由其父類加載器加載。而Tomcat不是這樣實現的,Tomcat爲了實現隔離性,沒有遵循這個約定,每個WebappCLoader加載自己目錄下的class文件,不會傳遞給父類加載器。
運行時數據區(內存結構)
程序計數器:很小的一段內存空間,記錄將要運行的下一行代碼的序號,確保線程正常執行。
虛擬機棧:方法對應棧幀,
一個棧幀有局部變量表(變量、引用類型)、
操作數棧(臨時存放操作的數值放到棧頂,即變量的初值,然後將這個值存儲到局部變量表的一個槽裏面,直到使用纔開始加載這些數據;;;加載時,仍然將此數據加載到操作數棧)、
動態鏈接:在程序運行期間,由符號引用【對象存儲的一個位置】轉換爲直接引用【將該對象的引用指向堆內存裏該對象的實例】。
返回地址【確保程序正常運行,一個是程序計數器,另一個是返回地址(可以看做是一個內存地址的索引),誰調用返回給誰】
線程私有的,與線程在同一時間創建。管理JAVA方法執行的內存模型。每個方法執行時都會創建一個楨棧來存儲方法的的變量表、操作數棧、動態鏈接方法、返回值、返回地址等信息。棧的大小決定了方法調用的可達深度(遞歸多少層次,或嵌套調用多少層其他方法,-Xss參數可以設置虛擬機棧大小)。棧的大小可以是固定的,或者是動態擴展的。如果請求的棧深度大於最大可用深度,則拋出stackOverflowError;如果棧是可動態擴展的,但沒有內存空間支持擴展,則拋出OutofMemoryError。
本地方法棧:jvm執行native方法服務
堆:分爲新生代(1/3)、老年代(2/3).一個新生對象,如果比較小,放在eden區,如果對象較大,直接放在老年代【分配擔保機制】。新生代區域如果內存滿了,該如何?
yGC,發生在Eden區。可達性分析法,判斷一個對象爲垃圾之後,回收這些垃圾對象,還剩下一些對象,這些對象還在用,轉移到Survivor區域。Survivor區域分爲兩塊,一塊from區,一塊to區。若Eden區存活的對象都往from區存放,存滿之後,發生一次ygc,from存活的會放到to區域,to區域會變成from區域,原來的from區域會被清空,自動變成to區域,這樣from與to進行了一次區域交換,這類存活的對象年齡+1,當加到15的時候,會被轉入老年代。
Full GC,回收所有的區域,不只是老年代。
根據不同對象的不同生命週期,來進行不同的垃圾回收
方法區:存在於本地內存(非堆內存)
儲存的是靜態變量+常量+類信息(構造方法)+運行時常量池,class模板文件。規範的叫法。對規範的實現的方法區,持久帶/永久帶(java1.8之前),元空間
堆裏面的實例對象根據方法區裏面的class對象創建出來的,虛擬機棧引用的對象是根據堆裏面的實例對象來的。
GC算法和收集器:
如何判斷對象可以被回收?
引用計數法:
局部變量表裏面的一個引用,被另外一個引用類型指向堆裏面的實例,它的引用計數器就+1,當它計數器爲0時,證明可以被回收,會將它的標記狀態由0改爲1,證明它可以被回收。缺點是無法解決循環引用的問題。
可達性分析法:
通過一系列被稱爲“GC Roots”的對象作爲起點,從這些節點向下搜索,節點走過的路徑成爲引用鏈,當一個對象到“GC Roots”沒有任何引用鏈,證明它是沒用的。
解決循環引用,是通過固定一些根節點,常見的例如【虛擬機棧的局部變量表、方法區的類靜態屬性引用的對象、方法區常量引用的對象、本地方法引用的對象】
如何判斷一個常量是廢棄常量?
沒有任何一個對象是指向這個引用常量的,這時候它就是廢棄常量,如果此時內存不夠,它會被清理出常量池,如果內存足夠,則 不需要清理。
如何判斷一個類是無用的類?
1、該類所有實例都被回收,沒任何引用。
2、加載這個類的classloader已經被回收。
3、該類對應的對象沒在任何地方被引用,也沒有任何通過反射區訪問這個類的。
垃圾回收算法:
標記-清除算法:兩個階段,標記和清除,首先標記出所有需要回收的對象,標記完成後統一回收被標記的對象。
不足之處:
效率問題:標記和清除兩個過程效率都不高。
空間問題:標記清除後產生大量不連續的碎片。
複製算法:把內存一分爲二,稱之爲A區域,B區域,其中A清空,B存儲,B標記之後清除,存活的轉移到A區域,並整理,A剩下的區域就是連續的。
最好應用在Survivor區,剛好一分爲二,from,to
標記-整理算法:先標記出需要回收的對象,然後清除掉,標記完成後倖存的對象經過整理,整理後的空間又開始連續,又可以存儲大的對象。
分代收集算法:不同的區域使用不同的垃圾收集器,在一個垃圾收集器又可用不同的垃圾回收算法。
垃圾收集器:
Serial(串行)收集器:新生代採用複製算法,老年代採用標記-整理算法。
應用程序線程,進行一次GC,之後將應用線程停掉,之前都是單線程去執行一個程序,沒太多的線程資源,此時GC垃圾回收線程就專門執行GC,應用程序線程不能跑停掉了,會產生STW(Stop the world,無法解決的問題,停頓時間,即垃圾收集器做垃圾回收中斷應用執行的時間,JVM調優指標之一),【假如你寫了一個web端,此時JVM執行GC,web端卡個一兩秒,用戶體驗特別不好】,只適用於程序簡單,對併發要求也不高,可以考慮它,執行效率是比較高的。
ParNew收集器:Serial收集器的多線程版本(不是真正意義上的多線程,是相對於Serial而言多了幾條線程)。新生代採用複製算法,老年代採用標記-整理算法。
Parallel Scavenge收集器(JDK1.8):並行收集器。
{並行:多條垃圾收集線程並行工作,此時用戶線程仍然處於等待狀態。}
{併發:用戶線程跟垃圾收集線程同時執行(可能非並行,可能會交替執行),用戶程序在繼續運行,垃圾收集器運行在另一個CPU上。}
Parallel Scavenge收集器關注點是吞吐量[垃圾收集時間和總時間的佔比],1/(n+1),吞吐量爲1-1/(n+1)。
CMS(Concurrent Mark Sweep)收集器:以獲取最短回收停頓時間爲目標的收集器。
應用程序線程運行期間產生GC,先進行初始標記不回收,等再次發生GC的時候再來進行下一步,進入到併發標記階段,同時開啓用戶線程與GC線程,GC線程會對運行中產生的浮動垃圾【用戶線程在運行的過程中還會產生一些垃圾】進行標記,標記之後進入下一個階段。重新標記,此時將所有的線程都停掉進行專門標記,將所有產生浮動垃圾或者沒注意到的垃圾都進行重新標記,結束之後進行到清理階段,此時開始開啓用戶線程,標記階段線程跑得比較快,找內存裏面沒用的對象是找的非常快的,因爲只需要找標記狀態,掃一遍,但不進行回收,一旦進行回收,它會變得慢起來,把用戶線程啓動,此時,用戶線程與GC同時運行,對用戶來說,此時STW是比較短暫的。最後一個步驟則是併發清除,用戶線程開啓,GC線程對標記的區域進行清除。一般來講新生代的stw是比老年代的stw短暫很多。應用在老年代
優點:需要很多次回收,時間停頓短,清理慢,把多次停頓做了一個分解。併發收集
缺點:
1、對CPU資源敏感
2、沒有辦法處理浮動垃圾
3,使用標記清除算法,導致收集結束時產生大量空間碎片。
接下來介紹最熱的G1回收器
G1回收器{爲高併發誕生的}
堆區域分代,但G1不分代,分塊,把內存分成一塊一塊的,Region區域,隨機把有一些區域設爲Eden,如果你的對象是比較小的,那找一塊小的區域放進去即可;如果你的對象比較大,存活時間比較長,找一個比較大點的內存空間放進去,這個時候它就會變成Old區域。
G1收集器的特點:
並行與併發:充分利用CPU、多核環境下硬件優勢,發揮每一個CPU核心的作用,來縮短STW停頓時間。
分代手機:從整體來看基於標記-整理算法,從局部來看是基於複製算法。
可預測停頓:對我們的JVM設置一個參數,比如說G1過程的延時,可以設置爲10ms,即每一次垃圾收集的過程都會在10ms內完成,但是它的次數會增多。
G1收集器的過程:(前面三個步驟類似於CMS)
初始標記
併發標記
最終標記
篩選回收:在Region區域做一個優先級的設置,比如說這個區域某對象佔比幾百M,其他對象佔比幾百K,這個時候該對象會被標記爲可回收對象,它就會被優先回收掉,因爲回收它的價值是最大的。篩選回收就是爲了實現可預測的停頓。
假如在10ms回收我們的內存,本來時間是1s回收,現在就有很多對象回收不了,這個時候會進行垃圾回收篩選,將優先列表裏面最優先級別的Region區域回收掉,這也是爲什麼G1稱之爲Garbage-First了。
怎麼選擇垃圾收集器?
1.優先調整堆的大小讓服務器自己選擇
2.如果服務器單核,沒有停頓時間的要求,串行或者JVM自選
3.如果允許停頓時間超過1s,並行或者JVM自選
4.如果內存小於100M,選串行收集器。
5.如果響應時間最重要,不能超過1s,選併發收集器