LeetCode 75,90%的人想不出最佳解的簡單題

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是LeetCode專題的44篇文章,我們一起來看下LeetCode的75題,顏色排序 Sort Colors。

這題的官方難度是Medium,通過率是45%,點贊2955,反對209(國際版數據),從這份數據上我們大概能看得出來,這題的難度不大,並且點贊遠遠高於反對,說明題目的質量很不錯。事實上也的確如此,這題足夠簡單也足夠有趣,值得一做。

題意

給定一個n個元素的數組,數組當中的每一個元素表示一個顏色。一共有紅白藍三種顏色,分別用0,1和2來表示。要求將這些顏色按照大小進行排序,返回排序之後的結果。

要求不能調用排序庫sort來解決問題。

桶排序

看完題目應該感受到了,如果沒有不能使用sort的限制,這題毫無難度。即使加上了限制難度也不大,我們既然不能調用sort,難道還不能自己寫個sort嗎?Python寫個快排也才幾行而已。

自己寫sort當然是可以的,顯然這是下下策。因為元素只有3個值,互相之間的大小關係也就只有那麼幾種,排序完全沒有必要。比較容易想到,我們可以統計一下這三個數值出現的次數,幾個0幾個1幾個2,我們再把這些數拼在一起,還原之前的數據不就可以了嗎?

這樣的確可行,但實際上這也是一種排序方案,叫做基數排序,也稱為桶排序,還有些地方稱為小學生排序(大概是小學生都能懂的意思吧)。基數排序的思想非常簡單,我們創建一個數組,用它的每一位來表示某個元素是否在原數組當中出現過。出現過則+1,沒出現過則一直是0。我們標記完原數組之後,再遍歷一遍標記的數組,由於下標天然有序,所以我們就可以得到排序之後的結果了。

如果你還有些迷糊也沒有關係,我們把代碼寫出來就明白了,由於這題讓我們提供一個inplace的方法,所以我們在最後的時候需要對nums當中的元素重新賦值。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """  Do not return anything, modify nums in-place instead.  """
        bucket = [0 for _ in range(3)]
        for i in nums:
            bucket[i] += 1

        ret = []
        for i in range(3):
            ret += [i] * bucket[i]

        nums[:] = ret[:]

和排序相比,我們只是遍歷了兩次數據,第一次是遍歷了原數組獲得了其中0,1和2的數量,第二次是將獲得的數據重新填充回原數組當中。相比於快排或者是其他一些排序算法的耗時,桶排序只遍歷了兩次數組,明顯要快得多。但遺憾的是這並不是最佳的方法,題目當中明確說了,還有隻需要遍歷一次原數組的方法。

two pointers

在我們介紹具體的算法之前,我們先來分析一下問題。既然顏色只有三種,那麼當我們排完序之後,整個數組會被分成三個部分,頭部是0,中間是1,尾部是2。

我們可以用一個區間來收縮1的範圍,假設我們當前區間的首尾元素分別是l和r。當我們讀到0的時候,我們就將它和l交換,然後將l向後移動一位。當我們讀到2的時候,則將它和r進行交換,將r向左移動一位。也就是說我們保證l和r之間的元素只有1。

我們之前曾經介紹過這種維護一個區間的做法,雖然都是維護了一個區間,但是操作上是有一些區別的。之前介紹的two pointers算法,也叫做尺取法,本質上是通過移動區間的右側邊界來容納新的元素,通過移動左邊界彈出數據的方式來維護區間內所有元素的合法性。而當前的做法中,一開始獲得的就是一個非法的區間,我們通過元素的遍歷以及區間的移動,最後讓它變得合法。兩者的思路上有一些細微的差別,但形式是一樣的,就是通過移動左右兩側的邊界來維護或者是達到合法。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """  Do not return anything, modify nums in-place instead.  """
        l, r = 0, len(nums)-1
        i = 0
        while i < len(nums):
            if i > r:
                break
   # 如果遇到0,則和左邊交換
            if nums[i] == 0:
                nums[l], nums[i] = nums[i], nums[l]
                l += 1
            # 如果遇到2,則和右邊交換
            # 交換之後i需要-1,因為可能換來一個0
            elif nums[i] == 2:
                nums[r], nums[i] = nums[i], nums[r]
                r -= 1
                continue
            i += 1

這種方法我們雖然只遍歷了數組一次,但是由於交換的次數過多,整體運行的速度比上面的方法還要慢。所以遍歷兩次數組並不一定就比只遍歷一次要差,畢竟兩者都是的算法,相差的只是一個常數。遍歷的次數只是構成常數的部分之一。

除了這個方法之外,我們還有其他維護區間的方法。

維護區間

接下來要說的方法非常巧妙,我個人覺得甚至要比上面的方法還有巧妙。

我們來假想一下這麼一個場景,假設我們不是在原數組上操作數據,而是從其中讀出數據放到新的數組當中。我們先不去想應該怎麼擺放這個問題,我們就來假設我們原數組當中的數據已經放好了若干個,那麼這個時候的新數組會是什麼樣?顯然,應該是排好序的,前面若干個0,中間若干個1,最後若干個2。

那麼問題來了,假設這個時候我們讀到一個0,那麼應該怎麼放呢?為了簡化敘述我們把它畫成圖:

我們假設藍色部分是0,綠色部分是1,粉色部分是2。a是0最右側的下標,b是1部分最右側的下標,c是2部分最右側的下標。那麼這個時候,當我們需要放入一個0的時候,應該怎麼辦?

我們結合圖很容易想明白,我們需要把0放在a+1的位置,那麼我們需要把後面1和2的部分都往右側移動一格,讓出一格位置出來放0。我們移動數組顯然帶來的開銷會過於大,實際上沒有必要移動整個部分,只需要移動頭尾元素即可。比如1的部分左側被0佔掉了一格,那麼為了保持長度不變,右側也需要延伸一格。同理,2的部分右側也需要延伸一格。那麼整個操作用代碼來表示就是:nums[a+1] = 0,nums[b+1] = 1, nums[c+1] = 2。

假設我們讀入的數是1,那麼我們需要把b延長一個單位,但是這樣帶來的後果是2的部分被侵佔,所以需要將2也延長,補上被1侵佔的一個單位。如果讀到的是2,那麼直接延長2即可,因為2後面沒有其他顏色了。

假設我們有一個空白的數組,我們可以這麼操作,但其實我們沒有必要專門創建一個數組,我們完全可以用原數組自己填充自己。因為我們從原數組上讀取的數和擺放的數是一樣的,我們直接把数字擺放在原數組的頭部,佔用之前讀取的數即可。

光說可能還有些迷糊,看下代碼馬上就清楚了:

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """  Do not return anything, modify nums in-place instead.  """
        # 記錄0,1和2的末尾位置
        zero, one, two = -1, -1, -1
        n = len(nums)
        for i in range(n):
            # 如果擺放0
            # 那麼1和2都往後平移一位,讓一個位置出來擺放0
            if nums[i] == 0:
                nums[two+1] = 2
                nums[one+1] = 1
                nums[zero+1] = 0
                zero += 1
                one += 1
                two += 1
            elif nums[i] == 1:
                nums[two+1] = 2
                nums[one+1] = 1
                one += 1
                two += 1
            else:
                nums[two+1] = 2
                two += 1

總結

到這裏,這道題的解法基本上都講完了。

相信大家也都看出來了,從難度上來說這題真的不難,相信大家都能想出解法來,但是要想到最優解還是有些困難的。一方面需要我們對題目有非常深入的理解,一方面也需要大量的思考。這類題目沒有固定的解法,需要我們根據題目的要求以及實際情況自行設計解法,這也是最考驗思維能力以及算法設計能力的問題,比考察某個算法會不會的問題要有意思得多。

希望大家都能從這題當中獲得樂趣,如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

詳解 Seata Golang 客戶端 AT 模式及其使用

源碼seata-golang

概述

  我們知道 Seata Java Client 的 AT 模式,通過代理數據源,實現了對業務代碼無侵入的分佈式事務協調機制,將與 Transaction Coordinator (TC) 交互的邏輯、Commit 的邏輯、Rollback 的邏輯,隱藏在切面和代理數據源相應的代碼中,使開發者無感知。那如果這個方法,要用 Golang 來實現一遍,應該如何操作呢?關於這個問題,我想了很久,最初的設想是,對 database/sql 的 mysql driver 進行增強,在對包 github.com/go-sql-driver/mysql 研究了一段時間后,還是沒有頭緒,不知如何下手,最後轉而增強 database/sql 包。由於 AT 模式必須保證本地事務的正確處理,在具體業務開發時,首先要通過 db.Begin() 獲得一個 Tx 對象,然後再 tx.Exec() 執行數據庫操作,最後 tx.Commit() 提交或 tx.Rollback() 回滾。這種處理方式算是一個 Golang 數據庫事務處理的基本操作。 所以對 database/sql 的增強,我們重點關注這幾個方法 db.Begin()tx.Exec()tx.Commit()tx.Rollback

事務提交、回滾

  通過 Seata Java Client 的相關代碼,我們知道,在本地事務提交的時候,主要是將分支事務註冊到 TC 上,並將數據庫操作產生的 undoLog 一起寫入到 undoLog 表;本地事務回滾的時候,需要將分支事務(即本地事務)的執行狀態報告給 TC,使 TC 好知道是否通知參与全局事務的其他分支回滾。

func (tx *Tx) Commit() error {
        //註冊分支事務
	branchId,err := tx.register()
	if err != nil {
		return errors.WithStack(err)
	}
	tx.tx.Context.BranchId = branchId

	if tx.tx.Context.HasUndoLog() {
                //將 undoLog 寫入 undoLog 表
		err = manager.GetUndoLogManager().FlushUndoLogs(tx.tx)
		if err != nil {
			err1 := tx.report(false)
			if err1 != nil {
				return errors.WithStack(err1)
			}
			return errors.WithStack(err)
		}
		err = tx.tx.Commit()
		if err != nil {
			err1 := tx.report(false)
			if err1 != nil {
				return errors.WithStack(err1)
			}
			return errors.WithStack(err)
		}
	} else {
		return tx.tx.Commit()
	}
	if tx.reportSuccessEnable {
		tx.report(true)
	}
	tx.tx.Context.Reset()
	return nil
}

  db.Begin() 會產生一個 Tx 對象,tx.Exec() 會產生 undoLog,tx.Commit() 將 undoLog 刷到數據庫中。那麼 undoLog 保存到哪裡呢?答案是 TxContext 中。

type TxContext struct {
	*context.RootContext
	Xid string
	BranchId int64
	IsGlobalLockRequire bool

	LockKeysBuffer *model.Set
	SqlUndoItemsBuffer []*undo.SqlUndoLog
}

  Commit() 方法中的 tx.tx.Context,第一個 tx 是封裝的 Tx 對象,第二個 tx 是 database/sql 的 Tx,tx.tx.Context 則是 TxContext。UndoLogManager 則是操作 undoLog 的核心對象,處理 undoLog 的插入、刪除,並查詢出 undoLog 用於回滾。

func (tx *Tx) Rollback() error {
	err := tx.tx.Rollback()
	if tx.tx.Context.InGlobalTransaction() && tx.tx.Context.IsBranchRegistered() {
                // 報告 TC 分支事務執行失敗
		tx.report(false)
	}
	tx.tx.Context.Reset()
	return err
}

  通過上面的代碼呢,我們知道增強型 Tx 對象需要向 TC 註冊分支事務,並報告分支事務的執行狀態,相應代碼如下:

func (tx *Tx) register() (int64,error) {
	return dataSourceManager.BranchRegister(meta.BranchTypeAT,tx.tx.ResourceId,"",tx.tx.Context.Xid,
		nil,tx.tx.Context.BuildLockKeys())
}

func (tx *Tx) report(commitDone bool) error {
	retry := tx.reportRetryCount
	for retry > 0 {
		var err error
		if commitDone {
			err = dataSourceManager.BranchReport(meta.BranchTypeAT, tx.tx.Context.Xid, tx.tx.Context.BranchId,
				meta.BranchStatusPhaseoneDone,nil)
		} else {
			err = dataSourceManager.BranchReport(meta.BranchTypeAT, tx.tx.Context.Xid, tx.tx.Context.BranchId,
				meta.BranchStatusPhaseoneFailed,nil)
		}
		if err != nil {
			logging.Logger.Errorf("Failed to report [%d/%s] commit done [%t] Retry Countdown: %d",
				tx.tx.Context.BranchId,tx.tx.Context.Xid,commitDone,retry)
			retry = retry -1
			if retry == 0 {
				return errors.WithMessagef(err,"Failed to report branch status %t",commitDone)
			}
		}
	}
	return nil
}

  和 TC 進行通信的主要邏輯還是在 DataSourceManager 裏面。AT 模式涉及的兩個關鍵對象 DataSourceManager、UndoLogManager 就浮出水面。一個用於遠程 TC 交互,一個用於本地數據庫處理。

事務執行

func (tx *Tx) Exec(query string, args ...interface{}) (sql.Result, error) {
	var parser = p.New()
        // 解析業務 sql
	act,_ := parser.ParseOneStmt(query,"","")
	deleteStmt,isDelete := act.(*ast.DeleteStmt)
	if isDelete {
		executor := &DeleteExecutor{
			tx:            tx.tx,
			sqlRecognizer: mysql.NewMysqlDeleteRecognizer(query,deleteStmt),
			values:        args,
		}
		return executor.Execute()
	}

	insertStmt,isInsert := act.(*ast.InsertStmt)
	if isInsert {
		executor := &InsertExecutor{
			tx:            tx.tx,
			sqlRecognizer: mysql.NewMysqlInsertRecognizer(query,insertStmt),
			values:        args,
		}
		return executor.Execute()
	}

	updateStmt,isUpdate := act.(*ast.UpdateStmt)
	if isUpdate {
		executor := &UpdateExecutor{
			tx:            tx.tx,
			sqlRecognizer: mysql.NewMysqlUpdateRecognizer(query,updateStmt),
			values:        args,
		}
		return executor.Execute()
	}

	return tx.tx.Tx.Exec(query,args)
}

  執行業務 sql,並生成 undoLog 的關鍵,在於識別業務 sql 執行了什麼操作:插入?刪除?修改?這裏使用 tidb 的 sql parser 去解析業務 sql,再使用相應的執行器去執行業務 sql,生成 undoLog 保存在 Tx_Context 中。

