導(dǎo)語:作為java領(lǐng)域最受歡迎的任務(wù)調(diào)度庫之一,quartz為開發(fā)者提供了豐富的任務(wù)調(diào)度功能,比如讓某段程序在每天18:00準時執(zhí)行。本文將通過demo和源碼,講解quartz如何使用、主要功能有哪些、原理是什么,并挑選幾段有用的源碼片段進行解讀。

1、quartz簡介
quartz,即石英的意思,隱喻如石英表般對時間的準確把握。
quartz是一個由java編寫的任務(wù)調(diào)度庫,由OpenSymphony組織開源出來。那么問題來了,任務(wù)調(diào)度是個什么東西?舉個栗子,現(xiàn)在有N個任務(wù)(程序),要求在指定時間執(zhí)行,比如每周二3點執(zhí)行任務(wù)A、每天相隔5s執(zhí)行任務(wù)B等等,這種多任務(wù)擁有多種執(zhí)行策略就是任務(wù)調(diào)度。而quartz的核心作用,是使任務(wù)調(diào)度變得豐富、高效、安全,開發(fā)者只需要調(diào)幾個quartz接口并做簡單配置,即可實現(xiàn)上述需求。
quartz號稱能夠同時對上萬個任務(wù)進行調(diào)度,擁有豐富的功能特性,包括任務(wù)調(diào)度、任務(wù)持久化、可集群化、插件等。目前最新版本是2.2.3,從github[1]上看,2.2.4已在開發(fā)中。
quartz有競品嗎?有,那就是java Timer。quartz相對于java Timer的優(yōu)勢包括任務(wù)持久化、配置更豐富、線程池等等,詳見官網(wǎng)[2]解釋,兩者在Spring上的用法可見這篇文章[^OpenSymphony Quartz和java Timer]。
接下來,筆者將從一個簡單的demo開始,順著demo里使用到的quartz接口,逐個分析quartz主要功能及其原理。限于篇幅,demo中未涉及的功能,本文不涉及,比如集群化等。最后,挑選2段對大家日常開發(fā)有用的源碼進行解讀。
讀這篇文章有什么用
- 對一個任務(wù)調(diào)度系統(tǒng)產(chǎn)生初步的原理級了解
- 更正確地使用quartz
- 學(xué)到如何采用多線程進行任務(wù)調(diào)度的源碼
- 學(xué)到如何避免GC的源碼
- 精(xian)力(de)充(dan)沛(teng),隨便找篇技術(shù)文讀讀
[^OpenSymphony Quartz和java Timer]: yaerfeng:Spring定時器配置的兩種實現(xiàn)方式OpenSymphony Quartz和java Timer詳解
2、使用代碼demo[3]
本文的demo程序由2個java文件和quartz.properties組成,quartz.properties是可選的,因為quartz有默認配置。demo實現(xiàn)從當前時間開始,每隔2s執(zhí)行一次JobImpl類的execute()方法。
- TestQuartz.java
/**
* 從當前時間開始,每隔2s執(zhí)行一次JobImpl#execute()
* @author hlx
*/
public class TestQuartz {
public static void main(String[] args) throws SchedulerException, InterruptedException {
// 創(chuàng)建調(diào)度器
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
// 創(chuàng)建任務(wù)
JobDetail jobDetail = JobBuilder.newJob(JobImpl.class).withIdentity("myJob", "jobGroup").build();
// 創(chuàng)建觸發(fā)器
// withIntervalInSeconds(2)表示每隔2s執(zhí)行任務(wù)
Date triggerDate = new Date();
SimpleScheduleBuilder schedBuilder = SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever();
TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger().withIdentity("myTrigger", "triggerGroup");
Trigger trigger = triggerBuilder.startAt(triggerDate).withSchedule(schedBuilder).build();
// 將任務(wù)及其觸發(fā)器放入調(diào)度器
scheduler.scheduleJob(jobDetail, trigger);
// 調(diào)度器開始調(diào)度任務(wù)
scheduler.start();
}
}
- JobImpl.java
/**
* @author hlx
*/
public class JobImpl implements Job {
public void execute(JobExecutionContext context) {
System.out.println("job impl running");
}
}
- quartz.properties(可選)
#調(diào)度器名,默認名是QuartzScheduler
org.quartz.scheduler.instanceName: TestQuartzScheduler
#============================================================================
# Configure ThreadPool 配置線程池
#============================================================================
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
#============================================================================
# Configure JobStore 配置任務(wù)存儲方式
#============================================================================
#相當于掃描頻率
org.quartz.jobStore.misfireThreshold: 60000
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
我們順著注釋看到,TestQuartz.main()依次創(chuàng)建了scheduler(調(diào)度器)、job(任務(wù))、trigger(觸發(fā)器),其中,job指定了JobImpl,trigger保存job的觸發(fā)執(zhí)行策略(每隔2s執(zhí)行一次),scheduler將job和trigger綁定在一起,最后scheduler.start()啟動調(diào)度,每隔2s觸發(fā)執(zhí)行JobImpl.execute(),打印出job impl running。
對于quartz.properties,簡單場景下,開發(fā)者不用自定義配置,使用quartz默認配置即可,但在要求較高的使用場景中還是要自定義配置,比如通過org.quartz.threadPool.threadCount設(shè)置足夠的線程數(shù)可提高多job場景下的運行性能。更詳盡的配置見官網(wǎng)配置說明頁。
如果讓我們設(shè)計一個任務(wù)調(diào)度系統(tǒng),會像quartz那樣將job、trigger、scheduler解藕嗎?quartz這樣設(shè)計的原因,筆者認為有兩點:
- job與trigger解藕,其實就是將任務(wù)本身和任務(wù)執(zhí)行策略解藕,這樣可以方便實現(xiàn)N個任務(wù)和M個執(zhí)行策略自由組合,比較容易理解;
- scheduler單獨分離出來,相當于一個指揮官,可以從全局做調(diào)度,比如監(jiān)聽哪些trigger已經(jīng)ready、分配線程等等,如果沒有scheduler,則trigger間會競爭混亂,難以實現(xiàn)諸如trigger優(yōu)先級等功能,也無法合理使用資源。
下面,筆者將分別就job、trigger、scheduler進行原理分析。
3、job(任務(wù))
job由若干個class和interface實現(xiàn)。
Job接口
開發(fā)者想要job完成什么樣的功能,必須且只能由開發(fā)者自己動手來編寫實現(xiàn),比如demo中的JobImpl,這點無容置疑。但要想讓自己的job被quartz識別,就必須按照quartz的規(guī)則來辦事,這個規(guī)則就是job實現(xiàn)類必須實現(xiàn)Job接口,比如JobImpl就實現(xiàn)了Job。
Job只有一個execute(JobExecutionContext),JobExecutionContext保存了job的上下文信息,比如綁定的是哪個trigger。job實現(xiàn)類必須重寫execute(),執(zhí)行job實際上就是運行execute()。
JobDetailImpl類 / JobDetail接口
JobDetailImpl類實現(xiàn)了JobDetail接口,用來描述一個job,定義了job所有屬性及其get/set方法。了解job擁有哪些屬性,就能知道quartz能提供什么樣的能力,下面筆者用表格列出job若干核心屬性。
| 屬性名 | 說明 |
|---|---|
| class | 必須是job實現(xiàn)類(比如JobImpl),用來綁定一個具體job |
| name | job名稱。如果未指定,會自動分配一個唯一名稱。所有job都必須擁有一個唯一name,如果兩個job的name重復(fù),則只有最前面的job能被調(diào)度 |
| group | job所屬的組名 |
| description | job描述 |
| durability | 是否持久化。如果job設(shè)置為非持久,當沒有活躍的trigger與之關(guān)聯(lián)的時候,job會自動從scheduler中刪除。也就是說,非持久job的生命期是由trigger的存在與否決定的 |
| shouldRecover | 是否可恢復(fù)。如果job設(shè)置為可恢復(fù),一旦job執(zhí)行時scheduler發(fā)生hard shutdown(比如進程崩潰或關(guān)機),當scheduler重啟后,該job會被重新執(zhí)行 |
| jobDataMap | 除了上面常規(guī)屬性外,用戶可以把任意kv數(shù)據(jù)存入jobDataMap,實現(xiàn)job屬性的無限制擴展,執(zhí)行job時可以使用這些屬性數(shù)據(jù)。此屬性的類型是JobDataMap,實現(xiàn)了Serializable接口,可做跨平臺的序列化傳輸 |
JobBuilder類
// 創(chuàng)建任務(wù)
JobDetail jobDetail = JobBuilder.newJob(JobImpl.class).withIdentity("myJob", "jobGroup").build();
上面代碼是demo一個片段,可以看出JobBuilder類的作用:接收job實現(xiàn)類JobImpl,生成JobDetail實例,默認生成JobDetailImpl實例。
這里運用了建造者模式:JobImpl相當于Product;JobDetail相當于Builder,擁有job的各種屬性及其get/set方法;JobBuilder相當于Director,可為一個job組裝各種屬性。
4、trigger(觸發(fā)器)
trigger由若干個class和interface實現(xiàn)。
SimpleTriggerImpl類 / SimpleTrigger接口 / Trigger接口
SimpleTriggerImpl類實現(xiàn)了SimpleTrigger接口,SimpleTrigger接口繼承了Trigger接口,它們表示觸發(fā)器,用來保存觸發(fā)job的策略,比如每隔幾秒觸發(fā)job。實際上,quartz有兩大觸發(fā)器:SimpleTrigger和CronTrigger,限于篇幅,本文僅介紹SimpleTrigger。
Trigger諸類保存了trigger所有屬性,同job屬性一樣,了解trigger屬性有助于我們了解quartz能提供什么樣的能力,下面筆者用表格列出trigger若干核心屬性。
在quartz源碼或注釋中,經(jīng)常使用fire(點火)這個動詞來命名屬性名,表示觸發(fā)job。
| 屬性名 | 屬性類型 | 說明 |
|---|---|---|
| name | 所有trigger通用 | trigger名稱 |
| group | 所有trigger通用 | trigger所屬的組名 |
| description | 所有trigger通用 | trigger描述 |
| calendarName | 所有trigger通用 | 日歷名稱,指定使用哪個Calendar類,經(jīng)常用來從trigger的調(diào)度計劃中排除某些時間段 |
| misfireInstruction | 所有trigger通用 | 錯過job(未在指定時間執(zhí)行的job)的處理策略,默認為MISFIRE_INSTRUCTION_SMART_POLICY。詳見這篇blog^Quartz misfire
|
| priority | 所有trigger通用 | 優(yōu)先級,默認為5。當多個trigger同時觸發(fā)job時,線程池可能不夠用,此時根據(jù)優(yōu)先級來決定誰先觸發(fā) |
| jobDataMap | 所有trigger通用 | 同job的jobDataMap。假如job和trigger的jobDataMap有同名key,通過getMergedJobDataMap()獲取的jobDataMap,將以trigger的為準 |
| startTime | 所有trigger通用 | 觸發(fā)開始時間,默認為當前時間。決定什么時間開始觸發(fā)job |
| endTime | 所有trigger通用 | 觸發(fā)結(jié)束時間。決定什么時間停止觸發(fā)job |
| nextFireTime | SimpleTrigger私有 | 下一次觸發(fā)job的時間 |
| previousFireTime | SimpleTrigger私有 | 上一次觸發(fā)job的時間 |
| repeatCount | SimpleTrigger私有 | 需觸發(fā)的總次數(shù) |
| timesTriggered | SimpleTrigger私有 | 已經(jīng)觸發(fā)過的次數(shù) |
| repeatInterval | SimpleTrigger私有 | 觸發(fā)間隔時間 |
TriggerBuilder類
// 創(chuàng)建觸發(fā)器
// withIntervalInSeconds(2)表示每隔2s執(zhí)行任務(wù)
Date triggerDate = new Date();
SimpleScheduleBuilder schedBuilder = SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever();
TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger().withIdentity("myTrigger", "triggerGroup");
Trigger trigger = triggerBuilder.startAt(triggerDate).withSchedule(schedBuilder).build();
上面代碼是demo一個片段,可以看出TriggerBuilder類的作用:生成Trigger實例,默認生成SimpleTriggerImpl實例。同JobBuilder一樣,這里也運用了建造者模式。
5、scheduler(調(diào)度器)
scheduler主要由StdScheduler類、Scheduler接口、StdSchedulerFactory類、SchedulerFactory接口、QuartzScheduler類實現(xiàn),它們的關(guān)系見下面UML圖。

