蘭州出臺9條優(yōu)化措施西安seo優(yōu)化系統(tǒng)
1、總體路線
2、程序計數(shù)器
Program Counter Register 程序計數(shù)器(寄存器)
作用:是記錄下一條 jvm 指令的執(zhí)行地址行號。
特點(diǎn):
- 是線程私有的
- 不會存在內(nèi)存溢出
解釋器會解釋指令為機(jī)器碼交給 cpu 執(zhí)行,程序計數(shù)器會記錄下一條指令的地址行號,這樣下一次解釋器會從程序計數(shù)器拿到指令然后進(jìn)行解釋執(zhí)行。
多線程的環(huán)境下,如果兩個線程發(fā)生了上下文切換,那么程序計數(shù)器會記錄線程下一行指令的地址行號,以便于接著往下執(zhí)行。
3、棧
定義
每個線程運(yùn)行需要的內(nèi)存空間,稱為虛擬機(jī)棧
每個棧由多個棧幀(Frame)組成,對應(yīng)著每次調(diào)用方法時所占用的內(nèi)存
每個線程只能有一個活動棧幀,對應(yīng)著當(dāng)前正在執(zhí)行的方法
問題辨析:
- 垃圾回收是否涉及棧內(nèi)存?
不會。棧內(nèi)存是方法調(diào)用產(chǎn)生的,方法調(diào)用結(jié)束后會彈出棧。
- 棧內(nèi)存分配越大越好嗎?
不是。因為物理內(nèi)存是一定的,棧內(nèi)存越大,可以支持更多的遞歸調(diào)用,但是可執(zhí)行的線程數(shù)就會越少。
- 方法內(nèi)的局部變量是否線程安全
如果方法內(nèi)部的變量沒有逃離方法的作用范圍,它是線程安全的
如果是局部變量引用了對象,并逃離了方法的作用范圍,那就要考慮線程安全問題。
m1是線程安全的,m2,m3都不算線程安全的。
棧內(nèi)存溢出
棧幀過大、過多、或者第三方類庫操作,都有可能造成棧內(nèi)存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定棧內(nèi)存大小!
public class Demo1_19 {public static void main(String[] args) throws JsonProcessingException {Dept d = new Dept();d.setName("Market");Emp e1 = new Emp();e1.setName("zhang");e1.setDept(d);Emp e2 = new Emp();e2.setName("li");e2.setDept(d);d.setEmps(Arrays.asList(e1, e2));// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }ObjectMapper mapper = new ObjectMapper();System.out.println(mapper.writeValueAsString(d));}
}class Emp {private String name;@JsonIgnoreprivate Dept dept;public String getName() {return name;}public void setName(String name) {this.name = name;}public Dept getDept() {return dept;}public void setDept(Dept dept) {this.dept = dept;}
}
class Dept {private String name;private List<Emp> emps;public String getName() {return name;}public void setName(String name) {this.name = name;}public List<Emp> getEmps() {return emps;}public void setEmps(List<Emp> emps) {this.emps = emps;}
}
線程運(yùn)行診斷
案例一:cpu 占用過多
解決方法:Linux 環(huán)境下運(yùn)行某些程序的時候,可能導(dǎo)致 CPU 的占用過高,這時需要定位占用 CPU 過高的線程
- top 命令,查看是哪個進(jìn)程占用 CPU 過高
- ps H -eo pid, tid(線程id), %cpu | grep 剛才通過 top 查到的進(jìn)程號 通過 ps 命令進(jìn)一步查看是哪個線程占用 CPU 過高
- jstack 進(jìn)程 id 通過查看進(jìn)程中的線程的 nid ,剛才通過 ps 命令看到的 tid 來對比定位,注意 jstack 查找出的線程 id 是 16 進(jìn)制的,需要轉(zhuǎn)換。
本地方法棧
一些帶有 native
關(guān)鍵字的方法就是需要 JAVA 去調(diào)用本地的C或者C++方法,因為 JAVA 有時候沒法直接和操作系統(tǒng)底層交互,所以需要用到本地方法棧,服務(wù)于帶 native 關(guān)鍵字的方法。
4、堆
定義
Heap 堆
- 通過new關(guān)鍵字創(chuàng)建的對象都會被放在堆內(nèi)存
特點(diǎn)
- 它是線程共享,堆內(nèi)存中的對象都需要考慮線程安全問題
- 有垃圾回收機(jī)制
堆內(nèi)存溢出
java.lang.OutofMemoryError :java heap space. 堆內(nèi)存溢出
可以使用 -Xmx8m
來指定堆內(nèi)存大小。
堆內(nèi)存診斷
jps 工具
查看當(dāng)前系統(tǒng)中有哪些 java 進(jìn)程
jmap 工具
查看堆內(nèi)存占用情況 jmap - heap 進(jìn)程id
jconsole 工具
圖形界面的,多功能的監(jiān)測工具,可以連續(xù)監(jiān)測
5、方法區(qū)
定義
Java 虛擬機(jī)有一個在所有 Java 虛擬機(jī)線程之間共享的方法區(qū)域。方法區(qū)域類似于用于傳統(tǒng)語言的編譯代碼的存儲區(qū)域,或者類似于操作系統(tǒng)進(jìn)程中的“文本”段。它存儲每個類的結(jié)構(gòu),例如運(yùn)行時常量池、字段和方法數(shù)據(jù),以及方法和構(gòu)造函數(shù)的代碼,包括特殊方法,用于類和實例初始化以及接口初始化方法區(qū)域是在虛擬機(jī)啟動時創(chuàng)建的。
組成
方法區(qū)內(nèi)存溢出
1.8 之前會導(dǎo)致永久代內(nèi)存溢出
使用 -XX:MaxPermSize=8m 指定永久代內(nèi)存大小
1.8 之后會導(dǎo)致元空間內(nèi)存溢出
使用 -XX:MaxMetaspaceSize=8m 指定元空間大小
運(yùn)行時常量池
二進(jìn)制字節(jié)碼包含(類的基本信息,常量池,類方法定義,包含了虛擬機(jī)的指令)
首先看看常量池是什么,編譯如下代碼:
public class Test {public static void main(String[] args) {System.out.println("Hello World!");}}
然后使用 javap -v Test.class 命令反編譯查看結(jié)果。
常量池:
就是一張表,虛擬機(jī)指令根據(jù)這張常量表找到要執(zhí)行的類名、方法名、參數(shù)類型、字面量信息
運(yùn)行時常量池:
常量池是 *.class 文件中的,當(dāng)該類被加載以后,它的常量池信息就會放入運(yùn)行時常量池,并把里面的符號地址變?yōu)檎鎸嵉刂?/p>
6、StringTable
- 常量池中的字符串僅是符號,只有在被用到時才會轉(zhuǎn)化為對象
- 利用串池的機(jī)制,來避免重復(fù)創(chuàng)建字符串對象
- 字符串變量拼接的原理是StringBuilder
- 字符串常量拼接的原理是編譯器優(yōu)化
- 可以使用
intern
方法,主動將串池中還沒有的字符串對象放入串池中
例1:
// StringTable [ "a", "b" ,"ab" ] hashtable 結(jié)構(gòu),不能擴(kuò)容
public class Demo1_22 {// 常量池中的信息,都會被加載到運(yùn)行時常量池中, 這時 a b ab 都是常量池中的符號,還沒有變?yōu)?java 字符串對象// ldc #2 會把 a 符號變?yōu)?"a" 字符串對象// ldc #3 會把 b 符號變?yōu)?"b" 字符串對象// ldc #4 會把 ab 符號變?yōu)?"ab" 字符串對象public static void main(String[] args) {String s1 = "a"; // 懶惰的String s2 = "b";String s3 = "ab";String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")String s5 = "a" + "b"; // javac 在編譯期間的優(yōu)化,結(jié)果已經(jīng)在編譯期確定為abSystem.out.println(s3 == s5);//trueSystem.out.println(s3 == s4);//flase}
}
intern方法 1.8
調(diào)用字符串對象的 intern 方法,會將該字符串對象嘗試放入到串池中
- 如果串池中沒有該字符串對象,則放入成功
如果有該字符串對象,則放入失敗 - 無論放入是否成功,都會返回串池中的字符串對象
注意:此時如果調(diào)用 intern 方法成功,堆內(nèi)存與串池中的字符串對象是同一個對象;如果失敗,則不是同一個對象
public class Demo1_23 {// ["ab", "a", "b"]public static void main(String[] args) {String x = "ab";String s = new String("a") + new String("b");// 堆 new String("a") new String("b") new String("ab")String s2 = s.intern(); // 將這個字符串對象嘗試放入串池,如果有則并不會放入,如果沒有則放入串池, 會把串池中的對象返回System.out.println( s2 == x);//trueSystem.out.println( s == x );//false}}
面試題
/*** 演示字符串相關(guān)面試題*/
public class Demo1_21 {public static void main(String[] args) {String s1 = "a";String s2 = "b";String s3 = "a" + "b"; // abString s4 = s1 + s2; // new String("ab")String s5 = "ab";String s6 = s4.intern();// 問System.out.println(s3 == s4); // falseSystem.out.println(s3 == s5); // trueSystem.out.println(s3 == s6); // trueString x2 = new String("c") + new String("d"); // new String("cd")
// x2.intern();String x1 = "cd";x2.intern();
// 問,如果調(diào)換了【最后兩行代碼】的位置呢,如果是jdk1.6呢System.out.println(x1 == x2);//false}
}
理解intern方法:intern是將字符串放到常量池里,常量池如果沒有,就把自己的地址放到常量池,如果有,就返回常量池里面的對象地址。
StringTable位置
jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。
StringTable 垃圾回收
-Xmx10m 指定堆內(nèi)存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次數(shù),耗費(fèi)時間等信息
/*** 演示 StringTable 垃圾回收* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc*/
public class Code_05_StringTableTest {public static void main(String[] args) {int i = 0;try {for(int j = 0; j < 10000; j++) { // j = 100, j = 10000String.valueOf(j).intern();i++;}}catch (Exception e) {e.printStackTrace();}finally {System.out.println(i);}}}
StringTable調(diào)優(yōu)
在介紹性能調(diào)優(yōu)之前不得不說一說StringTable的底層實現(xiàn),前面已經(jīng)提到了StringTable底層是一個HashTable,HashTable長什么樣呢?其實就是數(shù)組+鏈表,每個元素是一個key-value。當(dāng)存入一個元素的時候,就會將其key通過hash函數(shù)計算得出數(shù)組的下標(biāo)并存放在對應(yīng)的位置。
比如現(xiàn)在有一個key-value,這個key通過hash函數(shù)計算結(jié)果為2,那么就把value存放在數(shù)組下標(biāo)為2的位置。但是如果現(xiàn)在又有一個key通過hash函數(shù)計算出了相同的結(jié)果,比如也是2,但2的位置已經(jīng)有值了,這種現(xiàn)象就叫做哈希沖突,怎么解決呢?這里采用了鏈表法:
鏈表法就是將下標(biāo)一樣的元素通過鏈表的形式串起來,如果數(shù)組容量很小但是元素很多,那么發(fā)生哈希沖突的概率就會提高。大家都知道,鏈表的效率遠(yuǎn)沒有數(shù)組那么高,哈希沖突過多會影響性能。所以為了減少哈希沖突的概率,所以可以適當(dāng)?shù)脑黾訑?shù)組的大小。數(shù)組的每一格在StringTable中叫做bucket,我們可以增加bucket的數(shù)量來提高性能,默認(rèn)的數(shù)量為60013個,來看一個對比:
long startTime = System.nanoTime();
String str = "hello";
for(int i = 0;i < 500000;i++) {String s = str + i;s.intern();
}
long endTime = System.nanoTime();
System.out.println("花費(fèi)的時間為:"+(endTime-startTime)/1000000 + "毫秒");
先通過一個虛擬機(jī)參數(shù)將bucket指定的小一點(diǎn),來個2000吧:
-XX:StringTableSize=2000
運(yùn)行一下:
一共花費(fèi)了1.2秒。再來將bucket的數(shù)量增加一點(diǎn),來個20000個:
-XX:StringTableSize=20000
運(yùn)行一下:
可以看到,這次只花了0.19秒,性能有了明顯的提升,說明這樣確實可以優(yōu)化StringTable。
7、直接內(nèi)存
定義
Direct Memory
- 常見于 NIO 操作時,用于數(shù)據(jù)緩沖區(qū)
- 分配回收成本較高,但讀寫性能高
- 不受 JVM 內(nèi)存回收管理
使用直接內(nèi)存的好處
文件讀寫流程:
因為 java 不能直接操作文件管理,需要切換到內(nèi)核態(tài),使用本地方法進(jìn)行操作,然后讀取磁盤文件,會在系統(tǒng)內(nèi)存中創(chuàng)建一個緩沖區(qū),將數(shù)據(jù)讀到系統(tǒng)緩沖區(qū), 然后在將系統(tǒng)緩沖區(qū)數(shù)據(jù),復(fù)制到 java 堆內(nèi)存中。缺點(diǎn)是數(shù)據(jù)存儲了兩份,在系統(tǒng)內(nèi)存中有一份,java 堆中有一份,造成了不必要的復(fù)制。
使用了 DirectBuffer 文件讀取流程
直接內(nèi)存是操作系統(tǒng)和 Java 代碼都可以訪問的一塊區(qū)域,無需將代碼從系統(tǒng)內(nèi)存復(fù)制到 Java 堆內(nèi)存,從而提高了效率。
使用ByteBuffer.allocateDirect
/*** IO:阻塞式 NIO (New IO / Non-Blocking IO):非阻塞式* byte[] / char[] Buffer* Stream Channel** 查看直接內(nèi)存的占用與釋放*/
public class BufferTest {private static final int BUFFER = 1024 * 1024 * 1024; //1GBpublic static void main(String[] args){//直接分配本地內(nèi)存空間ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);System.out.println("直接內(nèi)存分配完畢,請求指示!");Scanner scanner = new Scanner(System.in);scanner.next();System.out.println("直接內(nèi)存開始釋放!");byteBuffer = null;System.gc();scanner.next();}}
使用unsafe類
/*** 直接內(nèi)存分配的底層原理:Unsafe*/
public class Demo1_27 {static int _1Gb = 1024 * 1024 * 1024;public static void main(String[] args) throws IOException {Unsafe unsafe = getUnsafe();// 分配內(nèi)存long base = unsafe.allocateMemory(_1Gb);unsafe.setMemory(base, _1Gb, (byte) 0);System.in.read();// 釋放內(nèi)存unsafe.freeMemory(base);System.in.read();}public static Unsafe getUnsafe() {try {Field f = Unsafe.class.getDeclaredField("theUnsafe");f.setAccessible(true);Unsafe unsafe = (Unsafe) f.get(null);return unsafe;} catch (NoSuchFieldException | IllegalAccessException e) {throw new RuntimeException(e);}}
}
8、垃圾回收
如何判斷對象可以回收
引用計數(shù)法
當(dāng)一個對象被引用時,引用對象的值加一,當(dāng)一個對象不被引用時,引用對象的值減一,當(dāng)值為 0 時,就表示該對象不被引用,可以被垃圾收集器回收。
這個引用計數(shù)法聽起來不錯,但是有一個弊端,如下圖所示,循環(huán)引用時,兩個對象的計數(shù)都為1,導(dǎo)致兩個對象都無法被釋放,造成內(nèi)存泄露。
可達(dá)性分析算法
- JVM 中的垃圾回收器通過可達(dá)性分析來探索所有存活的對象
- 掃描堆中的對象,看能否沿著 GC Root 對象為起點(diǎn)的引用鏈找到該對象,如果找不到,則表示可以回收
- 可以作為 GC Root 的對象
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象。
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中 JNI(即一般說的Native方法)引用的對象
- 已啟動且未停止的 Java 線程
public static void main(String[] args) throws IOException {ArrayList<Object> list = new ArrayList<>();list.add("a");list.add("b");list.add(1);System.out.println(1);System.in.read();list = null;System.out.println(2);System.in.read();System.out.println("end");}
對于以上代碼,可以使用如下命令將堆內(nèi)存信息轉(zhuǎn)儲成一個文件,然后使用
Eclipse Memory Analyzer 工具進(jìn)行分析。
第一步:
使用 jps 命令,查看程序的進(jìn)程
第二步
使用 jmap -dump:format=b,live,file=1.bin 16104 命令轉(zhuǎn)儲文件
dump:轉(zhuǎn)儲文件
format=b:二進(jìn)制文件
file:文件名
16104:進(jìn)程的id
第三步:打開 Eclipse Memory Analyzer 對 1.bin 文件進(jìn)行分析。
分析的 gc root,找到了 ArrayList 對象,然后將 list 置為null,再次轉(zhuǎn)儲,那么 list 對象就會被回收。
java定義常量是在方法區(qū)還是堆區(qū)
在Java中,常量通常被定義為靜態(tài)final字段,它們被存儲在方法區(qū)中的運(yùn)行時常量池中。這意味著它們在程序運(yùn)行期間只被分配一次,并且可以被所有對象共享。堆區(qū)是用于存儲對象實例的區(qū)域,而不是常量。
四種引用
強(qiáng)引用
只有所有 GC Roots 對象都不通過【強(qiáng)引用】引用該對象,該對象才能被垃圾回收
軟引用(SoftReference)
僅有軟引用引用該對象時,在垃圾回收后,內(nèi)存仍不足時會再次出發(fā)垃圾回收,回收軟引用對象可以配合引用隊列來釋放軟引用自身
弱引用(WeakReference)
僅有弱引用引用該對象時,在垃圾回收時,無論內(nèi)存是否充足,都會回收弱引用對象
可以配合引用隊列來釋放弱引用自身
虛引用(PhantomReference)
必須配合引用隊列使用,主要配合 ByteBuffer 使用,被引用對象回收時,會將虛引用入隊,由 Reference Handler 線程調(diào)用虛引用相關(guān)方法釋放直接內(nèi)存
終結(jié)器引用(FinalReference)
無需手動編碼,但其內(nèi)部配合引用隊列使用,在垃圾回收時,終結(jié)器引用入隊(被引用對象暫時沒有被回收),再由 Finalizer 線程通過終結(jié)器引用找到被引用對象并調(diào)用它的 finalize 方法,第二次 GC 時才能回收被引用對象。
演示軟引用
/*** 演示 軟引用* -Xmx20m -XX:+PrintGCDetails -verbose:gc*/
public class Code_08_SoftReferenceTest {public static int _4MB = 4 * 1024 * 1024;public static void main(String[] args) throws IOException {method2();}// 設(shè)置 -Xmx20m , 演示堆內(nèi)存不足,public static void method1() throws IOException {ArrayList<byte[]> list = new ArrayList<>();for(int i = 0; i < 5; i++) {list.add(new byte[_4MB]);}System.in.read();}// 演示 軟引用public static void method2() throws IOException {ArrayList<SoftReference<byte[]>> list = new ArrayList<>();for(int i = 0; i < 5; i++) {SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);System.out.println(ref.get());list.add(ref);System.out.println(list.size());}System.out.println("循環(huán)結(jié)束:" + list.size());for(SoftReference<byte[]> ref : list) {System.out.println(ref.get());}}
}
method1 方法解析:
首先會設(shè)置一個堆內(nèi)存的大小為 20m,然后運(yùn)行 mehtod1 方法,會拋異常,堆內(nèi)存不足,因為 mehtod1 中的 list 都是強(qiáng)引用。
method2 方法解析:
在 list 集合中存放了 軟引用對象,當(dāng)內(nèi)存不足時,會觸發(fā) full gc,將軟引用的對象回收。細(xì)節(jié)如圖:
上面的代碼中,當(dāng)軟引用引用的對象被回收了,但是軟引用還存在,所以,一般軟引用需要搭配一個引用隊列一起使用。
修改 method2 如下:
// 演示 軟引用 搭配引用隊列
public static void method3() throws IOException {ArrayList<SoftReference<byte[]>> list = new ArrayList<>();// 引用隊列ReferenceQueue<byte[]> queue = new ReferenceQueue<>();for(int i = 0; i < 5; i++) {// 關(guān)聯(lián)了引用隊列,當(dāng)軟引用所關(guān)聯(lián)的 byte[] 被回收時,軟引用自己會加入到 queue 中去SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);System.out.println(ref.get());list.add(ref);System.out.println(list.size());}// 從隊列中獲取無用的 軟引用對象,并移除Reference<? extends byte[]> poll = queue.poll();while(poll != null) {list.remove(poll);poll = queue.poll();}System.out.println("=====================");for(SoftReference<byte[]> ref : list) {System.out.println(ref.get());}}
弱引用
/*** 演示弱引用* -Xmx20m -XX:+PrintGCDetails -verbose:gc*/
public class Demo2_5 {private static final int _4MB = 4 * 1024 * 1024;public static void main(String[] args) {// list --> WeakReference --> byte[]List<WeakReference<byte[]>> list = new ArrayList<>();for (int i = 0; i < 10; i++) {WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);list.add(ref);for (WeakReference<byte[]> w : list) {System.out.print(w.get()+" ");}System.out.println();}System.out.println("循環(huán)結(jié)束:" + list.size());}
}