不同的平臺(tái),內(nèi)存模型是不一樣的,但是jvm的內(nèi)存模型規(guī)范是統(tǒng)一的。其實(shí)java的多線程并發(fā)問題最終都會(huì)反映在java的內(nèi)存模型上,所謂線程安全無非是要控制多個(gè)線程對(duì)某個(gè)資源的有序訪問或修改。總結(jié)java的內(nèi)存模型,要解決兩個(gè)主要的問題:可見性和有序性。
我們都知道計(jì)算機(jī)有高速緩存的存在,處理器并不是每次處理數(shù)據(jù)都是取內(nèi)存的。JVM定義了自己的內(nèi)存模型,屏蔽了底層平臺(tái)內(nèi)存管理細(xì)節(jié),對(duì)于java開發(fā)人員,要清楚在jvm內(nèi)存模型的基礎(chǔ)上,如果解決多線程的可見性和有序性。
那么,何謂可見性? 多個(gè)線程之間是不能互相傳遞數(shù)據(jù)通信的,它們之間的溝通只能通過共享變量來進(jìn)行。Java內(nèi)存模型(JMM)規(guī)定了jvm有主內(nèi)存,主內(nèi)存是多個(gè)線程共享的。當(dāng)new一個(gè)對(duì)象的時(shí)候,也是被分配在主內(nèi)存中,每個(gè)線程都有自己的工作內(nèi)存,工作內(nèi)存存儲(chǔ)了主存的某些對(duì)象的副本,當(dāng)然線程的工作內(nèi)存大小是有限制的。當(dāng)線程操作某個(gè)對(duì)象時(shí),執(zhí)行順序如下:
(1) 從主存復(fù)制變量到當(dāng)前工作內(nèi)存 (read and load)
(2) 執(zhí)行代碼,改變共享變量值 (use and assign)
(3) 用工作內(nèi)存數(shù)據(jù)刷新主存相關(guān)內(nèi)容 (store and write)
JVM規(guī)范定義了線程對(duì)主存的操作指令:read,load,use,assign,store,write。當(dāng)一個(gè)共享變量在多個(gè)線程的工作內(nèi)存中都有副本時(shí),如果一個(gè)線程修改了這個(gè)共享變量,那么其他線程應(yīng)該能夠看到這個(gè)被修改后的值,這就是多線程的可見性問題。
那么,什么是有序性呢 ?線程在引用變量時(shí)不能直接從主內(nèi)存中引用,如果線程工作內(nèi)存中沒有該變量,則會(huì)從主內(nèi)存中拷貝一個(gè)副本到工作內(nèi)存中,這個(gè)過程為read-load,完 成后線程會(huì)引用該副本。當(dāng)同一線程再度引用該字段時(shí),有可能重新從主存中獲取變量副本(read-load-use),也有可能直接引用原來的副本 (use),也就是說 read,load,use順序可以由JVM實(shí)現(xiàn)系統(tǒng)決定。
線程不能直接為主存中中字段賦值,它會(huì)將值指定給工作內(nèi)存中的變量副本(assign),完成后這個(gè)變量副本會(huì)同步到主存儲(chǔ)區(qū)(store- write),至于何時(shí)同步過去,根據(jù)JVM實(shí)現(xiàn)系統(tǒng)決定.有該字段,則會(huì)從主內(nèi)存中將該字段賦值到工作內(nèi)存中,這個(gè)過程為read-load,完成后線 程會(huì)引用該變量副本,當(dāng)同一線程多次重復(fù)對(duì)字段賦值時(shí),比如:
Java代碼