事務開啟

  db.Begin() 返回增強型的 Tx 對象。

func (db *DB) Begin(ctx *context.RootContext) (*Tx,error) {
	tx,err := db.DB.Begin()
	if err != nil {
		return nil,err
	}
	proxyTx := &tx2.ProxyTx{
		Tx:         tx,
		DSN:        db.conf.DSN,
		ResourceId: db.GetResourceId(),
		Context:    tx2.NewTxContext(ctx),
	}
	return &Tx{
		tx: proxyTx,
		reportRetryCount: db.conf.ReportRetryCount,
		reportSuccessEnable: db.conf.ReportSuccessEnable,
	},nil
}

seata-golang at 模式的使用

sample 代碼

  • 首先執行 scripts 腳本,初始化數據庫
    如果之前沒有初始化過 seata 數據庫,先執行 seata-golang/scripts/server/db/mysql.sql 腳本
  • 修改 dsn 數據庫配置,修改下列文件:
seata-golang/tc/app/profiles/dev/config.yml
seata-golang/samples/at/product_svc/conf/client.yml
seata-golang/samples/at/product_svc/conf/client.yml
  • 將下列文件中的 configPath 修改為 client.yml 配置文件的路徑
seata-golang/samples/at/product_svc/main.go
seata-golang/samples/at/order_svc/main.go
seata-golang/samples/at/aggregation_svc/main.go
  • 依次運行 tc、order_svc、product_svc、aggragation_svc,訪問下列地址開始測試:
http://localhost:8003/createSoCommit
http://localhost:8003/createSoRollback

TC 啟動參考參与 Seata 社區到 go 與 Seata 的邂逅

seata-golang 後續安排

  接下來不打算再增加新的 feature。Java 版 Seata 畢竟發展了一年多時間,並且有很多社區成員一起維護,Go 版本目前主要是我在開發,時間不到2個月,現有的代碼,僅是完成了框架,還需要大量優化,改bug,後續的工作重心在於使 seata-golang 穩定運行,生產可用,希望對分佈式事務感興趣且對 Go 感興趣的同學一起加入進來,一起做些事情。進入微信群,請加我微信:scottlewis,備註進群。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

解Bug之路-記一次JVM堆外內存泄露Bug的查找

解Bug之路-記一次JVM堆外內存泄露Bug的查找

前言

JVM的堆外內存泄露的定位一直是個比較棘手的問題。此次的Bug查找從堆內內存的泄露反推出堆外內存,同時對物理內存的使用做了定量的分析,從而實錘了Bug的源頭。筆者將此Bug分析的過程寫成博客,以饗讀者。
由於物理內存定量分析部分用到了linux kernel虛擬內存管理的知識,讀者如果有興趣了解請看ulk3(《深入理解linux內核第三版》)

內存泄露Bug現場

一個線上穩定運行了三年的系統,從物理機遷移到docker環境后,運行了一段時間,突然被監控系統發出了某些實例不可用的報警。所幸有負載均衡,可以自動下掉節點,如下圖所示:

登錄到對應機器上后,發現由於內存佔用太大,觸發OOM,然後被linux系統本身給kill了。

應急措施

緊急在出問題的實例上再次啟動應用,啟動后,內存佔用正常,一切Okay。

奇怪現象

當前設置的最大堆內存是1792M,如下所示:

-Xmx1792m -Xms1792m -Xmn900m -XX:PermSi
ze=256m -XX:MaxPermSize=256m -server -Xss512k 

查看操作系統層面的監控,發現內存佔用情況如下圖所示:

上圖藍色的線表示總的內存使用量,發現一直漲到了4G后,超出了系統限制。
很明顯,有堆外內存泄露了。

查找線索

gc日誌

一般出現內存泄露,筆者立馬想到的就是查看當時的gc日誌。
本身應用所採用框架會定時打印出對應的gc日誌,遂查看,發現gc日誌一切正常。對應日誌如下:

查看了當天的所有gc日誌,發現內存始終會回落到170M左右,並無明顯的增加。要知道JVM進程本身佔用的內存可是接近4G(加上其它進程,例如日誌進程就已經到4G了),進一步確認是堆外內存導致。

排查代碼

打開線上服務對應對應代碼,查了一圈,發現沒有任何地方顯式利用堆外內存,其沒有依賴任何額外的native方法。關於網絡IO的代碼也是託管給Tomcat,很明顯,作為一個全世界廣泛流行的Web服務器,Tomcat不大可能有堆外內存泄露。

進一步查找

由於在代碼層面沒有發現堆外內存的痕迹,那就繼續找些其它的信息,希望能發現蛛絲馬跡。

Dump出JVM的Heap堆

由於線上出問題的Server已經被kill,還好有其它幾台,登上去發現它們也 佔用了很大的堆外內存,只是還沒有到觸發OOM的臨界點而已。於是就趕緊用jmap dump了兩台機器中應用JVM的堆情況,這兩台留做現場保留不動,然後將其它機器迅速重啟,以防同時被OOM導致服務不可用。
使用如下命令dump:

jmap -dump:format=b,file=heap.bin [pid]

使用MAT分析Heap文件

挑了一個heap文件進行分析,堆的使用情況如下圖所示:

一共用了200多M,和之前gc文件打印出來的170M相差不大,遠遠沒有到4G的程度。
不得不說MAT是個非常好用的工具,它可以提示你可能內存泄露的點:

這個cachedBnsClient類有12452個實例,佔用了整個堆的61.92%。
查看了另一個heap文件,發現也是同樣的情況。這個地方肯定有內存泄露,但是也佔用了130多M,和4G相差甚遠。

查看對應的代碼

系統中大部分對於CachedBnsClient的調用,都是通過註解Autowired的,這部分實例數很少。
唯一頻繁產生此類實例的代碼如下所示:

@Override
    public void fun() {
            BnsClient bnsClient = new CachedBnsClient();
          // do something
    		return  ;
	}

此CachedBnsClient僅僅在方法體內使用,並沒有逃逸到外面,再看此類本身

public class CachedBnsClient   {
    private ConcurrentHashMap<String, List<String>> authCache = new ConcurrentHashMap<String, List<String>>();
    private ConcurrentHashMap<String, List<URI>> validUriCache = new ConcurrentHashMap<String, List<URI>>();
    private ConcurrentHashMap<String, List<URI>> uriCache = new ConcurrentHashMap<String, List<URI>>();
	......
}

沒有任何static變量,同時也沒有往任何全局變量註冊自身。換言之,在類的成員(Member)中,是不可能出現內存泄露的。
當時只粗略的過了一過成員變量,回過頭來細想,還是漏了不少地方的。

更多信息

由於代碼排查下來,感覺這塊不應該出現內存泄露(但是事實確是如此的打臉)。這個類也沒有顯式用到堆外內存,而且只佔了130M,和4G比起來微不足道,還是先去追查主要矛盾再說。

使用jstack dump線程信息

現場信息越多,越能找出蛛絲馬跡。先用jstack把線程信息dump下來看下。
這一看,立馬發現了不同,除了正常的IO線程以及框架本身的一些守護線程外,竟然還多出來了12563多個線程。

