摘要:而字節(jié)碼運(yùn)行在之上,所以不用關(guān)心字節(jié)碼是在哪個(gè)操作系統(tǒng)編譯的,只要符合規(guī)范,那么,這個(gè)字節(jié)碼文件就是可運(yùn)行的。好處防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼安全性角度特別說(shuō)明類(lèi)加載器在成功加載某個(gè)類(lèi)之后,會(huì)把得到的類(lèi)的實(shí)例緩存起來(lái)。
前言
只有光頭才能變強(qiáng)
JVM在準(zhǔn)備面試的時(shí)候就有看了,一直沒(méi)時(shí)間寫(xiě)筆記。現(xiàn)在到了一家公司實(shí)習(xí),閑的時(shí)候就寫(xiě)寫(xiě),刷刷JVM博客,刷刷電子書(shū)。
學(xué)習(xí)JVM的目的也很簡(jiǎn)單:
能夠知道JVM是什么,為我們干了什么,具體是怎么干的。能夠理解到一些初學(xué)時(shí)不懂的東西
在面試的時(shí)候有談資
能裝逼
(圖片來(lái)源:https://zhuanlan.zhihu.com/p/25511795,侵刪)
聲明:全文默認(rèn)指的是HotSpot VM一、簡(jiǎn)單聊聊JVM 1.1先來(lái)看看簡(jiǎn)單的Java程序
現(xiàn)在我有一個(gè)JavaBean:
public class Java3y { ? ? // 姓名 ? ? private String name; ? ? // 年齡 ? ? private int age; ? ??? ?//.....各種get/set方法/toString }
一個(gè)測(cè)試類(lèi):
public class Java3yTest { ? ? public static void main(String[] args) { ? ? ? ?? ? ? ? ? Java3y java3y = new Java3y(); ? ? ? ? java3y.setName("Java3y"); ? ? ? ? System.out.println(java3y); ? ? } }
我們?cè)诔鯇W(xué)的時(shí)候肯定用過(guò)javac來(lái)編譯.java文件代碼,用過(guò)java命令來(lái)執(zhí)行編譯后生成的.class文件。
Java源文件:
在使用IDE點(diǎn)擊運(yùn)行的時(shí)候其實(shí)就是將這兩個(gè)命令結(jié)合起來(lái)了(編譯并運(yùn)行),方便我們開(kāi)發(fā)。
生成class文件
解析class文件得到結(jié)果
1.2編譯過(guò)程.java文件是由Java源碼編譯器(上述所說(shuō)的javac.exe)來(lái)完成,流程圖如下所示:
Java源碼編譯由以下三個(gè)過(guò)程組成:
分析和輸入到符號(hào)表
注解處理
語(yǔ)義分析和生成class文件
1.2.1編譯時(shí)期-語(yǔ)法糖語(yǔ)法糖可以看做是編譯器實(shí)現(xiàn)的一些“小把戲”,這些“小把戲”可能會(huì)使得效率“大提升”。
最值得說(shuō)明的就是泛型了,這個(gè)語(yǔ)法糖可以說(shuō)我們是經(jīng)常會(huì)使用到的!
泛型只會(huì)在Java源碼中存在,編譯過(guò)后會(huì)被替換為原來(lái)的原生類(lèi)型(Raw Type,也稱(chēng)為裸類(lèi)型)了。這個(gè)過(guò)程也被稱(chēng)為:泛型擦除。
有了泛型這顆語(yǔ)法糖以后:
代碼更加簡(jiǎn)潔【不用強(qiáng)制轉(zhuǎn)換】
程序更加健壯【只要編譯時(shí)期沒(méi)有警告,那么運(yùn)行時(shí)期就不會(huì)出現(xiàn)ClassCastException異?!?/p>
可讀性和穩(wěn)定性【在編寫(xiě)集合的時(shí)候,就限定了類(lèi)型】
了解泛型更多的知識(shí):
https://segmentfault.com/a/1190000014120746
1.3JVM實(shí)現(xiàn)跨平臺(tái)至此,我們通過(guò)javac.exe編譯器編譯我們的.java源代碼文件生成出.class文件了!
這些.class文件很明顯是不能直接運(yùn)行的,它不像C語(yǔ)言(編譯cpp后生成exe文件直接運(yùn)行)
這些.class文件是交由JVM來(lái)解析運(yùn)行!
JVM是運(yùn)行在操作系統(tǒng)之上的,每個(gè)操作系統(tǒng)的指令是不同的,而JDK是區(qū)分操作系統(tǒng)的,只要你的本地系統(tǒng)裝了JDK,這個(gè)JDK就是能夠和當(dāng)前系統(tǒng)兼容的。
而class字節(jié)碼運(yùn)行在JVM之上,所以不用關(guān)心class字節(jié)碼是在哪個(gè)操作系統(tǒng)編譯的,只要符合JVM規(guī)范,那么,這個(gè)字節(jié)碼文件就是可運(yùn)行的。
所以Java就做到了跨平臺(tái)--->一次編譯,到處運(yùn)行!
1.4class文件和JVM的恩怨情仇 1.4.1類(lèi)的加載時(shí)機(jī)現(xiàn)在我們例子中生成的兩個(gè).class文件都會(huì)直接被加載到JVM中嗎??
虛擬機(jī)規(guī)范則是嚴(yán)格規(guī)定了有且只有5種情況必須立即對(duì)類(lèi)進(jìn)行“初始化”(class文件加載到JVM中):
創(chuàng)建類(lèi)的實(shí)例(new 的方式)。訪問(wèn)某個(gè)類(lèi)或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值,調(diào)用類(lèi)的靜態(tài)方法
反射的方式
初始化某個(gè)類(lèi)的子類(lèi),則其父類(lèi)也會(huì)被初始化
Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類(lèi)的類(lèi),直接使用java.exe命令來(lái)運(yùn)行某個(gè)主類(lèi)(包含main方法的那個(gè)類(lèi))
當(dāng)使用JDK1.7的動(dòng)態(tài)語(yǔ)言支持時(shí)(....)
所以說(shuō):
Java類(lèi)的加載是動(dòng)態(tài)的,它并不會(huì)一次性將所有類(lèi)全部加載后再運(yùn)行,而是保證程序運(yùn)行的基礎(chǔ)類(lèi)(像是基類(lèi))完全加載到j(luò)vm中,至于其他類(lèi),則在需要的時(shí)候才加載。這當(dāng)然就是為了節(jié)省內(nèi)存開(kāi)銷(xiāo)。
1.4.2如何將類(lèi)加載到j(luò)vmclass文件是通過(guò)類(lèi)的加載器裝載到j(luò)vm中的!
Java默認(rèn)有三種類(lèi)加載器:
各個(gè)加載器的工作責(zé)任:
1)Bootstrap ClassLoader:負(fù)責(zé)加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實(shí)現(xiàn),不是ClassLoader子類(lèi)
2)Extension ClassLoader:負(fù)責(zé)加載java平臺(tái)中擴(kuò)展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目錄下的jar包
3)App ClassLoader:負(fù)責(zé)記載classpath中指定的jar包及目錄中class
工作過(guò)程:
1、當(dāng)AppClassLoader加載一個(gè)class時(shí),它首先不會(huì)自己去嘗試加載這個(gè)類(lèi),而是把類(lèi)加載請(qǐng)求委派給父類(lèi)加載器ExtClassLoader去完成。
2、當(dāng)ExtClassLoader加載一個(gè)class時(shí),它首先也不會(huì)自己去嘗試加載這個(gè)類(lèi),而是把類(lèi)加載請(qǐng)求委派給BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib里未查找到該class),會(huì)使用ExtClassLoader來(lái)嘗試加載;
4、若ExtClassLoader也加載失敗,則會(huì)使用AppClassLoader來(lái)加載
5、如果AppClassLoader也加載失敗,則會(huì)報(bào)出異常ClassNotFoundException
其實(shí)這就是所謂的雙親委派模型。簡(jiǎn)單來(lái)說(shuō):如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類(lèi),而是把請(qǐng)求委托給父加載器去完成,依次向上。
好處:
防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼(安全性角度)
特別說(shuō)明:
類(lèi)加載器在成功加載某個(gè)類(lèi)之后,會(huì)把得到的 java.lang.Class類(lèi)的實(shí)例緩存起來(lái)。下次再請(qǐng)求加載該類(lèi)的時(shí)候,類(lèi)加載器會(huì)直接使用緩存的類(lèi)的實(shí)例,而不會(huì)嘗試再次加載。
1.4.2類(lèi)加載詳細(xì)過(guò)程加載器加載到j(luò)vm中,接下來(lái)其實(shí)又分了好幾個(gè)步驟:
加載,查找并加載類(lèi)的二進(jìn)制數(shù)據(jù),在Java堆中也創(chuàng)建一個(gè)java.lang.Class類(lèi)的對(duì)象。
連接,連接又包含三塊內(nèi)容:驗(yàn)證、準(zhǔn)備、初始化。
?? ?- 1)驗(yàn)證,文件格式、元數(shù)據(jù)、字節(jié)碼、符號(hào)引用驗(yàn)證;
?? ?- 2)準(zhǔn)備,為類(lèi)的靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值;
?? ?- 3)解析,把類(lèi)中的符號(hào)引用轉(zhuǎn)換為直接引用
初始化,為類(lèi)的靜態(tài)變量賦予正確的初始值。
1.4.3JIT即時(shí)編輯器一般我們可能會(huì)想:JVM在加載了這些class文件以后,針對(duì)這些字節(jié)碼,逐條取出,逐條執(zhí)行-->解析器解析。
但如果是這樣的話,那就太慢了!
我們的JVM是這樣實(shí)現(xiàn)的:
就是把這些Java字節(jié)碼重新編譯優(yōu)化,生成機(jī)器碼,讓CPU直接執(zhí)行。這樣編出來(lái)的代碼效率會(huì)更高。
編譯也是要花費(fèi)時(shí)間的,我們一般對(duì)熱點(diǎn)代碼做編譯,非熱點(diǎn)代碼直接解析就好了。
熱點(diǎn)代碼解釋?zhuān)阂?、多次調(diào)用的方法。二、多次執(zhí)行的循環(huán)體
使用熱點(diǎn)探測(cè)來(lái)檢測(cè)是否為熱點(diǎn)代碼,熱點(diǎn)探測(cè)有兩種方式:
采樣
計(jì)數(shù)器
目前HotSpot使用的是計(jì)數(shù)器的方式,它為每個(gè)方法準(zhǔn)備了兩類(lèi)計(jì)數(shù)器:
方法調(diào)用計(jì)數(shù)器(Invocation ?Counter)
回邊計(jì)數(shù)器(Back ?EdgeCounter)。
在確定虛擬機(jī)運(yùn)行參數(shù)的前提下,這兩個(gè)計(jì)數(shù)器都有一個(gè)確定的閾值,當(dāng)計(jì)數(shù)器超過(guò)閾值溢出了,就會(huì)觸發(fā)JIT編譯。
1.4.4回到例子中按我們程序來(lái)走,我們的Java3yTest.class文件會(huì)被AppClassLoader加載器(因?yàn)镋xtClassLoader和BootStrap加載器都不會(huì)加載它[雙親委派模型])加載到JVM中。
隨后發(fā)現(xiàn)了要使用Java3y這個(gè)類(lèi),我們的Java3y.class文件會(huì)被AppClassLoader加載器(因?yàn)镋xtClassLoader和BootStrap加載器都不會(huì)加載它[雙親委派模型])加載到JVM中
詳情參考:
https://www.mrsssswan.club/2018/06/30/jvm-start1/---淺解JVM加載class文件
https://zhuanlan.zhihu.com/p/28476709---JVM雜談之JIT
擴(kuò)展閱讀:
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/---深入探討 Java 類(lèi)加載器
https://www.ibm.com/developerworks/cn/java/j-lo-just-in-time/---深入淺出 JIT 編譯器
https://www.zhihu.com/question/46719811---Java 類(lèi)加載器(ClassLoader)的實(shí)際使用場(chǎng)景有哪些?
1.5類(lèi)加載完以后JVM干了什么?在類(lèi)加載檢查通過(guò)后,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存。
1.5.1JVM的內(nèi)存結(jié)構(gòu)首先我們來(lái)了解一下JVM的內(nèi)存結(jié)構(gòu)的怎么樣的:
基于jdk1.8畫(huà)的JVM的內(nèi)存結(jié)構(gòu)--->我畫(huà)得比較細(xì)。
簡(jiǎn)單看了一下內(nèi)存結(jié)構(gòu),簡(jiǎn)單看看每個(gè)區(qū)域究竟存儲(chǔ)的是什么(干的是什么):
堆:存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存
虛擬機(jī)棧:虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存結(jié)構(gòu):每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口等信息
本地方法棧:本地方法棧則是為虛擬機(jī)使用到的Native方法服務(wù)。
方法區(qū):存儲(chǔ)已被虛擬機(jī)加載的類(lèi)元數(shù)據(jù)信息(元空間)
程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器
1.5.2例子中的流程我來(lái)宏觀簡(jiǎn)述一下我們的例子中的工作流程:
1、通過(guò)java.exe運(yùn)行Java3yTest.class,隨后被加載到JVM中,元空間存儲(chǔ)著類(lèi)的信息(包括類(lèi)的名稱(chēng)、方法信息、字段信息..)。
2、然后JVM找到Java3yTest的主函數(shù)入口(main),為main函數(shù)創(chuàng)建棧幀,開(kāi)始執(zhí)行main函數(shù)
3、main函數(shù)的第一條命令是Java3y java3y = new Java3y();就是讓JVM創(chuàng)建一個(gè)Java3y對(duì)象,但是這時(shí)候方法區(qū)中沒(méi)有Java3y類(lèi)的信息,所以JVM馬上加載Java3y類(lèi),把Java3y類(lèi)的類(lèi)型信息放到方法區(qū)中(元空間)
4、加載完Java3y類(lèi)之后,Java虛擬機(jī)做的第一件事情就是在堆區(qū)中為一個(gè)新的Java3y實(shí)例分配內(nèi)存, 然后調(diào)用構(gòu)造函數(shù)初始化Java3y實(shí)例,這個(gè)Java3y實(shí)例持有著指向方法區(qū)的Java3y類(lèi)的類(lèi)型信息(其中包含有方法表,java動(dòng)態(tài)綁定的底層實(shí)現(xiàn))的引用
5、當(dāng)使用java3y.setName("Java3y");的時(shí)候,JVM根據(jù)java3y引用找到Java3y對(duì)象,然后根據(jù)Java3y對(duì)象持有的引用定位到方法區(qū)中Java3y類(lèi)的類(lèi)型信息的方法表,獲得setName()函數(shù)的字節(jié)碼的地址
6、為setName()函數(shù)創(chuàng)建棧幀,開(kāi)始運(yùn)行setName()函數(shù)
從微觀上其實(shí)還做了很多東西,正如上面所說(shuō)的類(lèi)加載過(guò)程(加載-->連接(驗(yàn)證,準(zhǔn)備,解析)-->初始化),在類(lèi)加載完之后jvm為其分配內(nèi)存(分配內(nèi)存中也做了非常多的事)。由于這些步驟并不是一步一步往下走,會(huì)有很多的“混沌bootstrap”的過(guò)程,所以很難描述清楚。
擴(kuò)展閱讀(先有Class對(duì)象還是先有Object):https://www.zhihu.com/question/30301819
參考資料:
http://www.cnblogs.com/qiumingcheng/p/5398610.html---Java程序編譯和運(yùn)行的過(guò)程
https://zhuanlan.zhihu.com/p/25713880---Java JVM 運(yùn)行機(jī)制及基本原理
1.6簡(jiǎn)單聊聊各種常量池在寫(xiě)這篇文章的時(shí)候,原本以為我對(duì)String s = "aaa";類(lèi)似這些題目已經(jīng)是不成問(wèn)題了,直到我遇到了String.intern()這樣的方法與諸如String s1 = new String("1") + new String("2"); 混合一起用的時(shí)候
我發(fā)現(xiàn),我還是太年輕了。
首先我是先閱讀了美團(tuán)技術(shù)團(tuán)隊(duì)的這篇文章:https://tech.meituan.com/in_depth_understanding_string_intern.html---深入解析String#intern
嗯,然后就懵逼了。我摘抄一下他的例子:
public static void main(String[] args) { ? ? String s = new String("1"); ? ? s.intern(); ? ? String s2 = "1"; ? ? System.out.println(s == s2); ? ? String s3 = new String("1") + new String("1"); ? ? s3.intern(); ? ? String s4 = "11"; ? ? System.out.println(s3 == s4); }
打印結(jié)果是
jdk7,8下false true
調(diào)換一下位置后:
public static void main(String[] args) { ? ? String s = new String("1"); ? ? String s2 = "1"; ? ? s.intern(); ? ? System.out.println(s == s2); ? ? String s3 = new String("1") + new String("1"); ? ? String s4 = "11"; ? ? s3.intern(); ? ? System.out.println(s3 == s4); }
打印結(jié)果為:
jdk7,8下false false
文章中有很詳細(xì)的解析,但我簡(jiǎn)單閱讀了幾次以后還是很懵逼。所以我知道了自己的知識(shí)點(diǎn)還存在漏洞,后面閱讀了一下R大之前寫(xiě)過(guò)的文章:
http://rednaxelafx.iteye.com/blog/774673#comments---請(qǐng)別再拿“String s = new String("xyz");創(chuàng)建了多少個(gè)String實(shí)例”來(lái)面試了吧
看完了之后,就更加懵逼了。
后來(lái),在zhihu上看到了這個(gè)回答:
https://www.zhihu.com/question/55994121---Java 中new String("字面量") 中 "字面量" 是何時(shí)進(jìn)入字符串常量池的?
結(jié)合網(wǎng)上資料和自己的思考,下面整理一下對(duì)常量池的理解~~
1.6.1各個(gè)常量池的情況針對(duì)于jdk1.7之后:
運(yùn)行時(shí)常量池位于堆中
字符串常量池位于堆中
常量池存儲(chǔ)的是:
字面量(Literal):文本字符串等---->用雙引號(hào)引起來(lái)的字符串字面量都會(huì)進(jìn)這里面
符號(hào)引用(Symbolic References)
類(lèi)和接口的全限定名(Full Qualified Name)
字段的名稱(chēng)和描述符(Descriptor)
方法的名稱(chēng)和描述符
常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類(lèi)加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放--->來(lái)源:深入理解Java虛擬機(jī) JVM高級(jí)特性與最佳實(shí)踐(第二版)
現(xiàn)在我們的運(yùn)行時(shí)常量池只是換了一個(gè)位置(原本來(lái)方法區(qū),現(xiàn)在在堆中),但可以明確的是:類(lèi)加載后,常量池中的數(shù)據(jù)會(huì)在運(yùn)行時(shí)常量池中存放!
別人總結(jié)的常量池:
它是Class文件中的內(nèi)容,還不是運(yùn)行時(shí)的內(nèi)容,不要理解它是個(gè)池子,其實(shí)就是Class文件中的字節(jié)碼指令
字符串常量池:
HotSpot VM里,記錄interned string的一個(gè)全局表叫做StringTable,它本質(zhì)上就是個(gè)HashSet。注意它只存儲(chǔ)對(duì)java.lang.String實(shí)例的引用,而不存儲(chǔ)String對(duì)象的內(nèi)容
字符串常量池只存儲(chǔ)引用,不存儲(chǔ)內(nèi)容!
再來(lái)看一下我們的intern方法:
?* When the intern method is invoked, if the pool already contains a ?* string equal to this {@code String} object as determined by ?* the {@link #equals(Object)} method, then the string from the pool is ?* returned. Otherwise, this {@code String} object is added to the ?* pool and a reference to this {@code String} object is returned. ?
如果常量池中存在當(dāng)前字符串,那么直接返回常量池中它的引用。
如果常量池中沒(méi)有此字符串, 會(huì)將此字符串引用保存到常量池中后, 再直接返回該字符串的引用!
1.6.2解析題目本來(lái)打算寫(xiě)注釋的方式來(lái)解釋的,但好像挺難說(shuō)清楚的。我還是畫(huà)圖吧...
public static void main(String[] args) { ? ? ? String s = new String("1"); ? ? s.intern(); ? ? String s2 = "1"; ? ? System.out.println(s == s2);// false ? ? System.out.println("-----------關(guān)注公眾號(hào):Java3y-------------"); }
第一句:String s = new String("1");
第二句:s.intern();發(fā)現(xiàn)字符串常量池中已經(jīng)存在"1"字符串對(duì)象,直接返回字符串常量池中對(duì)堆的引用(但沒(méi)有接收)-->此時(shí)s引用還是指向著堆中的對(duì)象
第三句:String s2 = "1";發(fā)現(xiàn)字符串常量池已經(jīng)保存了該對(duì)象的引用了,直接返回字符串常量池對(duì)堆中字符串的引用
很容易看到,兩條引用是不一樣的!所以返回false。
? ? public static void main(String[] args) { ? ? ? ? System.out.println("-----------關(guān)注公眾號(hào):Java3y-------------"); ? ? ? ? String s3 = new String("1") + new String("1"); ? ? ? ? s3.intern(); ? ? ? ? String s4 = "11"; ? ? ? ? System.out.println(s3 == s4); // true ? ? }
第一句:String s3 = new String("1") + new String("1");注意:此時(shí)"11"對(duì)象并沒(méi)有在字符串常量池中保存引用。
第二句:s3.intern();發(fā)現(xiàn)"11"對(duì)象并沒(méi)有在字符串常量池中,于是將"11"對(duì)象在字符串常量池中保存當(dāng)前字符串的引用,并返回當(dāng)前字符串的引用(但沒(méi)有接收)
第三句:String s4 = "11";發(fā)現(xiàn)字符串常量池已經(jīng)存在引用了,直接返回(拿到的也是與s3相同指向的引用)
根據(jù)上述所說(shuō)的:最后會(huì)返回true~~~
如果還是不太清楚的同學(xué),可以試著接收一下intern()方法的返回值,再看看上述的圖,應(yīng)該就可以理解了。
下面的就由各位來(lái)做做,看是不是掌握了:
? ? public static void main(String[] args) { ? ? ? ? String s = new String("1"); ? ? ? ? String s2 = "1"; ? ? ? ? s.intern(); ? ? ? ? System.out.println(s == s2);//false ? ? ? ? String s3 = new String("1") + new String("1"); ? ? ? ? String s4 = "11"; ? ? ? ? s3.intern(); ? ? ? ? System.out.println(s3 == s4);//false ? ? }
還有:
? ? public static void main(String[] args) { ? ? ? ? String s1 = new String("he") + new String("llo"); ? ? ? ? String s2 = new String("h") + new String("ello"); ? ? ? ? String s3 = s1.intern(); ? ? ? ? String s4 = s2.intern(); ? ? ? ? System.out.println(s1 == s3);// true ? ? ? ? System.out.println(s1 == s4);// true ? ? }1.7GC垃圾回收
可以說(shuō)GC垃圾回收是JVM中一個(gè)非常重要的知識(shí)點(diǎn),應(yīng)該非常詳細(xì)去講解的。但在我學(xué)習(xí)的途中,我已經(jīng)發(fā)現(xiàn)了有很好的文章去講解垃圾回收的了。
所以,這里我只簡(jiǎn)單介紹一下垃圾回收的東西,詳細(xì)的可以到下面的面試題中查閱和最后給出相關(guān)的資料閱
讀吧~
在C++中,我們知道創(chuàng)建出的對(duì)象是需要手動(dòng)去delete掉的。我們Java程序運(yùn)行在JVM中,JVM可以幫我們“自動(dòng)”回收不需要的對(duì)象,對(duì)我們來(lái)說(shuō)是十分方便的。
雖然說(shuō)“自動(dòng)”回收了我們不需要的對(duì)象,但如果我們想變強(qiáng),就要變禿..不對(duì),就要去了解一下它究竟是怎么干的,理論的知識(shí)有哪些。
首先,JVM回收的是垃圾,垃圾就是我們程序中已經(jīng)是不需要的了。垃圾收集器在對(duì)堆進(jìn)行回收前,第一件事情就是要確定這些對(duì)象之中哪些還“存活”著,哪些已經(jīng)“死去”。判斷哪些對(duì)象“死去”常用有兩種方式:
引用計(jì)數(shù)法-->這種難以解決對(duì)象之間的循環(huán)引用的問(wèn)題
可達(dá)性分析算法-->主流的JVM采用的是這種方式
現(xiàn)在已經(jīng)可以判斷哪些對(duì)象已經(jīng)“死去”了,我們現(xiàn)在要對(duì)這些“死去”的對(duì)象進(jìn)行回收,回收也有好幾種算法:
標(biāo)記-清除算法
復(fù)制算法
標(biāo)記-整理算法
分代收集算法
(這些算法詳情可看下面的面試題內(nèi)容)~
無(wú)論是可達(dá)性分析算法,還是垃圾回收算法,JVM使用的都是準(zhǔn)確式GC。JVM是使用一組稱(chēng)為OopMap的數(shù)據(jù)結(jié)構(gòu),來(lái)存儲(chǔ)所有的對(duì)象引用(這樣就不用遍歷整個(gè)內(nèi)存去查找了,空間換時(shí)間)。
并且不會(huì)將所有的指令都生成OopMap,只會(huì)在安全點(diǎn)上生成OopMap,在安全區(qū)域上開(kāi)始GC。
在OopMap的協(xié)助下,HotSpot可以快速且準(zhǔn)確地完成GC Roots枚舉(可達(dá)性分析)。
上面所講的垃圾收集算法只能算是方法論,落地實(shí)現(xiàn)的是垃圾收集器:
Serial收集器
ParNew收集器
Parallel Scavenge收集器
Serial Old收集器
Parallel Old收集器
CMS收集器
G1收集器
上面這些收集器大部分是可以互相組合使用的
1.8JVM參數(shù)與調(diào)優(yōu)很多做過(guò)JavaWeb項(xiàng)目(ssh/ssm)這樣的同學(xué)可能都會(huì)遇到過(guò)OutOfMemory這樣的錯(cuò)誤。一般解決起來(lái)也很方便,在啟動(dòng)的時(shí)候加個(gè)參數(shù)就行了。
上面也說(shuō)了很多關(guān)于JVM的東西--->JVM對(duì)內(nèi)存的劃分啊,JVM各種的垃圾收集器啊。
內(nèi)存的分配的大小啊,使用哪個(gè)收集器啊,這些都可以由我們根據(jù)需求,現(xiàn)實(shí)情況來(lái)指定的,這里就不詳細(xì)說(shuō)了,等真正用到的時(shí)候才回來(lái)填坑吧~~~~
參考資料:
http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html---JVM系列三:JVM參數(shù)設(shè)置、分析
二、JVM面試題拿些常見(jiàn)的JVM面試題來(lái)做做,加深一下理解和查缺補(bǔ)漏:
1、詳細(xì)jvm內(nèi)存結(jié)構(gòu)
2、講講什么情況下回出現(xiàn)內(nèi)存溢出,內(nèi)存泄漏?
3、說(shuō)說(shuō)Java線程棧
4、JVM 年輕代到年老代的晉升過(guò)程的判斷條件是什么呢?
5、JVM 出現(xiàn) fullGC 很頻繁,怎么去線上排查問(wèn)題?
6、類(lèi)加載為什么要使用雙親委派模式,有沒(méi)有什么場(chǎng)景是打破了這個(gè)模式?
7、類(lèi)的實(shí)例化順序
8、JVM垃圾回收機(jī)制,何時(shí)觸發(fā)MinorGC等操作
9、JVM 中一次完整的 GC 流程(從 ygc 到 fgc)是怎樣的
10、各種回收器,各自?xún)?yōu)缺點(diǎn),重點(diǎn)CMS、G1
11、各種回收算法
12、OOM錯(cuò)誤,stackoverflow錯(cuò)誤,permgen space錯(cuò)誤
題目來(lái)源:
https://www.jianshu.com/p/a07d1d4004b0
2.1詳細(xì)jvm內(nèi)存結(jié)構(gòu)根據(jù) JVM 規(guī)范,JVM 內(nèi)存共分為虛擬機(jī)棧、堆、方法區(qū)、程序計(jì)數(shù)器、本地方法棧五個(gè)部分。
具體可能會(huì)聊聊jdk1.7以前的PermGen(永久代),替換成Metaspace(元空間)
原本永久代存儲(chǔ)的數(shù)據(jù):符號(hào)引用(Symbols)轉(zhuǎn)移到了native heap;字面量(interned strings)轉(zhuǎn)移到了java heap;類(lèi)的靜態(tài)變量(class statics)轉(zhuǎn)移到了java heap
Metaspace(元空間)存儲(chǔ)的是類(lèi)的元數(shù)據(jù)信息(metadata)
元空間的本質(zhì)和永久代類(lèi)似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過(guò)元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。
替換的好處:一、字符串存在永久代中,容易出現(xiàn)性能問(wèn)題和內(nèi)存溢出。二、永久代會(huì)為 GC 帶來(lái)不必要的復(fù)雜度,并且回收效率偏低
圖片來(lái)源:https://blog.csdn.net/tophawk/article/details/78704074
參考資料:
https://www.cnblogs.com/paddix/p/5309550.html
2.2講講什么情況下回出現(xiàn)內(nèi)存溢出,內(nèi)存泄漏?內(nèi)存泄漏的原因很簡(jiǎn)單:
對(duì)象是可達(dá)的(一直被引用)
但是對(duì)象不會(huì)被使用
常見(jiàn)的內(nèi)存泄漏例子:
?public static void main(String[] args) { ? ? ? ? Set set = new HashSet(); ? ? ? ? for (int i = 0; i < 10; i++) { ? ? ? ? ? ? Object object = new Object(); ? ? ? ? ? ? set.add(object); ? ? ? ? ? ? // 設(shè)置為空,這對(duì)象我不再用了 ? ? ? ? ? ? object = null; ? ? ? ? } ? ? ? ? // 但是set集合中還維護(hù)這obj的引用,gc不會(huì)回收object對(duì)象 ? ? ? ? System.out.println(set); ? ? }
解決這個(gè)內(nèi)存泄漏問(wèn)題也很簡(jiǎn)單,將set設(shè)置為null,那就可以避免上訴內(nèi)存泄漏問(wèn)題了。其他內(nèi)存泄漏得一步一步分析了。
內(nèi)存泄漏參考資料:
https://www.ibm.com/developerworks/cn/java/l-JavaMemoryLeak/
內(nèi)存溢出的原因:
內(nèi)存泄露導(dǎo)致堆棧內(nèi)存不斷增大,從而引發(fā)內(nèi)存溢出。
大量的jar,class文件加載,裝載類(lèi)的空間不夠,溢出
操作大量的對(duì)象導(dǎo)致堆內(nèi)存空間已經(jīng)用滿了,溢出
nio直接操作內(nèi)存,內(nèi)存過(guò)大導(dǎo)致溢出
解決:
查看程序是否存在內(nèi)存泄漏的問(wèn)題
設(shè)置參數(shù)加大空間
代碼中是否存在死循環(huán)或循環(huán)產(chǎn)生過(guò)多重復(fù)的對(duì)象實(shí)體、
查看是否使用了nio直接操作內(nèi)存。
參考資料:
https://www.cnblogs.com/bingosblog/p/6661527.html
http://www.importnew.com/14604.html
2.3說(shuō)說(shuō)線程棧這里的線程棧應(yīng)該指的是虛擬機(jī)棧吧...
JVM規(guī)范讓每個(gè)Java線程擁有自己的獨(dú)立的JVM棧,也就是Java方法的調(diào)用棧。
當(dāng)方法調(diào)用的時(shí)候,會(huì)生成一個(gè)棧幀。棧幀是保存在虛擬機(jī)棧中的,棧幀存儲(chǔ)了方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)連接和方法返回地址等信息
線程運(yùn)行過(guò)程中,只有一個(gè)棧幀是處于活躍狀態(tài),稱(chēng)為“當(dāng)前活躍棧幀”,當(dāng)前活動(dòng)棧幀始終是虛擬機(jī)棧的棧頂元素。
通過(guò)jstack工具查看線程狀態(tài)
參考資料:
http://wangwengcn.iteye.com/blog/1622195
https://www.cnblogs.com/Codenewbie/p/6184898.html
https://blog.csdn.net/u011734144/article/details/60965155
2.4JVM 年輕代到年老代的晉升過(guò)程的判斷條件是什么呢?部分對(duì)象會(huì)在From和To區(qū)域中復(fù)制來(lái)復(fù)制去,如此交換15次(由JVM參數(shù)MaxTenuringThreshold決定,這個(gè)參數(shù)默認(rèn)是15),最終如果還是存活,就存入到老年代。
如果對(duì)象的大小大于Eden的二分之一會(huì)直接分配在old,如果old也分配不下,會(huì)做一次majorGC,如果小于eden的一半但是沒(méi)有足夠的空間,就進(jìn)行minorgc也就是新生代GC。
minor gc后,survivor仍然放不下,則放到老年代
動(dòng)態(tài)年齡判斷 ,大于等于某個(gè)年齡的對(duì)象超過(guò)了survivor空間一半 ,大于等于某個(gè)年齡的對(duì)象直接進(jìn)入老年代
2.5JVM 出現(xiàn) fullGC 很頻繁,怎么去線上排查問(wèn)題這題就依據(jù)full GC的觸發(fā)條件來(lái)做:
如果有perm gen的話(jdk1.8就沒(méi)了),要給perm gen分配空間,但沒(méi)有足夠的空間時(shí),會(huì)觸發(fā)full gc。
?? ?- 所以看看是不是perm gen區(qū)的值設(shè)置得太小了。
System.gc()方法的調(diào)用
?? ?- 這個(gè)一般沒(méi)人去調(diào)用吧~~~
?當(dāng)統(tǒng)計(jì)得到的Minor GC晉升到舊生代的平均大小大于老年代的剩余空間,則會(huì)觸發(fā)full gc(這就可以從多個(gè)角度上看了)
?? ?- 是不是頻繁創(chuàng)建了大對(duì)象(也有可能eden區(qū)設(shè)置過(guò)小)(大對(duì)象直接分配在老年代中,導(dǎo)致老年代空間不足--->從而頻繁gc)
?? ?- 是不是老年代的空間設(shè)置過(guò)小了(Minor GC幾個(gè)對(duì)象就大于老年代的剩余空間了)
雙親委托模型的重要用途是為了解決類(lèi)載入過(guò)程中的安全性問(wèn)題。
假設(shè)有一個(gè)開(kāi)發(fā)者自己編寫(xiě)了一個(gè)名為java.lang.Object的類(lèi),想借此欺騙JVM?,F(xiàn)在他要使用自定義ClassLoader來(lái)加載自己編寫(xiě)的java.lang.Object類(lèi)。
然而幸運(yùn)的是,雙親委托模型不會(huì)讓他成功。因?yàn)镴VM會(huì)優(yōu)先在Bootstrap ClassLoader的路徑下找到java.lang.Object類(lèi),并載入它
Java的類(lèi)加載是否一定遵循雙親委托模型?
在實(shí)際開(kāi)發(fā)中,我們可以通過(guò)自定義ClassLoader,并重寫(xiě)父類(lèi)的loadClass方法,來(lái)打破這一機(jī)制。
SPI就是打破了雙親委托機(jī)制的(SPI:服務(wù)提供發(fā)現(xiàn))。SPI資料:
?? ?- https://zhuanlan.zhihu.com/p/28909673
?? ?- https://www.cnblogs.com/huzi007/p/6679215.html
?? ?- https://blog.csdn.net/sigangjun/article/details/79071850
參考資料:
https://blog.csdn.net/markzy/article/details/53192993
2.7類(lèi)的實(shí)例化順序1. 父類(lèi)靜態(tài)成員和靜態(tài)初始化塊 ,按在代碼中出現(xiàn)的順序依次執(zhí)行
2. 子類(lèi)靜態(tài)成員和靜態(tài)初始化塊 ,按在代碼中出現(xiàn)的順序依次執(zhí)行
3. 父類(lèi)實(shí)例成員和實(shí)例初始化塊 ,按在代碼中出現(xiàn)的順序依次執(zhí)行
4. 父類(lèi)構(gòu)造方法
5. 子類(lèi)實(shí)例成員和實(shí)例初始化塊 ,按在代碼中出現(xiàn)的順序依次執(zhí)行
6. 子類(lèi)構(gòu)造方法
檢驗(yàn)一下是不是真懂了:
class Dervied extends Base { ? ? private String name = "Java3y"; ? ? public Dervied() { ? ? ? ? tellName(); ? ? ? ? printName(); ? ? } ? ? public void tellName() { ? ? ? ? System.out.println("Dervied tell name: " + name); ? ? } ? ? public void printName() { ? ? ? ? System.out.println("Dervied print name: " + name); ? ? } ? ? public static void main(String[] args) { ? ? ? ? new Dervied(); ? ? } } class Base { ? ? private String name = "公眾號(hào)"; ? ? public Base() { ? ? ? ? tellName(); ? ? ? ? printName(); ? ? } ? ? public void tellName() { ? ? ? ? System.out.println("Base tell name: " + name); ? ? } ? ? public void printName() { ? ? ? ? System.out.println("Base print name: " + name); ? ? } }
輸出數(shù)據(jù):
Dervied tell name: null Dervied print name: null Dervied tell name: Java3y Dervied print name: Java3y
第一次做錯(cuò)的同學(xué)點(diǎn)個(gè)贊,加個(gè)關(guān)注不過(guò)分吧(hahaha
2.8JVM垃圾回收機(jī)制,何時(shí)觸發(fā)MinorGC等操作當(dāng)young gen中的eden區(qū)分配滿的時(shí)候觸發(fā)MinorGC(新生代的空間不夠放的時(shí)候).
2.9JVM 中一次完整的 GC 流程(從 ygc 到 fgc)是怎樣的YGC和FGC是什么?
YGC :對(duì)新生代堆進(jìn)行g(shù)c。頻率比較高,因?yàn)榇蟛糠謱?duì)象的存活壽命較短,在新生代里被回收。性能耗費(fèi)較小。
FGC :全堆范圍的gc。默認(rèn)堆空間使用到達(dá)80%(可調(diào)整)的時(shí)候會(huì)觸發(fā)fgc。以我們生產(chǎn)環(huán)境為例,一般比較少會(huì)觸發(fā)fgc,有時(shí)10天或一周左右會(huì)有一次。
什么時(shí)候執(zhí)行YGC和FGC
a.eden空間不足,執(zhí)行 young gc
b.old空間不足,perm空間不足,調(diào)用方法System.gc() ,ygc時(shí)的悲觀策略, dump live的內(nèi)存信息時(shí)(jmap –dump:live),都會(huì)執(zhí)行full gc
2.10各種回收算法GC最基礎(chǔ)的算法有三種:
標(biāo)記 -清除算法
復(fù)制算法
標(biāo)記-壓縮算法
我們常用的垃圾回收器一般都采用分代收集算法(其實(shí)就是組合上面的算法,不同的區(qū)域使用不同的算法)。
具體:
標(biāo)記-清除算法,“標(biāo)記-清除”(Mark-Sweep)算法,如它的名字一樣,算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象。
復(fù)制算法,“復(fù)制”(Copying)的收集算法,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。
標(biāo)記-壓縮算法,標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存
分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/p> 2.11各種回收器,各自?xún)?yōu)缺點(diǎn),重點(diǎn)CMS、G1
圖來(lái)源于《深入理解Java虛擬機(jī):JVM高級(jí)特效與最佳實(shí)現(xiàn)》,圖中兩個(gè)收集器之間有連線,說(shuō)明它們可以配合使用.
Serial收集器,串行收集器是最古老,最穩(wěn)定以及效率高的收集器,但可能會(huì)產(chǎn)生較長(zhǎng)的停頓,只使用一個(gè)線程去回收。
ParNew收集器,ParNew收集器其實(shí)就是Serial收集器的多線程版本。
Parallel收集器,Parallel Scavenge收集器類(lèi)似ParNew收集器,Parallel收集器更關(guān)注系統(tǒng)的吞吐量。
Parallel Old收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程“標(biāo)記-整理”算法
CMS收集器,CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。它需要消耗額外的CPU和內(nèi)存資源,在CPU和內(nèi)存資源緊張,CPU較少時(shí),會(huì)加重系統(tǒng)負(fù)擔(dān)。CMS無(wú)法處理浮動(dòng)垃圾。CMS的“標(biāo)記-清除”算法,會(huì)導(dǎo)致大量空間碎片的產(chǎn)生。
G1收集器,G1 (Garbage-First)是一款面向服務(wù)器的垃圾收集器,主要針對(duì)配備多顆處理器及大容量?jī)?nèi)存的機(jī)器. 以極高概率滿足GC停頓時(shí)間要求的同時(shí),還具備高吞吐量性能特征。
2.12stackoverflow錯(cuò)誤,permgen space錯(cuò)誤stackoverflow錯(cuò)誤主要出現(xiàn):
在虛擬機(jī)棧中(線程請(qǐng)求的棧深度大于虛擬機(jī)棧鎖允許的最大深度)
permgen space錯(cuò)誤(針對(duì)jdk之前1.7版本):
大量加載class文件
常量池內(nèi)存溢出
三、總結(jié)總的來(lái)說(shuō),JVM在初級(jí)的層面上還是偏理論多,可能要做具體的東西才會(huì)有更深的體會(huì)。這篇主要是入個(gè)門(mén)吧~
這篇文章懶懶散散也算把JVM比較重要的知識(shí)點(diǎn)理了一遍了,后面打算學(xué)學(xué),寫(xiě)寫(xiě)SpringCloud的東西。
參考資料:
《深入理解Java虛擬機(jī) JVM高級(jí)特性與最佳實(shí)踐(第二版)》
純潔的微笑jvm專(zhuān)欄:https://zhuanlan.zhihu.com/p/25511795
SexyCode jvm專(zhuān)欄:https://blog.csdn.net/column/details/15618.html?&page=1
javaGC流程:https://blog.csdn.net/yangyang12345555/article/details/79257171
如果文章有錯(cuò)的地方歡迎指正,大家互相交流。習(xí)慣在微信看技術(shù)文章,想要獲取更多的Java資源的同學(xué),可以關(guān)注微信公眾號(hào):Java3y。為了大家方便,剛新建了一下qq群:742919422,大家也可以去交流交流。謝謝支持了!希望能多介紹給其他有需要的朋友
文章的目錄導(dǎo)航:
https://zhongfucheng.bitcron.com/post/shou-ji/wen-zhang-dao-hang
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/71570.html
摘要:最近聽(tīng)很多面試的小伙伴說(shuō),網(wǎng)上往往是一篇一篇的多線程的文章,除了書(shū)籍沒(méi)有什么學(xué)習(xí)多線程的一系列文章。將此線程標(biāo)記為線程或用戶(hù)線程。 最近聽(tīng)很多面試的小伙伴說(shuō),網(wǎng)上往往是一篇一篇的Java多線程的文章,除了書(shū)籍沒(méi)有什么學(xué)習(xí)多線程的一系列文章。但是僅僅憑借一兩篇文章很難對(duì)多線程有系統(tǒng)的學(xué)習(xí),而且面試的時(shí)候多線程這方面的知識(shí)往往也是考察的重點(diǎn),所以考慮之下決定寫(xiě)一系列關(guān)于Java多線程的文章...
摘要:正如我標(biāo)題所說(shuō),簡(jiǎn)歷被拒??戳宋液?jiǎn)歷之后說(shuō)頭條競(jìng)爭(zhēng)激烈,我背景不夠,點(diǎn)到為止。。三準(zhǔn)備面試其實(shí)從三月份投遞簡(jiǎn)歷開(kāi)始準(zhǔn)備面試到四月份收,也不過(guò)個(gè)月的時(shí)間,但這都是建立在我過(guò)去一年的積累啊。 本文是 無(wú)精瘋 同學(xué)投稿的面試經(jīng)歷 關(guān)注微信公眾號(hào):進(jìn)擊的java程序員K,即可獲取最新BAT面試資料一份 在此感謝 無(wú)精瘋 同學(xué)的分享 目錄: 印象中的頭條 面試背景 準(zhǔn)備面試 ...
摘要:正如我標(biāo)題所說(shuō),簡(jiǎn)歷被拒??戳宋液?jiǎn)歷之后說(shuō)頭條競(jìng)爭(zhēng)激烈,我背景不夠,點(diǎn)到為止。。三準(zhǔn)備面試其實(shí)從三月份投遞簡(jiǎn)歷開(kāi)始準(zhǔn)備面試到四月份收,也不過(guò)個(gè)月的時(shí)間,但這都是建立在我過(guò)去一年的積累啊。 本文是 無(wú)精瘋 同學(xué)投稿的面試經(jīng)歷 關(guān)注微信公眾號(hào):進(jìn)擊的java程序員K,即可獲取最新BAT面試資料一份 在此感謝 無(wú)精瘋 同學(xué)的分享目錄:印象中的頭條面試背景準(zhǔn)備面試頭條一面(Java+項(xiàng)目)頭條...
在社會(huì)化分工、軟件行業(yè)細(xì)分專(zhuān)業(yè)化的趨勢(shì)下,會(huì)真的參與到底層系統(tǒng)實(shí)現(xiàn)的人肯定是越來(lái)越少(比例上說(shuō))。真的會(huì)參與到JVM實(shí)現(xiàn)的人肯定是少數(shù)。 但如果您對(duì)JVM是如何實(shí)現(xiàn)的有興趣、充滿好奇,卻苦于沒(méi)有足夠系統(tǒng)的知識(shí)去深入,那么可以參考RednaxelaFX整理的這個(gè)書(shū)單。 showImg(http://segmentfault.com/img/bVbGzn); 本豆列的脈絡(luò)是: 1. JV...
摘要:分類(lèi)問(wèn)題回到本系列的第一篇文章機(jī)器學(xué)習(xí)從入門(mén)到放棄之算法,在里面有這樣的一個(gè)問(wèn)題黃點(diǎn)代表類(lèi)電影的分布,綠色代表類(lèi)電影的分布,紫色代表需要分類(lèi)的電影樣本。 分類(lèi)問(wèn)題 回到本系列的第一篇文章機(jī)器學(xué)習(xí)從入門(mén)到放棄之KNN算法,在里面有這樣的一個(gè)問(wèn)題 showImg(https://sfault-image.b0.upaiyun.com/106/875/1068758747-576918491...
閱讀 676·2021-11-24 09:39
閱讀 2343·2021-11-22 13:54
閱讀 2211·2021-09-23 11:46
閱讀 3257·2019-08-30 15:55
閱讀 2692·2019-08-30 15:54
閱讀 2419·2019-08-30 14:18
閱讀 1556·2019-08-29 14:15
閱讀 2745·2019-08-29 13:49