JDK1.8-Java虛擬機運行時數據區域和HotSpot虛擬機的記憶體模型

来源:https://www.cnblogs.com/tanshaoshenghao/archive/2019/04/17/10721256.html
-Advertisement-
Play Games

[TOC] 介紹 初學Java虛擬機幾天, 被方法區, 永久代這些混雜的概念搞混了. 我覺得學習這部分知識應該把官方定義的虛擬機運行時數據區域和虛擬機記憶體結構分開敘述, 要不然容易誤導. 本文先介紹官方文檔規定的運行時數據區域, 然後以JDK1.8的HotSpot虛擬機為例, 介紹虛擬機的記憶體結構. ...


目錄

介紹

  • 初學Java虛擬機幾天, 被方法區, 永久代這些混雜的概念搞混了. 我覺得學習這部分知識應該把官方定義的虛擬機運行時數據區域和虛擬機記憶體結構分開敘述, 要不然容易誤導.
  • 本文先介紹官方文檔規定的運行時數據區域, 然後以JDK1.8的HotSpot虛擬機為例, 介紹虛擬機的記憶體結構.

官方文檔規定的運行時數據區域

  • 官方文檔中規定的運行時數據區一共就幾塊: PC計數器, 虛擬機棧, 本地方法棧, 堆區, 方法區, 運行時常量池. 這裡的官方規定是說, 如果你要做一個Java虛擬機的話, 必須要包含這幾個區域, 但是這幾個區域在你的虛擬機中是用哪塊記憶體實現的, 這由虛擬機製作者決定.

程式計數器

  • The pc Register, 程式計數器. 如果瞭解過電腦系統, 對這個名詞應該不陌生了, 它指向下一條指令的地址, 程式靠它跑起來.
  • Java虛擬機支持多線程, 每條線程都有自己的程式計數器.
  • 如果當前線程正在執行一個Java方法, 它的計數器記錄的是正在執行的Java虛擬機指令的地址. 如果執行的是本地方法(比如系統的C語言函數), 計數器中的值為空(Undefined).
  • 正因為程式計數器記錄的是指令地址, 所以它占用的空間較少, Java虛擬機規範中並沒有規定這塊記憶體有OutOfMemoryError(記憶體溢出)的情況.

 

Java虛擬機棧

  • Java Virtual Machine Stacks, Java虛擬機棧.
  • Java虛擬機棧是線程私有的, 生命周期與線程相同. 虛擬機棧存放棧幀, 棧幀用於存儲局部變數表, 部分結果值, 方法的初始化參數和返回信息, 方法的執行通過棧幀的壓棧和出棧實現.

本地方法棧

  • 本地方法棧和上面的虛擬機棧是相似的, 從名字也看出, 虛擬機方法棧是用來執行Java代碼的, 而本地方法棧則是用來執行本地系統代碼的, 比如C代碼.
  • 也因為規範中沒有規定本地方法棧執行的代碼, 如果想執行Java代碼也是可以的, 我們可以看到Oracle官方的虛擬機HotSpot虛擬機把Java虛擬機棧和本地方法棧合二為一, 這麼做避免了要為不同的語言設計棧, 提高了虛擬機的性能.

虛擬機棧和本地方法棧溢出

  • 那麼當出現錯誤信息後, 我們在什麼錯誤信息下可以去排查是否虛擬機棧和本地方法棧這兩塊記憶體出錯呢? 這裡以HotSpot虛擬機為例講解(HotSpot把兩塊棧結構合在一起實現了), 在JDK1.8的虛擬機規範中對這兩塊棧空間可能出現的錯誤給出了相同的描述.
  • 一: 如果一條線程所需要的記憶體大於虛擬機所分配給它的記憶體, 將拋出StackOverflowError異常.
  • 二: 如果棧記憶體可以擴展並嘗試擴展時可用的記憶體不足, 或者創建新線程併為其分配棧記憶體時可能的記憶體不足, 會拋出OutOfMemoryError
  • 下麵先演示第一個StackOverflowError異常
//設置虛擬機參數 -Xss128k, 設置單個線程的棧空間大小為128k
public class StackErrorTest1 {
    private int stackLength = 1;

    public void stackLeak(){
         stackLength++;
         stackLeak();
    }