"Thread-5" daemon prio=10 tid=0x00007fb79426e000 nid=0x7346 waiting on condition [0x00007fb7b5678000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
	at java.lang.Thread.sleep(Native Method)
	at com.xxxxx.CachedBnsClient$1.run(CachedBnsClient.java:62)

而且這些正好是運行再CachedBnsClient的run方法上面!這些特定線程的數量正好是12452個,和cachedBnsClient數量一致!

再次check對應代碼

原來剛才看CachedBnsClient代碼的時候遺漏掉了一個關鍵的點!

    public CachedBnsClient(BnsClient client) {
        super();
        this.backendClient = client;
        new Thread() {
            @Override
            public void run() {
                for (; ; ) {
                    refreshCache();
                    try {
                        Thread.sleep(60 * 1000);
                    } catch (InterruptedException e) {
                        logger.error("出錯", e);
                    }
                }
            }
            ......
        }.start();
    }

這段代碼是CachedBnsClient的構造函數,其在裏面創建了一個無限循環的線程,每隔60s啟動一次刷新一下裏面的緩存!

找到關鍵點

在看到12452個等待在CachedBnsClient.run的業務的一瞬間筆者就意識到,肯定是這邊的線程導致對外內存泄露了。下面就是根據線程大小計算其泄露內存量是不是確實能夠引起OOM了。

發現內存計算對不上

由於我們這邊設置的Xss是512K,即一個線程棧大小是512K,而由於線程共享其它MM單元(線程本地內存是是現在線程棧上的),所以實際線程堆外內存佔用數量也是512K。進行如下計算:

12563 * 512K = 6331M = 6.3G

整個環境一共4G,加上JVM堆內存1.8G(1792M),已經明顯的超過了4G。

(6.3G + 1.8G)=8.1G > 4G

如果按照此計算,應用應用早就被OOM了。

怎麼回事呢?

為了解決這個問題,筆者又思考了好久。如下所示:

Java線程底層實現

JVM的線程在linux上底層是調用NPTL(Native Posix Thread Library)來創建的,一個JVM線程就對應linux的lwp(輕量級進程,也是進程,只不過共享了mm_struct,用來實現線程),一個thread.start就相當於do_fork了一把。
其中,我們在JVM啟動時候設置了-Xss=512K(即線程棧大小),這512K中然後有8K是必須使用的,這8K是由進程的內核棧和thread_info公用的,放在兩塊連續的物理頁框上。如下圖所示:

眾所周知,一個進程(包括lwp)包括內核棧和用戶棧,內核棧+thread_info用了8K,那麼用戶態的棧可用內存就是:

512K-8K=504K

如下圖所示:

Linux實際物理內存映射

事實上linux對物理內存的使用非常的摳門,一開始只是分配了虛擬內存的線性區,並沒有分配實際的物理內存,只有推到最後使用的時候才分配具體的物理內存,即所謂的請求調頁。如下圖所示:

查看smaps進程內存使用信息

使用如下命令,查看

cat /proc/[pid]/smaps > smaps.txt

實際物理內存使用信息,如下所示:

7fa69a6d1000-7fa69a74f000 rwxp 00000000 00:00 0 
Size:                504 kB
Rss:                  92 kB
Pss:                  92 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:        92 kB
Referenced:           92 kB
Anonymous:            92 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB

7fa69a7d3000-7fa69a851000 rwxp 00000000 00:00 0 
Size:                504 kB
Rss:                 152 kB
Pss:                 152 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:       152 kB
Referenced:          152 kB
Anonymous:           152 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB

搜索下504KB,正好是12563個,對了12563個線程,其中Rss表示實際物理內存(含共享庫)92KB,Pss表示實際物理內存(按比例共享庫)92KB(由於沒有共享庫,所以Rss==Pss),以第一個7fa69a6d1000-7fa69a74f000線性區來看,其映射了92KB的空間,第二個映射了152KB的空間。如下圖所示:

挑出符合條件(即size是504K)的幾十組看了下,基本都在92K-152K之間,再加上內核棧8K

(92+152)/2+8K=130K,由於是估算,取整為128K,即反映此應用平均線程棧大小。

注意,實際內存有波動的原因是由於環境不同,從而走了不同的分支,導致棧上的增長不同。

重新進行內存計算

JVM一開始申請了

-Xmx1792m -Xms1792m

即1.8G的堆內內存,這裡是即時分配,一開始就用物理頁框填充。
12563個線程,每個線程棧平均大小128K,即:

128K * 12563=1570M=1.5G的對外內存

取個整數128K,就能反映出平均水平。再拿這個128K * 12563 =1570M = 1.5G,加上JVM的1.8G,就已經達到了3.3G,再加上kernel和日誌傳輸進程等使用的內存數量,確實已經接近了4G,這樣內存就對應上了!(注:用於定量內存計算的環境是一台內存用量將近4G,但還沒OOM的機器)

為什麼在物理機上沒有應用Down機

筆者登錄了原來物理機,應用還在跑,發現其同樣有堆外內存泄露的現象,其物理內存使用已經達到了5個多G!幸好物理機內存很大,而且此應用發布還比較頻繁,所以沒有被OOM。
Dump了物理機上應用的線程,

一共有28737個線程,其中28626個線程等待在CachedBnsClient上。 

同樣用smaps查看進程實際內存信息,其平均大小依舊為

128K,因為是同一應用的原因

繼續進行物理內存計算

1.8+(28737 * 128k)/1024K =(3.6+1.8)=5.4G

進一步驗證了我們的推理。

這麼多線程應用為什麼沒有卡頓

因為基本所有的線程都睡眠在

 Thread.sleep(60 * 1000);//一次睡眠60s

上。所以僅僅佔用了內存,實際佔用的CPU時間很少。

總結

查找Bug的時候,現場信息越多越好,同時定位Bug必須要有實質性的證據。例如內存泄露就要用你推測出的模型進行定量分析。在定量和實際對不上的時候,深挖下去,你會發現不一樣的風景!

公眾號

關注筆者公眾號,獲取更多乾貨文章:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

※回頭車貨運收費標準

Action的三種實現方式,struts.xml配置的詳細解釋及其簡單執行過程(二)

勿以惡小而為之,勿以善小而不為————————–劉備

勸諸君,多行善事積福報,莫作惡

上一章簡單介紹了Struts2的’兩個蝴蝶飛,你好’ (一),如果沒有看過,請觀看上一章

一 Action的三種實現方式

上一章開發的HelloAction和HelloAction2,並沒有繼承任何類或者實現任何接口,但是必須有一個execute() 方法,方法返回值是String類型。

這樣的代碼不容易理解,更並不能使人看得出這個類是干什麼的,甚至不能區分這個控制器類與普通的Java類有什麼區別,通常開發中不這樣做。

我們開發者在開發Struts2框架的時候,希望自己寫的這個Action類能夠具有易理解性,且已經支持某些功能,如參數接收,文件上傳等。

一.一 第一種實現方式(普通Java類,裏面只包含execute()方法)

package com.yjl.web.action;
import org.apache.log4j.Logger;
/**
* @author 兩個蝴蝶飛
* @version 創建時間:2018年8月23日 上午9:41:32
* @description 第一種實現方式,普通java類,
* 有一個execute()方法,也可以多寫幾個方法,用action中的標籤method來控制,可以正常訪問。
*/
public class Hello1Action {
	private static Logger logger=Logger.getLogger(Hello1Action.class);
	public String execute() {
		logger.info("兩個蝴蝶飛,web層你好");
		return "success";
	}
}

不具有開發時要求的規範性,且不支持某些struts2自身提供的功能。

方法名稱只有一個 execute()

一.二 第二種實現方式(實現Action接口)

package com.yjl.web.action;
import com.opensymphony.xwork2.Action;
/**
* @author 兩個蝴蝶飛
* @version 創建時間:2018年8月23日 上午10:54:03
* @description 第二種實現方式,實現Action接口,重寫裏面的execute()方法
* 有一個execute()方法和五個String類型的常量
*/
public class Hello2Action implements Action{
	@Override
	public String execute() throws Exception {
		return Action.SUCCESS;
		//return Action.ERROR;
		//return Action.LOGIN;
		//return Action.NONE;
		//return Action.INPUT;
	}
}

注意,Action接口是xwork2包下的接口。

實現了Action接口,使開發者能夠看出來這是一個Action,具有了一定程度上的開發規範,

但是實現了Action接口,所以必須要重寫execute()方法。

一般自己寫Action,構思好之後上來就直接add(), edit(), delete(). select() 這些業務方法,

每次都要重寫execute()方法,不太方便。 而且這種方式不具有struts2中某些功能,如驗證框架和國際化。

Action中接口中有五個常用的結果字符串(好多方法都返回success,error,login,input,none,故將其封裝了一下) .

這些字符串雖然是大寫,然而真實的值是全部小寫.

package com.opensymphony.xwork2;

public abstract interface Action
{
  public static final String SUCCESS = "success";
  public static final String NONE = "none";
  public static final String ERROR = "error";
  public static final String INPUT = "input";
  public static final String LOGIN = "login";
  
  public abstract String execute()
    throws Exception;
}

一.三 繼承ActionSupport類(官方推薦)

	package com.yjl.web.action;
	import com.opensymphony.xwork2.ActionSupport;
	/**
	* @author 兩個蝴蝶飛
	* @version 創建時間:2018年8月23日 上午11:04:20
	* @description 第三種方式,繼承ActionSupport類。
	* ActionSupport類實現了Action接口,也有Action中的五個常量.
	*/
	public class Hello3Action extends ActionSupport{
		public String list() {
			return "list";
		}
	}

繼承了ActionSupport類,不需要重新寫execute()方法,直接寫業務方法即可。

ActionSupport類,已經實現了 Action接口。 其具備Action中的五個常量,並且該類還實現了其他接口,

源代碼:

	public class ActionSupport implements Action, Validateable, ValidationAware, TextProvider, LocaleProvider, Serializable{
    ...
	public String execute() throws Exception
  	{
		//默認返回的是 success 字符串 
  		 return "success";
 	}
 	...
}

如驗證框架(Validateable,ValidationAware),國際化(LocaleProvider)。

以後開發中,使用 繼承 ActionSupport 類的形式。

二 配置文件 struts.xml中節點的詳細解釋

在src下有一個struts.xml的配置文件,它配置了開發者自己編寫實現的Action,是struts2框架的核心,不能改變文件名稱。(注意,是struts.xml,並不是struts2.xml,並沒有那個2)。

在struts.xml中,最上面是一個約束, 是一個根節點。

二.一 修改常量節點

在struts-core.jar核心包下,有一個包org.apache.struts2包下,有一個default.properties屬性文件,裏面記錄了很多常用的常量,

其中常見的有:

struts.i18n.encoding=UTF-8 
struts.multipart.maxSize=2097152
struts.action.extension=action,,
struts.enable.DynamicMethodInvocation = false
struts.devMode = false
struts.ui.theme=xhtml
struts.ognl.allowStaticMethodAccess=false

建議修改后的值為:

###國際化操作,編碼格式為UTF-8
struts.i18n.encoding=UTF-8
###上傳文件時最大的上傳大小,默認為2M. 根據項目情況具體填寫值,建議後面加兩個00
struts.multipart.maxSize=209715200
###struts的訪問後綴名, struts1框架默認的是 .do 
struts.action.extension=action,,
###struts是否可以訪問靜態方法
struts.enable.DynamicMethodInvocation =true
###struts是否是開發者模式
struts.devMode =true
###struts中ui標籤的主題,建議為simple
struts.ui.theme=simple
###ognl中是否可以訪問靜態方法,為true
struts.ognl.allowStaticMethodAccess=true

可以在struts.xml中進行相應的修改,如

 <!--修改國際化編碼 -->
<constant name="struts.i18n.encoding" value="UTF-8"></constant>
<!--修改是否為開發者模式 -->
<constant name="struts.devMode" value="true"></constant>

按照name,value值的形式進行填寫。

也可以在src下新建一個struts.properties,然後將這些值放置進去,struts也會自動struts.propeties中的常量值的。

也可以在web.xml中,在 中,以 局部參數的形式傳遞進去。

建議使用第一種方式,在struts.xml中用 ,畢竟這個文件常常打開,出錯了也容易發現。

二.二 分模塊開發

在實際的項目中,有很多的模塊,如果所有的配置都放在一個struts.xml,那麼一旦這個struts.xml被其他人誤操作導致了錯誤,那麼其他人的項目將無法運行的,當配置內容過多時,struts.xml的內容太長,不便於維護,所以最好是分模塊開發,一個模塊用一個配置文件,然後再利用 進行導入, 類似 於jsp中的 靜態包含一樣。

所以建議每一個模塊都寫一個模塊.xml,然後在struts.xml中引入即可。如有三個模塊 User模塊和Class,Course,那麼可以將User的配置放置在user.xml中,Class配置放置在class.xml中,course模塊放置在course.xml,在struts.xml中只需要

	<include file="user.xml"></include>
	<include file="class.xml"></include>
	<include file="course.xml"></include>

靜態包含即可。 注意,file的文件路徑引用是否輸入正確。

正確的位置引用,點擊ctrl+模塊.xml時,可以跳轉到相應的.xml文件中。如果沒有跳轉和反應,那說明位置引用錯誤,需要重新檢查一下。

二.三 包節點

在struts.xml配置文件中,最重要的節點就是package節點。 package,分包。 可以將action進行分包處理。

這樣每一個action或者每一組action用package進行隔開,便於維護,類似於java中package的概念。

二.三.一 <package> 節點的使用

<package name="hello" extends="struts-default" namespace="/">
        <!--具體的Action-->
</package>

package中name節點是package的名字,是獨一無二的,不能夠重複。 最好與模塊名相同或者起一個有意義的名稱。

extends節點表示繼承,即package之間可以相互的繼承,來避免重複化功能的編寫。 默認為struts-default。

struts-default中struts已經定義了很多功能,開發者自己寫的包只需要extends 這個包名struts-default,

就擁有了struts已經定義好的功能。 如攔截器功能,文件上傳功能。

用戶也可以自己繼承自己所寫的包 。如父包名為

那麼子包只需要 , 這樣child包不但擁有struts-default的功能,也擁有parent包中的特殊功能,這也是Java的多重繼承的體現。 所以package的name 要符合標識符的規範,具有可讀性。

namespace節點表示命名空間,以/開頭,默認是”/” 。是為了在訪問路徑和訪問請求url方面體現package的分包作用. package中的name是在配置文件中體現分包,namespace是在url中體現分包。 建議開發中,namespace的路徑名與name保持一致。 package中的namespace的值與子節點action中name的值,共同構成了完整的訪問請求路徑。

二.三.二 <package></package> 子節點<action></action>節點的使用

在Hello3Action中定義兩個方法,一個是list()查詢,一個是add()添加的方法。

package com.yjl.web.action;
import org.apache.log4j.Logger;
import com.opensymphony.xwork2.ActionSupport;
/**
* @author 兩個蝴蝶飛
* @version 創建時間:2018年8月23日 上午11:04:20
* @description 測試action標籤中method的方法訪問
*/
public class Hello3Action extends ActionSupport{
	private static final long serialVersionUID = 8737138848863458260L;
	Logger logger=Logger.getLogger(Hello3Action.class);
	public String list() {
		logger.info("執行list方法");
		return "list";
	}
	public String add() {
		logger.info("執行add方法");
		return "add";
	}
}

標籤,有三個基本的屬性,

	<action name="list" class="com.yjl.web.action.Hello3Action"
        method="list">

</action>

其中name為action的名字,表示區別一個package包下的不同的action。 其中這個name的值,不應該隨便取,應該是要訪問的方法名。

在瀏覽器客戶端請求的url為 /項目名/package的namespace名稱/action的name名稱.action;

class為要訪問的那個Action的全限定名稱,是class,用.(點)進行分隔。

其中,class 可以省略, 省略默認為 ActionSupport 類, 全限定名稱為: com.opensymphony.xwork2.ActionSupport
method為要訪問的那個方法名稱,類 extends ActionSupport 后,有很多很多的方法,如list(), add(), delete()等,那麼怎麼知道具體要訪問哪個方法呢? 用method這個屬性. method=”要方法的方法名” ,是方法名。

action還有一個節點是converter,表示所用的是哪一個類型轉換器。(後面會有相應的解釋)

很清楚, action 中的 class指定了訪問的是哪一個action, method 指定了訪問的是哪一個具體的方法, 利用了反射技術實現。

在本實例了有兩個方法,所以要進行寫兩個Action, 一個Action類中會有多個方法,難道要一個個配置多個Action嗎?

Struts2提供了一些簡單的方式

二.三.三 配置Action的三種形式

二.三.三.一 通過配置method的屬性完成

簡單舉例如下:

	<action name="list" class="com.yjl.web.action.Hello3Action"
		method="list">
			
	</action>
  <action name="add" class="com.yjl.web.action.Hello3Action"
		method="add">
            
    </action>

缺點: 有幾個方法,就要配置有幾個action,當方法過多時,不易維護。

二.三.三.二 通過配置 通配符完成。

簡單舉例如下:

	<action name="Hello3_*" class="com.yjl.web.action.Hello3Action"
		method="{1}">
			
		</action>

name的值為: 類簡寫名(去掉Action后)_* method中的值取第一個{1},從1開始,不是從0開始。

這樣訪問Hello3Action中的list方法,訪問路徑就是 Hello3_list

訪問Hello3Action中的add方法,訪問路徑就是Hello3_add

簡化了action的相關配置。

也有的人配置的更狠, 會配置成_, 即:

	<action name="*_*" class="com.yjl.web.action.{1}Action"
		method="{2}">
			
		</action>

User類中的list就是User_list, User類中的add就是User_add,

Class類中的list就是Class_list,Class類中的add就是Class_add

這樣雖說簡化了開發,但卻不利用 result 節點的維護 ,不建議這樣配置。

好多類的好多方法返回值,都寫在這一個action 下面,會亂。

二.三.三.三 動態方法訪問

不是用 * 通配符,而是用! 號。 即:

想訪問UserAction中list方法() 前端寫url為 userAction!list.action
想訪問UserAction中add方法() 前端寫url為 userAction!add.action
想訪問ClassAction中list方法() 前端寫url為 classAction!list.action
想訪問ClassAction中add方法() 前端寫url為 classAction!add.action

這樣訪問也特別的方便。

這樣的話, action中只需要配置name和class即可。 method已經由外部指定了,不需要寫method的值了。

需要先添加變量 struts.enable.DynamicMethodInvocation, 使其變成 true,開啟。

	<constant name="struts.enable.DynamicMethodInvocation" value="true"></constant>

如果是UserAction的話,配置應該是:

<action name="userAction" class="com.yjl.web.action.UserAction" >
			
</action>

ClassAction的話,配置應該是

<action name="classAction" class="com.yjl.web.action.ClassAction" >
			
</action>

二.三.四 action子節點result的配置

result表示結果,是對方法的返回值進行相應的分析。有兩個屬性,name和type

	<result name="success" type="dispatcher">/index.jsp</result>

其中name的值要與方法的返回值保持一致。

如 list方法返回值是return SUCCESS,那麼這個list方法的返回值對應的result的值就是 ,

如果返回是”hello”, 那麼這個name的返回值就是

如果在action中配置通配符, name=Hello3_*形式,method=”{1}”, 那麼為了簡化result的配置,可以將result配置成 name={1},

相應的.jsp,可以變成 /{1}.jsp。

但這樣必須保證Action中方法的名稱與返回值的名稱相同,並且與跳轉到的jsp的名稱也要相同, 這樣不太好。

result中type五種常見的形式, dispatcher(轉發到jsp),redirect(重定向到jsp), chain(轉發到另外一個方法),redirectAction(重定向到另外一個方法),stream(上傳和下載流)

其中dispathcer和redirect是跳轉到jsp,如果想要傳遞數據,用dispather,

如果不想傳遞數據,用redirect (dispathcer是轉發,redirect是重定向)

chain,redirectAction是跳轉到action的操作,一般用於這同一個類中兩個方法之間的跳轉,

如add()添加成功之後,需要跳轉到list()方法進行显示結果,這時就可以配置成:

	<result name="add" type="redirectAction">Hello3_list</result>

地址url也會相應的改變,如果是chain的話,地址欄是不會改變的。 chain是轉發到action, redirectAction是重定向到action.

也可以在不同包之間的action進行的跳轉 。

如 add 方法 想到跳轉到 /class 命名空間下的 Hello2Action 的 list 方法。

<result name="add" type="redirectAction">
	<!-- 要跳轉到哪一個命名空間,即哪一個包 -->
	<param name="namespace">/class</param>
	<!-- 要跳轉到哪一個Action 不加後綴 -->
	<param name="actionName">Hello2Action</param>
	<!-- 跳轉到哪一個方法 -->
	<param name="method">list</param>
	<!-- 可能要傳遞的參數. 用ognl表達式,根據情況添加 -->
	<param name="id">${id}</param>
</result>

通過 param 標籤來配置帶參還是不帶參。

二.四 全局結果頁面與局部結果頁面。

這個全局是相對於package來說的,是package中的全局,並不是所有的struts.xml中的全局,所以全局結果的節點位置應該放在package節點裏面,與action節點平行。 用 節點。

常用的全局結果頁面有兩種:

error錯誤頁面,頁面出錯了都显示這個頁面,

login 登錄頁面, 如果沒有登錄,輸入任何url都會跳轉到login頁面(認證時用)

noprivilege 沒有權限頁面,如果用戶沒有權限訪問了某一個頁面,會給出相應的提示(授權時用)

<global-results>
			<result name="error">/error/error.jsp</result>
			<result name="login">/login.jsp</result>
            <result name="noprivilege">/noprivilege.jsp</result>
</global-results>

當全局結果頁面與局部結果頁面發生衝突時,以局部結果頁面為準。

全局配置時:

<global-results>
			<result name="success">/successGlobal.jsp</result>
</global-results>

在該包下的某個action 的方法result 也返回了 success

	<result name='success'>success.jsp</result>

那麼,當邏輯視圖為 success時,最終將返回 success.jsp

二.五 配置跳轉頁面

在開發中,常常有這麼一種情況,

請求login.jsp 時,為 /login, 那麼就跳轉到 login.jsp 頁面,

語法為 register.jsp 時,為 /register, 那麼就跳轉到 register 頁面。

這個時候,配置 為:

	<action name="*">
			<result>/WEB-INF/content/{1}.jsp</result>
	</action>

將頁面放置在 content 文件夾下面,避免用戶直接訪問 jsp頁面。

注意,要將此 action 放置在最後, 當所有上面的action都不匹配時,才匹配這一個action.

三 Struts2的執行流程

當用戶在客戶端發送一個請求后,如常用的標準的http://localhost:8080/Struts_Hello/user/User_add.action時,

會經過前端控制器(StrutsPrepareAndExecuteFilter) 過濾器,執行一連串的過濾器鏈,然後根據user 找到了對應的package的namespape,進入到具體的package包下。 利用通配符的方式進行訪問,User_add會進行匹配相應的action,根據class和method找到是哪一個類的哪一個方法,在實例化類Action之前,會先執行攔截器。通過反射實例化類,運行方法, 方法運行成功之後,有一個返回值,這個返回值會與剛才action下的 中的name進行相應的匹配,匹配到哪一個,就執行哪一個result。 如果是diapatcher或者redirect,就显示到相應的.jsp頁面(帶有數據), 如果是chain或者redirectAction,那麼就去執行那一個方法,之後進行返回具體的視圖。

執行過程圖如下:

謝謝您的觀看!!!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

程序員實用JDK小工具歸納,工作用得到

在JDK的安用裝目錄bin下,有一些有非常實用的小工具,可用於分析JVM初始配置、內存溢出異常等問題,我們接下來將對些常用的工具進行一些說明。

JDK小工具簡介

在JDK的bin目錄下面有一些小工具,如javac,jar,jstack,jstat等,在日常編譯運行過程中有着不少的“額外”功能,那麼它們是怎麼工作的呢?雖然這些文件本身已經被編譯成可執行二進制文件了,但是其實它們的功能都是由tools.jar這個工具包(配合一些dll或者so本地庫)完成的,每個可執行文件都對應一個包含main函數入口的java類(有興趣可以閱讀openJDK相關的源碼,它們的對應關係如下(更多可去openJDK查閱):

javac com.sun.tools.javac.Main
jar sun.tools.jar.Main
jps sun.tools.jps.Jps
jstat sun.tools.jstat.Jstat
jstack    sun.tools.jstack.JStack
...

tools.jar的使用

我們一般開發機器上都會安裝JDK+jre,這時候,要用這些工具,直接運行二進制可執行文件就行了,但是有時候,機器上只有jre而沒有JDK,我們就無法用了么?

如果你知道如上的對應關係的話,我們就可以”構造”出這些工具來(當然也可以把JDK安裝一遍,本篇只是介紹另一種選擇),比如我們編寫

//Hello.java
public class Hello{
    public static void main(String[] args)throws Exception{
        while(true){
            test1();
            Thread.sleep(1000L);
        }
    }
    public static void test1(){
        test2();
    }
    public static void test2(){
        System.out.println("invoke test2");
    }
}

可以驗證如下功能轉換關係

1.編譯源文件:

javac Hello.java => java -cp tools.jar com.sun.tools.javac.Main Hello.java

結果一樣,都可以生成Hello.class文件
然後我們開始運行java -cp . Hello

2.查看java進程:

jps => java -cp tools.jar sun.tools.jps.Jps

結果一樣,如下:

4615 Jps
11048 jar
3003 Hello

3.動態查看內存:

jstat -gcutil 3003 100 3 => java -cp tools.jar sun.tools.jstat.Jstat -gcutil 3003 100 3

發現結果是一樣的

  S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
  0.00 0.00 4.00 0.00 17.42 19.65 0 0.000 0 0.000 0.000
  0.00 0.00 4.00 0.00 17.42 19.65 0 0.000 0 0.000 0.000
  0.00 0.00 4.00 0.00 17.42 19.65 0 0.000 0 0.000 0.000

4.查看當前運行棧信息
正常情況,執行如下命令結果也是一樣,可以正常輸出

jstack 3003 =》 java -cp tools.jar sun.tools.jstack.JStack 3003

但是有的jre安裝不正常的時候,會報如下錯誤

Exception in thread "main" java.lang.UnsatisfiedLinkError: no attach in java.library.path

這是因為jstack的運行需要attach本地庫的支持,我們需要在系統變量裏面配置上其路徑,假如路徑為/home/JDK/jre/bin/libattach.so
命令轉換成

jstack 3003 =》 java -Djava.library.path=/home/JDK/jre/bin -cp tools.jar sun.tools.jstack.JStack 3003

就可以實現了
在linux系統中是libattach.so,而在windows系統中是attach.dll,它提供了一個與本機jvm通信的能力,利用它可以與本地的jvm進行通信,許多java小工具就可能通過它來獲取jvm運行時狀態,也可以對jvm執行一些操作

attach使用

1. 編寫agent.jar代理包

  • 編寫一個Agent類
//Agent.java
public class Agent{
    public static void agentmain(String args, java.lang.instrument.Instrumentation inst) {
        System.out.println("agent : " + args);
    }
}
  • 編譯Agent
java -cp tools.jar com.sun.tools.javac.Main Agent.java
//或者
javac Agent.java
  • 再編manifest.mf文件
//manifest.mf
Manifest-Version: 1.0
Agent-Class: Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
  • 把Agent.class和manifest.mf進行打包成agent.jar
java -cp tools.jar sun.tools.jar.Main -cmf manifest.mf agent.jar Agent.class
//或者
jar -cmf manifest.mf agent.jar Agent.class

2.attach進程

  • 編寫如下attach類,編譯並執行
//AttachMain.java
public class AttachMain {
    public static void main(String[] args) throws Exception {
        com.sun.tools.attach.VirtualMachine vm = com.sun.tools.attach.VirtualMachine.attach(args[0]);
        vm.loadAgent("agent.jar", "inject params");
        vm.detach();
    }
}
  • 編譯:
java -cp tools.jar com.sun.tools.javac.Main -cp tools.jar AttachMain.java
//或者
javac -cp tools.jar AttachMain.java
  • 執行attach
java -cp .:tools.jar AttachMain 3003
  • 查看Hello進程有如下輸出:
invoke test2
invoke test2
invoke test2
invoke test2
invoke test2
invoke test2
invoke test2
agent : inject params
invoke test2

說明attach成功了,而且在目標java進程中引入了agent.jar這個包,並且在其中一個線程中執行了manifest文件中agentmain類的agentmain方法,詳細原理可以見JVMTI的介紹,例如oracle的介紹

3. 用attach製作小工具

  • 寫一個使進程OutOfMemory/StackOverFlow的工具
    有了attach的方便使用,我們可以在agentmain中新起動一個線程(為避免把attach線程污染掉),在裏面無限分配內存但不回收,就可以產生OOM或者stackoverflow
    代碼如下:
//Agent.java for OOM
public class Agent{
    public static void agentmain(String args, java.lang.instrument.Instrumentation inst) {
        new Thread() {
            @Override
            public void run() {
                java.util.List<byte[]> list = new java.util.ArrayList<byte[]>();
                try {
                    while(true) {
                        list.add(new byte[100*1024*1024]);
                        Thread.sleep(100L);
                    }
                } catch (InterruptedException e) {
                }
            }
        }.start();
    }
}
//Agent.java for stackoverflow
public class Agent{
    public static void agentmain(String args, java.lang.instrument.Instrumentation inst) {
        new Thread() {
            @Override
            public void run() {
                stackOver();
            }
            private void stackOver(){
                stackOver();
            }
        }.start();
    }
}

當測試OOM的時候,hello進程的輸出為:

invoke test2
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
        at Agent$1.run(Agent.java:9)
invoke test2
invoke test2
invoke test2

說明發生OOM了, 但是OOM線程退出了,其它線程還在正常運行。

如果我們需要進程在OOM的時候產生一些動作,我們可以在進程啟動的時候增加一些OOM相關的VM參數

  • OOM的時候直接kill掉進程:-XX:OnOutOfMemoryError=”kill -9 %p”
    結果如下:
invoke test2
invoke test2
#
# java.lang.OutOfMemoryError: Java heap space
# -XX:OnOutOfMemoryError="kill -9 %p"
#   Executing /bin/sh -c "kill -9 26829"...
Killed
  • OOM的時候直接退出進程:-XX:+ExitOnOutOfMemoryError
    結果如下:
invoke test2
invoke test2
Terminating due to java.lang.OutOfMemoryError: Java heap space
  • OOM的時候進程crash掉:-XX:+CrashOnOutOfMemoryError
    結果如下:
invoke test2
invoke test2
Aborting due to java.lang.OutOfMemoryError: Java heap space
invoke test2#
# A fatal error has been detected by the Java Runtime Environment:
#
#  Internal Error (debug.cpp:308)
, pid=42675, tid=0x00007f3710bf4700
#  fatal error: OutOfMemory encountered: Java heap space
#
# JRE version: Java(TM) SE Runtime Environment (8.0_171-b11) (build 1.8.0_171-b11)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode linux-amd64 compressed oops)
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /root/hanlang/test/hs_err_pid42675.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.java.com/bugreport/crash.jsp
#
Aborted
  • OOM的時候dump內存:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof
    結果生成dump文件

asm的應用

1.asm使用原理

asm是一個java字節碼工具,提供一種方便的函數/屬性級別修改已經編譯好的.class文件的方法, asm的簡單使用原理介紹如下:

  • 通過ClassReader讀取.class文件的字節碼內容,並生成語法樹;
  • ClassReader的方法accept(ClassVisitor classVisitor, int parsingOptions)功能是讓classVisitor遍歷語法樹,默認ClassVisitor是一個代理類,需要有一個具體的實現在遍歷語法樹的時候做一些處理;
  • 用ClassWriter是ClassVisitor的一個實現,它的功能是把語法樹轉換成字節碼;
  • 通常我們會定義一個自己的ClassVisitor,可以重寫裏面的一些方法來改寫類處理邏輯,然後讓ClassWriter把處理之後的語法樹轉換成字節碼;

2.下面是具體的實現步驟:

  • 引入asm依賴包
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>7.0</version>
</dependency>
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm-commons</artifactId>
    <version>7.0</version>
</dependency>
//或者引入如下包
asm-commons-7.0.jar
asm-analysis-7.0.jar
asm-tree-7.0.jar
asm-7.0.jar
  • 定義一個ClassVisitor,功能是在所有方法調用前和調用後分別通過System.out.println打印一些信息
    輸入為字節碼,輸出也是字節碼
//MyClassVisitor.java
public class MyClassVisitor extends ClassVisitor {
    private static final Type SYSTEM;
    private static final Type OUT;
    private static final Method PRINTLN;
    static {
        java.lang.reflect.Method m = null;
        try {
            m = PrintStream.class.getMethod("println", new Class<?>[] {String.class});
        } catch (Exception e) {
        }
        SYSTEM = Type.getType(System.class);
        OUT = Type.getType(PrintStream.class);
        PRINTLN = Method.getMethod(m);
    }

    private String cName;

    public MyClassVisitor(byte[] bytes) {
        super(Opcodes.ASM7, new ClassWriter(ClassWriter.COMPUTE_FRAMES));
        new ClassReader(bytes).accept(this, ClassReader.EXPAND_FRAMES);
    }
    String format(String name) {
        return name.replaceAll("<", "_").replaceAll("\\$|>", "");
    }
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cName = format(name);
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if ((access & 256) != 0) {
            return super.visitMethod(access, name, desc, signature, exceptions);
        }
        return new MyMethodAdapter(super.visitMethod(access, name, desc, signature, exceptions), access, name, desc);
    }

    public byte[] getBytes() {
        return ((ClassWriter) cv).toByteArray();
    }

    class MyMethodAdapter extends AdviceAdapter {
        private String mName;

        public MyMethodAdapter(MethodVisitor methodVisitor, int acc, String name, String desc) {
            super(Opcodes.ASM7, methodVisitor, acc, name, desc);
            this.mName = format(name);
        }

        @Override
        protected void onMethodEnter() {
            getStatic(SYSTEM, "out", OUT);
            push(cName + "." + mName + " start");
            this.invokeVirtual(OUT, PRINTLN);
        }

        @Override
        protected void onMethodExit(int opcode) {
            getStatic(SYSTEM, "out", OUT);
            push(cName + "." + mName + " end");
            this.invokeVirtual(OUT, PRINTLN);
        }
    }
}
  • 定義一個簡單的classLoader來加載轉換后的字節碼
//MyLoader.java
class MyLoader extends ClassLoader {
    private String cname;
    private byte[] bytes;
    public MyLoader(String cname, byte[] bytes) {
        this.cname = cname;
        this.bytes = bytes;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = null;
        if (clazz == null && cname.equals(name)) {
            try {
                clazz = findClass(name);
            } catch (ClassNotFoundException e) {
            }
        }
        if (clazz == null) {
            clazz = super.loadClass(name, resolve);
        }
        return clazz;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> clazz = this.findLoadedClass(name);
        if (clazz == null) {
            clazz = defineClass(name, bytes, 0, bytes.length);
        }
        return clazz;
    }
}
  • 加載轉換Hello類,然後反向調用其方法

//將如下main函數加入MyClassVisitor.java中

public static void main(String[] args) throws Exception {
    try (InputStream in = Hello.class.getResourceAsStream("Hello.class")) {
        byte[] bytes = new byte[in.available()];
        in.read(bytes);
        String cname = Hello.class.getName();
        Class<?> clazz = new MyLoader(cname, new MyClassVisitor(bytes).getBytes()).loadClass(cname);
        clazz.getMethod("test1").invoke(null);
    }
}
  • 編譯
java -cp tools.jar com.sun.tools.javac.Main -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. *.java
//或者
javac -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. *.java
  • 運行
java -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. MyClassVisitor
//結果如下:
Hello.test1 start
Hello.test2 start
invoke test2
Hello.test2 end
Hello.test1 end

asm的使用很廣泛,最常用的是在spring aop裏面切面的功能就是通過asm來完成的

3. 利用asm與Instrument製作調試工具

  • Instrument工具

Instrument類有如下方法,可以增加一個類轉換器

addTransformer(ClassFileTransformer transformer, boolean canRetransform)

執行如下方法的時候,對應的類將會被重新定義

retransformClasses(Class<?>... classes)
  • 與asm配合使用
    當我們修改Agent.java代碼為下面內容
//Agent
public class Agent {
    public static void agentmain(String args, Instrumentation inst) {
        try {
            URLClassLoader loader = (URLClassLoader)Agent.class.getClassLoader();
            Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            method.setAccessible(true);//代碼級引入依賴包
            method.invoke(loader, new File("asm-7.0.jar").toURI().toURL());
            method.invoke(loader, new File("asm-analysis-7.0.jar").toURI().toURL());
            method.invoke(loader, new File("asm-tree-7.0.jar").toURI().toURL());
            method.invoke(loader, new File("asm-commons-7.0.jar").toURI().toURL());
            inst.addTransformer(new ClassFileTransformer() {
                @Override
                public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                    ProtectionDomain protectionDomain, byte[] bytes) {
                    return new MyClassVisitor(bytes).getBytes();
                }
            }, true);
            inst.retransformClasses(Class.forName("Hello"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 編譯並打包成agent.jar
//編譯
javac -cp asm-commons-7.0.jar:asm-analysis-7.0.jar:asm-tree-7.0.jar:asm-7.0.jar:. *.java
//打包
jar -cmf manifest.mf agent.jar MyLoader.class MyClassVisitor.class MyClassVisitor\$MyMethodAdapter.class Agent.class Agent\$1.class
  • attach進程修改字節碼
//執行
java -cp .:tools.jar AttachMain 3003
//執行前後Hello進程的輸出變化為
invoke test2
invoke test2
invoke test2
Hello.test1 start
Hello.test2 start
invoke test2
Hello.test2 end
Hello.test1 end
Hello.test1 start
Hello.test2 start
invoke test2
Hello.test2 end
Hello.test1 end

利用asm及instrument工具來實現熱修改字節碼現在有許多成熟的工具,如btrace(https://github.com/btraceio/btrace,jvm-sandbox https://github.com/alibaba/jvm-sandbox)

 

點擊關注,第一時間了解華為雲新鮮技術~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

除了FastJson,你也應該了解一下Jackson(二)

概覽

上一篇文章介紹了Jackson中的映射器ObjectMapper,以及如何使用它來實現Json與Java對象之間的序列化和反序列化,最後介紹了Jackson中一些序列化/反序列化的高級特性。而本文將會介紹Jackson中的一些常用的(序列化/反序列化)註解,並且通過示例來演示如何使用這些註解,從而來提高我們在處理Json上的工作效率。

序列化註解

@JsonAnyGetter

@JsonAnyGetter註解允許靈活地使用映射(鍵值對,如Map)字段作為標準屬性。

我們聲明如下Java類:

@Data
@Accessors(chain = true)
public static class ExtendableBean {
    public String name;
    private Map<String, String> properties;

    @JsonAnyGetter
    public Map<String, String> getProperties() {
        return properties;
    }
}

編寫測試代碼,測試@JsonAnyGetter:

@Test
public void testJsonAnyGetter() throws JsonProcessingException {
    ExtendableBean extendableBean = new ExtendableBean();
    Map<String, String> map = new HashMap<>();
    map.put("age", "13");
    extendableBean.setName("dxsn").setProperties(map);
    log.info(new ObjectMapper().writeValueAsString(extendableBean));
  	//打印:{"name":"dxsn","age":"13"}
    assertThat(new ObjectMapper().writeValueAsString(extendableBean)).contains("name");
    assertThat(new ObjectMapper().writeValueAsString(extendableBean)).contains("age");
}

如上,可以看properties屬性中的鍵值對(Map)被擴展到了ExtendableBean的Json對象中。

@JsonGetter

@JsonGetter註解是@JsonProperty註解的替代品,用來將一個方法標記為getter方法。

我們創建以下Java類

@Data
@AllArgsConstructor
@NoArgsConstructor
public static class MyBean {
    public int id;
    private String name;

    @JsonGetter("name")
    public String getTheName() {
        return name;
    }
}

如上,我們在類中聲明了一個getTheName()方法,並且使用@JsonGetter(“name”)修飾,此時,該方法將會被Jackson認作是name屬性的get方法。

編寫測試代碼:

@Test
public void testJsonGetter() throws JsonProcessingException {
    MyBean myBean = new MyBean(1, "dxsn");
    String jsonStr = new ObjectMapper().writeValueAsString(myBean);
    log.info(jsonStr);
    assertThat(jsonStr).contains("id");
    assertThat(jsonStr).contains("name");
}

可以看到,jackson將私有屬性name,也進行了序列化。

@JsonPropertyOrder

我們可以使用@JsonPropertyOrder註解來指定Java對象的屬性序列化順序。

@JsonPropertyOrder({"name", "id"})
//order by key's name
//@JsonPropertyOrder(alphabetic = true)
@Data
@Accessors(chain = true)
public static class MyOrderBean {
  public int id;
  public String name;
}

編寫測試代碼:

@Test
public void testJsonPropertyOrder1() throws JsonProcessingException {
    MyOrderBean myOrderBean = new MyOrderBean().setId(1).setName("dxsn");
    String jsonStr = new ObjectMapper().writeValueAsString(myOrderBean);
    log.info(jsonStr);
    assertThat(jsonStr).isEqualTo("{\"name\":\"dxsn\",\"id\":1}");
}

如上,可以看到序列化得到的Json對象中屬性的排列順序正是我們在註解中指定的順序。

@JsonRawValue

@JsonRawValue註解可以指示Jackson按原樣序列化屬性。

在下面的例子中,我們使用@JsonRawValue嵌入一些定製的JSON作為一個實體的值:

@Data
@AllArgsConstructor
@NoArgsConstructor
public static class RawBean {
    public String name;
    @JsonRawValue
    public String json;
}

編寫測試代碼:

@Test
public void testJsonRawValue() throws JsonProcessingException {
    RawBean rawBean = new RawBean("dxsn", "{\"love\":\"true\"}");
    log.info(new ObjectMapper().writeValueAsString(rawBean));
  	//輸出:{"name":"dxsn","json":{"love":"true"}}
    String result = new ObjectMapper().writeValueAsString(rawBean);
    assertThat(result).contains("dxsn");
    assertThat(result).contains("{\"love\":\"true\"}");
}

@JsonValue

@JsonValue表示Jackson將使用一個方法來序列化整個實例。

下面我們創建一個枚舉類:

@AllArgsConstructor
public static enum TypeEnumWithValue {
    TYPE1(1, "Type A"), TYPE2(2, "Type 2");
    private Integer id;
    private String name;

    @JsonValue
    public String getName() {
        return name;
    }
}

如上,我們在getName()上使用@JsonValue進行修飾。

編寫測試代碼:

@Test
public void testJsonValue() throws JsonProcessingException {
    String  jsonStr = new ObjectMapper().writeValueAsString(TypeEnumWithValue.TYPE2);
    log.info(jsonStr);
    assertThat(jsonStr).isEqualTo("Type 2");
}

可以看到,枚舉類的對象序列化后的值即getName()方法的返回值。

@JsonRootName

如果啟用了包裝(wrapping),則使用@JsonRootName註解可以指定要使用的根包裝器的名稱。

下面我們創建一個使用@JsonRootName修飾的Java類:

@JsonRootName(value = "user")
@Data
@AllArgsConstructor
public static class UserWithRoot {
    public int id;
    public String name;
}

編寫測試:

@Test
public void testJsonRootName() throws JsonProcessingException {
    UserWithRoot userWithRoot = new UserWithRoot(1, "dxsn");
    ObjectMapper mapper = new ObjectMapper();
  	//⬇️重點!!!
    mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
  
    String result = mapper.writeValueAsString(userWithRoot);
    log.info(result);
  	//輸出:{"user":{"id":1,"name":"dxsn"}}
    assertThat(result).contains("dxsn");
    assertThat(result).contains("user");
}

上面代碼中,我們通過開啟ObjectMapper的SerializationFeature.WRAP_ROOT_VALUE。可以看到UserWithRoot對象被序列化后的Json對象被包裝在user中,而非單純的{"id":1,"name":"dxsn"}

@JsonSerialize

@JsonSerialize註解表示序列化實體時要使用的自定義序列化器。

我們定義一個自定義的序列化器:

public static class CustomDateSerializer extends StdSerializer<Date> {
    private static SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");

    public CustomDateSerializer() {
        this(null);
    }

    public CustomDateSerializer(Class<Date> t) {
        super(t);
    }

    @Override
    public void serialize(
        Date value, JsonGenerator gen, SerializerProvider arg2) throws IOException, JsonProcessingException {
        gen.writeString(formatter.format(value));
    }
}

使用自定義的序列化器,創建Java類:

@Data
@AllArgsConstructor
public static class Event {
    public String name;
    @JsonSerialize(using = CustomDateSerializer.class)
    public Date eventDate;
}

編寫測試代碼:

@Test
public void testJsonSerialize() throws ParseException, JsonProcessingException {
    SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
    String toParse = "20-12-2014 02:30:00";
    Date date = formatter.parse(toParse);
    Event event = new Event("party", date);
    String result = new ObjectMapper().writeValueAsString(event);
    assertThat(result).contains(toParse);
}

可以看到,使用@JsonSerialize註解修飾指定屬性后,將會使用指定的序列化器來序列化該屬性。

反序列化註解

@JsonCreator

我們可以使用@JsonCreator註解來優化/替換反序列化中使用的構造器/工廠。

當我們需要反序列化一些與我們需要獲取的目標實體不完全匹配的JSON時,它非常有用。

現在,有如下一個Json對象:

{"id":1,"theName":"My bean"}

我們聲名了一個Java類:

@Data
public static class BeanWithCreator {
    private int id;
    private String name;
}

此時,在我們的目標實體中沒有theName字段,只有name字段。現在,我們不想改變實體本身,此時可以通過使用@JsonCreator和@JsonProperty註解來修飾構造函數:

@Data
public static class BeanWithCreator {
    private int id;
    private String name;

    @JsonCreator
    public BeanWithCreator(@JsonProperty("id") int id, @JsonProperty("theName") String name) {
        this.id = id;
        this.name = name;
    }
}

編寫測試:

@Test
public void beanWithCreatorTest() throws JsonProcessingException {
    String str = "{\"id\":1,\"theName\":\"My bean\"}";
    BeanWithCreator bean = new ObjectMapper()
        .readerFor(BeanWithCreator.class)
        .readValue(str);
 	 	assertThat(bean.getId()).isEqualTo(1);
    assertThat(bean.getName()).isEqualTo("My bean");
}

可以看到,即使Json對象中的字段名和實體類中不一樣,但由於我們手動指定了映射字段的名字,從而反序列化成功。

@JacksonInject

@JacksonInject表示java對象中的屬性將通過注入來賦值,而不是從JSON數據中獲得其值。

創建如下實體類,其中有字段被@JacksonInject修飾:

public static class BeanWithInject {
    @JacksonInject
    public int id;
    public String name;
}

編寫測試:

@Test
public void jacksonInjectTest() throws JsonProcessingException {
    String json = "{\"name\":\"dxsn\"}";
    InjectableValues inject = new InjectableValues.Std()
        .addValue(int.class, 1);
    BeanWithInject bean = new ObjectMapper().reader(inject)
        .forType(BeanWithInject.class)
        .readValue(json);
    assertThat(bean.id).isEqualTo(1);
    assertThat(bean.name).isEqualTo("dxsn");
}

如上,我們在測試中將json字符串(僅存在name字段)進行反序列化,其中id通過注入的方式對屬性進行賦值。

@JsonAnySetter

@JsonAnySetter允許我們靈活地使用映射(鍵值對、Map)作為標準屬性。在反序列化時,JSON的屬性將被添加到映射中。

創建一個帶有@JsonAnySetter的實體類:

public static class ExtendableBean {
    public String name;
    public Map<String, String> properties;

    @JsonAnySetter
    public void add(String key, String value) {
        if (properties == null) {
            properties = new HashMap<>();
        }
        properties.put(key, value);
    }
}

編寫測試:

@Test
public void testJsonAnySetter() throws JsonProcessingException {
    String json = "{\"name\":\"dxsn\", \"attr2\":\"val2\", \"attr1\":\"val1\"}";
    ExtendableBean extendableBean = new ObjectMapper().readerFor(ExtendableBean.class).readValue(json);
    assertThat(extendableBean.name).isEqualTo("dxsn");
    assertThat(extendableBean.properties.size()).isEqualTo(2);
}

可以看到,json對象中的attr1,attr2屬性在反序列化之後進入了properties。

@JsonSetter

@JsonSetter是@JsonProperty的替代方法,它將方法標記為屬性的setter方法。
當我們需要讀取一些JSON數據,但目標實體類與該數據不完全匹配時,這非常有用,因此我們需要優化使其適合該數據。

創建如下實體類:

@Data
public static class MyBean {
    public int id;
    private String name;

    @JsonSetter("name")
    public void setTheName(String name) {
        this.name = "hello " + name;
    }
}

編寫測試:

@Test
public void testJsonSetter() throws JsonProcessingException {
    String json = "{\"id\":1,\"name\":\"dxsn\"}";
    MyBean bean = new ObjectMapper().readerFor(MyBean.class).readValue(json);
    assertThat(bean.getName()).isEqualTo("hello dxsn");
}

可以看到,json對象中的name屬性為“dxsn”,我們通過在MyBean類中定義了使用@JsonSetter(“name”)註解修飾的方法,這表明該類的對象在反序列話的時候,name屬性將來自此方法。最後MyBean對象中name的值變為了hello dxsn。

@JsonDeserialize

@JsonDeserialize註解指定了在反序列化的時候使用的反序列化器。

如下,定義了一個自定義的反序列化器:

public static class CustomDateDeserializer extends StdDeserializer<Date> {
    private static SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");

    public CustomDateDeserializer() {
        this(null);
    }

    public CustomDateDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Date deserialize(JsonParser jsonparser, DeserializationContext context) throws IOException {
        String date = jsonparser.getText();
        try {
            return formatter.parse(date);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }
}

創建一個使用@JsonDeserialize(using = CustomDateDeserializer.class)修飾的實體類:

public static class Event {
    public String name;
    @JsonDeserialize(using = CustomDateDeserializer.class)
    public Date eventDate;
}

編寫測試:

@Test
public void whenDeserializingUsingJsonDeserialize_thenCorrect()
    throws IOException {
    String json = "{\"name\":\"party\",\"eventDate\":\"20-12-2014 02:30:00\"}";
    SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
    Event event = new ObjectMapper().readerFor(Event.class).readValue(json);
    assertThat(event.name).isEqualTo("party");
    assertThat(event.eventDate).isEqualTo(df.format(event.eventDate));

}

可以看到,在Event對象中,eventDate屬性通過自定義的反序列化器,將“20-12-2014 02:30:00”反序列化成了Date對象。

@JsonAlias

@JsonAlias在反序列化期間為屬性定義一個或多個替代名稱。讓我們通過一個簡單的例子來看看這個註解是如何工作的:

@Data
public static class AliasBean {
    @JsonAlias({"fName", "f_name"})
    private String firstName;
    private String lastName;
}

如上,我們編寫了一個使用@JsonAlias修飾的AliasBean實體類。

編寫測試:

@Test
public void whenDeserializingUsingJsonAlias_thenCorrect() throws IOException {
    String json = "{\"fName\": \"John\", \"lastName\": \"Green\"}";
    AliasBean aliasBean = new ObjectMapper().readerFor(AliasBean.class).readValue(json);
    assertThat(aliasBean.getFirstName()).isEqualTo("John");
}

可以看到,即使json對象中的字段名是fName,但是由於在AliasBean中使用@JsonAlias修飾了firstName屬性,並且指定了兩個別名。所以反序列化之後fName被映射到AliasBean對象的firstName屬性上。

更多

除上述註解之外,Jackson還提供了很多額外的註解,這裏不一一列舉,接下來會例舉幾個常用的註解:

  • @JsonProperty:可以在類的指定屬性上添加@JsonProperty註解來表示其對應在JSON中的屬性名。
  • @JsonFormat:此註解在序列化對象中的日期/時間類型屬性時可以指定一種字符串格式輸出,如:@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = “dd-MM-yyyy hh:mm:ss”)。
  • @JsonUnwrapped:@JsonUnwrapped定義了在序列化/反序列化時應該被扁平化的值。
  • @JsonIgnore:序列化/反序列化時忽略被修飾的屬性。
  • ……

總結

本文主要介紹了Jackson常用的序列化/反序列化註解,最後介紹了幾個常用的通用註解。Jackson中提供的註解除了本文列舉的還有很多很多,使用註解可以讓我們的序列化/反序列化工作更加輕鬆。如果你想將某庫換成Jackson,希望這篇文章可以幫到你。

本文涉及的代碼地址:https://gitee.com/jeker8chen/jackson-annotation-demo

歡迎訪問筆者博客:blog.dongxishaonian.tech

關注筆者公眾號,推送各類原創/優質技術文章 ⬇️

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

微前端與項目實施方案研究

一、前言

微前端(micro-frontends)是近幾年在前端領域出現的一個新概念,主要內容是將前端應用分解成一些更小、更簡單的能夠獨立開發、測試、部署的小塊,而在用戶看來仍然是內聚的單個產品。微前端的理念源於微服務,是將龐大的整體拆成可控的小塊,並明確它們之間的依賴關係,而它的價值在於能將低耦合的代碼與組件進行組合,基座+基礎協議模式能接入大量應用,進行統一的管理和輸出,許多公司與團隊也都在不斷嘗試和優化相關解決技術與設計方案,為這一概念的落地和推廣添磚加瓦。結合自身遇到的問題,適時引用微前端架構能起到明顯的提效賦能作用。

二、背景

目前我司擁有大量的內部系統,這些系統採用相同的技術棧,在實際開發和使用過程中,逐漸暴露出如下幾個問題:

1.有大量可復用的部分,雖然有組件庫,但是依賴版本難統一;
2.靜態資源體積過大,影響頁面加載和渲染速度;
3.應用切換目前是通過鏈接跳轉的方式實現,會有白屏和等待時長的問題,對用戶體驗不夠友好;
針對上述幾個問題,決定採用微前端架構對內部系統進行統一的管理,本文也是圍繞微前端落地的技術預研方案。

三、方案調研

目前業界有多種解決方案,有各自的優缺點,具體如下:

  • 路由轉發:路由轉發嚴格意義上不屬於微前端,多個子模塊之間共享一個導航即可 簡單,易實現 體驗不好,切換應用整個頁面刷新;

  • 嵌套 iframe:每個子應用一個 iframe 嵌套 應用之間自帶沙箱隔離 重複加載腳本和樣式;

  • 構建時組合:獨立倉儲,獨立開發,構建時整體打包,合併應用 方便依賴管理,抽取公共模塊 無法獨立部署,技術棧,依賴版本必須統一;

  • 運行時組合:每個子應用獨立構建,運行時由主應用負責應用管理,加載,啟動,卸載,通信機制 良好的體驗,真正的獨立開發,獨立部署 複雜,需要設計加載,通信機制,無法做到徹底隔離,需要解決依賴衝突,樣式衝突問題;

    開源微前端框架也有多種,例如阿里出品的qiankun,icestark,還有針對angular提出的mooa等,都能快速接入項目,但結合公司內部系統的特點,直接採用會有有些限制,例如要實現定製界面,無刷新加載應用,且不能對現有項目的開發和部署造成影響,因此決定自研相關技術。

四、架構設計

4.1 應用層

應用層包括所有接入微服務工作台的內部系統,他們各自開發與部署,接入前後沒有多大影響,只是需要針對微服務層單獨輸出打包一份靜態資源;

4.2 微服務層

微服務層作為核心模塊,擁有資源加載、路由管理、狀態管理和用戶認證管理幾大功能,具體內容將在後面詳細闡述,架構整體工作流程如下:

4.3 基礎支撐層

基礎支撐層作為基座,提供微服務運行的環境和容器,同時接入其他後端服務,豐富實用場景和業務功能;

五、技術重難點

要實現自定義微前端架構,難點在於需要管理和整合多個應用,確保應用之間獨立運行,彼此不受影響,需要解決如下幾個問題:

5.1 資源管理

5.1.1資源加載

每個應用有一個應用資源管理和註冊的文件(app.regiser.js),其中包含路由信息,應用配置信息(configs.js)和靜態資源清單,當首次切換到某應用時,首先加載app.register.js文件,完成路由和應用信息的註冊,然後根據當前瀏覽器路由地址加載對應的靜態文件,完成頁面渲染,從而將各應用的靜態資源串聯起來,其中註冊入口文件通過webpack插件來實現,具體實現如下:
FuluAppRegisterPlugin.prototype.apply = function(compiler) {
   appId = extraAppId();
   var entry = compiler.options.entry;
   if (isArray(entry)) {
            for (var i = 0; i &lt; entry.length; i++) {
                if (isIndexFile(entry[i])) { // 入口文件
                    indexFileEdit(entry[i]);
                    entry[i] = entry[i].replace(indexEntryRegx, indeEntryTemp); // 替換入口文件
                    i = entry.length;
                }
            }
    } else {
            if (isIndexFile(entry)) { // 入口文件
                indexFileEdit(entry); // 重新生成和編輯入口文件
                compiler.options.entry = compiler.options.entry.replace(indexEntryRegx, indeEntryTemp); // 替換入口文件
            }
    }
    compiler.hooks.done.tap('fulu-app-register-done', function(compilation) {
            fs.unlinkSync(tempFilePath); // 刪除臨時文件
            return compilation;
    });
    compiler.hooks.emit.tap('fulu-app-register', function(compilation) {
        var contentStr = 'window.register("'+ appId + '", {\nrouter: [ \n ' + extraRouters() + ' \n],\nentry: {\n'; // 全局註冊方法
        var entryCssArr = [];
        var entryJsArr = [];
        for (var filename in compilation.assets) {
            if (filename.match(mainCssRegx)) { // 提取css文件
                entryCssArr.push('\"' + filename + '\"');
            } else if (filename.match(mainJsRegx) || filename.match(manifestJsRegx) || filename.match(vendorsJsRegx)) { // 提取js文件
                entryJsArr.push('\"' + filename + '\"');
            }
        }
        contentStr += ('css: ['+ entryCssArr.join(', ') +'],\n'); // css資源清單
        contentStr += ('js: ['+ entryJsArr.join(', ') +'],\n }\n});\n'); // js資源清單
        compilation.assets['resources/js/' + appId + '-app-register.js'] = { // 生成appid-app-register.js入口文件
            source: function() {
                return contentStr;
            },
            size: function() {
                return contentStr.length;
            }
        };
        return compilation;
    });
};
5.1.2資源文件名
微服務輸出打包模式下,靜態資源統一打包形式以項目id開頭,形如10000092-main.js, 文件名稱的修改通過webpack的插件實現;

核心實現代碼如下:

FuluAppRegisterPlugin.prototype.apply = function(compiler) {
    ......
    compiler.options.output.filename = addIdToFileName(compiler.options.output.filename, appId);
    compiler.options.output.chunkFilename = addIdToFileName(compiler.options.output.chunkFilename, appId);
    compiler.options.plugins.forEach((c) =&gt; {
        if (c.options) {
            if (c.options.filename) {
                c.options.filename = addIdToFileName(c.options.filename, appId);
            }
            if (c.options.chunkFilename) {
                c.options.chunkFilename = addIdToFileName(c.options.chunkFilename, appId);
            }
        }
    });
   ......
};

5.2 路由管理

路由分為應用級和菜單級兩大類,應用類以應用id為前綴,將各應用區分開,避免路由地址重名的情況,菜單級的路由由各應用的路由系統自行管理,結構如下:

5.3 狀態分隔

前端項目通過狀態管理庫來進行數據的管理,為了保證各應用彼此間獨立,因此需要修改狀態庫的映射關係,這一部分需要藉助於webpack插件來進行統一的代碼層面調整,包括model和view兩部分代碼,model定義了狀態對象,view藉助工具完成狀態對象的映射,調整規則為【應用id+舊狀態對象名稱】,下面來講解一下插件的實現;

插件的實現原理是藉助AST的搜索語法匹配源代碼中的狀態編寫和綁定的相關代碼,然後加上應用編號前綴,變成符合預期的AST,最後輸出成目標代碼:
module.exports = function(source) {
      var options = loaderUtils.getOptions(this);
	stuff = 'app' + options.appId;
	isView = !!~source.indexOf('React.createElement'); // 是否是視圖層
	allFunc = [];
	var connectFn = "function connect(state) {return Object.keys(state).reduce(function (obj, k) { var nk = k.startsWith('"+stuff+"') ? k.replace('"+stuff+"', '') : k; obj[nk] = state[k]; return obj;}, {});}";
	connctFnAst = parser.parse(connectFn);
	const ast = parser.parse(source, { sourceType: "module", plugins: ['dynamicImport'] });
	traverse(ast, {
		CallExpression: function(path) {
			if (path.node.callee && path.node.callee.name === 'connect') { // export default connext(...)
				if (isArray(path.node.arguments)) {
					var argNode = path.node.arguments[0];
					if (argNode.type === 'FunctionExpression') { // connect(() => {...})
						traverseMatchFunc(argNode);
					} else if (argNode.type === 'Identifier' && argNode.name !== 'mapStateToProps') { // connect(zk)
						var temp_node = allFunc.find((fnNode) => {
							return fnNode.id.name === argNode.name;
						});
						if (temp_node) {
							traverseMatchFunc(temp_node);
						}
					}
				}
			} else if (path.node.callee && path.node.callee.type === 'SequenceExpression') {
				if (isArray(path.node.callee.expressions)) {
					for (var i = 0; i < path.node.callee.expressions.length; i++) {
						if (path.node.callee.expressions[i].type === 'MemberExpression'
							&& path.node.callee.expressions[i].object.name === '_dva'
							&& path.node.callee.expressions[i].property.name === 'connect') {
								traverseMatchFunc(path.node.arguments[0]);
								i = path.node.callee.expressions.length;
						}
					}
				}
			}
		},
		FunctionDeclaration: function(path) {
			if (path.node.id.name === 'mapStateToProps' && path.node.body.type === 'BlockStatement') {
				traverseMatchFunc(path.node);
			}
			allFunc.push(path.node);
		},
		ObjectExpression: function(path) {
			if (isView) {
				return;
			}
			if (isArray(path.node.properties)) {
				var temp = path.node.properties;
				for (var i = 0; i < temp.length; i++) {
					if (temp[i].type === 'ObjectProperty' && temp[i].key.name === 'namespace') {
						temp[i].value.value = stuff + temp[i].value.value;
						i = temp.length;
					}
				}
			}
		}
	});
	return core.transformFromAstSync(ast).code;
};

5.4 框架容器渲染

完成以上步驟的改造,就可以實現容器中的頁面渲染,這一部分涉及到組件庫框架層面的調整,大流程如下圖:

六、構建流程

6.1 使用插件

構建過程中涉及到兩款自開發的插件,分別是fulu-app-register-plugin和fulu-app-loader;

6.1.1 安裝
npm i fulu-app-register-plugin fulu-app-loader -D;
6.1.2 配置

webpack配置修改:

const FuluAppRegisterPlugin = require('fulu-app-register-plugin');
module: {
   rules: [{
         test: /\.jsx?$/,
         loader: 'fulu-app-loader',
      }
   ]
}
plugins: [
    new FuluAppRegisterPlugin(),
    ......
]

6.2.編譯

編譯過程與目前項目保持一致,相比以前,多輸出了一份微前端項目編譯代碼,流程如下:

七、遺留問題

7.1 js環境隔離

由於各應用都加載到同一個運行環境,因此如果修改了公共的部分,則會對其他系統產生不可預知的影響,目前沒有比較好的辦法來解決,後續將持續關注這方面的內容,逐漸優化達到風險可制的效果。

7.2.獲取token

目前應用切換使用重定向來完成token獲取,要實現如上所述的微前端效果,需要放棄這種方式,改用接口調用異步獲取,或者其他解決方案。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

用隊列實現棧,用棧實現隊列,聽起來有點繞,都搞懂了就掌握了精髓!

目錄

  • 一、背景
  • 二、概念
    • 2.1 棧
    • 2.2 隊列
  • 三、棧和隊列的相互實現
    • 3.1 用隊列實現棧
    • 3.2 用棧實現隊列
  • 四、總結

一、背景

棧和隊列是數據結構中最常用到的兩種結構,有非常廣泛的運用,該篇文章將通過動畫的手段,展示棧和隊列相互實現的底層原理,讓我們真正搞懂棧和隊列的特性。

二、概念

2.1 棧

棧[Stack]:是一種限定僅在表尾進行插入和刪除操作的線性表;即後進先出(LIFO-last in first out),最後插入的元素最先出來

  • 入棧(push)
  • 出棧 (pop)
2.2 隊列

 隊列[Queue]:是一種限定僅在表頭進行刪除操作,僅在表尾進行插入操作的線性表;即先進先出(FIFO-first in first out):最先插入的元素最先出來。

  • 入隊(enqueue)
  • 出隊(dequeue)

三、棧和隊列的相互實現

3.1 用隊列實現棧
  • 模擬入棧的實現原理
    — 棧的特性是新加入的元素出現在棧頂,保證後進先出。
    — 隊列的特性為新加入的元素出現在隊尾,隊列的隊尾元素最後出隊。
    按以上兩個前提,我們可以讓隊頭至隊尾前的其它所有元素依次出隊再入隊,直至在隊尾新加入的元素被移到隊頭,也即實現了讓新壓入的元素保留在棧頂

  • 模擬出棧的實現原理
    — 由於在入棧時保證隊列中新加入隊尾的元素被移到了隊頭,出棧只需彈出隊頭元素即可。

  • 完整代碼實現

/**
 * 用隊列模擬實現棧
 *
 * @author zhuhuix
 * @date 2020-06-09
 */
public class QueueImplStack {

    // 定義隊列
    private Queue<Integer> queue;

    public QueueImplStack() {
        queue = new LinkedList();
    }

    // 入棧--在隊尾加入元素后,讓其他元素按順序出隊再入隊,保持新加入的元素永遠在隊頭
    public void push(Integer e) {
        queue.offer(e);
        int size = queue.size();
        int i = 0;
        while (i < size - 1) {
            queue.offer(queue.poll());
            i++;
        }
    }

    // 出棧--將隊尾前的其它所有元素出隊再入隊,直至隊尾元素移到隊頭
    public Integer pop() {
        return queue.poll();
    }

    // 查看棧頂元素--即隊頭元素
    public Integer peek() {
        return queue.peek();
    }

    // 是否為空
    public boolean isEmpty() {
        return queue.isEmpty();
    }

    public static void main(String[] args) {
        QueueImplStack stack = new QueueImplStack();
        stack.push(1);
        System.out.println(stack.peek());
        stack.push(2);
        System.out.println(stack.peek());
        stack.push(3);
        System.out.println(stack.peek());
        System.out.println("=============");
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.isEmpty());

    }
}

3.2 用棧實現隊列
  • 模擬入隊的實現原理
    — 隊列的特性最新入隊的元素需排在隊尾,最先入隊的元素排在隊頭,按隊頭到隊尾的順序依次出隊。
    — 對應到棧的數據結構上,也即需將新加入的元素保留在棧頂,保證先進先出。
    按以上兩個前提,需在存放數據的棧的基礎上再增加一個輔助棧,在每次入隊時,先將存放數據的棧彈入輔助棧,再把需加入的新元素壓入數據棧底,最後把輔助棧中的元素彈出依次壓入數據棧,這樣保證了新加入的元素,沉在棧底。

    模擬出隊的實現原理
    — 由於在入隊時,通過數據棧與輔助棧的交換,實現了后加入的元素沉在棧底,先進入的元素保留在棧頂,直接通過出棧彈出即可。

    • 完整代碼實現
/**
 * 用棧模擬實現隊列
 *
 * @author zhuhuix
 * @date 2020-06-09
 */
public class StackImplQueue {
    // 數據棧
    private Stack<Integer> stack;
    // 輔助棧
    private Stack<Integer> aux;

    StackImplQueue() {
        stack = new Stack<>();
        aux = new Stack<>();
    }

    // 入隊--通過數據棧與輔助棧相互交換,保證新加入的元素沉在數據棧底
    public void enqueue(Integer e) {
        while (!stack.isEmpty()) {
            aux.push(stack.pop());
        }
        stack.push(e);
        while(!aux.isEmpty()){
            stack.push(aux.pop());
        }

    }

    // 出隊--彈出數據棧元素
    public Integer dequeue(){
        return stack.pop();
    }

    // 查看隊頭元素
    public Integer peek(){
        return stack.peek();
    }

    // 是否為空
    public boolean isEmpty(){
        return stack.isEmpty();
    }

    public static void main(String[] args) {
        StackImplQueue queue = new StackImplQueue();
        queue.enqueue(1);
        System.out.println(queue.peek());
        queue.enqueue(2);
        System.out.println(queue.peek());
        queue.enqueue(3);
        System.out.println(queue.peek());
        System.out.println("=============");
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());
        System.out.println(queue.dequeue());

    }
}

四、總結

通過以上棧和隊列相互交叉的實踐,我們對棧和隊列的重大特性有了深入了解:

  • 棧和隊列都是線性連續結構,增加和刪除元素不會影響破此連續性
  • 棧通過棧頂的操作實現元素的增加與刪除,也即只能在一端進行操作
  • 隊列通過隊尾增加元素,隊頭刪除元素,也即可以在兩端操作

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

10_隱馬爾可夫模型

  今天是2020年3月13日星期五。不知不覺已經在家待了這麼多天了,從上一節EM算法開始,數學推導越來越多,用mathtype碼公式真的是太漫長了。本來該筆記是打算把《統計學習方法》這本書做詳細的解讀,起初面對書里大量的數學推導,感到非常恐懼。假期“空窗”時間不少,才有了細嚼慢咽學習的機會。其實很大的原因是自己掌握的東西太少,知道的算法太少,所以才對這本書恐懼。買了一直放着不願意學。現在到隱馬爾可夫模型,再有一章條件隨機場,監督學習部分就結束了。這一個月來,最大的收穫是知道了“怎麼學”。

  新的章節拋出一個新的算法模型,往往丈二和尚摸不着頭腦,什麼都是新的。越是拖延進度越慢,更不能一口吃個胖子指望看一遍就能懂。書讀百遍,其意自見,一遍不懂就再看一遍,一遍有一遍的收穫。但這個過程千萬不要盯着一本書看,一定要多找博客,多看知乎、CSDN,保持審視的態度,保留自己的見解。另外,我是喜歡直接看文字,實在不懂了才去翻視頻看,覺得這種模式挺適合我。

  學到第十章,發現書中的很多東西,沒必要面面俱到,要適當的取捨和放過。因為畢竟這本書不是一次性消耗品,是值得深究和研習的。第一次不懂的東西,完全可以學習完所有章節,建立大的思維格局后,再重新考慮小細節。

  接下來的所有章節,從例子出發,引入各個概念;手寫推導過程;圖解算法流程;最後實現代碼。掰扯開來,其實也就是三個問題:該模型是什麼樣子的(考慮如何引入);該模型為什麼可以這樣做(考慮如何理解推導過程);該模型怎麼應用(考慮代碼實現和應用場景)。

 GitHub:https://github.com/wangzycloud/statistical-learning-method

 隱馬爾科夫模型

引入

  隱馬爾科夫模型描述由隱藏的馬爾可夫鏈隨機生成觀測序列的過程,屬於生成模型。把這句話倒着思考一下:(1)該模型屬於生成模型,會不會類似EM算法中考慮的,一個觀測數據y的生成,中間需要經過一個隱藏狀態z。(2)很明顯這裏生成的不是單個數據,而是單個數據構成的一個序列,並存在時序關係。(3)馬爾可夫鏈是什麼?在生成數據序列的過程中扮演什麼角色?

  先區分兩個概念,“狀態and觀測”。在我的理解里,“狀態”也好,“觀測”也罷,不過是表達隨機變量的一個說法。狀態會有多個狀態,觀測會有多個觀測,但同時只允許一個狀態或者一個觀測出現。例如,現在有四個盒子(灰、黃、綠、藍),李華在五天內選盒子取球,規定每天只能取一個盒子(每個盒子被選的概率一樣大)。問,李華這五天可能會有多少種取盒子的序列,並問取到某種序列的概率是多少?如下:

  你知道的,這個組合數不小。因為每個盒子被選到的概率一樣大,所以每個序列出現的概率相同。李華每天在盒子里取球(紅、白),現在限制每個盒子紅球、白球數目相同(紅、白球各有五個)。問,李華五天內取到球的顏色的序列有多少種,並問取到某種序列的概率是多少?

   顯然,這個組合數要小一些。因為每個盒子中紅白球數目相同,且此時盒子的選擇(狀態)對球的選取無影響,所以每個序列出現的概率相同。可是如果每個盒子中,紅白球的數量不是五五開,各不相同呢?李華五天內取球的某個序列的概率,就不再相同了。另外,除了受到盒子內紅白球的概率分佈影響,還要受到某天會抽到哪個盒子的概率分佈影響。

  在上述例子中,把可能取到的盒子情況,稱作“狀態”;把可能會取到的球的情況,稱作“觀測”。在隱馬爾科夫模型中,盒子會取到的各種狀態,我們是觀測不到的。而球的各種情況我們是知道的,可以被觀測到。

  取球要受到盒子所在狀態的影響,示意圖如下:

  此時,還不能叫做隱馬爾可夫模型的示例。需要繼續給“取盒子->取球->得到觀測序列”的過程施加限制條件。比如說,t時刻取到某個盒子,要受到t-1時刻盒子的狀態影響。一個簡單的例子,t-1時刻盒子是綠盒子,t時刻一定取灰色盒子,且t-1時刻取到綠盒子不對t+1、t+2、…、T時刻產生影響。具體一點,就是讓“當前時刻隱藏狀態”只受上一時刻“隱藏狀態”影響,且與所處的時刻t無關

  通過一步步施加的各個條件,此時可以稱作隱馬爾可夫模型的示例了。

隱馬爾科夫模型的基本概念

  先上例子,盒子和球模型。

  在這個例子中有兩個隨機序列,一個是盒子的序列(狀態序列),也就是每次選取到的盒子序列,這個過程是隱藏的,觀測不到從哪個盒子取球;一個是球的顏色序列(觀測序列),我們只能知道取出來的各個球的顏色。

  先分析一下取盒子環節,這是一個環環相扣的過程。從當前t-1時刻的盒子出發,考慮t時刻會取到哪個盒子,要符合規則。如當前盒子是1,根據上述規則,下一個盒子一定是盒子2。考慮t+2時刻會取到哪個盒子,要站在t+1時刻的盒子狀態上,決定取哪一個盒子。所謂的馬爾可夫性,很重要的一點,就是t-1時刻的狀態只決定t時刻的狀態(盒子1之後一定會取到盒子2),並不能決定t+1時刻狀態的取值(盒子1之後,決定不了盒子2之後會取哪個盒子)。

  再看一下取球環節,對應着描述中的從盒子中隨機取球的過程。每個盒子裡邊紅、白球的數目不同,不同的盒子取到紅色球的概率不同。當前盒子有屬於自己的概率分佈,取球的概率不盡相同。

  用數學語言完善完善以下過程:盒子可以構成一個集合;當前時刻的盒子如何確定下一個盒子,需要有狀態轉移概率;球可以夠成一個集合;從不同盒子裡邊取球,需要知道每個盒子的概率分佈;取了多少個球,需要有序列長度;最開始怎麼選第一個盒子。

  根據所給的條件,有以下:

  重點看一下狀態轉移矩陣。

  熟悉了這個例子,再來理解數學上的各個概念。

  這裏的狀態隨機序列就是每次取到盒子組成的序列,觀測序列就是球顏色的序列。隱馬爾科夫模型由狀態的初始概率分佈、狀態中間的轉移概率分佈以及觀測概率分佈組成。

  對應着看,Q就是例子中盒子的集合,V就是球顏色的集合,I是盒子序列,O是顏色序列。

  令A為狀態轉移矩陣:

  這裏的變量i有點混亂,注意區分。公式10.2中,(1)aij中的i是狀態轉移矩陣A中的第i行的意思,aij也就是矩陣A中的第i行第j個元素,該值表示從第i個元素轉移到第j個元素的概率;(2)it+1it中的i是指該狀態序列中的第t+1、第t個狀態,這裏i是序列的意思;(3)qi中的i是在狀態集合中取到哪個狀態的意思。

  t+1時刻能夠取到哪個狀態,要受到t時刻狀態的影響。也就是在t時刻狀態取某個值的條件下,t+1時刻才會有什麼樣的取值。矩陣A維度為N*N,也就是要知道該時刻每個狀態對下一時刻每個狀態的影響。

  觀測有M種,vk可以理解為觀測集合V中的第k個觀測。在盒子和球的例子中,可以看到每個觀測的取值,是由隱變量的狀態->哪個盒子決定的,並且只與當前的盒子有關係,每個盒子有各自取球的概率分佈。用概率符號表示就是公式10.4,表示在狀態為第j個盒子的情況下,觀測到vk的概率。

  用π來表示初始概率向量,也就是t=1序列起始時,根據一定的概率分佈選擇第一個盒子。

  在這裏,狀態轉移概率矩陣A與初始狀態概率向量π確定了隱藏的馬爾可夫鏈,可生成不可觀測的狀態序列。觀測概率矩陣B確定了如何從狀態生成觀測,與狀態序列綜合確定了如何產生觀測序列。

  從上述描述及定義10.1可以看到,隱馬爾科夫模型做了兩個基本假設:

  (1)再次回顧盒子和球模型,盒子的選擇是不是只規定了時序上前後相鄰的盒子該怎麼選;而沒有第一次選盒子1,第三次一定會選到盒子3這樣的規定。也就是在任意時刻t的狀態只依賴於其前一時刻t-1的狀態,這就是馬爾科夫鏈“齊次”的重要性質。

  (2)觀測獨立性假設是指我們觀測到的每一次現象(紅球、白球),只與該球所在盒子的概率分佈有關,與其它盒子的概率分佈沒有一點關係!與其它時刻的觀測沒有一點關係!

  觀測序列的生成過程可以由算法10.1描述。

   HMM和CRF,與之前學習的各個模型,差別是比較大的,學習思路是要換一換。理解了隱馬爾科夫模型的基本概念,下一步就是要考慮該模型可以做什麼?怎麼做?這裏我接觸的不多,只能順着書本的思路,學習隱馬爾可夫模型的三個基本問題。

  1)概率計算問題。很自然的,考慮一下某個觀測序列O出現的概率P(O|λ)

  2)學習問題。已知觀測序列,用極大似然估計的方法估計模型參數λ=(A,B,π)

  3)預測問題,也稱解碼問題。知道模型參數,給定觀測序列,求最有可能的對應的狀態序列。

概率計算算法

1)直接計算

  已知模型參數λ=(A,B,π)和觀測序列O=(o1,o2,…,oT),計算觀測序列O出現的概率P(O|λ)。很容易想到,可以按照概率公式直接進行計算。把得到觀測數據的過程,想象成兩個階段:選取狀態和生成觀測。第一步得到狀態序列,第二步得到觀測序列,可以應用乘法原理。不同的觀測序列可以得到不同的觀測序列,可以應用加法原理。類似於全概率公式,通過列舉所有可能的狀態序列I,求各個狀態序列I上生成觀測O的概率(也就是I,O的聯合概率),然後對所有可能的狀態序列求和,得到P(O|λ)

  容易理解,公式(10.10)為全部狀態序列中某個狀態序列I的概率計算公式;公式(10.11)為在該狀態序列I條件下,觀測序列為O時的條件概率計算方法;公式(10.12)為聯合概率公式;公式(10.13)對所有可能的狀態I求和,也就是求O的邊緣概率(考慮在I出現的所有情況條件下,O出現的概率)。簡單分析下,若狀態數目為N,一共有T個狀態序列,所以狀態序列的可能性為NT。每一種狀態序列都要相應乘T個觀測概率,所以最後的時間複雜度為O(TNT)。用這種方法計算觀測序列O出現的概率,是非常低效的。接下來介紹高效的計算方式,前向-後向算法。