- for(int i=0;i<10;i++)
- a++;
線程有可能只對(duì)工作內(nèi)存中的副本進(jìn)行賦 值,只到最后一次賦值后才同步到主存儲(chǔ)區(qū),所以assign,store,weite順序可以由JVM實(shí)現(xiàn)系統(tǒng)決定。假設(shè)有一個(gè)共享變量x,線程a執(zhí)行 x=x+1。從上面的描述中可以知道x=x+1并不是一個(gè)原子操作,它的執(zhí)行過程如下:
1 從主存中讀取變量x副本到工作內(nèi)存
2 給x加1
3 將x加1后的值寫回主 存
如果另外一個(gè)線程b執(zhí)行x=x-1,執(zhí)行過程如下:
1 從主存中讀取變量x副本到工作內(nèi)存
2 給x減1
3 將x減1后的值寫回主存
那么顯然,最終的x的值是不可靠的。假設(shè)x現(xiàn)在為10,線程a加1,線程b減1,從表面上看,似乎最終x還是為10,但是多線程情況下會(huì)有這種情況發(fā)生:
1:線程a從主存讀取x副本到工作內(nèi)存,工作內(nèi)存中x值為10
2:線程b從主存讀取x副本到工作內(nèi)存,工作內(nèi)存中x值為10
3:線程a將工作內(nèi)存中x加1,工作內(nèi)存中x值為11
4:線程a將x提交主存中,主存中x為11
5:線程b將工作內(nèi)存中x值減1,工作內(nèi)存中x值為9
6:線程b將x提交到中主存中,主存中x為9
同樣,x有可能為11,如果x是一個(gè)銀行賬戶,線程a存款,線程b扣款,顯然這樣是有嚴(yán)重問題的,要解決這個(gè)問題,必須保證線程a和線程b是有序執(zhí)行的,并且每個(gè)線程執(zhí)行的加1或減1是一個(gè)原子操作。看看下面代碼:
Java代碼

- public class Account {
-
- private int balance;
-
- public Account(int balance) {
- this.balance = balance;
- }
-
- public int getBalance() {
- return balance;
- }
-
- public void add(int num) {
- balance = balance + num;
- }
-
- public void withdraw(int num) {
- balance = balance - num;
- }
-
- public static void main(String[] args) throws InterruptedException {
- Account account = new Account(1000);
- Thread a = new Thread(new AddThread(account, 20), "add");
- Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");
- a.start();
- b.start();
- a.join();
- b.join();
- System.out.println(account.getBalance());
- }
-
- static class AddThread implements Runnable {
- Account account;
- int amount;
-
- public AddThread(Account account, int amount) {
- this.account = account;
- this.amount = amount;
- }
-
- public void run() {
- for (int i = 0; i < 200000; i++) {
- account.add(amount);
- }
- }
- }
-
- static class WithdrawThread implements Runnable {
- Account account;
- int amount;
-
- public WithdrawThread(Account account, int amount) {
- this.account = account;
- this.amount = amount;
- }
-
- public void run() {
- for (int i = 0; i < 100000; i++) {
- account.withdraw(amount);
- }
- }
- }
- }
第一次執(zhí)行結(jié)果為10200,第二次執(zhí)行 結(jié)果為1060,每次執(zhí)行的結(jié)果都是不確定的,因?yàn)榫€程的執(zhí)行順序是不可預(yù)見的。這是java同步產(chǎn)生的根源,synchronized關(guān)鍵字保證了多個(gè) 線程對(duì)于同步塊是互斥的,synchronized作為一種同步手段,解決java多線程的執(zhí)行有序性和內(nèi)存可見性,而volatile關(guān)鍵字之解決多線 程的內(nèi)存可見性問題。后面將會(huì)詳細(xì)介紹。
synchronized關(guān)鍵字
上面說了,java用synchronized關(guān)鍵字做為多線程并發(fā)環(huán)境的執(zhí)行有序性的保證手段之一。當(dāng)一段代碼會(huì)修改共享變量,這一段代碼成為互斥區(qū)或臨界區(qū),為了保證共享變量的正確性,synchronized標(biāo)示了臨界區(qū)。典型的用法如下:
Java代碼

- synchronized(鎖){
- 臨界區(qū)代碼
- }
為了保證銀行賬戶的安全,可以操作賬戶的方法如下:
Java代碼