    public static void main(String[] args) {
        StackErrorTest1 set1 = new StackErrorTest1();
        try{
            set1.stackLeak();
        }catch (Throwable e){
            System.out.println("stack length:" + set1.stackLength);
            e.printStackTrace();
        }
    }
}
//輸出異常信息
stack length:1000
java.lang.StackOverflowError
    at jvm.StackErrorTest1.stackLeak(StackErrorTest1.java:7)
    at jvm.StackErrorTest1.stackLeak(StackErrorTest1.java:8)
    ...
  • 所以當遇到StackOverflowError時可以考慮是否是是虛擬機的棧容量太小, 比如這裡的無窮遞歸, 棧空間不夠用. 當然生產環境中肯定不會寫無窮遞歸, 這時可以通過設置-Xss參數調整單條線程的棧記憶體大小.
  • 上面描述的棧記憶體可以擴展並嘗試擴展時可用的記憶體不足導致出現OutOfMemoryError的情況暫時沒有好的演示代碼, 在周志明的《深入理解Java虛擬機》中提到"定義了大量本地變數,增大方法幀中本地變數表的長度, 結果仍拋出StackOverflowError". 不知道是不是沒有觸發虛擬機動態擴充棧空間, 所以仍然判定是棧所需的空間超出了虛擬機規定的大小. 總結來說無論是棧幀太大還是棧空間太小都會拋出StackOverflowError, 可以考慮調整-Xss參數.
  • 上面還提到當創建新線程並分配新的棧空間時, 如果可用的記憶體不夠, 會拋出OutOfMemoryError異常, 下麵是這種情況的代碼演示.
public class StackErrorTest2 {

    private void keepRunning(){
        while(true){
        }
    }

    public void stackLeakByThread(){
        while(true){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    keepRunning();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args){
        StackErrorTest2 set2 = new StackErrorTest2();
        set2.stackLeakByThread();
    }
}
//運行結果, 來源《深入理解Java虛擬機》
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
  • 這段代碼也來自深入理解jvm, 書中也說明跑這段代碼要小心, 因為Java的線程是映射到內核線程上的, 果不其然我的機子一跑就死機了.
  • 問什麼會出現這樣的錯誤? 32位Windows系統分配給一個進程的記憶體最大為2GB(32位能定址4GB地址空間, 除去內核的空間剩2GB, 64位則大得多). 這2GB減去最大堆容量, 減去方法區的容量, 剩下的就是虛擬機棧和本地方法區棧的記憶體空間了. (補充: PC計數器占的空間很小, 運行時常量池在方法區中, HotSpot中虛擬機棧和本地方法棧一起實現, 所以能分成這麼三大塊記憶體).
  • 瞭解了三大塊記憶體區後(HotSpot下), 解決思路也出來了: 1. 減小最大堆記憶體, 騰出更多位置給棧空間. 2. 如果程式的線程數量不可以減少, 那麼就看看是否可以減少每條線程的棧記憶體.
  • 當然用一臺配置高的機器, 該用64位的Java虛擬機也是一種方法.

Java堆

  • Java堆是隨著虛擬機的啟動而創建的, 用於存放對象實例, 所有的對象實例和數組都在堆記憶體分配, 它被所有線程共用. Java堆是Java虛擬機管理的記憶體中最大的一塊, 也是垃圾回收器管理的主要區域. 從記憶體回收的角度看, Java堆記憶體還可以被繼續劃分, 並且和具體的虛擬機實現有關.
  • 當前主流的虛擬機都是支持堆記憶體動態擴展的, 就是說當堆記憶體的大不夠時, 它會擴充容量; 當不要太多的空間時, 它能自己進行壓縮. 我們可以人為地通過-Xmx和-Xms設定堆記憶體的最大值和最小值(初始大小). 如果我們把-Xmx和-Xms設置為相同的值, 就等同於設定了固定大小的Java堆. (這是gc調優的一種手段)
  • 若堆記憶體分配記憶體時發現已經沒有更過可用空間時, 會拋出OutOfMemoryError.

演示堆記憶體溢出