2)前向算法

  先來看一個新概念“前向概率”:

  放在示意圖上如藍色虛線框α3(i)=P(o1,o2,o3,it=qi|λ),可以從聯合概率的角度理解。具體為 αt=3(灰色盒子)=P(o1=紅球,o2=白球,o3=紅球,it=3=灰色盒子|λ)

  在前向概率的基礎上,定義前向算法為:

  步驟(1),計算初值。注意這裏α1(i)應用向量來表示,在t=1,觀測取到o1時,各個隱藏狀態i都有到達o1的概率。計算分兩步,從初始概率πi到隱狀態qi,再從qi經發射矩陣到觀測o1,需要對每個隱藏狀態i計算。

  步驟(2),前向算法遞推公式。αt(i)遞推到αt+1(i),公式(10.16)中的bi(ot+1)可以理解為下圖第二步,由所在的狀態qi經發射矩陣得到觀測ot+1aji可以理解為下圖中的第一步,也就是由t時刻狀態qj經轉移矩陣在t+1時刻狀態為qi的過程。

  公式(10.16)中的求和符號,實際上反映的是t+1時刻取qi時,其實t時刻的任何一個狀態都可以轉移到qi,因此要把t時刻的每種狀態都考慮到。

  步驟(3),終止。公式(10.17)的求和符號挺好理解的,因為,對iT=qi求和,實際上相當於求觀測序列O的邊緣概率。

  再來看一下書中的詳細解釋:

   通過畫圖,是不是要好理解一些~前向算法高效就高效在利用先前的局部計算結果,通過路徑結構將局部結果“遞推”到全局。

  看一下例10.2,基本上就可以理解這個計算過程了。