- public synchronized void add(int num) {
- balance = balance + num;
- }
- public synchronized void withdraw(int num) {
- balance = balance - num;
- }
剛才不是說了synchronized的用法是這樣的嗎:
Java代碼

- synchronized(鎖){
- 臨界區(qū)代碼
- }
那么對(duì)于public synchronized void add(int num)這種情況,意味著什么呢?其實(shí)這種情況,鎖就是這個(gè)方法所在的對(duì)象。同理,如果方法是public static synchronized void add(int num),那么鎖就是這個(gè)方法所在的class。
理論上,每個(gè)對(duì)象都可以做為鎖,但一個(gè)對(duì)象做為鎖時(shí),應(yīng)該被多個(gè)線程共享,這樣才顯得有意義,在并發(fā)環(huán)境下,一個(gè)沒有共享的對(duì)象作為鎖是沒有意義的。假如有這樣的代碼:
Java代碼

- public class ThreadTest{
- public void test(){
- Object lock=new Object();
- synchronized (lock){
- //do something
- }
- }
- }
lock變量作為一個(gè)鎖存在根本沒有意義,因?yàn)樗静皇枪蚕韺?duì)象,每個(gè)線程進(jìn)來都會(huì)執(zhí)行Object lock=new Object();每個(gè)線程都有自己的lock,根本不存在鎖競(jìng)爭(zhēng)。
每個(gè)鎖對(duì)象都有兩個(gè)隊(duì)列,一個(gè)是就緒隊(duì)列,一個(gè)是阻塞隊(duì)列,就緒隊(duì)列存儲(chǔ)了將要獲得鎖的線程,阻塞隊(duì)列存儲(chǔ)了被阻塞的線程,當(dāng)一個(gè)被線程被喚醒 (notify)后,才會(huì)進(jìn)入到就緒隊(duì)列,等待cpu的調(diào)度。當(dāng)一開始線程a第一次執(zhí)行account.add方法時(shí),jvm會(huì)檢查鎖對(duì)象account 的就緒隊(duì)列是否已經(jīng)有線程在等待,如果有則表明account的鎖已經(jīng)被占用了,由于是第一次運(yùn)行,account的就緒隊(duì)列為空,所以線程a獲得了鎖, 執(zhí)行account.add方法。如果恰好在這個(gè)時(shí)候,線程b要執(zhí)行account.withdraw方法,因?yàn)榫€程a已經(jīng)獲得了鎖還沒有釋放,所以線程 b要進(jìn)入account的就緒隊(duì)列,等到得到鎖后才可以執(zhí)行。
一個(gè)線程執(zhí)行臨界區(qū)代碼過程如下:
1 獲得同步鎖
2 清空工作內(nèi)存
3 從主存拷貝變量副本到工作內(nèi)存
4 對(duì)這些變量計(jì)算
5 將變量從工作內(nèi)存寫回到主存
6 釋放鎖
可見,synchronized既保證了多線程的并發(fā)有序性,又保證了多線程的內(nèi)存可見性。
生產(chǎn)者/消費(fèi)者模式
生產(chǎn)者/消費(fèi)者模式其實(shí)是一種很經(jīng)典的線程同步模型,很多時(shí)候,并不是光保證多個(gè)線程對(duì)某共享資源操作的互斥性就夠了,往往多個(gè)線程之間都是有協(xié)作的。
假設(shè)有這樣一種情況,有一個(gè)桌子,桌子上面有一個(gè)盤子,盤子里只能放一顆雞蛋,A專門往盤子里放雞蛋,如果盤子里有雞蛋,則一直等到盤子里沒雞蛋,B專門 從盤子里拿雞蛋,如果盤子里沒雞蛋,則等待直到盤子里有雞蛋。其實(shí)盤子就是一個(gè)互斥區(qū),每次往盤子放雞蛋應(yīng)該都是互斥的,A的等待其實(shí)就是主動(dòng)放棄鎖,B 等待時(shí)還要提醒A放雞蛋。
如何讓線程主動(dòng)釋放鎖
很簡(jiǎn)單,調(diào)用鎖的wait()方法就好。wait方法是從Object來的,所以任意對(duì)象都有這個(gè)方法。看這個(gè)代碼片段:
Java代碼