  • 堆記憶體是存放對象實例的地方, 這個應該比較好理解, 直接上代碼
/**
 * VM Args: -Xms20m -Xmx20m
 */
public class HeapErrorTest {
    static class Object{
    }

    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        while(true){
            list.add(new Object());
        }
    }
}
//運行結果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at java.util.Arrays.copyOf(Arrays.java:3181)
  • 由結果可以看到當堆記憶體溢出後除了有java.lang.OutOfMemoryError外, 還會提示Java heap space. 在這個例子中, 我們明確地知道了是由於堆記憶體不夠大而造成的溢出. 然而在生產環境中, 當系統報出堆記憶體溢出時, 我們首先要搞清楚是因為記憶體泄漏導致的記憶體溢出, 還是純粹的記憶體溢出.
  • 記憶體溢出指的是分配記憶體的時候, 沒有足夠的空間供其使用. 記憶體泄漏指的是在分配一塊記憶體使用完後沒有釋放, 在Java中對應的場景是沒有被垃圾回收器回收. 一點點的記憶體泄漏用戶可能感受不到, 但是當泄漏的記憶體積少成多的時候, 會耗盡記憶體, 導致記憶體溢出.
  • 有一些常用的分析記憶體溢出的手段和工具, 這裡就不詳細敘述了, 可以參考書籍或網上的資料. 當我們判斷是記憶體泄漏導致的溢出後, 可以根據工具定位出現泄漏的代碼位置; 如果不存在泄漏只是單純的溢出的話, 可以通過設置虛擬參數調整堆記憶體大小(前提是機器的配置能夠支持相應的記憶體大小), 或者看看代碼中是否存在一些生命周期很長的對象實例, 看看能否作出修改.

方法區

  • 方法區用於存儲以被虛擬機載入的類信息, 常量, 靜態變數, 即時編譯器編譯後的代碼數據等, 它是所有線程共用的. 虛擬機規範中說方法區在邏輯上是堆的一部分, 但是它的別名叫"non-Heap"也就是非堆的意思, 表明它和堆記憶體是兩塊獨立的記憶體. 至於說在邏輯上是堆區的一部分, 是因為在物理實現上, 方法區的記憶體地址包含於堆中, 所以說是邏輯上的一部分, 實際用的時候是完全不同的部分. 這麼設計可能是因為便於垃圾收集器統一管理吧.

運行時常量池

  • 運行時常量池的記憶體由方法區分配, 也就是說它屬於方法區的一部分. 它用於存儲Class文件中的類版本, 欄位, 方法, 介面和常量池等, 也用於存放編譯期生成的各種字面量和符號引用.
  • 運行時常量池區別於Class文件常量池的一個重要特征是具備動態特性. 也就說並非在Class文件中定義的常量才能進入運行時常量池, 在程式運行的過程中也有可能將新的常量放入池中.

演示方法區溢出

  • 演示方法區溢出和堆區的思路一樣, 不斷往方法堆中加入東西使其溢出. 只是方法區中保存的是類信息, 我們通過不斷動態生成類演示
  • 本代碼示例來源於深入理解jvm, 但是其中的參數需要改變, 該書的最新版本是基於JDK1.7的, JDK1.7中方法區是在永久代中實現的, 而JDK1.8中已經沒有永久代了, 方法區中Metaspace元數據區中, 通過設置-XX:MetaspaceSize-XX:MaxMetaspaceSize來指定方法區的大小
/**
 * VM Args: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 */
public class MethodAreaTest {

    static class Object{
    }

    public static void main(String[] args) {
        int count = 0;
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Object.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public java.lang.Object intercept(java.lang.Object o, Method method, java.lang.Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(objects, objects);
                }
            });
            enhancer.create();
            System.out.println(++count);
        }
    }
}

運行結果:
Caused by: java.lang.OutOfMemoryError: Metaspace
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    ... 8 more

 

HotSpot虛擬機的記憶體模型

  • 在介紹完Java虛擬機運行時數據區域後, 接著以HotSpot虛擬機為例介紹虛擬機記憶體模型.
  • 首先有一個重要的概念要搞清楚, 要不然容易犯暈.
  • 在前面介紹Java運行時數據區域時我們談到PC計數器, 虛擬機棧, 本地方法棧這3塊記憶體都是線程私有的, 它們的隨線程的創建而分配, 隨線程的結束而釋放, 也就是說Java虛擬機是明確知道這三塊記憶體是什麼時候該被回收的, 只要線程沒執行完就不能回收, 否則線程跑不起來.
  • 而我們在談論虛擬機的記憶體模型時, 通常要和垃圾回收結合在一起討論. 既然上面的三塊記憶體回收的時間已定, 暫時不需要過多考慮, 虛擬機分配記憶體時給它們留有空間就行.
  • 但另外的兩塊記憶體堆記憶體和方法區則不一樣, 它們是所有線程共用的, 在這裡面記憶體的分配和釋放具有不確定性. 比如說在多態的情況下, 一個介面對應的實現類不同, 具體的實現方法也不同, 虛擬機只有在程式運行的過程中才知道要創建哪些對象, 這部分記憶體的分配和釋放都是動態的, 垃圾收集器關註的也是這部分的內容.
  • 所以說我們後續描述的虛擬機記憶體模型是建立在Java堆記憶體和方法區上的.