3)後向算法

  相應的,後向算法先了解“後向概率”這個概念。

   放在示意圖上,如綠色虛線框β2(i)=P(o3,…,oT-1,oT,it=qi|λ),可從條件概率的角度理解。具體為βt=2(綠色盒子)=P(o3=紅球,oT-1=紅,oT=紅球|it=2=綠色盒子,λ)

    在後向概率的基礎上,定義後向算法為:

  步驟(1),初始化後向概率。這裏將最終時刻的所有狀態qi規定為βT(i)=1以下示意圖簡單分析。

  這就好像是βt(i)=P(ot+1,ot+2,…,oT|it=qi, λ)變成了βt(i)=P(it=qi, λ),此時對於it的所有取值,it=qi,是一個不爭的事實。

  步驟(2),後向算法遞推公式。這裏的遞推方向是反向由T+1遞推到T,圖示如下:

  這裏由T+1遞推到T,仍然需要①②兩處的連接。①是公式(10.20)中的aij,②是公式(10.20)中的bj(ot+1)。求和符號是t時刻qi到t+1時刻qj所有情況的匯總。取(qi=灰色盒子,ot+1=白球)進行分析:

   T+1遞推到T,我覺得圖畫的應該差不多了…①②部分是怎樣起到連接作用的…大概就是上圖這樣吧…我解釋不出來…當然了,知乎也好CSDN也好,有詳細推導公式,我就不班門弄斧了。書面解釋如下:

  於是,利用前向概率和後向概率的定義,可以將觀測序列概率P(O|λ)同一寫成:

  示意圖好像是這個樣子:

  公式(10.22)中,先來看前向概率的求和部分,i=1時,αt(1)是t時刻盒子為灰盒子,觀測序列為(o1,o2,…ot,it=q1)的概率;相應的,αt(2)是t時刻盒子為黃盒子,觀測序列為(o1,o2,…ot,it=q2)的概率;αt(3)是t時刻盒子為綠盒子,觀測序列為(o1,o2,…ot,it=q3)的概率;αt(4)是t時刻盒子為藍盒子,觀測序列為(o1,o2,…ot,it=q4)的概率。那麼求和自然就代表着,不考慮盒子的影響,觀測序列為(o1,o2,…ot)的邊緣概率。對應示意圖,也就是消除了t時刻狀態的影響。

  同理,後向概率的求和部分,在示意圖中相當於消除了t+1時刻狀態的影響。①對應着公式(10.22)中的aij,建立連接。②對應着公式(10.22)中的bj(ot+1),將ot+1時刻的觀測計入統計。

