Java全链路复习面经-基础篇(2.5万字全文)
序言
主要分為兩篇,一篇基礎篇,涵蓋Java基礎,數據庫,JVM,計算機網絡等知識
另一篇為框架篇,主要為流行框架,如Spring、SpringMVC、Mybatis、SpringBoot、SpringCloud、Redis、Linux等知識
文章目錄
- 序言
- 基礎篇
- 談談對面向對象思想的理解
- JDK,JRE,JVM有什么區別?
- Java的基本數據類型有哪些?
- ==和equals的區別 | &和&&的區別
- final的作用
- String,StringBuffer,StringBuilder區別
- String s = "java"與String s = new String("java")
- 接口和抽象類的區別
- 算法題-求N的階乘
- 什么是向上轉型?什么是向下轉型?
- Int和Integer的區別(重點)
- 方法的重寫和重載的區別
- List和Set的區別
- 談談ArrayList和LinkedList的區別
- 如何在雙向鏈表A和B之間插入C
- Object類中的方法介紹
- equals()與 hashcode() 的區別
- 除了NEW還有什么創建類的方式
- Exception 和 Error 的區別
- 反射原理及使用場景
- Java會發生內存泄漏問題嗎?有哪些場景會發生內存泄漏
- ArrayList的擴容機制
- ConcurrentHashMap 的 put 方法過程
- HashMap的擴容機制
- 為什么HashMap長度都是2的次冪
- HashMap為什么使用紅黑樹而不使用別的樹?
- 為什么鏈表長度大于閾值 8 時才將鏈表轉為紅黑樹?
- CopyOnWriteArrayList
- Servlet和Filter的區別
- 悲觀鎖和樂觀鎖
- CAS
- Java對象頭存放什么內容
- Volatile 關鍵字以及底層實現
- 線程池,線程池的作用
- AQS
- 談談HashSet的存儲原理
- 談談LinkedHashMap和HashMap的區別(重點)
- 談談ConcurrentHashMap,HashMap,Hashtable的區別
- ArrayList vs Vector vs CopyOnWriteArrayList
- 開發一個自己的棧,你會怎么寫?
- 補充:集合知識小結
- 談談IO流的分類及選擇
- serialVersionUID的作用是什么
- 請描述下Java的異常體系
- 羅列常見的5個運行時異常和非運行時異常
- throw跟throws的區別
- 一道關于try catch finally返回值的問題
- 創建線程的方式
- 一個普通main方法的執行,是單線程模式還是多線程模式?為什么
- 請描述線程的生命周期
- 談談你對ThreadLocal的理解
- 談談AJax的工作原理
- 談談Servlet的生命周期
- 描述Session跟Cookie的區別(重要)
- 補 Get請求與Post請求的區別
- 轉發和重定向的區別
- Iterator和ListIterator的區別?
- 并發和并行的區別
- 什么是序列化?
- 說說synchronized底層原理
- synchronized和volatile的區別
- synchronized和lock的區別
- 什么是死鎖?如何防止死鎖?
- 什么是反射?可以解決什么問題?
- 什么是悲觀鎖,什么是樂觀鎖?
- 數據庫說明
- 談談數據庫設計的三大范式及反范式
- 左連接,右連接,內連接,如何編寫SQL,他們的區別是什么?
- 如何解決SQL注入?
- JDBC如何實現對事務的控制及事務邊界
- 談談事務的特點
- 談談事務的隔離級別
- InnoDB是如何支持事務的 ?
- 介紹一下MVCC
- 索引如何避免全表掃描(索引失效)
- 最左前綴
- 查詢很慢怎么解決 ?
- 計算機網絡
- 說說TCP和UDP的區別
- TCP/IP協議基礎
- 計算機網絡層次
- SSL概述
- HTTP 和 HTTPS 的區別
- 談談什么是TCP的三次握手,可不可以只握手兩次?
- 談談什么是TCP的四次揮手?
- 瀏覽器輸入 URL 并回車的過程以及相關協議,DNS 查詢過程
- JVM虛擬機
- JVM內存區域是怎么劃分的?
- 說說JVM的運行時內存
- 談談垃圾回收算法
- 四種引用類型
- 垃圾回收器
- Full GC 觸發條件
- 對象創建過程
- 對象已經死亡
- 類的加載過程
- 談談類加載過程的雙親委托機制?
- JVM的內存模型
- JVM性能調優常用命令
基礎篇
談談對面向對象思想的理解
面向對象是一種編程思想。面向對象程序設計的核心思想是以對象為核心。除了面向對象之外還有面向過程,二者是兩種不同的開發思想。
當我們需要完成生成隨機數這一個功能時,如果是以面向過程的思想進行開發,則更加專注于設計的這個實現的算法;但是以面向對象的特性來完成這個功能時,我們更強調的是對象,通過找一個能夠生成隨機數功能的對象來幫我們完成(如Random),作為開發者并不需要關注這個代碼是怎么實現的,找到合適的對象,然后調用對象的方法即可。
面向對象的四大特性:封裝、集成、多態、抽象
封裝性:封裝簡單的說就是將細節隱藏起來,外界只管調用,無需在意細節,這也提高了程序的安全性;在Java當中最常見的兩個封裝就是定義方法、private關鍵字,其中定義一個類,也是封裝。
繼承:就是從已有的類中得到繼承關系的過程。通過繼承,能夠使得代碼在保持原有基礎上,能得到更多拓展。通常將對象的共有特征進行抽取得到父類。
- 父類和子類的關系:子類也是父類。如,講師、助教也是屬于員工的范疇。
- this代指當前的對象,而super代指父類對象(超類)
多態:是指運行不同子類型的對象對同一消息作出不同的響應。
講師和助教都屬于員工,他們都有共同的行為,如:工作。但是講師的工作是上課,助教的工作是幫助教學,雖然他們都是工作,但是工作內容是不一樣的
多態的格式:超類型的引用指向派生類 或 父類型的引用指向子類
//類: 父類 對象名 = new 子類() Employee one = new Teacher(); //此時將教師向上轉型為員工 //接口: 接口 接口名 =new 接口的實現類()使用多態的最大好處就是能夠降低代碼的耦合性;
JDK,JRE,JVM有什么區別?
分別是都是三個單詞的縮寫
JDK: Java Development Kit ,指的是Java開發的工具包,提供了Java開發環境和運行環境
JRE:Java Runtime Environment,Java運行環境,包含Java虛擬機及一些基礎類庫
JVM:Java Virtual Machine,Java虛擬機,提供執行字節碼文件的能力
JVM實現跨平臺的流程:
將編寫好的.java文件進行編譯成.class文件,此時不同平臺的JVM就會去運行這個.class文件,從而實現跨平臺的特性。
Java的基本數據類型有哪些?
整形:byte、short、int、long
浮點型:double,float
布爾型:boolean
字符型:char
一共八種基本數據類型。
String是引用類型
以下是占用字節情況:
| byte | 1 |
| short | 2 |
| int | 4 |
| long | 8 |
| double | 8 |
| float | 4 |
| char | 2 |
==和equals的區別 | &和&&的區別
==和equals的區別:
-
== 是比較運算符,equals是Object的一個方法
-
== 在比較在基本類型使用中,是判斷數值是否相等,而在比較引用類型時,則比較引用類型的地址值是否相等
-
equals是用于比較內容是否相等,但是在Object的源碼當中可以發現其實內部也是使用 == 運算符的
因此在沒有重寫equals方法前,它的作用是與 == 一樣的。
為了實現內容相同,通常需要對equals進行重寫,如String類就對該方法進行了重寫,從而實現了內容上的比對。
以下是String類重寫了equals的源碼:
public boolean equals(Object anObject) {if (this == anObject) {//判斷是否是當前對象return true;//是同一個對象的話就返回true}if (anObject instanceof String) {//判斷是否都是String類或子類 String anotherString = (String)anObject;//是的話就對內容就行逐個比對int n = value.length;if (n == anotherString.value.length) {char v1[] = value;char v2[] = anotherString.value;int i = 0;while (n-- != 0) {if (v1[i] != v2[i])return false;i++;}return true;}}//不是當前類或子類或接口實現類,就返回false;return false;}&和&&的區別:
&和&&都能夠進行邏輯與運算
if(a==b && a>0) //當左邊a==b的結果為false時,會直接短路掉右邊,因此右邊的a>0是不會繼續執行的 if(a==b & a>0) //當左邊a==b的結果為false時,不會短路掉右邊,因此右邊的a>0會繼續執行的final的作用
final的中文意譯為 最終的;最后的
因此被final修飾的類、方法、變量也都是’最終的‘
也就是說:
- 被final修飾的變量為常量,不可再變。
- 被final修飾的方法不可以被重寫
- 被final修飾的類不可以再變,也不能被繼承。
如果修飾的是引用變量,則引用變量的地址值不能發生改變。
但是可以修飾該變量下類的屬性。
//例如 final Person person = new Person(30,"tomcat"); //此時仍然可以對Person的屬性進行賦值 person.setAge(35);String,StringBuffer,StringBuilder區別
查看源碼可以發現,String類在內部使用了final關鍵字,因此一旦創建對象就意味著不可以改變自身,在重新賦值時等于重新生成了一個新的字符串對象。
StringBuffer是可以改變自身對象的,因此在推薦在需要經常對字符串內容的場景下使用。
線程安全性:
String是由final修飾的,定義后對象的內容不可變,因此是線程安全的。
StringBuffer在內部的方法下加入了syschronized,執行的效率低,但是是線程安全的。
StringBuilder跟StringBuffer一樣,區別在于StringBuilder沒有使用同步鎖,執行效率高,是線程不安全的類。
String s = "java"與String s = new String(“java”)
通過第一種形式:String s = "java"來得到的引用類型s,此時JVM會將這個變量放入常量池當中,當有別的變量的值也為’java’時,就會共享這一份String
通過String snew = new String(“java”)這種形式得到的變量snew ,是通過new的形式得到的,此時會在虛擬機的堆內存中開辟內存空間。
一道關于String和數組的筆試題
public static void main(String[] args) {int[] a = {1,2,3,4,5};String str = "abc";test(a,str);System.out.print(str+" and ");System.out.println(Arrays.toString(a));}private static void test(int[] a, String str) {a[0] = 10;str = "def";}顯示的結果為?
abc and [10, 2, 3, 4, 5]
在JVM運行時內存區域中,變量參數存在棧中,new出來的對象或數組存放在堆中。
在Java當中方法參數傳遞為值傳遞,當傳遞的參數是基本類型時,就把基本類型的值傳遞給方法;當傳遞的參數是引用類型時,此時會把對象在堆內存中的引用地址傳遞給方法
當把字符串和數組當參數調用方法時,實際傳遞的是其在堆內存中的地址,如此處為 0x666、0x999
因此當修改數組時,實際就是修改該堆內存的數組
然而,String為final修飾的內容,實際上是不可以更改的,因此仍然顯示“abc”
接口和抽象類的區別
抽象類與接口之間的區別:
從語法來說:
-
抽象類:方法可以有抽象的,也可以有非抽象, 有構造器
- 接口:方法都是抽象,屬性都是常量,默認有public static final修飾
-
JDK1.8之后:
-
接口里面可以有實現的方法,注意要在方法的聲明上加上default或者static
從開發的角度:
抽象類:
抽象類可以有抽象方法也可以有具體的方法,如在MVC三層架構中,Dao層編寫多個UserDao,此時可以抽象類,抽取常用的CRUD方法作為每一個dao的通用方法,而需要同一個方法的不同實現時,就在抽象類中編寫抽象方法。
接口:
接口更像是一種開發的規范,雖然在JDK1.8之后也可以寫實例方法,但是一般不這么采用。
在MVC三層架構的實際開發中,service調用dao,開發中需要將實現類注入到IOC容器當中,實際調用是由接口調用,由接口指向實現類。
當接口的實現類不能滿足功能時,可以隨時將新的實現類注入到IOC容器中,由于是用接口調用實現類,此時接口作為一個種規范,并不需要調整相關的代碼,從而實現低耦合高內聚的目的。
算法題-求N的階乘
遞歸就是自己調用自己,在JVM中每調用一次方法就會將該方法壓入棧內,調用的次數過多還未來得及出棧,此時就會發送溢棧——棧內存溢出,所以遞歸的次數不能過多。
做遞歸題重要的就是要找到跳出遞歸的突破口
public class DgTest {public static void main(String[] args) {Integer integer = test01(10);System.out.println("結果為"+integer);}/*** 使用遞歸計算n的階乘*/public static Integer test01(Integer count){if(count <= 0){throw new RuntimeException("非法參數");}if(count==1){return 1;}return test01(count-1)*count;}}總結遞歸調用:
- 遞歸調用要注意出口位置
- 遞歸調用次數不要過多,防止棧內存溢出
什么是向上轉型?什么是向下轉型?
向上向下轉型發生在父子類當中。
如醫生和老師都是繼承于員工類,員工又繼承于Object類,此時的關系如圖所示。
當類的類型又Object往下變化時,就為向上轉型,反之則為向上轉型。
用代碼的形式描述:
Employee teacher = new Teacher();//由Teacher向上轉型為Employee Docter docter = new Employee();//由Employee向下轉型為Docter在繼承的描述關系中,A繼承于B,也可以稱B就是A;所以我們可以說醫生、老師就是員工,員工就是Object。因此發生向下轉型時,是不會報錯的。
但是反過來,員工可以有很多種,既可以為老師也可以為醫生,如果需要向下轉型, 如果父類不能確定是指定的子類,則父類是不可以隨意轉型為子類的,不然就會報 ClassCastException 。
因此在需要向下轉型時,需要先用instanceof來判斷是否是子類。
Employee employee = new Docter(); if(employee instanceof Docter){ Docter docter = new Employee();//由Employee向下轉型為Docter }Int和Integer的區別(重點)
看以下例子:
Integer a = new Integer(3);Integer b = 3; //自動裝箱,會調用Integer的valueOf()方法int c = 3;System.out.print(a==b);//false,new出來的對象在堆內存中會開辟新的空間System.out.print(a==c);//true,a會自動拆箱為int,然后跟c進行比較自動裝箱:Integer型的對象直接賦值一個int數值時,會自動調用Integer的valueOf()方法,將int轉換為Integer;
自動拆箱:當一個Integer與int進行比較時,Integer類型的對象會自動拆箱為int,然后跟c進行比較。
以下例子:
Integer i3 = 126;Integer i4 = 126; System.out.println(i3 == i4);//true ---------------------- Integer i6 = 128; Integer i7 = 128; System.out.println(i6 == i7);//false以上兩個例子都是自動裝箱,因此都會自動調用Integer的valueOf()方法,但是兩個結果卻是不一樣的。
翻看valueOf的源碼:
static final int low = -128;static final int high = 127;public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high)//先進行范圍比較 //如果在[-128,127]之間,就從緩存中返回 return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i);//否則就直接new}可以看到首先會對i進行范圍篩選,如果是在[-128,127],就會將常量池中的對象返回,否則就會new新的對象。
當取值在這個范圍內時就是true,否則就是false
方法的重寫和重載的區別
重載發生在一個類中,同名的方法如果有不同的參數列表則視為重載。與返回值類型無關
以下不構成重載public double add(int a,int b)public int add(int a,int b)重寫發發生在子類和父類之間,重寫要求子類的重寫方法與父類的方法有相同的方法名、參數列表。返回值類型也要一致
注意:
- 重載與方法的返回值無關,存在于父類和子類以及同一個類中
- 在父子類中,構造方法不能被重寫,被final、static修飾的類不能重寫
List和Set的區別
最直觀的區別就是
LIST是有序的,可以存儲重復元素
SET是無序的,不可以存儲重復元素。
補充:
list包含有
- ArrayList:底層是數組,查詢,快連續內存空間方便尋址;增刪慢,會發送數據遷移?!?strong>是線程不安全類
- LinkList:底層是雙向鏈表,查詢慢,需要通過指針尋找,增刪快,改變其前后節點即可。—是線程不安全類
- vertor:底層是數組,查詢快,增刪慢—是線程安全類
set包含有:
- HashSet:底層使用Hash排序,先比對Hash值,如果Hash值一樣再比對內容是否一致
- TreeSet:會對Set的內容進行排序
set因為使用HashMap作為底層,因此是線程不安全的。
**TreeSet是指排序,set的無序是指存入取出的順序不一樣 **
談談ArrayList和LinkedList的區別
學過數據結構的話應該很容易明白,其實就是數組和鏈表之間的區別。
ArrayList底層是數組,會在內存中開辟一段連續空間,增刪慢,查找快——不夠嚴謹的表述
LinkedList底層是雙向鏈表,在內存中的空間是不連續的,特點是增刪快,查找慢——不夠嚴謹的表述
嚴謹表述:
數組的增刪慢也有特殊情況,如果在數組的頭部和尾部進行增改刪操作的話這個速度也是不慢的。
LinkedList使用了雙向鏈表,在頭部和尾部都有指針,因此當訪問頭部和尾部時,速度也是不慢的。
ArrayList和LinkList查詢和增刪速度的說明
當進行查找時,ArrayList底層使用數組,在內部開辟了連續的內存空間,因此當需要按位序查找時,速度比LinkList快是沒的說的,但是當需求發送變化,如果是按值查找,此時二者都需要進行遍歷查找,二者的速度就顯得半斤八兩。
當進行增刪操作時,如果發生的位置在集合的中間部分,由于ArrayList需要對數據進行遷移,而LinkList在內存當中只需要更改指針位置,此時LinkList在增刪操作時是有很大優勢的。但如果是對末尾進行增刪操作,ArrayList不需要做數據遷移(未超出數組長度),LinkList為雙向鏈表,此時二者的速度也是半斤八兩。
ArrayList的擴容機制
ArrayList初始的數組長度為10,當對集合的操作達到預警值時,就會將創建新數組,通過位運算,將數組的長度擴大為原數組的1.5倍,之后將原數組的數據遷移到新數組當中。
如何在雙向鏈表A和B之間插入C
一個數據結構的題目。—> 雙鏈表A、B節點之間插入一個C節點
雙鏈表:一個節點由三個部分組成,分別為前置指針、數據、后置指針。
前后指針分別指向前一個節點、后一個節點的地址值。
偽代碼:
c.next = a.next;a.next.pre = c;a.next = c;c.pre = a;Object類中的方法介紹
public final native Class<?> getClass();getClass使用了native 方法,內部使用了C/C++實現,用于返回當前運行時對象的 Class 對象
public native int hashCode();hashCode用于返回對象的哈希碼,重寫了Equals方法就需要重寫hashCode方法
public boolean equals(Object obj) {return (this == obj);}equals用于比較 2 個對象的內存地址是否相等,在需要比較內容是否相同時需要重寫該方法
protected native Object clone() throws CloneNotSupportedException;tclone方法hrows CloneNotSupportedException//naitive 方法,用于創建并返回當前對象的一份
public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());}toString返回類的名字@實例的哈希碼的 16 進制的字符串。建議 Object 所有的子類都重寫這個方法。
public final native void notify()notify()使用了native 方法,并且不能重寫。喚醒一個在此對象監視器上等待的線程(監視器相當于就是鎖的概念)。如果有多個線程在等待只會任意喚醒一個。
public final native void notifyAll()notifyAll()使用了native 方法,并且不能重寫。跟 notify 一樣,唯一的區別就是會喚醒在此對象監視器上等待的所有線程,而不是一個線程。
public final native void wait(long timeout) throws InterruptedExceptionnative 方法,并且不能重寫。暫停線程的執行。注意:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 。timeout 是等待時間。
protected void finalize() throws Throwable { }finalize方法表示實例被垃圾回收器回收的時候觸發的操作
equals()與 hashcode() 的區別
hashCode介紹:
hashCode() 的作用是獲取哈希碼,也稱為散列碼;它實際上是返回一個 int 整數。這個哈希碼的作用是確定該對象在哈希表中的索引位置。hashcode 方法是本地方法,也就是用 c 語言或 c++ 實現的,該方法通常用來將對象的 內存地址轉換為整數之后返回。
為什么重寫 equals 時必須重寫 hashCode 方法?
如果兩個對像是相等的,它的哈希嗎一定也是相同的,此時調用equals方法也能返回true,但是哈希碼相同的對象它們不一定會相等;
當我們重寫equals時,判斷對象是否相等的邏輯有原來的==變為了判斷屬性是否一致,因此為了對象的一致性,就必須重寫hashCode方法
重寫equals時,是為了用自身的方式去判斷兩個自定義對象是否相等,然而如果此時剛好需要我們用自定義的對象去充當hashmap的健值使用時,就會出現我們認為的同一對象,卻因為hash值不同而導致hashmap中存了兩個對象,從而才需要進行hashcode方法的覆蓋。
什么情況下可以不重寫 hashCode ?
當所在類不使用 HashSet、Hashtable、HashMap 等散列集合進行存儲的時候,可以不使用 hashcode。
除了NEW還有什么創建類的方式
創建類的方式:
使用反射來創建類
public void test(){ String str = "com.smoyu.blog.Test"; Class class = Class.forName(str); Test t = (Class)class.newInstance(); }Exception 和 Error 的區別
Error 類和 Exception 類的父類都是父類都是 throwable 類
Error 類一般是指與虛擬機相關的問題,如系統崩潰,虛擬機錯誤,內存空間不足,方法調用棧溢出等。對于這類錯誤的導致的應用程序中斷,僅靠程序本身無法恢復和和預防**,遇到這樣的錯誤,建議讓程序終止。
Exception 類表示程序可以處理的異常,可以捕獲且可能恢復。遇到這類異常,應該盡可能處理異常,使程序恢復運行,而不應該隨意終止異常。
反射原理及使用場景
Java 反射機制是在運行狀態中,對于任意一個類可以知道它的所有屬性和方法;對于任意一個對象,可以調用它的所有屬性和方法;這種動態獲取信息以及動態調用對象的方法的功能稱為 Java 的反射機制。
使用反射機制的前提條件是獲得代表的字節碼的 Class 文件(兩種方式):
1.知道具體類的情況下可以使用:
Class alunbarClass = TargetObject.class;
但是我們一般是不知道具體類的,基本都是通過遍歷包下面的類來獲取 Class 對象
2.通過 Class.forName()傳入類的路徑獲取:
Class alunbarClass1 = Class.forName(“cn.javaguide.TargetObject”);
**優缺點:優點:**運行期類型的判斷,動態加載類,提高代碼靈活度。
缺點:
應用場景:
Java會發生內存泄漏問題嗎?有哪些場景會發生內存泄漏
舉例部分常用場景:
ArrayList的擴容機制
總結:ArrayList底層源碼是基于數組來實現的,并且默認初始的容量為10,擴容時使用位運算的形式,每次以原size的1.5倍創建新數組,通過調用 elementData = Arrays.copyOf(elementData, newCapacity); 將原數據移動到新數組中
源碼分析:
transient Object[] elementData; // 定義一個數組private int size;/*** Default initial capacity.*/private static final int DEFAULT_CAPACITY = 10;//初始容量為10get(int index)方法:
public E get(int index) {rangeCheck(index); //檢查數組是否越界return elementData(index);//直接返回數組的下標}當調用add(E e) 方法時,會判斷是否擴容
public boolean add(E e) {ensureCapacityInternal(size + 1); // 判斷是否需要擴容elementData[size++] = e; //執行添加操作return true;}此時,add方法中調用了ensureCapacityInternal(size + 1); ,繼續翻閱該方法
private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); //此處嵌套了兩個方法}此時又繼續調用方法ensureExplicitCapacity方法,且內部執行了calculateCapacity用于擴容
private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0) //判斷是否需要執行擴容,如果該長度大于數組的長度,就執行擴容,且每次擴容按1.5進行grow(minCapacity); } private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}最終調用了grow方法
private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1); //if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity); }此處第三四行代碼:
- int oldCapacity = elementData.length;
- int newCapacity = oldCapacity + (oldCapacity >> 1); //
oldCapacity >> 1)執行了右移操作,相當于 oldCapacity / 2
這里就是擴容大小確定的地方,相當于新的最大容量是 舊的數組長度+舊數組的0.5長度
ConcurrentHashMap 的 put 方法過程
- 根據 key 計算出 hashcode 。
- 判斷數組桶是否為空,為空則初始化數組桶。
- 即為當前 key 定位出的 Node,如果為空表示當前位置可以寫入數據,利用 CAS 嘗試寫入,失敗則自旋保證成功。
- 如果當前位置的 hashcode == MOVED == -1,則需要進行擴容。
- 如果都不滿足,則利用 synchronized 鎖寫入數據。判斷是否為鏈表,為鏈表則遍歷鏈表加入新數據,如果相等則
- 覆蓋節點。判斷為紅黑樹,則進行一個紅黑樹的插入。
- 如果數量大于 TREEIFY_THRESHOLD 則要轉換為紅黑樹。
HashMap的擴容機制
1、首先判斷 OldCap 有沒有超過最大值。
2、當 hashmap 中的元素個數超過數組大小*loadFactor 時,就會進行數組擴容,loadFactor 的默認值為 0.75,也就是說,默認情況下,數組大小為 16,那么當 hashmap中元素個數超過 160.75=12的時候,就把數組的大小擴展為 216=32,即擴大一倍。
3、然后重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知 hashmap 中元素的個數,那么預設元素的個數能夠有效的提高 hashmap 的性能。比如說,我們有 1000 個元素 new HashMap(1000), 70但是理論上來講 new HashMap(1024)更合適,不過上面已經說過,即使是 1000,hashmap 也自動會將其設置為 1024。但是 new HashMap(1024)還不是更合適的,因為 0.751000 < 1000, 也就是說為了讓 0.75 size > 1000, 我們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了 resize 的問題。
為什么HashMap長度都是2的次冪
原因在于當長度是2次冪時滿足以下公式:
hash%length==hash&(length-1)使用& 運算的速度比直接取余更快。
HashMap為什么使用紅黑樹而不使用別的樹?
因為紅黑樹不追求完美的平衡,只要求達到部分平衡,可以減少增刪結點時的左旋轉和右旋轉的次數
為什么鏈表長度大于閾值 8 時才將鏈表轉為紅黑樹?
因為樹結點占用的存儲空間是普通結點的兩倍。因此紅黑樹比鏈表更加耗費空間。結點較少的時候,時間復雜度上鏈表不會比紅黑樹高太多,但是能大大減少空間。
當鏈表元素個數大于等于 8 時,鏈表換成樹結構;若桶中鏈表元素個數小于等于 6 時,樹結構還原成鏈表。因為紅黑樹的平均查找長度是 log(n),長度為 8 的時候,平均查找長度為 3,如果繼續使用鏈表,查找長度為 8/2=4,這才有轉換為樹的必要。
CopyOnWriteArrayList
它的實現就是寫時復制,在往集合中添加數據的時候,先拷貝存儲的數組,然后添加元素到拷貝好的數組中,然后用現在的數組去替換成員變量的數組(就是 get 等讀取操作讀取的數組)。這個機制和讀寫鎖是一樣的,但是比讀寫鎖有改進的地方,那就是讀取的時候可以寫入的 ,這樣省去了讀寫之間的競爭,同時寫入的時候怎么辦呢?當然果斷還是加鎖。
適用場景:copyonwrite 的機制雖然是線程安全的,但是在 add 操作的時候不停的拷貝是一件很費時的操作,所以使用到這個集合的時候盡量不要出現頻繁的添加操作,而且在迭代的時候數據也是不及時的,數據量少還好說,數據太多的時候,實時性可能就差距很大了。在多讀取,少添加的時候,他的效果還是不錯的(數據量大無所謂,只要你不添加,他都是好用的)。
CopyOnWriteArrayList 的 get 方法:get 的方法就是普通集合的 get 沒有什么特殊的地方,但是成員變量的聲明還是有講究的,是個用 volatile 聲明的數組,這樣就保證了讀取的那一刻讀取的是最新的數據。
CopyOnWriteArrayList 的 add 方法: add 方法了,可以明顯看出是明顯需要 reentrantlock 加鎖的,接下來就是復制數據和添加數據的過程,在 setArray 的過程中,把新的數組賦值給成員變量 array(這里是引用的指向,java 保證賦值的過程是一個原子操作)。
Servlet和Filter的區別
作用不同
Servlet 是一個運行在 web 服務端的 java 程序, 用于接收和響應請求。我們通常寫的各種 Java Web 應用本質上就是一個 Servlet 程序。
Tomcat 是一個 Servlet 容器,用于部署 Serlvet 程序。
Filter 是一個運行在 web 服務端的 java 程序, 用于攔截請求和攔截響應
方法不同
Servlet 只能接收請求和處理響應
Filter 可以接收請求和處理響應, 還可以攔截請求
生命周期不同
Servlet:第一次請求訪問的時候, 創建對象
Filter:web 應用加載的時候, 創建對象
訪問的優先級
Filter 訪問優先于 Servlet
悲觀鎖和樂觀鎖
CAS
Java對象頭存放什么內容
Volatile 關鍵字以及底層實現
線程池,線程池的作用
AQS
談談HashSet的存儲原理
HashSet與HashMap非常類似,最明顯的區別就是HashSet是屬于單列集合,HashMap為雙列集合。
翻看HashSet源碼,可以發現HashSet底層使用了HashMap
public HashSet() { map = new HashMap<>(); }HashSet在調用add(E e)方法時,會將傳入的對象作為key存入到HashMap中,統一以PRESENT作為假的value。
public boolean add(E e) { return map.put(e, PRESENT)==null; }所以HashSet是以哈希表存儲的,目的是為了解決唯一性的問題。
哈希表原理:
在JKD1.7以前,哈希表使用數組+鏈表的結構完成的。
在JDK1.8后,哈希表使用數組+鏈表的結構,但是當鏈表的長度>8時就會改用紅黑樹。
哈希存儲確保唯一性過程:
主要用到兩個方法,第一個就是Object的hashCode()方法,第二個就是使用了equals()方法進行內容上的比對。
首先第一步會對需要存入的對象進行哈希運算,并且與數組的長度-1進行位運算,從而得到該對象在數組存儲的位置。
當發生哈希沖突時,此時就會進行進一步的比較,通過調用equals方法來比較內容,如果是相同的的內容則不會繼續存儲,如果是不同的的內容,此時就會以鏈表的形式繼續存儲。
JDK1.8時做了優化,當鏈表的長度大于8時,就會將鏈表轉換為紅黑樹。
談談LinkedHashMap和HashMap的區別(重點)
共同點:二者都是屬于Map
HashMap使用了哈希表的存儲形式:使用數組+鏈表的結構
HashMap的默認初始化大小為16,最大的裝載因子默認為0.75,當HashMap的元素個數達到總容量的0.75時,就會在原先的基礎上擴容兩倍。
當鏈表的長度大于8,且數組的容量大于64時就會將鏈表轉化為紅黑樹。
LinkedHashMap與HashMap的主要不同就在于LinkedHashMap使用了鏈表+哈希表(散列表)的結構。
談談ConcurrentHashMap,HashMap,Hashtable的區別
- Hashtable是線程安全的類,但是效率低
- HashMap是線程不安全的類,效率高
- Hashtable的key|value是不允許NULL,HashMap允許key|value為NUll值
- Hashtable的父類為Dictionary,HashMap的父類為AbstractMap,但是它們都實現了Map接口
ConcurrentHashMap在線程安全和效率上是一個折中的選擇,它屬于并發包java.util.concurrent下的一個類
ConcurrentHashMap是線程安全的類,它能夠兼顧效率的原因是在于它把數據分段,執行分段鎖,需要改哪部分就給哪部分上鎖,將范圍變小,從而提高效率。
ConcurrentHashMap優化:
JDK1.7采用分段鎖的方式,而JDK1.8采用CAS和synchronized的組合模式
ArrayList vs Vector vs CopyOnWriteArrayList
ArrayList 和Vector的區別:
ArrayList :是線程不安全的類
Vector:和ArrayList 基本類似,但是在每個方法都使用了synchronized,因此是線程安全的。
在List集合中除了Vector是線程安全的,此外還有java.util.concurrent 包下的CopyOnWriteArrayList
CopyOnWriteArrayList是線程安全的類,與ArrayList 基本類似。其中所有可變操作( add , set ,等等)通過對底層數組的最新副本實現。
-
也就是說CopyOnWriteArrayList是讀寫分離的機制,在讀的時候不加鎖。
-
每次數組操作,都會把數組拷貝一份出來,在新數組上進行操作,操作成功之后再賦值回
去。
開發一個自己的棧,你會怎么寫?
-
棧(stack)在數據結構中是屬于順序表的范疇,主要特點為先進后出(First in last Out)
-
隊列(Queue)在數據結構中是屬于順序表的范疇,主要特點為先進先出(First in First Out)
棧的底層還是采用數組的形式,但是存和取要實現先進后出的特點
棧主要有兩個方法,一個是push(E item)方法,另一個是E peek(),一個存,一個取。
在Java中哪些情況使用了棧?
如JVM當中就使用了棧
補充:集合知識小結
Java集合中主要有兩大體系,分別為Collection和Map
主要區別在于Collection是單列集合,Map是雙列集合。
Collection主要有List和Set兩大類:
- List下的集合主要有ArrayList(數組,線程不安全)、LinkList(鏈表,線程不安全)、Vector(數組,線程安全)、CopyOnWriteArrayList(數組,線程安全)
- Set主要有HashSet(底層哈希)和LinkedHashSet(底層哈希+鏈表),LinkedHashSet使用鏈表可以實現排序(能排序,但不是有序。)
- List是有序的,Set是無序的
Map是雙列集合:
- 主要分為HashMap(底層哈希,線程不安全)、HashTable(哈希,線程安全)
- ConcurrentHashMap是一個效率和安全折中的選擇,既保證了線程安全,也兼顧了效率。它能夠兼顧效率的原因是在于它把數據分段,執行分段鎖,需要改哪部分就給哪部分上鎖,將范圍變小,從而提高效率
- 哈希在JDK1.8之后的優化在于當鏈表的長度大于8時,會將數組+鏈表的結構轉化為數組+紅黑樹的結構。
- LinkedHashMap是Map的子類,使用了鏈表+哈希的數據結構,能夠記錄插入的順序。
- HashMap的key|value可以為空,HashTable的key|value都不能為空。
- HashMap的父類是AbstractMap,HashTable的父類是Dictionary,但是它們都實現了Map接口。
談談IO流的分類及選擇
I指的是In,O指的是Out,即輸入輸出的意思。
此處輸入輸出,是站在程序的角度來理解的:
- 從程序中將文件信息寫入到本地,就是輸出-Out
- 將文件從本地讀到程序中,就是輸入-In
主要涵蓋四個流:InputString、OutPutString、Reader、Writer
InputString和OutPutString為字節流,可以輸入輸出任何格式的文件。
Reader和Writer為字符流,可以輸入輸出字符內容的文本。
serialVersionUID的作用是什么
當執行序列化時,我們寫對象到磁盤中,會根據當前這個類的結構生成一個版本號ID
當反序列化時,程序會比較磁盤中的序列化版本號ID跟當前的類結構生成的版本號ID是否一致,如果一致則反序列化成功,否則,反序列化失敗
加上版本號,有助于當我們的類結構發生了變化,依然可以之前已經序列化的對象反序列化成功
請描述下Java的異常體系
Java中的異常體系都繼承了Throwable,因此常見的Error和Exception都是它的子類。
按照異常類型可以劃分:
- Error:程序中不可處理的重大錯誤,常見的有堆內存溢出( OutOfMemoryError,在內存中創建的對象過多 )、棧內存溢出(StackOverFlowError,如遞歸次數過多)
- Exception,也稱為異常,指程序可以捕獲處理的的異常。
Exception下又含有運行時異常(RuntimeException)、編譯異常(IoException)
RuntimeException:也稱為邏輯異常,常見的有NullPointerException、ArrayindexOutBoundsException等。
IoException:在編譯時就會提示的異常,當出現該異常時就要對該異常進行處理,否則就會編譯不通過。
根據可查(checked Exception)和不可查異常(unchecked Exception)分類:
-
checked Exception:除了RuntimeException外的所有Exception都屬于可查異常,當程序編譯時發生該異常時,就必須處理,否則編譯不通過。
-
unchecked Exception: RuntimeException和Error屬于不可查異常,沒有try、catch編譯時仍然可以通過
羅列常見的5個運行時異常和非運行時異常
運行時異常:
NullPointerException
ArrayIndexOutBoundsException
ArithmeticException (算術異常,如1/0)
IllegaArguementException (參數異常)
NumberFormatException(數字格式異常)
非運行時異常:
IOException 表示發生某種類型的I / O異常。 此類是由失敗或中斷的I / O操作產生的一般異常類。
SQLException 提供有關數據庫訪問錯誤或其他錯誤的信息的異常
FileNotFoundException 指示嘗試打開由指定路徑名表示的文件失敗。
NoSuchFileException 當嘗試訪問不存在的文件時拋出檢查的異常。
NoSuchMethodException 當無法找到特定方法時拋出
throw跟throws的區別
throw作用在代碼塊內,throws作用在方法上。
---------throws--------------public void test01() throws Exception{ //代碼塊}----------throw---------------public void test02(){ try{ //代碼塊 }catch(Exception){ //代碼塊 throw new RuntimeException("發生了異常"); }}一道關于try catch finally返回值的問題
public int demo() {try {int a = 5/0;return 1;}catch (Exception e){return 2;}finally {return 3;}}結果為3,finally的優先級大于catch
創建線程的方式
創建線程有四種方式:
繼承Thread 實現Runable接口 實現Callable接口(可以獲取線程執行之后的返回值) 從線程池中獲取方式一:繼承Thread類
public class Demo01 {/*** 繼承Thread的方式*/@Testpublic void test01(){Demo demo = new Demo();demo.start();} } //定義一個類,繼承Thread,重寫run方法 class Demo extends Thread{@Overridepublic void run() {System.out.println("線程創建了:"+Thread.currentThread().getName());} }方式二:實現Runable接口
//使用lambda的形式 /** * 實現 Runable 接口 */ @Test public void test02(){ new Thread(() -> System.out.println("線程創建了:"+Thread.currentThread().getName())).start(); }-----------------//使用匿名內部類 @Testpublic void test03(){ new Thread(new Runnable() { @Override public void run() { System.out.println("線程創建了:"+Thread.currentThread().getName()); } }).start();}方式三:實現Callable接口(可以獲取線程執行之后的返回值)
//繼承Callable接口 class Demo2 implements Callable<String>{ @Override public String call() throws Exception { System.out.println("多線程方法執行了"); return "9999"; }} ---------調用測試方法-----------@Test public void test04(){ FutureTask futureTask = new FutureTask(new Demo2()); new Thread(futureTask).start(); }run方法和start()的區別:
- run方法是Runable接口中的一個抽象方法,start是Thread中的方法
- run是線程需要執行的代碼,start是將Thread線程啟動起來,會開辟新線程。
- 通過調用start方法,會開辟新線程去執行run方法;直接調用run方法是不會開啟新的線程的。
面試提問:選擇Thread還是Runable?二者什么區別
一個普通main方法的執行,是單線程模式還是多線程模式?為什么
多線程模式。
除了Main方法的線程外,還有垃圾回收線程GC。
請描述線程的生命周期
線程的生命周期:
- 新建狀態 (New): 線程對象對創建后,即進入了新建狀態,如:Thread t = new MyThread();
- 就緒狀態(Runnable):當線程對象調用了start方法時,就處于就緒狀態
- 運行狀態 (Running): 線程處于就緒狀態,且得到CPU時間片時,CPU就會執行該線程任務,此時就為運行狀態
- 阻塞狀態(Blocked):運行狀態的線程,調用sleep、wait方法,或未得到synchronized鎖時,就會進入阻塞狀態
- 終止狀態 (Terminated): 當線程任務執行完時,就進入了終止狀態。
調用wait方法,需要使用notify()或notifyAll()來喚醒線程
notify:會隨機喚醒一個線程
notifyAll:喚醒所有的線程
談談你對ThreadLocal的理解
ThreadLocal這個類提供線程局部變量。 這些變量與其正常的對應方式不同,因為訪問一個的每個線程(通過其get或set方法)都有自己獨立初始化的變量副本。 ThreadLocal實例通常是希望將狀態與線程關聯的類中的私有靜態字段(例如,用戶ID或事務ID)。
ThreadLocal能夠為每個線程創建一個獨立的副本,從而避免了多線程環境下操作共享資源的安全問題
public class ThreadLocalTest { //定義一個成員變量 static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); /** * 定義一個方法,開辟兩個線程,打印其值觀察是否是同一個線程 * 如果打印的線程是不同的值,就表面threadLocal會為每個線程創建獨立副本 **/ public static void main(String[] args) throws Exception { Task task = new Task(); new Thread(task).start(); Thread.sleep(10);//休眠一下,防止太快看不出效果 new Thread(task).start(); } static class Task implements Runnable{ @Override public void run() { //獲取一下當前的threadLocal Long local = threadLocal.get(); if(local == null){ //如果local為Null就手動賦值 threadLocal.set(System.currentTimeMillis()); //打印查看結果 System.out.println("當前的local:"+threadLocal.get()); } } }}此時可以發現打印出來的結果是不一樣的。
ThreadLocal最主要的兩個方法get和set,通過分析:
可以看到,無論是get還是set,都有最核心的兩句話
//獲取當前線程Thread t = Thread.currentThread();//從ThreadMap中獲取當前線程的mapThreadLocalMap map = getMap(t);也就是說,無論是存還是取使用ThreadLocal操作的都是當前線程的對象,這也就說明了ThreadLocal使同一變量在每一個線程中有各自的副本,只有指定的線程能拿到特點的數據。
案例說明:
Java程序對數據庫的連接操作都是基于JDBC來完成的,在開發中根據不同的功能分為了web層、service層,dao層。
其中對于事務的控制是放在service層中解決的。
public class UserService { private UserDao userDao = new UserDao(); private LogDao logDao = new LogDao(); public void add(){ userDao.add(); logDao.add(); } }如此處service調用了兩個dao,那么兩個dao是如何實現事務的控制的?
在JDBC中,如要實現事務,就必須要保證兩個dao是有相同的Connection連接對象,因此只需要保證Connection是相同的即可。
因為整個UserService都是同一個線程去調用userDao和logDao,因此就可以使用ThreadLocal來解決這個問題
/** * 使用 ThreadLocal 來存儲Connection對象 * 因此service是使用同一個線程來調用dao的 * 因此可以使用ThreadLocal訪問當前線程的特有數據 */ public class ConnectionUtils { private ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); public static Connection getConnection(){ Connection connection = threadLocal.get(); if(connection == null){ connection = new Connection(); threadLocal.set(connection); } return connection; }}通過ThreadLocal會為每個線程創造一個副本的特點, 因此可以將Connection存儲到ThreadLocal對象中,只要是同一個線程訪問的,就可以拿到相同的Connection對象。
談談AJax的工作原理
Ajax 的全稱是Asynchronous JavaScript and XML(異步的JavaScript 和 XML),ajax中最后的x指的是xml,早起都是使用xml作為數據交互,現在json更為輕量,基本都是使用json數據。
在前后端不分離的時代,沒有引入Ajax時,客戶端發送的請求流程是如下這樣的:
客戶端發起請求,服務器將整個HTML+CSS數據都響應給客戶端
引入Ajax的工作模式是這樣的:
相比較傳統模式,引入了一個AJax引擎,通過使用XMLHttpRequest發送異步請求到服務器,服務器將數據響應到AJAX引擎,利用回調函數進行數據的渲染,響應HTML+CSS數據,從而實現異步刷新。
談談Servlet的生命周期
Servlet的生命周期
Servlet創建 -> 初始化 -> 調用service方法 ->調用doGet/doPost方法 -> 銷毀
Servlet是一種單例類型的,從創建到銷毀只會被執行一次
創建 - > 執行一次
初始化 ->執行一次
銷毀 -> 執行一次
其中,Servlet默認是在第一次訪問時才被創建,也可以通過配置的形式讓servlet在容器啟動時就創建。
Servlet不是線程安全的類,在并發條件下會有線程不安全的情況,因此因避免操作共享數據。
描述Session跟Cookie的區別(重要)
- Session存儲在服務器中,Cookie存儲在瀏覽器(客戶端)中
- Session存儲的對象為鍵值對形式,Cookie儲存的數據格式為字符串(可轉為JSON)
- Session生命默認為30分鐘, Cookie 默認隨著瀏覽器的關閉而關閉,也可以通過 設置有效期來控制Cookie的生命。
理解:
在日常過程中,小明問小軍:“你喜歡吃什么” ,小軍回答:“我喜歡吃四飯燒鴨飯”,那么這個過程
其實就是一問一答的過程,或者換句話說,就是一次會話(Session)
當客戶端發起請求,瀏覽器就會響應結果,那么我們可以說是一次請求,一次響應,整個過程就是
一次通信會話。
由于HTTP通信協議是無狀態的,服務器無法直接鑒別當前客戶信息,我們就需要一種技術,讓服
務器能夠記住當前用戶。因為我們不可能登錄A頁面后,需要訪問B頁面時又重新登錄,訪問C頁面的時
候又重新登錄。
我們需要的效果是,當我登錄了當前的網站,隨后用戶的相關信息都能被記錄,服務器能夠識別當
前用戶信息,登錄了A頁面后,無論訪問B頁面還是C頁面,服務器都能記住我,那么就需要這種會話技
術Session了。
所以,當我關閉瀏覽器時,服務器就會認為你跟它的會話已經結束,那么存儲在Session的用戶信
息也會隨之而刪除,這也是為什么關閉瀏覽器下次就需要重新登錄的原因了。
Cookie在百科是的為:“儲存在用戶本地終端上的數據”,也就是說,Cookie是能信息保存到當前
的客戶端本地(當然也包括用戶信息了),而Session存儲的用戶信息保存到服務器上,這是二者的不同。
當保存到本地時,即便關閉瀏覽器,當我下次打開網站,瀏覽器就能在請求服務器的同時將Cookie
一起發送給服務器,從而讓服務器能夠正常鑒別用戶信息,所以有時候也可以做長時間的登錄這個功
能。
由于Cookie是存儲在客戶端本地的,所以就會有被攻擊和篡改的可能,因此這也讓Cookie存儲用
戶信息變得不安全。比如有可能會遭到CSRF的攻擊,這里沒有詳細的解釋
補 Get請求與Post請求的區別
經常會聽到,你的這個請求到底是get的請求呢還是post請求呢?
從簡單的來講二者區別:
- post的請求比get請求更安全,因為get的請求直接把參數暴露到URL上。就比如做登錄用post而不
是get,get會使你的密碼暴露出去,顯然更加不安全。 - get的請求參數往往在url中,而post的參數是放在請求體當中的。如果不了解請求頭、請求體這些
知識,可以去了解一下HTTP協議。 - GET在瀏覽器回退時是無害的,而POST會再次提交請求。
主要是這些,百度上還有更詳細的,就看你的需求了
轉發和重定向的區別
轉發:是指發生在服務器內部的跳轉,此時只有一次請求
重定向:指發生客戶端之間的跳轉,有兩次請求。
區別:
- 轉發請求一次,重定向請求多次
- 轉發發生在服務器內部之間,重定向發生在客戶端
重定向實例:
如登錄時,需要發起一個 POST請求,登錄成功時就會返回狀態碼302,然后重定向到首頁(用戶中心)
Iterator和ListIterator的區別?
Iterator [?t?’re?t?] :可以遍歷Set和List,而ListIterator只能遍歷List
Iterator只能單向遍歷,而ListIterator可以雙向遍歷
ListIterator繼承與Iterator接口
并發和并行的區別
并發和并行是兩個不同的概念
- 并發指的是某一個處理器同時處理多個請求,如同一個CPU處理多個任務時,通過時間片的方式快速的切換完成多個任務。
- 并行是指多個處理器去同時處理多個請求,此時好比有多個CPU去同時處理多個任務,使用多核處理器,處理多任務的能力自然而然強。
什么是序列化?
序列化是為了保持對象在內存中的狀態,并且可以把保存的對象狀態再讀出來。
什么時候需要用到java序列化?
1,需要將內存的對象狀態保存到文件中
2,需要通過socket通信進行對象傳輸時
3,我們將系統拆分成多個服務之后,服務之間傳輸對象,需要序列化
說說synchronized底層原理
這個我們要分情況來分析:
1,JDK1.6之前
synchronized是由一對monitor-enter和monitor-exit指令實現的。
這對指令的實現是依靠操作系統內部的互斥鎖來實現的,期間會涉及到用戶態到內存態的切換,所以這個操作是一個重量級的操作,性能較低。
2,JDK1.6之后
JVM對synchronized進行了優化,改了三個經歷的過程
偏向鎖-》輕量級鎖-》重量級鎖
偏向鎖:
在鎖對象保存一個thread-id字段,剛開始初始化為空,當第一次線程訪問時,則將thread-id設置為當前線程id,此時,我們稱為持有偏向鎖。
當再次進入時,就會判斷當前線程id與thread-id是否一致
如果一致,則直接使用此對象
如果不一致,則升級為輕量級鎖,通過自旋鎖循環一定次數來獲取鎖
如果執行了一定次數之后,還是沒能獲取鎖,則會升級為重量級鎖。
鎖升級是為了降低性能的消耗。s
synchronized和volatile的區別
1,作用的位置不同
synchronized是修飾方法,代碼塊
volatile**[?v?l?ta?l]** 是修飾變量
2,作用不同
synchronized,可以保證變量修改的可見性及原子性,可能會造成線程的阻塞
volatile僅能實現變量修改的可見性,但無法保證原子性,不會造成線程的阻塞
volatile的實現:
volatile是一個輕量級的線程同步機制。它的特性之一,是保證了變量在線程之間的可見性。
所謂的可見性是指當一個線程修改了變量的值之后,其他線程可以感知到該變化。
為什么會有可見性問題?
是因為由于硬件速度的不同,CPU的速度要明顯快于主內存。
所以為了解決速度不匹配的問題,在CPU到主內存之間就會有多級的緩存。
那么這個時候就會發生,一個線程修改了數據,數據還沒有及時刷到主內存,那么其他線程讀取到的數據就依然還是舊的,這就是可見性問題發生的根源。
通過給變量設置volatile關鍵字修飾,可以保證變量在線程修改完之后,會刷新到共享內存,這樣其他線程就可以讀取到最新的內容
volatile保證了在多個線程之間是可見的,但不能保證原子性操作。
volatile vs synchronized
synchronized也是保證了線程的可見性,同時也具備了多線程之間的互斥性
3,如何使用volatile ?
直接修飾變量即可
private volatile int count;
4,volatile底層實現原理
當變量被聲明為volatile后,線程每次都會都從主內存去讀取,而不是讀取自己的工作內存,這樣就實現了線程之間的可見性
synchronized和lock的區別
1,作用的位置不同
synchronized可以給方法,代碼塊加鎖
lock只能給代碼塊加鎖
2,鎖的獲取鎖和釋放機制不同
synchronized無需手動獲取鎖和釋放鎖,發生異常會自動解鎖,不會出現死鎖。
lock需要自己加鎖和釋放鎖,如lock()和unlock(),如果忘記使用unlock(),則會出現死鎖,
所以,一般我們會在finally里面使用unlock().
補充:
//明確采用人工的方式來上鎖
lock.lock();
//明確采用手工的方式來釋放鎖
lock.unlock();
synchronized修飾成員方法時,默認的鎖對象,就是當前對象
synchronized修飾靜態方法時,默認的鎖對象,當前類的class對象,比如User.class
synchronized修飾代碼塊時,可以自己來設置鎖對象,比如
synchronized(this){
//線程進入,就自動獲取到鎖
//線程執行結束,自動釋放鎖
}
什么是死鎖?如何防止死鎖?
如何防止死鎖?
減少同步代碼塊嵌套操作
降低鎖的使用粒度,不要幾個功能共用一把鎖
盡量采用tryLock(timeout)的方法,可以設置超時時間,這樣超時之后,就可以主動退出,防止死鎖(關鍵)
什么是反射?可以解決什么問題?
反射是指程序在運行狀態中,
1,可以對任意一個類,都能夠獲取到這個類的所有屬性和方法。
2,對于任意一個對象,都可以調用它的任意一個方法和屬性
反射是一種能力
一種在程序運行時,動態獲取當前類對象的所有屬性和方法的能力,可以動態執行方法,給屬性賦值等操作的能力
Class代表的就是所有的字節碼對象的抽象,類
反射,讓我們的java程序具備動態性
這種動態獲取類信息及調用對象方法的功能稱為反射
這種能力帶來很多的好處,在我們的許多框架的背后實現上,都采用了反射的機制來實現動態效果。
框架是提供一種編程的約定
比如@Autowrie 就能實現自動注入
@Autowrie
private IUserService userService;
注解的解析程序,來掃描當前的包下面有哪些屬性加了這個注解,一旦有這個注解,就要去容器里面獲取對應的類型的實現,然后給這個屬性賦值。
什么是悲觀鎖,什么是樂觀鎖?
1,悲觀鎖是利用數據庫本身的鎖機制來實現,會鎖記錄。
實現的方式為:select * from t_table where id = 1 for update
2,樂觀鎖是一種不鎖記錄的實現方式,采用CAS模式,采用version字段來作為判斷依據。
每次對數據的更新操作,都會對version+1,這樣提交更新操作時,如果version的值已被更改,則更新失敗。
3,樂觀鎖的實現為什么要選擇version字段,如果選擇其他字段,比如業務字段store(庫存),那么可能會出現所謂的ABA問題
數據庫說明
數據庫的完整知識可以參考我的這篇文章,這是我總結的一個比較完整的知識點全集,字數在1W+
https://blog.csdn.net/wq2323/article/details/111768680
談談數據庫設計的三大范式及反范式
數據庫的三大范式
第一范式:列不可分
第二范式:要有主鍵
第三范式:不可存在傳遞依賴
比如商品表里面關聯商品類別表,那么只需要一個關聯字段product_type_id即可,其他字段信息可以通過表關聯查詢即可得到
如果商品表還存在一個商品類別名稱字段,如product_type_name,那就屬于存在傳遞依賴的情況,第三范式主要是從空間的角度來考慮,避免產生冗余信息,浪費磁盤空間
反范式設計:(第三范式)
為什么會有反范式設計?
原因一:提高查詢效率(讀多寫少)
比如上述的描述中,顯示商品信息時,經常需要伴隨商品類別信息的展示,
所以這個時候,為了提高查詢效率,可以通過冗余一個商品名稱字段,這個可以將原先的表關聯查詢轉換為單表查詢
原因二:保存歷史快照信息
比如訂單表,里面需要包含收貨人的各項信息,如姓名,電話,地址等等,這些都屬于歷史快照,需要冗余保存起來,
不能通過保存用戶地址ID去關聯查詢,因為用戶的收貨人信息可能會在后期發生變更
左連接,右連接,內連接,如何編寫SQL,他們的區別是什么?
左連接:以左表為主
select a.,b. from a left join b on a.b_id = b.id;
右連接:以右表為主
select a.,b. from a right join b on a.b_id = b.id;
內連接:只列出兩張表關聯查詢符合條件的記錄
select a.,b. from a inner join b on a.b_id = b.id;
如何解決SQL注入?
SQL注入,是指通過字符串拼接的方式構成了一種特殊的查詢語句
比如:select * from t_user where usename=’’ and password=’’
’ or 1=1 #
select * from t_user where usename=’’ or 1=1 # ’ and password=’’
解決方案
采用預處理對象,采用PreparedStatement對象,而不是Statement對象
可以解決SQL注入的問題
另外也可以提高執行效率,因為是預先編譯執行
SQL執行過程(語法校驗->編譯->執行)
延伸
MyBatis如何解決了SQL注入的問題?采用#
MyBatis的#和KaTeX parse error: Expected 'EOF', got '#' at position 5: 的差異,#?可以解決SQL注入,而號不能解決
JDBC如何實現對事務的控制及事務邊界
JDBC對事務的操作是基于Connection來進行控制的,具體代碼如下:
try { //開啟事務 connection.setAutoCommit(false); //做業務操作 //doSomething(); //提交事務 connection.commit();}catch(Exception e){ //回滾事務 try { connection.rollback(); } catch (SQLException e1) { e1.printStackTrace(); }}但,注意,事務的邊界我們是放在業務層進行控制,因為業務層通常包含多個dao層的操作。
談談事務的特點
原子性是基礎,隔離性是手段,一致性 是約束條件,而持久性是我們的目的。
簡稱,ACID
原子性( Atomicity )、一致性( Consistency )、隔離性( Isolation )和持久性( Durability )
原子性:
事務是數據庫的邏輯工作單位,事務中包含的各操作要么都完成,要么都不完成
(要么一起成功,要么一起失敗)
一致性:
事務一致性是指數據庫中的數據在事務操作前后都必須滿足業務規則約束。
比如A轉賬給B,那么轉賬前后,AB的賬戶總金額應該是一致的。
隔離性:
一個事務的執行不能被其它事務干擾。即一個事務內部的操作及使用的數據對其它并發事務是隔離的,并發執行的各個事務之間不能互相干擾。
(設置不同的隔離級別,互相干擾的程度會不同)
持久性:
事務一旦提交,結果便是永久性的。即使發生宕機,仍然可以依靠事務日志完成數據的持久化。
日志包括回滾日志(undo)和重做日志(redo),當我們通過事務修改數據時,首先會將數據庫變化的信息記錄到重做日志中,然后再對數據庫中的數據進行修改。這樣即使數據庫系統發生奔潰,我們還可以通過重做日志進行數據恢復。
談談事務的隔離級別
有以下4個級別:
l READ UNCOMMITTED 讀未提交,臟讀、不可重復讀、幻讀有可能發生。
l READ COMMITTED 讀已提交,可避免臟讀的發生,但不可重復讀、幻讀有可能發生。
l REPEATABLE READ 可重復讀,可避免臟讀、不可重復讀的發生,但幻讀有可能發生。
l SERIALIZABLE 串行化,可避免臟讀、不可重復讀、幻讀的發生,但性能會影響比較大。
特別說明:
幻讀,是指在本地事務查詢數據時只能看到3條,但是當執行更新時,卻會更新4條,所以稱為幻讀
InnoDB是如何支持事務的 ?
通過日志的形式來確保事務。
redo log 與 undo log 實現的。
介紹一下MVCC
多版本并發控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存儲引擎實現隔離級別的一種具體方式,用于實現提交讀和可重復讀這兩種隔離級別。而未提交讀隔離級別總是讀取最新的數據行,要求很低,無需使用MVCC??纱谢綦x級別需要對所有讀取的行都加鎖,單純使用 MVCC 無法實現。
索引如何避免全表掃描(索引失效)
為了避免全表掃描,首先應考慮在 where 及 order by 涉及的列上建立索引。
列舉幾個比較常見的:
最左前綴
最左前綴是針對聯合索引來說,例如三個字段組成的聯合索引,abc、ab、a=ac。使用 abc 會完全使用聯合索引的 abc三列,使用 ab 只會使用聯合索引中的兩列,使用 a 或者 ac 只會使用聯合索引中的 a 列。
需要滿足兩個條件:需要查詢的列和組合索引的列順序一致 查詢不要跨列
之所以會有最左原則,是因為聯合索引的 B+樹是按照第一個關鍵字進行索引排列的。
查詢很慢怎么解決 ?
1、 開啟慢查詢日志,根據日志找到哪條 SQL 語句執行的慢。要調用 set 將 slow_query_log 參數設置成 1,開啟這個慢查詢日志。(日志分析工具 mysqldumpslow)
2、 用 Explain 關鍵字分析這條 SQL 語句,查看是否建立了索引。如果沒有,就可以考慮建立索引。如果建立了索引,但是并沒有使用到,就分析它為什么沒有使用到索引。
計算機網絡
說說TCP和UDP的區別
首先,兩者都是傳輸層的協議。其次,
tcp提供可靠的傳輸協議,傳輸前需要建立連接,面向字節流,傳輸慢
udp無法保證傳輸的可靠性,無需創建連接,以報文的方式傳輸,效率高
TCP/IP協議基礎
| TCP 20 | FTP | 文件傳輸中的數據傳輸 |
| TCP 21 | FTP | 文件傳輸中的控制命令 |
| TCP 22 | SSH | 安全遠程登錄 |
| TCP 23 | Telnet[?telnet] | 遠程登錄 |
| TCP 25 | SMTP | 電子郵件傳輸 |
| TCP 80 | HTTP | WWW服務 |
| TCP 110 | POP3 | 郵件接收 |
| TCP 139 | NETBIOS SESSION SERVICE | Samba服務 |
| TCP/UDP 53 | DNS | 域名解析服務 |
| UDP 69 | TFTP | 簡單的文件傳輸 |
計算機網絡層次
思維導圖:
1、鏈路層
鏈路層有時也稱作數據鏈路層或網絡接口層,通常包括操作系統中的設備驅動程序和計算機中對應的網絡接口卡。
2、網絡層
網絡層處理分組在網絡中的活動,例如分組的選路。在TCP/IP協議族中,網絡層協議包括ARP(Address Resolution Protocol,地址解析協議)、IP協議(Internet Protocol,網際協議)、ICMP協議(Internet Control Message Protocol,網際控制報文協議)和IGMP協議(Internet Group Management Protocol,網際組管理協議)。RARP(Reverse Address Resolution Protocol,逆地址解析協議)。
3、傳輸層
傳輸層主要為兩臺主機上的應用程序提供端到端的通信。在TCP/IP協議族中,有兩個互不相同的傳輸協議:TCP(Transmission Control Protocol,傳輸控制協議)和UDP(User Datagram Protocol,用戶數據報協議)。
4、應用層
應用層負責處理特定的應用程序細節。幾乎各種不同的TCP/IP實現都會提供下面這些通用的應用程序:Telnet遠程登錄(23)、SMTP(25)、FTP(21/20)、HTTP(80)、POP3(110)DNS(53)
SSL概述
概述
SSL (Secure Sockets Layer)由Netscape公司1996年開發,其目的是在客戶端和服務器端之間建立安全通道,從而提高WEB數據傳輸的安全性。
目前主流瀏覽器都支持SSL
SSL的作用
- 保密性 SSL采用的對稱加密技術保證信息的機密性。
- 完整性。通信的發送方通過散列函數產生消息驗證碼(MAC),接收方通過驗證MAC來保證信息的完整性。SSL 提供完整性校驗服務,使所有經過SSL協議處理的業務都能全部準確、無誤地到達目的地。
- 身份認證(鑒別) SSL采用數字證書進行身份認證。
- SSL協議在可靠的傳輸層協議(如:TCP)之上。SSL協議的優勢在于它是與應用層協議獨立無關的。
即:保密性、完整性、身份認證,沒有不可否認性
HTTP 和 HTTPS 的區別
端口: HTTP 的 URL 由“http”起始且默認的端口是 80,而 HTTPS 的 URL 由“https”起始且默認端口是 443;
安全性和資源消耗:
HTTP 協議運行在 TCP 之上,所有傳輸的內容都是明文,客戶端和服務端都無法驗證對方的身份。
HTTPS 是運行在 SSL/TLS 之上的 HTTP 協議, SSL/TLS 是運行在 TCP 之上。 所有傳輸的內容都經過加密,加密
采用對稱加密,但對稱加密的密鑰用服務器方的證書進行了非對稱加密(采用對稱和非對稱兩種加密方式相結合
的加密方式) 。所以說, HTTP 安全性沒有 HTTPS 高,但是 HTTPS 比 HTTP 耗費更多服務器資源。
對稱密鑰加密:這種加密方式的加密算法是公開的,密鑰是保密的,加密和解密采用同一個密鑰,也就是任何人得到了
密鑰就能進行加密和解密
弊端:在進行通信時,也必須把密鑰發給對方,否則對方無法解密,而在把密鑰發給對方的過程中,就存在安全問題23
非對稱密鑰加密:這種加密方式有兩把密鑰,一把公鑰,可以公開告訴任何人,一把私鑰,只能自己持有。
弊端:使用公鑰對消息加密速度比較慢
談談什么是TCP的三次握手,可不可以只握手兩次?
談談什么是TCP的四次揮手?
瀏覽器輸入 URL 并回車的過程以及相關協議,DNS 查詢過程
JVM虛擬機
JVM內存區域是怎么劃分的?
JVM內存主要有三個部分劃分:線程私有的、線程共享的、直接內存
程序計數器
程序計數器在內存中分配了一塊較小的區域,它可以看作是當前線程執行行數的指示器。
Java多線程的實現通常采用CPU時間片的方式進行快速切換,為了恢復CPU再次執行該線程時的位置,就需要一個能夠記住該線程執行行號的計數器。
虛擬機棧(Stack):
虛擬機棧的生命周期和線程的生命周期是相同的,當程序每調用一個方法時,就會產生一個棧幀;虛擬機棧主要存儲的信息有:局部變量表、方法出口等,只有當方法調用完成時,壓入虛擬機的棧幀才會從棧中退出。
局部變量表:主要存放的是在編譯時已知的基本類型變量和引用類型變量的引用地址,在內部存儲是以局部變量槽(Slot)來表示的。
本地方法棧(Native Stack)
與虛擬機棧類似,區別在于本地方法棧執行的是被 native修飾的本地方法,而虛擬機棧為普通的Java程序服務。
堆(heap)
Java的堆內存主要存放的是對象實例和數組,是虛擬機內存區域中最大的一塊區域,是線程共享的區域,也是垃圾回收器進行垃圾回收的一塊重要區域。
根據GC的角度來劃分的話,堆內存又劃分為新生代和老年代。
新生代分為eden區、from survivor、to survivor區
方法區
JVM中的方法區并不是一個實在的物理概念,方法區它用于存儲已經被虛擬機加載的類型信息、常量、靜態變量等數據,但是方法區僅僅是一個邏輯上的概念。這就好比接口與實現類的關系一樣,方法區僅僅是一個接口,一種規范,而永久代便是它的實現類。
在JDK1.6時代,使用永久代來實現方法區
到了JDK1.8的時候,摒棄了永久代的概念,采用了元空間的概念,即使用計算機的物理內存作為元空間的內存,同時將永久代的字符串常量池移出到堆內存中。
JDK1.6時期的方法區(永久代)
JDK8時期摒棄永久代的概念,使用元空間實現方法區,同時將常量池里的字符串池也挪到了堆中。
常量池與運行時常量池
常量池是一張表,JVM虛擬機根據常量表來找到需要訪問的類名、方法名、變量等信息。
當程序運行時,就會把常量池的信息挪到運行時常量池,此時JVM虛擬機訪問的類名、方法名、變量等信息的地址就會變成真實的物理地址,因此把該常量池稱為運行時常量池。
StringTable
又稱字符串常量池,屬于常量池的一部分(jdk1.6時期,1.8時期存放在堆中)
有以下特性:
- 在常量池內出現的字符串僅僅只是一個‘符號’,只有當變量引用時才會變成一個字符串對象。
- 字符串拼接時,實際上使用的是StringBuilder對象的append()方法
如以下字符串 a/b,使用+號時實際上調用的是StringBuilder
String a = "a"; String b = "b"; String c = "ab"; String ab = a+b; System.out.println(c == ab);//false實際上使用的是:
new StringBuilder().append(a).append(b).toString();
但是我們通過源碼發現
@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }在StringBuilder內部的toString()方法中,使用了new String()來得到字符串對象,這也就導致了c==ab為false的原因
該原理是通過編譯期優化得到的。
intern
new出來的字符串存放在堆中,而直接引用字符串的變量是引用常量池的,因此可以使用intern方法主動將還沒有放入字符串常量池的對象放入該常量池
- 1.8 將這個字符串對象嘗試放入串池,如果有則并不會放入,如果沒有則放入串池, 會把字符串常量池中的對象返回
- 1.6 將這個字符串對象嘗試放入串池,如果有則并不會放入,如果沒有會把此對象復制一份,放入字符串常量池, 并把該對象返回
說說JVM的運行時內存
根據GC的角度來劃分的話,堆內存又劃分為新生代和老年代。
新生代:
新生代一般存放新生的內存對象,大約占用堆內存的1/3,如果新生的對象內存過大,將會存放在老年代中。在新生代中會觸發MinorGC,新生代分為eden區、from survivor、to survivor區
eden區:
中文翻譯為伊甸園,即亞當和夏娃的原住地,因此寓意為Java對象的出生地。當新的Java對象過大時,會在老年代產生新對象;當內存不足時,就會觸發MinorGC,此時如果內存還無法容納新對象,就會發生錯誤。
from survivor:上一次 GC 的幸存者,作為這一次 GC 的被掃描者。
to survivo:保留了一次 MinorGC 過程中的幸存者
談談垃圾回收算法
確定對象是否能被回收的方法:引用計數法、可達性分析法
引用計數法
在Java中,對象是通過引用進行關聯的。當需要操作一個對象時,就對該對象的引用計數+1,當操作完時,就對該對象的計數-1;這種通過引用計數的方式來判斷對象是否可以被回收的方法就稱為引用計數法。當一個對象計數為0時,則表明該對象可以被垃圾回收。
弊端:
當對象循環引用時會造成對象無法被垃圾回收,從而發送內存泄漏。因此,Java虛擬機沒有采用引用計數法。
可達性分析法
以GC Roots對象作為起始點開始向下搜索,如果一些對象沒有被GC Roots所引用,那么就說這些對象是不可達的,因此可以判定該對象為可回收對象。
就好比盤子里的葡萄,用手提起葡萄根,連在根上沒有掉落的葡萄就是可達的,散落在盤子上的就稱為不可達的,因此在盤子上的葡萄就是可以被回收的。
哪些對象可以作為GC Roots對象呢?
- Java虛擬機棧幀內所引用的對象
- 方法區中被static修飾的對象
- 本地方法棧的對象
- 被上鎖的對象
垃圾回收算法:標記清除、標記整理、copy算法
標記清除
該垃圾回收算法分為兩個階段,標注和清除。標記階段標記出所有需要回收的對象,清除階段回收被標記的對象所占用的空間。
標記:如上圖,先對需要回收的垃圾對象標記為黑色()
清除:隨后將該內存清除
弊端:內存碎片化問題嚴重
標記整理
根據老年代的特點提出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但后續是讓所有存活的對象向一端移動,然后直接清理掉端邊界以外的內存。
該算法分為兩步:標記、整理
如圖,先對可回收的對象進行標記
與標記清除算法不同的是,標記整理第二步會將之前的對象往前挪動,使得對象之間的間隔更為緊湊,從而減少內存碎片的問題。
弊端:雖然減少了內存碎片,但是因為需要挪動對象,造成了性能上的損耗。
復制Copy算法
為了解決效率問題,。它可以將內存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完后,就將還存活的對象復制到另一塊去,然后再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。
步驟:
- 將內存區域分為兩塊,分別為from,to
- 對象一開始在from區域,該區域內存使用完發起垃圾回收時,將存活的對象復制到to區域
- 清除from區域的全部垃圾,同時將from和to的內存區域對調
- 原來的to改為from,from改為to
分代回收算法
分代回收算法的特點就是根據對象的生命周期不同,將存放對象的區域劃分為新生代和老年代。
在新生代中,每次收集都會有大量對象死去,所以可以選擇復制算法,只需要付出少量對象的復制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇**“標記-清除”或“標記-整理”**算法進行垃圾收集
老生代
特點是每次垃圾回收時只有少量對象需要被回收,存放在老年代的對象一般為頻繁使用或占用內存較大的對象
新生代
特點是每次垃圾回收時會有大量的對象需要被回收,絕大部分新生對象在新生代。
新生代劃分了三個區域,分別是Eden、from survivor、to survivor
新生代使用了Copy算法對內存對象進行回收。
流程:
- 新生對象在伊甸園出生
- 當觸發MinorGc時,會將幸存下來的對象放入from,未被回收的對象的年齡+1
- 對調from和to的內存地址,把原來的from變為to,to變為from
四種引用類型
強引用
在Java中,我們最長見到的就是強引用。所謂強引用,就是把一個對象賦給一個引用變量,該對象就稱為被強引用的對象。一個強引用對象,它是處于可達的狀態,因此該對象是不能被垃圾回收的。
軟引用
軟引用需要用 SoftReference 類來實現,對于只有軟引用的對象來說,當系統內存足夠時它不會被回收,當系統內存空間不足時它會被回收。軟引用通常用在對內存敏感的程序中。
弱引用
弱引用需要用 WeakReference 類來實現,它比軟引用的生存期更短,對于只有弱引用的對象來說,只要垃圾回收機制一運行,不管 JVM 的內存空間是否足夠,總會回收該對象占用的內存。
虛引用
虛引用需要 PhantomReference 類來實現,它不能單獨使用,必須和引用隊列聯合使用。 虛引用的主要作用是跟蹤對象被垃圾回收的狀態
總結:
強軟弱虛,強的不能被回收,軟的當系統內存不足時被被回收,弱的不管內存夠不夠都會被回收,虛的還需要結合引用隊列來回收。
| 強引用 | 不會被回收 | 對象的一般狀態 | JVM停止運行時 |
| 軟引用 | 內存不足時被回收 | 對象緩存 | 內存不足時 |
| 弱引用 | 無論內存是否不足都被回收 | 對象緩存 | 垃圾回收后 |
| 虛引用 | 未知-需要結合引用隊列使用 | 未知 | 未知 |
垃圾回收器
吞吐量:
CPU 用于運行用戶代碼的時間/CPU 總消耗時間,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)),高吞吐量可以最高效率地利用 CPU 時間,盡快地完成程序的運算任務,主要適用于在后臺運算而不需要太多交互的任務。
常見的垃圾回收器有:
Serial,翻譯為連續的,是最基本的新生代垃圾收集器,單線程,使用了復制算法,當發生垃圾回收的時候會暫停其他線程的活動(stop the world)
ParNew,Serial垃圾收集器的多線程版本,使用了復制算法
Parallel Scavenge**,是一個新生代垃圾收集器,使用了復制算法,是一個多線程的垃圾收集器,它重點關注的是程序達到一個可控制的吞吐量。
Serial Old,與Serial一樣是單線程的垃圾收集器,作用于老年代,使用標記整理算法
Parallel Old ,是Parallel Scavenge的年老代版本,使用多線程的標記-整理算法,在 JDK1.6才開始提供,提供一個關注吞吐量的模式。
CMS ,是一種老年代垃圾收集器,其最主要目標是獲取最短垃圾
回收停頓時間,它使用多線程的標記-清除算法。最短的垃圾收集停頓時間可以為交互比較高的程序提高用戶體驗
G1,基于標記-整理算法,不產生內存碎片,可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。G1 收集器避免全區域垃圾收集,它把堆內存劃分為大小固定的幾個獨立區域,并且跟蹤這些區域的垃圾收集進度,同時在后臺維護一個優先級列表,每次根據所允許的收集時間,優先回收垃圾最多的區域
Full GC 觸發條件
機管理內存。
等。為了避免以上原因引起的 Full GC,應當盡量不要創建過大的對象以及數組。除此之外,可以通過 -Xmn 虛擬
機參數調大新生代的大小,讓對象盡量在新生代被回收掉,不進入老年代。還可以通過 -XX:MaxTenuringThreshold
調大對象進入老年代的年齡,讓對象在新生代多存活一段時間。
中存放的為一些 Class 的信息、常量、靜態變量等數據。 當系統中要加載的類、反射的類和調用的方法較多時,
永久代可能會被占滿,在未配置為采用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,
那么虛擬機會拋出 java.lang.OutOfMemoryError。 為避免以上原因引起的 Full GC,可采用的方法為增大永久代空
間或轉為使用 CMS GC。
過程中浮動垃圾過多導致暫時性的空間不足),便會報 Concurrent Mode Failure 錯誤,并觸發 Full GC
對象創建過程
引用,并且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載
過程。
否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。 標記-清理不規整,標記-整理以及復制算法是規整
的。在創建對象的時候有一個很重要的問題,就是線程安全,因為在實際開發過程中,創建對象是很頻繁的事情,
作為虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機采用兩種方式來保證線程安全:
CAS+失敗重試: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項
操作,如果因為沖突失敗就重試,直到成功為止。虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性。
TLAB: 為每一個線程預先在 Eden 區分配一塊兒內存, JVM 在給線程中的對象分配內存時,首先在 TLAB 分配,
當對象大于 TLAB 中的剩余內存或 TLAB 的內存已用盡時,再采用上述的 CAS 進行內存分配
操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對
應的零值。85
到類的元數據信息、 對象的哈希碼、 對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬
機當前運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。
來看,對象創建才剛開始, 方法還沒有執行,所有的字段都還為零。所以一般來說,執行 new 指令之后會
接著執行 方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來
對象已經死亡
1;任何時候計數器為 0 的對象就是不可能再被使用的。 很難解決對象之間相互循環引用的問題。
索,節點所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用
的。
兩次標記過程:即第一次標記可達性分析法中不可達的對象。第二次的話就要先判斷該對象有沒有實現 finalize()方
法了,如果沒有實現就直接判斷該對象可回收;如果實現了就會先放在一個隊列中,并由虛擬機建立的一個低優
先級的線程去執行它,隨后就會進行第二次的小規模標記,在這次被標記的對象就會真正的被回收了。
類的加載過程
類的加載過程分別為:加載 -> 連接 -> 初始化 -> 使用 -> 卸載
在連接過程中又分為:驗證 -> 準備 ->解析
加載:主要完成下面三件事情:通過全類名獲得定義此類的二進制字節流,在內存中生成一個代表該類的 Class 對象,作為方法區這些數據的訪問入口。
驗證:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證
準備: 準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存都將在方法區中分配。 進行內存分配
的對象僅包括類變量(static),不包括實例變量;設置的初始值“通常情況”下是數據類型默認的零值,但是加上 final 之
后,就會在這個階段賦值具體值。
解析: 解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。 符號引用就是一組符號來描述目標,可以是
任何字面量。 直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
初始化:必須進行初始化的五種情況:
當遇到 new 、 getstatic、 putstatic 或 invokestatic 這 4 條直接碼指令時,比如 new 一個類,讀取一個靜
態字段(未被 final 修飾)、或調用一個類的靜態方法時。
使用 java.lang.reflect 包的方法對類進行反射調用時如 Class.forname("…"),newInstance()
初始化一個類, 如果其父類還未初始化,則先觸發該父類的初始化。
當虛擬機啟動時,用戶需要定義一個要執行的主類 (包含 main 方法的那個類), 虛擬機會先初始化這個類。
MethodHandle 和 VarHandle 可以看作是輕量級的反射調用機制,而要想使用這 2 個調用, 就必須先使用
findStaticVarHandle 來初始化要調用的類
談談類加載過程的雙親委托機制?
類加載器
JVM 中內置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自
java.lang.ClassLoader:
BootstrapClassLoader(啟動類加載器) :最頂層的加載類,由 C++實現,負責加載 %JAVA_HOME%/lib 目錄下的 jar 包
和類或者或被 -Xbootclasspath 參數指定的路徑中的所有類。 加載一些通用的類: Object 類等。
ExtensionClassLoader(擴展類加載器) :主要負責加載目錄 %JRE_HOME%/lib/ext 目錄下的jar包和類,或被 java.ext.dirs
系統變量所指定的路徑下的 jar 包。
AppClassLoader(應用程序類加載器) :面向我們用戶的加載器,負責加載當前應用 classpath 下的所有 jar 包和類。
Main 類是在 AppClassLoader 里面加載的
雙親委派模型
每一個類都有一個對應它的類加載器。系統中的 ClassLoder 在協同工作的時候會默認使用 雙親委派模型 。
雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應當應當有自己的父類加載器。這里的類加載
器之間的父子關系一般不會以繼承的關系來實現,而是使用組合關系來復用父類加載器的代碼。
AppClassLoader 的父類加載器為 ExtClassLoader
ExtClassLoader 的父類加載器為 null, null 并不代表 ExtClassLoader 沒有父類加載器,而是 BootstrapClassLoader。
在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則才會嘗試加載。
加載的時候,首先會把該請求委派該父類加載器的 loadClass() 處理,因此所有的請求最終都應該傳送到頂層的啟動類加載
器 BootstrapClassLoader 中。當父類加載器無法處理時,才由自己來處理。當父類加載器為 null 時,會使用啟動類加
載器 BootstrapClassLoader 作為父類加載器
雙親委派機制的好處:
雙親委派模型保證了 Java 程序的穩定運行,可以避免類的重復加載
保證了 Java 的核心 API 不被篡改。如果沒有使用雙親委派模型 ,而是每個類加載器加載自己的話就會出現一些問題 ,比如我們編寫一個稱為java.lang.Object 類的話,那么程序運行的時候,系統就會出現多個不同的 Object 類。
如何破壞:
自定義加載器的話,需要繼承 ClassLoader 。如果我們不想打破雙親委派模型,就重寫 ClassLoader 類中的 findClass() 方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。但是,如果想打破雙親委派模型則需要重寫 loadClass() 方法
JVM的內存模型
Java 內存區域和內存模型是不一樣的東西,內存區域是指 Jvm 運行時將數據分區域存儲,強調對內存空間的劃分。
內存模型(Java Memory Model,簡稱 JMM )是定義了線程和主內存之間的抽象關系,即 JMM 定義了 JVM 在計算機內存(RAM)中的工作方式。
Java 內存模型(Java Memory Model,JMM)是 java 虛擬機規范定義的,用來屏蔽掉 java 程序在各種不同的硬件和操作系統對內存的訪問的差異,這樣就可以實現 java 程序在各種不同的平臺上都能達到內存訪問的一致性??梢员苊庀?c等直接使用物理硬件和操作系統的內存模型在不同操作系統和硬件平臺下表現不同,比如有些 c/c程序可能
在 windows 平臺運行正常,而在 linux 平臺卻運行有問題
JVM性能調優常用命令
工具: JConsole: Java 監視與管理控制臺
Visual VM:多合一故障處理工具
命令: jps (JVM Process Status) : 類似 UNIX 的 ps 命令。用戶查看所有 Java 進程的啟動類、傳入參數和 Java 虛擬
機參數等信息;
jstat( JVM Statistics Monitoring Tool) : 用于收集 HotSpot 虛擬機各方面的運行數據;
jinfo (Configuration Info for Java) : Configuration Info forJava,顯示虛擬機配置信息;
jmap (Memory Map for Java) :生成堆轉儲快照;
jhat (JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它會建立一個 HTTP/HTML 服務器,讓用戶可以在
瀏覽器上查看分析結果;
jstack (Stack Trace for Java):生成虛擬機當前時刻的線程快照,線程快照就是當前虛擬機內每一條線程正在執行的
方法堆棧的集合
總結
以上是生活随笔為你收集整理的Java全链路复习面经-基础篇(2.5万字全文)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 伺服系统中惯量辨识的实现方案
- 下一篇: java美元兑换,(Java实现) 美元