- Object lock=new Object();//聲明了一個(gè)對(duì)象作為鎖
- synchronized (lock) {
- balance = balance - num;
- //這里放棄了同步鎖,好不容易得到,又放棄了
- lock.wait();
- }
如果一個(gè)線程獲得了鎖lock,進(jìn)入了同步塊,執(zhí)行l(wèi)ock.wait(),那么這個(gè)線程會(huì)進(jìn)入到lock的阻塞隊(duì)列。如果調(diào)用 lock.notify()則會(huì)通知阻塞隊(duì)列的某個(gè)線程進(jìn)入就緒隊(duì)列。
聲明一個(gè)盤子,只能放一個(gè)雞蛋
Java代碼

- import java.util.ArrayList;
- import java.util.List;
-
- public class Plate {
-
- List<Object> eggs = new ArrayList<Object>();
-
- public synchronized Object getEgg() {
- if (eggs.size() == 0) {
- try {
- wait();
- } catch (InterruptedException e) {
- }
- }
-
- Object egg = eggs.get(0);
- eggs.clear();// 清空盤子
- notify();// 喚醒阻塞隊(duì)列的某線程到就緒隊(duì)列
- System.out.println("拿到雞蛋");
- return egg;
- }
-
- public synchronized void putEgg(Object egg) {
- if (eggs.size() > 0) {
- try {
- wait();
- } catch (InterruptedException e) {
- }
- }
- eggs.add(egg);// 往盤子里放雞蛋
- notify();// 喚醒阻塞隊(duì)列的某線程到就緒隊(duì)列
- System.out.println("放入雞蛋");
- }
-
- static class AddThread extends Thread{
- private Plate plate;
- private Object egg=new Object();
- public AddThread(Plate plate){
- this.plate=plate;
- }
-
- public void run(){
- for(int i=0;i<5;i++){
- plate.putEgg(egg);
- }
- }
- }
-
- static class GetThread extends Thread{
- private Plate plate;
- public GetThread(Plate plate){
- this.plate=plate;
- }
-
- public void run(){
- for(int i=0;i<5;i++){
- plate.getEgg();
- }
- }
- }
-
- public static void main(String args[]){
- try {
- Plate plate=new Plate();
- Thread add=new Thread(new AddThread(plate));
- Thread get=new Thread(new GetThread(plate));
- add.start();
- get.start();
- add.join();
- get.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("測(cè)試結(jié)束");
- }
- }
執(zhí)行結(jié)果:
Html代碼

- 放入雞蛋
- 拿到雞蛋
- 放入雞蛋
- 拿到雞蛋
- 放入雞蛋
- 拿到雞蛋
- 放入雞蛋
- 拿到雞蛋
- 放入雞蛋
- 拿到雞蛋
- 測(cè)試結(jié)束
聲明一個(gè)Plate對(duì)象為plate,被線程A和線程B共享,A專門放雞蛋,B專門拿雞蛋。假設(shè)
1 開始,A調(diào)用plate.putEgg方法,此時(shí)eggs.size()為0,因此順利將雞蛋放到盤子,還執(zhí)行了notify()方法,喚醒鎖的阻塞隊(duì)列的線程,此時(shí)阻塞隊(duì)列還沒有線程。
2 又有一個(gè)A線程對(duì)象調(diào)用plate.putEgg方法,此時(shí)eggs.size()不為0,調(diào)用wait()方法,自己進(jìn)入了鎖對(duì)象的阻塞隊(duì)列。
3 此時(shí),來了一個(gè)B線程對(duì)象,調(diào)用plate.getEgg方法,eggs.size()不為0,順利的拿到了一個(gè)雞蛋,還執(zhí)行了notify()方法,喚 醒鎖的阻塞隊(duì)列的線程,此時(shí)阻塞隊(duì)列有一個(gè)A線程對(duì)象,喚醒后,它進(jìn)入到就緒隊(duì)列,就緒隊(duì)列也就它一個(gè),因此馬上得到鎖,開始往盤子里放雞蛋,此時(shí)盤子是 空的,因此放雞蛋成功。
4 假設(shè)接著來了線程A,就重復(fù)2;假設(shè)來料線程B,就重復(fù)3。
整個(gè)過程都保證了放雞蛋,拿雞蛋,放雞蛋,拿雞蛋。
volatile關(guān)鍵字
volatile是java提供的一種同步手段,只不過它是輕量級(jí)的同步,為什么這么說,因?yàn)?span style="color: red">volatile只能保證多線程的內(nèi)存可見性,不能保證多線程的執(zhí)行有序性。而 最徹底的同步要保證有序性和可見性,例如synchronized。任何被volatile修飾的變量,都不拷貝副本到工作內(nèi)存,任何修改都及時(shí)寫在主 存。因此對(duì)于Valatile修飾的變量的修改,所有線程馬上就能看到,但是volatile不能保證對(duì)變量的修改是有序的。什么意思呢?假如有這樣的代 碼:
Java代碼