// 創(chuàng)建調(diào)度器
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
......
// 將任務(wù)及其觸發(fā)器放入調(diào)度器
scheduler.scheduleJob(jobDetail, trigger);
// 調(diào)度器開始調(diào)度任務(wù)
scheduler.start();
上面代碼是demo一個片段,可以看出這里運用了工廠模式,通過factory類(StdSchedulerFactory)生產(chǎn)出scheduler實例(StdScheduler)。scheduler是整個quartz的關(guān)鍵,為此,筆者把demo中用到的scheduler接口的源碼加上中文注釋做個講解。
- StdSchedulerFactory.getScheduler()源碼
public Scheduler getScheduler() throws SchedulerException {
// 讀取quartz配置文件,未指定則順序遍歷各個path下的quartz.properties文件
// 解析出quartz配置內(nèi)容和環(huán)境變量,存入PropertiesParser對象
// PropertiesParser組合了Properties(繼承Hashtable),定義了一系列對Properties的操作方法,比如getPropertyGroup()批量獲取相同前綴的配置。配置內(nèi)容和環(huán)境變量存放在Properties成員變量中
if (cfg == null) {
initialize();
}
// 獲取調(diào)度器池,采用了單例模式
// 其實,調(diào)度器池的核心變量就是一個hashmap,每個元素key是scheduler名,value是scheduler實例
// getInstance()用synchronized防止并發(fā)創(chuàng)建
SchedulerRepository schedRep = SchedulerRepository.getInstance();
// 從調(diào)度器池中取出當前配置所用的調(diào)度器
Scheduler sched = schedRep.lookup(getSchedulerName());
......
// 如果調(diào)度器池中沒有當前配置的調(diào)度器,則實例化一個調(diào)度器,主要動作包括:
// 1)初始化threadPool(線程池):開發(fā)者可以通過org.quartz.threadPool.class配置指定使用哪個線程池類,比如SimpleThreadPool。先class load線程池類,接著動態(tài)生成線程池實例bean,然后通過反射,使用setXXX()方法將以org.quartz.threadPool開頭的配置內(nèi)容賦值給bean成員變量;
// 2)初始化jobStore(任務(wù)存儲方式):開發(fā)者可以通過org.quartz.jobStore.class配置指定使用哪個任務(wù)存儲類,比如RAMJobStore。先class load任務(wù)存儲類,接著動態(tài)生成實例bean,然后通過反射,使用setXXX()方法將以org.quartz.jobStore開頭的配置內(nèi)容賦值給bean成員變量;
// 3)初始化dataSource(數(shù)據(jù)源):開發(fā)者可以通過org.quartz.dataSource配置指定數(shù)據(jù)源詳情,比如哪個數(shù)據(jù)庫、賬號、密碼等。jobStore要指定為JDBCJobStore,dataSource才會有效;
// 4)初始化其他配置:包括SchedulerPlugins、JobListeners、TriggerListeners等;
// 5)初始化threadExecutor(線程執(zhí)行器):默認為DefaultThreadExecutor;
// 6)創(chuàng)建工作線程:根據(jù)配置創(chuàng)建N個工作thread,執(zhí)行start()啟動thread,并將N個thread順序add進threadPool實例的空閑線程列表availWorkers中;
// 7)創(chuàng)建調(diào)度器線程:創(chuàng)建QuartzSchedulerThread實例,并通過threadExecutor.execute(實例)啟動調(diào)度器線程;
// 8)創(chuàng)建調(diào)度器:創(chuàng)建StdScheduler實例,將上面所有配置和引用組合進實例中,并將實例存入調(diào)度器池中
sched = instantiate();
return sched;
}
上面有個過程是初始化jobStore,表示使用哪種方式存儲scheduler相關(guān)數(shù)據(jù)。quartz有兩大jobStore:RAMJobStore和JDBCJobStore。RAMJobStore把數(shù)據(jù)存入內(nèi)存,性能最高,配置也簡單,但缺點是系統(tǒng)掛了難以恢復(fù)數(shù)據(jù)。JDBCJobStore保存數(shù)據(jù)到數(shù)據(jù)庫,保證數(shù)據(jù)的可恢復(fù)性,但性能較差且配置復(fù)雜。