4)一些概率與期望值的計算

  利用前向概率和後向概率,可以得到關於單個狀態和兩個狀態概率的計算公式。頭幾遍看這幾個公式的時候,丈二和尚摸不着頭腦,不知道這幾個概率計算有什麼用,就沒怎麼好好看。編寫這部分代碼的時候,發現這幾個公式挺重要的。在學習算法小結,對估計模型參數非常有用。公式介紹的挺具體的,這裏就不在畫圖了…學習的時候隨手畫畫圖,就能理解了~

  1)求單個狀態的條件概率:

  還是畫吧,這裏γt(i)反映是在給定觀測序列O條件下,t時刻處於狀態qi的概率。如下圖,γt(i=灰色盒子)

  2)求兩個連續狀態的條件概率:

   如下圖所示ξt(i,j)反映的是,在給定觀測序列O的條件下,t時刻狀態為灰色盒子、t+1時刻狀態為綠色盒子的條件概率。

  3)一些有用的期望,在學習算法小節可以看到用處:

學習算法

  書中提到,我們進行隱馬爾可夫模型的學習,也就是對模型參數進行估計。根據訓練數據是包括觀測序列和對應的狀態序列,還是只有觀測序列,可以分別由監督學習和無監督學習實現,這裏監督學習方法實際上就是利用極大似然估計。

  1)監督學習方法。書中直接給出了參數估計公式,這裏簡單摘抄下~

   2)無監督學習方法。顧名思義,無監督方法也就是只有觀測序列,進行參數估計的方法。由於監督學習需要使用標註的訓練數據,而人工標註訓練數據往往代價很高。因此有時候就會利用無監督學習的方法。我們可以將觀測序列數據看作EM算法中的不完全數據,狀態序列數據看作EM算法中不可觀測的隱數據,那麼隱馬爾可夫模型就可以看作是含有隱變量的概率模型。於是,可以通過EM算法求解。

  詳細過程如下:

  1.確定完全數據的對數似然函數

  2.EM算法的E步:求Q函數Q(λ,λ^)

  3.EM算法的M步:極大化Q函數Q(λ,λ^)求模型參數A,B,π

  書本上有詳細的推導公式,看懂了2/3,先不摘抄了。有空了把理解了的整理上來,參數估計公式如下:

  於是,有以下Baum-Welch算法,從這裏可以發現一些期望的用處:

預測算法  

  預測算法,也就是根據已知的觀測序列,找到概率最大的狀態序列(最有可能的對應的狀態序列)。

  應用維特比算法,相當於有向無環圖求最短路徑,網上有大量詳細的資料,暫不整理了~

代碼效果

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

1.5T超大空間只要6.98萬起的自主SUV車主滿意嗎

9萬首先我是五菱的忠實粉絲,第一輛車就是五菱,跑了20萬公里發動機和變速箱還沒動過,這質量是我選擇560的原因,其次我要經常拉點貨,後排放倒后空間完全可以滿足我,而且一家人出去玩也方便很多,1。8L+5AMT開起來比手動擋省力得多,雖然有時候會有些頓挫感,但畢竟不是全自動,也能接受了。

前言

有什麼車上市不到一年,就可以做到超過330000輛的銷量成績呢,沒錯它就是自主SUV陣型中的又一神車-寶駿560,對於任何品牌和車型來說,這都是一個了不起的成績,從全系沒有自動擋,到增加了5AMT,隨後又推出了1.5T的車型,為了更適應消費者需求一直在改進,那麼車主對它的評價如何呢,一起來探討一下。

上汽通用五菱-寶駿560

指導價:6.98-9.48萬

車主:菜鳥凌凌漆

購買車型:2016款 1.8L 手動精英型