- public class VolatileTest{
- public volatile int a;
- public void add(int count){
- a=a+count;
- }
- }
當(dāng)一個(gè)VolatileTest對(duì)象被多個(gè)線程共享,a的值不一定是正確的,因?yàn)閍=a+count包含了好幾步操作,而此時(shí)多個(gè)線程的執(zhí)行是無序的,因?yàn)闆]有任何機(jī)制來保證多個(gè)線程的執(zhí)行有序性和原子性。volatile存在的意義是,任何線程對(duì)a的修改,都會(huì)馬上被其他線程讀取到,因?yàn)橹苯硬僮髦鞔妫瑳]有線程對(duì)工作內(nèi)存和主存的同步。所以,volatile的使用場(chǎng)景是有限的,在有限的一些情形下可以使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時(shí)滿足下面兩個(gè)條件:
1)對(duì)變量的寫操作不依賴于當(dāng)前值。
2)該變量沒有包含在具有其他變量的不變式中
volatile只保證了可見性,所以Volatile適合直接賦值的場(chǎng)景,如
Java代碼

- public class VolatileTest{
- public volatile int a;
- public void setA(int a){
- this.a=a;
- }
- }
在沒有volatile聲明時(shí),多線程環(huán) 境下,a的最終值不一定是正確的,因?yàn)閠his.a=a;涉及到給a賦值和將a同步回主存的步驟,這個(gè)順序可能被打亂。如果用volatile聲明了,讀 取主存副本到工作內(nèi)存和同步a到主存的步驟,相當(dāng)于是一個(gè)原子操作。所以簡(jiǎn)單來說,volatile適合這種場(chǎng)景:一個(gè)變量被多個(gè)線程共享,線程直接給這個(gè)變量賦值。這是一種很簡(jiǎn)單的同步場(chǎng)景,這時(shí)候使用volatile的開銷將會(huì)非常小。站內(nèi)很多人都問我,所謂線程的“工作內(nèi)存”到底是個(gè)什么東西?有的人認(rèn)為是線程的棧,其實(shí)這種理解是不正確的。看看JLS(java語言規(guī)范)對(duì)線程工作內(nèi)存的描述,線程的working memory只是cpu的寄存器和高速緩存的抽象描述。
可能 很 多人都覺得莫名其妙,說JVM的內(nèi)存模型,怎么會(huì)扯到cpu上去呢?在此,我認(rèn)為很有必要闡述下,免得很多人看得不明不白的。先拋開java虛擬機(jī)不談, 我們都知道,現(xiàn)在的計(jì)算機(jī),cpu在計(jì)算的時(shí)候,并不總是從內(nèi)存讀取數(shù)據(jù),它的數(shù)據(jù)讀取順序優(yōu)先級(jí)是:寄存器-高速緩存-內(nèi)存。線程耗費(fèi)的是CPU,線程 計(jì)算的時(shí)候,原始的數(shù)據(jù)來自內(nèi)存,在計(jì)算過程中,有些數(shù)據(jù)可能被頻繁讀取,這些數(shù)據(jù)被存儲(chǔ)在寄存器和高速緩存中,當(dāng)線程計(jì)算完后,這些緩存的數(shù)據(jù)在適當(dāng)?shù)?時(shí)候應(yīng)該寫回內(nèi)存。當(dāng)個(gè)多個(gè)線程同時(shí)讀寫某個(gè)內(nèi)存數(shù)據(jù)時(shí),就會(huì)產(chǎn)生多線程并發(fā)問題,涉及到三個(gè)特性:原子性,有序性,可見性。在《線程安全總結(jié)》這篇文章 中,為了理解方便,我把原子性和有序性統(tǒng)一叫做“多線程執(zhí)行有序性”。支持多線程的平臺(tái)都會(huì)面臨這種問題,運(yùn)行在多線程平臺(tái)上支持多線程的語言應(yīng)該提供解 決該問題的方案。
那么,我們看看JVM,JVM是一個(gè)虛擬的計(jì)算機(jī),它也會(huì)面臨多線程并發(fā)問題,java程序運(yùn)行在java虛擬機(jī)平臺(tái)上,java程序員不可能直接去控制底層線程對(duì)寄存器高速緩存內(nèi)存之間的同步,那么java從語法層面,應(yīng)該給開發(fā)人員提供一種解決方案,這個(gè)方案就是諸如 synchronized, volatile,鎖機(jī)制(如同步塊,就緒隊(duì)列,阻塞隊(duì)列)等等。這些方案只是語法層面的,但我們要從本質(zhì)上去理解它,不能僅僅知道一個(gè) synchronized 可以保證同步就完了。 在這里我說的是jvm的內(nèi)存模型,是動(dòng)態(tài)的,面向多線程并發(fā)的,沿襲JSL的“working memory”的說法,只是不想牽扯到太多底層細(xì)節(jié),因?yàn)?/span> 《線程安全總結(jié)》這篇文章意在說明怎樣從語法層面去理解java的線程同步,知道各個(gè)關(guān)鍵字的使用場(chǎng)景。
今 天有人問我,那java的線程不是有棧嗎?難道棧不是工作內(nèi)存嗎?工作內(nèi)存這四個(gè)字得放到具體的場(chǎng)景中描述,方能體現(xiàn)它具體的意義,在描述JVM的線程同 步時(shí),工作內(nèi)存指的是寄存器和告訴緩存的抽象描述,具體請(qǐng)自行參閱JLS。上面講的都是動(dòng)態(tài)的內(nèi)存模型,甚至已經(jīng)超越了JVM的范圍,那么JVM的內(nèi)存靜 態(tài)存儲(chǔ)是怎么劃分的?今天還有人問我,jvm的內(nèi)存模型不是有eden區(qū)嗎?也不見你提起。我跟他說,這是兩個(gè)角度去看的,甚至是兩個(gè)不同的范圍,動(dòng)態(tài)的 線程同步的內(nèi)存模型,涵蓋了cpu,寄存器,高速緩存,內(nèi)存;JVM的靜態(tài)內(nèi)存儲(chǔ)模型只是一種對(duì)內(nèi)存的物理劃分而已,它只局限在內(nèi)存,而且只局限在JVM 的內(nèi)存。那些什么線程棧,eden區(qū)都僅僅在JVM內(nèi)存。
說說JVM的線程棧和有個(gè)朋友反復(fù)跟我糾結(jié)的eden區(qū)吧。JVM的內(nèi)存,被劃分了很多的區(qū)域:
1.程序計(jì)數(shù)器
每一個(gè)Java線程都有一個(gè)程序計(jì)數(shù)器來用于保存程序執(zhí)行到當(dāng)前方法的哪一個(gè)指令。
2.線程棧
線 程的每個(gè)方法被執(zhí)行的時(shí)候,都會(huì)同時(shí)創(chuàng)建一個(gè)幀(Frame)用于存儲(chǔ)本地變量表、操作棧、動(dòng)態(tài)鏈接、方法出入口等信息。每一個(gè)方法的調(diào)用至完成,就意味 著一個(gè)幀在VM棧中的入棧至出棧的過程。如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果VM棧可以 動(dòng)態(tài)擴(kuò)展(VM Spec中允許固定長(zhǎng)度的VM棧),當(dāng)擴(kuò)展時(shí)無法申請(qǐng)到足夠內(nèi)存則拋出OutOfMemoryError異常。
3.本地方法棧
4.堆
每 個(gè)線程的棧都是該線程私有的,堆則是所有線程共享的。當(dāng)我們new一個(gè)對(duì)象時(shí),該對(duì)象就被分配到了堆中。但是堆,并不是一個(gè)簡(jiǎn)單的概念,堆區(qū)又劃分了很多 區(qū)域,為什么堆劃分成這么多區(qū)域,這是為了JVM的內(nèi)存垃圾收集,似乎越扯越遠(yuǎn)了,扯到垃圾收集了,現(xiàn)在的jvm的gc都是按代收集,堆區(qū)大致被分為三大 塊:新生代,舊生代,持久代(虛擬的);新生代又分為eden區(qū),s0區(qū),s1區(qū)。新建一個(gè)對(duì)象時(shí),基本小的對(duì)象,生命周期短的對(duì)象都會(huì)放在新生代的 eden區(qū)中,eden區(qū)滿時(shí),有一個(gè)小范圍的gc(minor gc),整個(gè)新生代滿時(shí),會(huì)有一個(gè)大范圍的gc(major gc),將新生代里的部分對(duì)象轉(zhuǎn)到舊生代里。
5.方法區(qū)
其 實(shí)就是永久代(Permanent Generation),方法區(qū)中存放了每個(gè)Class的結(jié)構(gòu)信息,包括常量池、字段描述、方法描述等等。VM Space描述中對(duì)這個(gè)區(qū)域的限制非常寬松,除了和Java堆一樣不需要連續(xù)的內(nèi)存,也可以選擇固定大小或者可擴(kuò)展外,甚至可以選擇不實(shí)現(xiàn)垃圾收集。相對(duì) 來說,垃圾收集行為在這個(gè)區(qū)域是相對(duì)比較少發(fā)生的,但并不是某些描述那樣永久代不會(huì)發(fā)生GC(至 少對(duì)當(dāng)前主流的商業(yè)JVM實(shí)現(xiàn)來說是如此),這里的GC主要是對(duì)常量池的回收和對(duì)類的卸載,雖然回收的“成績(jī)”一般也比較差強(qiáng)人意,尤其是類卸載,條件相 當(dāng)苛刻。
6.常量池
Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項(xiàng)信息是常量表(constant_pool table),用于存放編譯期已可知的常量,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)(永久代)存放。但是Java語言并不要求常量一定只有編譯期預(yù)置入 Class的常量表的內(nèi)容才能進(jìn)入方法區(qū)常量池,運(yùn)行期間也可將新內(nèi)容放入常量池(最典型的String.intern()方法)。
關(guān)于垃圾收集,在此不多說,流到垃圾收集那一章再 詳細(xì)說吧。關(guān)于java的同步,其實(shí)還有基于CPU原語的比較并交換的非阻塞算法(CAS),不過這個(gè)在java的并發(fā)包里已經(jīng)實(shí)現(xiàn)了很多,因此關(guān)于這 點(diǎn),就留到j(luò)ava并發(fā)包那一章介紹吧。后面我會(huì)專門寫一篇文章,JVM內(nèi)存與垃圾收集。