- QuartzScheduler.scheduleJob(JobDetail, Trigger)源碼
public Date scheduleJob(JobDetail jobDetail,
Trigger trigger) throws SchedulerException {
// 檢查調(diào)度器是否開啟,如果關(guān)閉則throw異常到上層
validateState();
......
// 獲取trigger首次觸發(fā)job的時間,以此時間為起點,每隔一段指定的時間觸發(fā)job
Date ft = trig.computeFirstFireTime(cal);
if (ft == null) {
throw new SchedulerException(
"Based on configured schedule, the given trigger '" + trigger.getKey() + "' will never fire.");
}
// 把job和trigger注冊進調(diào)度器的jobStore
resources.getJobStore().storeJobAndTrigger(jobDetail, trig);
// 通知job監(jiān)聽者
notifySchedulerListenersJobAdded(jobDetail);
// 通知調(diào)度器線程
notifySchedulerThread(trigger.getNextFireTime().getTime());
// 通知trigger監(jiān)聽者
notifySchedulerListenersSchduled(trigger);
return ft;
}
- QuartzScheduler.start()源碼
public void start() throws SchedulerException {
......
// 這句最關(guān)鍵,作用是使調(diào)度器線程跳出一個無限循環(huán),開始輪詢所有trigger觸發(fā)job
// 原理詳見“如何采用多線程進行任務(wù)調(diào)度”
schedThread.togglePause(false);
......
}
6、quartz線程模型