JVM實現的堆記憶體和方法區

  • 正如上述所說, 當談論JVM的記憶體結構時, 討論的重點就由整個運行時數據區域轉為對堆記憶體和方法區的討論, 因為這兩部分是垃圾回收的重點區域(如果兩者要比較的話, 重點收集區域是堆區).
  • 而HotSpot虛擬機的記憶體結構由三大部分組成: 新生代, 老年代和元數據區(JDK1.7及以前叫老年代). 其中新生代和老年代是虛擬機規範中Java堆記憶體的實現, 元數據區是規範中方法區的實現. 在講述為什麼這麼定義之前, 先明確這個關係對於理解概念是很重要的, 下麵有幅圖幫助理解.
  • 這裡有個小失誤, 題目中明明講的是JDK1.8, 為什麼還提永久代呢? 由於永久代存在的時間長, 永久代的說法經過這麼多年可能已經深入人心, 所以先併列講, 要知道永久代和元數據區是有本質的差別的, 這留到後面講, 先認清概念.
  • 希望圖片加描述能夠幫助你立即規範定義的數據區域和JVM記憶體結構之間的關係. 下麵將對HotSpot虛擬機的記憶體模型做進一步分析.

新生代和老年代.

  • Java堆記憶體被實現為新生代和老年代, 是為了更方便地進行垃圾回收. 我們知道對象是存儲在堆記憶體中的, 從字面上理解新生代就是新創建的對象區域, 老年代就是使用多次生命周期長的對象區域. 新生代對象生命周期通常較短, 很多用完即可以釋放; 老年代對象的生命周期較長, 可能在整個程式的運行過程中都是有用的.
  • 由於新對象和老對象具有不同的性質, 為對這兩種對象設計的垃圾回收演算法也不同, 所以要把它們分開.

新生代中的記憶體劃分

  • 新生代的記憶體被分為一個Eden區和兩個Survivor區. 為了講述為什麼要這麼分, 需簡單引入垃圾回收演算法.
  • 首先最基礎, 最簡單的垃圾回收演算法叫標記-清除演算法. 演算法流程和演算法名完全一致: 首先標記出哪些是可以回收的對象, 標記完後把對象清除. 如果按照這麼個流程, 新生代應該就是一塊簡單的記憶體就行, 現實結論告訴我們這個演算法是可以優化的.
  • 標記清除演算法的不足在於一塊完整的記憶體在經過標記-清除演算法後有些記憶體會被釋放掉, 這時會造成記憶體空間不連續, 可能不能夠存放一些較大的對象.
  • 標記-清除演算法的升級版是複製演算法, 它在標記-清除的思路上作出了些改變. 首先將記憶體分為兩塊, 當創建新對象分配記憶體的時候只用兩塊中的一塊A. 當進行垃圾回收的時候只對有對象的一塊A記憶體使用標記-清除演算法進行回收, 回收後剩餘的存活對象從記憶體A移到另一塊空的記憶體B中, 這樣A記憶體重新變為空記憶體, 繼續重覆此分配回收過程. 這個演算法似乎更好一些, 但是也只是兩塊記憶體, 說明還不是現實中的最優解.
  • 考慮新的演算法, 把記憶體分配成均等兩塊, 等同於能夠使用的記憶體變為原來的二分之一了, 根據IBM專門部分研究新生代中百分之98%的對象都是"朝生夕死"的, 也就是說在進行垃圾回收時98%的對象都被回收掉, 只有2%會從A記憶體移動到B記憶體. 這麼一想我們把兩塊記憶體割為相同的兩塊是不是有點太虧了?
  • 下麵揭曉答案: HotSpot虛擬機回收虛擬機時使用的是複製演算法, 但是它分成三塊記憶體, 一個占80%記憶體的Eden區(堆記憶體), 兩個分別占10%的Survivor區. 具體操作是這樣的: 程式運行時, 用Eden區和一個Survivor區A存放新創建的對象. 當發生垃圾回收時, 把存活下來的對象(很少)複製到另一塊Survivor區B中, 使得Eden區和Survivor區A重新為空, 然後繼續重覆這個分配回收的過程.
  • 所以說詳細點的Jvm的記憶體模型是下麵這樣的