裸車價格:7.68萬

五菱的車子實用性是經過市場的長時間考驗的,性價比也是較高的,圓潤飽滿的外觀是非常耐看的,雖說沒什麼太大的亮點,內飾中控布局也趨向轎車化的設計水平,無論是前後排,空間都非常寬敞,後排想怎麼坐就怎麼坐,1.8L的動力還是挺不錯的,起油比較快,操控性精準,尤其是在鄉村道路,底盤高的優勢就很明顯。

10月份提的車,目前行駛了2000多公里,油耗7.5L左右,續航能力很好。

車主:三石哥

購買車型:2016款 1.8L AMT智能手動豪華型

裸車價格:8.9萬

首先我是五菱的忠實粉絲,第一輛車就是五菱,跑了20萬公里發動機和變速箱還沒動過,這質量是我選擇560的原因,其次我要經常拉點貨,後排放倒后空間完全可以滿足我,而且一家人出去玩也方便很多,1.8L+5AMT開起來比手動擋省力得多,雖然有時候會有些頓挫感,但畢竟不是全自動,也能接受了。

11月份提的車,目前走了1800公里,市區油耗7.8左右,高速6.7左右,比之前麵包車要省油。

車主:青春的奧秘

購買車型:2016款 1.5T 手動豪華型

裸車價格:8.88萬

首先外觀時尚大氣,寶駿560 1.5T手動豪華版配置很齊全,功能強大,無鑰匙進入/啟動、倒車影像、定速巡航什麼的都有配備,動力非常充足,渦輪介入得比較早,加速很順暢,後排空間秒殺同級,北京癱什麼的隨便坐,電動助力的方向盤很輕盈,一個手也可以輕鬆操控,轉向很精準,這麼低的價格買到這麼好的車子,超值了。

9月份提的車,目前行駛4000公里,綜合油耗6.7,還是非常省油的。

總結:寶駿560一上市的歡迎程度就非常誇張,從單一的動力總成擴展到兩個,從純手動擋車型發展到增加了5AMT,現在唯一欠缺的就是全自動擋車型,上升的空間還有很大,逐步完善後再創銷量奇迹也是指日可待。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!