上圖是筆者在eclipse中調(diào)試demo時的線程圖,可以見到,除了第一條主線程外,還有10條工作線程,和1條調(diào)度器線程。
工作線程以{instanceName}_Worker-{[1-10]}命名。線程數(shù)目由quart.properties文件中的org.quartz.threadPool.threadCount配置項指定。所有工作線程都會放在線程池中,即所有工作線程都放在SimpleThreadPool實例的一個LinkedList<WorkerThread>成員變量中。WorkerThread是SimpleThreadPool的內(nèi)部類,這么設(shè)計可能是因為不想繼承SimpleThreadPool但又想調(diào)用其protected方法,或者想隱藏WorkerThread。線程池還擁有兩個LinkedList<WorkerThread>:availWorkers和busyWorkers,分別存放空閑和正在執(zhí)行job的工作線程。
調(diào)度器線程以{instanceName}_QuartzSchedulerThread命名。該線程將根據(jù)trigger找出要待運行job,然后從threadpool中拿出工作線程來執(zhí)行。調(diào)度器線程主體是QuartzSchedulerThread對象。
{instanceName}指的是quart.properties文件中的org.quartz.scheduler.instanceName配置值,這里是TestQuartzScheduler。[1-10]表示從1到10的任意數(shù)字。
7、精彩源碼解讀
本節(jié)中,筆者從quartz源碼中挑選了兩段代碼,之所以選擇這兩段代碼,是因為它們實現(xiàn)了線程間通信、加鎖同步、避免GC等功能,對工程師們很有幫助。
如何采用多線程進行任務(wù)調(diào)度
- QuartzSchedulerThread.java
// 調(diào)度器線程一旦啟動,將一直運行此方法
public void run() {
......
// while()無限循環(huán),每次循環(huán)取出時間將到的trigger,觸發(fā)對應(yīng)的job,直到調(diào)度器線程被關(guān)閉
// halted是一個AtomicBoolean類變量,有個volatile int變量value,其get()方法僅僅簡單的一句return value != 0,get()返回結(jié)果表示調(diào)度器線程是否開關(guān)
// volatile修飾的變量,存取必須走內(nèi)存,不能通過cpu緩存,這樣一來get總能獲得set的最新真實值,因此volatile變量適合用來存放簡單的狀態(tài)信息
// 顧名思義,AtomicBoolean要解決原子性問題,但volatile并不能保證原子性,詳見http://blog.csdn.net/wxwzy738/article/details/43238089
while (!halted.get()) {
try {
// check if we're supposed to pause...
// sigLock是個Object對象,被用于加鎖同步
// 需要用到wait(),必須加到synchronized塊內(nèi)
synchronized (sigLock) {
while (paused && !halted.get()) {
try {
// wait until togglePause(false) is called...
// 這里會不斷循環(huán)等待,直到QuartzScheduler.start()調(diào)用了togglePause(false)
// 調(diào)用wait(),調(diào)度器線程進入休眠狀態(tài),同時sigLock鎖被釋放
// togglePause(false)獲得sigLock鎖,將paused置為false,使調(diào)度器線程能夠退出此循環(huán),同時執(zhí)行sigLock.notifyAll()喚醒調(diào)度器線程
sigLock.wait(1000L);
} catch (InterruptedException ignore) {}
}
......
}
......
// 如果線程池中的工作線程個數(shù) > 0
if(availThreadCount > 0) {
......
// 獲取馬上到時間的trigger
// 允許取出的trigger個數(shù)不能超過一個閥值,這個閥值是線程池個數(shù)與org.quartz.scheduler.batchTriggerAcquisitionMaxCount配置值間的最小者
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
......
// 執(zhí)行與trigger綁定的job
// shell是JobRunShell對象,實現(xiàn)了Runnable接口
// SimpleThreadPool.runInThread(Runnable)從線程池空閑列表中取出一個工作線程
// 工作線程執(zhí)行WorkerThread.run(Runnable),詳見下方WorkerThread的講解
if (qsRsrcs.getThreadPool().runInThread(shell) == false) { ...... }
} else {......}
......
} catch(RuntimeException re) {......}
} // while (!halted)
......
}
- WorkerThread.java
public void run(Runnable newRunnable) {
synchronized(lock) {
if(runnable != null) {
throw new IllegalStateException("Already running a Runnable!");
}
runnable = newRunnable;
lock.notifyAll();
}
}
// 工作線程一旦啟動,將一直運行此方法
@Override
public void run() {
boolean ran = false;
// 工作線程一直循環(huán)等待job,直到線程被關(guān)閉,原理同QuartzSchedulerThread.run()中的halted.get()
while (run.get()) {
try {
// 原理同QuartzSchedulerThread.run()中的synchronized (sigLock)
// 鎖住lock,不斷循環(huán)等待job,當job要被執(zhí)行時,WorkerThread.run(Runnable)被調(diào)用,job運行環(huán)境被賦值給runnable
synchronized(lock) {
while (runnable == null && run.get()) {
lock.wait(500);
}
// 開始執(zhí)行job
if (runnable != null) {
ran = true;
// runnable.run()將觸發(fā)運行job實現(xiàn)類(比如JobImpl.execute())
runnable.run();
}
}
} catch (InterruptedException unblock) {
......
}
}
......
}
總的來說,核心代碼就是在while循環(huán)中調(diào)用Object.wait(),等待可以跳出while循環(huán)的條件成立,當條件成立時,立馬調(diào)度Object.notifyAll()使線程跳出while。通過這樣的代碼,可以實現(xiàn)調(diào)度器線程等待啟動、工作線程等待job等功能。
如何避免GC
Quartz里提供了一種方案,用來避免某些對象被GC。方案其實簡單而實用,就是QuartzScheduler類創(chuàng)建了一個列表ArrayList<Object>(5) holdToPreventGC,如果某對象被add進該列表,則意味著QuartzScheduler實例引用了此對象,那么此對象至少在QuartzScheduler實例存活時不會被GC。
哪些對象要避免GC?通過源碼可看到,調(diào)度器池和db管理器對象被放入了holdToPreventGC,但實際上兩種對象是static的,而static對象屬于GC root,應(yīng)該是不會被GC的,所以即使不放入holdToPreventGC,這兩種對象也不會被GC,除非被class unload或jvm生命結(jié)束。
static變量所指對象在heap中,如果變量不再指向該對象,比如賦值為null,對象會被GC