由JDK1.7及以前的永久代到JDK1.8的元數據區

  • 搞定完堆區在JVM記憶體模型中的實現, 下麵談論方法區的實現.
  • 在JDK1.7及以前, JVM使用永久代來實現方法區. 這裡用"實現"二字是經過斟酌的, 因為永久代並不等同於方法區. 從名字也可以看出它和新生代, 老年代是一脈相承的, 邏輯上是一體的, 命名為永久代是因為這部分記憶體很少幾乎不被回收. 這一很少幾乎不被回收的特性正好對應方法區中存儲的類信息, 常量, 靜態變數等元素. 所以說用永久代來實現方法區.
  • 但是用永久代來實現方法區並不是最優解, 比如容易出現記憶體溢出問題(具體分析去除永久代, 改用Metaspace的原因可以參考文章末尾所列出的資料). 在JDK1.8中JVM改為使用元數據區來實現方法區.
  • 元數據區和永久代有著本質的區別, 永久代屬於虛擬機記憶體的一部分, 也就是說當在操作系統中啟動虛擬機進程時為它分配了一塊記憶體, 而虛擬機為永久代分配記憶體時用的是它自己分配得的記憶體.
  • 而元數據區Metaspace是直接在本地記憶體(Native Memory)中申請的, 這樣元數據區的大小(方法區大小)只會受本地記憶體大小限制, 和虛擬機進程所分得記憶體無關.
  • 所以最後JVM記憶體模型圖的終極版應該是這樣子
  • 到此為止, 本篇結束, 希望對你有幫助.

參考資料

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 文章首發: "結構型模式:適配器模式" 七大結構型模式之一:適配器模式。 簡介 姓名 :適配器模式 英文名 :Adapter Pattern 價值觀 :老媒人,牽線搭橋 個人介紹 : Convert the interface of a class into another interface cl ...
  • 背景 隨著容器化、雲原生等的流行,DevOps團隊也在不斷鼓吹「以無狀態為榮,以有狀態為恥」。因為有狀態的服務難以部署、難以擴展。下麵我舉幾個自己工作中實際的例子。 實例1-依賴系統目錄結構 剛轉來基礎架構的時候,接手了一個服務,原來是個應屆生寫的。所以可以理解,也就是基本能完成功能,反正也不是核心 ...
  • 首先我們要下載一個PowerDesigner,自己上百度下載哈!嘻嘻!!! 我這個是漢化版的 然後點這個,再到空白的地方點一下就創建好了。 然後單擊右邊箭頭然後雙擊 不管是製作的圖還是代碼生成的圖都可以一鍵生成代碼文本文件 PowerDesigner的功能還是很強大的,還有很多新花樣等著我們去挖掘! ...
  • 在web.xml文件中配置字元編碼過濾器: ...
  • 工廠模式的學習篇幅比較長,小編第一次看書的時候,就一口氣花了一個多小時,還是通讀。後面又斷斷續續地繼續瞭解了下,力爭做到清晰的認知,給大家一個簡單的學習方式。所以,這次模塊分的可能會比之前的多,涉及到多個工廠模式。好的,我們繼續沖鴨!!! 除了使用new操作符之外,還有更多製造對象的方法。我們將瞭解 ...
  • IOC:控制反轉(Inversion of Control,英文縮寫為 IOC) 簡單來講就是把代碼的控制權從調用方(用戶)轉變成被調用方(服務端) 以前的代碼控制權在調用方,所以要每當程式要更新修改功能時,一定要大量修改調用方的代碼才行,工程量大,維護麻煩。 後來有了IOC,可以將所有的功能模塊交 ...
  • What 本篇應該是穩定性「三十六計」系列的一篇:超時重試。但是「設置預設的超時和重試是一個基礎設施的基本素養」這句話我在我們組內三次開會的時候都說了。表達了我的一個理念。 Why 為什麼一個基礎設施要設置預設的超時和重試?想象下麵一個場景。 TCP協議里有一些基本的概念:MSL、TTL、RTT。 ...
  • 第一次寫文章 很粗略 請多多指教 有什麼疑問或者問題歡迎發郵件給我 [email protected] 鏈接:http://note.youdao.com/noteshare?id=ea9b9d16c549d6ddc827dd2e70c185b1&sub=B5DEED2633F242309ECEA96 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...