1、程序計數器:程序計數器是一塊較小的內存空間,它能夠看作是當前線程所執行的字節碼的行號指示器。html
程序計數器處於線程獨佔區java
若是線程執行的是java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址。若是正在執行的是native方法,這個計數器的值爲undefinedc++
此區域是惟一一個在java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域git
java虛擬機棧: 描述的是java方法執行的動態內存模型github
棧幀: 每一個方法的執行都會建立一個棧幀,伴隨着方法從建立到執行完成。用於存儲局部變量表,操做數棧,動態連接,方法出口等。web
局部變量表: 存放編譯時期可知的各類基本數據類型,引用類型,returnAddress類型 算法
局部表量表的內存空間在編譯時期完成分配,當進入一個方法時,這個方法須要在幀分配多少內存是固定的,在方法運行期間是不會改變局部變量表的大小的。sql
方法區: 存儲虛擬機加載的類信息,常量、靜態變量,即時編譯器編譯後的代碼數據等數組
運行時常量池:在方法區中 ,數據被放置在了一個hashset中,因此會進行去重tomcat
對象的建立過程: new類名-->根據new的參數在常量池中定位一個類的符號的引用-->若是沒有找到這個符號的引用,說明類尚未被加載、解析和初始化 -->
虛擬機爲對象分配內存(位於堆中)-->將分配的內存初始化爲零值(不包括對象頭)-->調用對象的(init)方法
內存分配的方式:一、指針碰撞 二、空閒列表
用什麼方式進行內存分配,取決於垃圾回收的方式
建立對象時線程安全問題的解決: 一、線程同步 二、本地線程分配緩衝
對象的結構:
Header(對象頭)
· 自身運行時數據 (Mark work)
· 哈希值 GC分代年齡 鎖狀態標誌 線程持有的鎖 偏向線程ID 偏向時間戳
· 類型指針
InstanceData : 相同寬度的字段會被分配到一塊兒
Padding: 填充內存的做用,HotSpot自動對象管理系統要求對象的大小必須是八個字節的整數倍,因此對象起始部分若是沒有對齊就須要padding來進行填充
對象的訪問定位:
使用句柄:對象的引用指向堆中的一塊區域,該區域叫句柄池,句柄池中保存了實例對象的地址
直接指針 :直接指向對象的內容
垃圾回收:
一、如何斷定對象爲垃圾對象
· 引用計數法 :在對象中添加一個引用計數器,當有地方引用這個對象的時候,引用計數器的值就+1,當引用失效的時候,這個計數器的值就-1
-verBose:gc 打印垃圾回收機制的信息(簡單信息)
--XX:+printGCDetail :打印詳細的信息
引用計數法存在的問題:當外部不對對象進行引用,但堆中的對象彼此之間互相引用,這樣的對象用引用計數法沒法進行回收
· 可達性分析法 :首先定義一個GCroot,而後從GCroot向下走,可以走到就證實這個對象還能被使用,走不到就證實這個對象能夠被回收了
能夠做爲GCroot的對象: ·虛擬機棧 ·方法區的類屬性所引用的對象 ·方法區中的常量所引用的對象 ·本地方法棧中所引用的對象
二、 如何進行回收
·回收的策略
·標記清除
·複製算法
·標記-整理法
·分代回收算法
·垃圾回收器(不一樣的收集器適用的區域和範圍不一樣,新生代和老年代的垃圾回收器是不能相同的)
·serial: 最基本、發展最悠久 單線程的垃圾收集器
·parnew:多線程的,該收集器的關注點是縮短垃圾收集的時間,提升用戶的體驗度
·parallel: parallel和 parnew都是新生代收集器,都是多線程的收集器。該收集器的目標是達到一個可控制的吞吐量
吞吐量:CPU用於運行用戶代碼的時間與CPU總消耗時間的比值。
吞吐量 = (執行用戶代碼的時間)/(執行用戶代碼的時間+垃圾回收所佔用的時間)
-xx:MaxGCPauseMillis 垃圾收集器停頓的時間
-xx:GCTimeTadio: 吞吐量的大小
·CMS:(適用於老年代)
工做過程: 初始標記 併發標記 從新標記 併發清理
優勢:併發收集 低停頓
缺點:佔用大量的cpu資源 沒法處理浮動垃圾 出現Concurrent Mode Failure 空間碎片
·G1: 可以充分的利用多核CPU的優點,實現並行併發,分代收集,空間整合,可預測的停頓
步驟: 初始標記 併發標記 最終標記 篩選回收
三、什麼時候回收
內存分配策略:
一、優先分配到Eden
二、打對象直接分配到老年代
三、長期存活的對象分配到老年代
四、空間分配擔保
五、動態對象的年齡判斷
當JDK查看主機的運行環境內存大於2G,且爲多核時,默認JDK是服務端(Server VM),當JDK所處的環境做爲一個服務server運行時,默認的垃圾收集器就是parallel,若是做爲客戶端,默認的垃圾收集器就是serial收集器。
指定使用什麼垃圾回收器: -verbose:gc -XX:+printGCDetails -XX:+UseSerialGC -Xms:20M -Xmx:20M -Xmn:10M(指定新生代內存)
大對象直接進入到老年代: -XX:PretenureSizeThreshold (設置大對象進入老年代的閾值)
長期存活的對象將進入老年代:-XX:MaxTenuringThreshold 15
空間分配擔保: -XX:+HandlePromotionFailure ("+"表明開啓空間擔保,「-」表明禁用空間擔保),檢測可否容納新生代對象的標準:老年代剩餘的連續的內存空間是否大於歷次進入老年代對象的平內存大小
逃逸分析與棧上分配:
經過逃逸分析找出沒有發生逃逸的對象,而後就可以對這些對象進行棧上分配:
逃逸分析:分析對象的做用域,若是一個對象只在方法體內部有效,沒有被外部引用,就認爲這個對象沒有發生逃逸,就能夠分配到棧上去
注:在使用對象的時候能將對象放到方法體中,就不要對他進行外部的引用,能夠提升程序的效率,由於方法體中的對象會被建立到棧中,當方法執行完後棧內存就會被自動的釋放
package javaJVM; public class Test02 { public Test02 obj; /** * 方法返回StackAllocation對象,發生逃逸 * @return */ public Test02 getInstance() { return obj == null?new Test02():obj; } /** * 爲成員屬性賦值,發生逃逸 * */ public void setObj() { this.obj = new Test02(); } /** * 對象的做用域只在當前方法中有效,內有發生逃逸 * */ public void useStackAllocation() { Test02 t = new Test02(); } /** * 引用成員變量的值,發生逃逸 * */ public void useStackAllocation2() { Test02 t1 = getInstance(); } }
虛擬機工具的使用:
jps: 可以查看本地虛擬機的惟一ID,lvmid local virtual machine id
在window的cmd窗口,輸入jps -m 查看運行時傳入主類的參數
在window的cmd窗口,輸入jps -v 查看虛擬機的具體運行參數,以及虛擬機的配置信息
Jstat: 類裝載,內存,垃圾收集,jit編譯的信息,監視虛擬機運行的各類狀態信息,jstat是依賴於jps的,必須先經過jps得到具體進程的進程號,才能查看
使用jstat的命令: jstat -gcutil 線程號 毫秒值 次數
官方文檔地址: http://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
注: 元空間的本質和永久代相似,都是對jvm規範中方法區的實現,不過元空間和永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制
jinfo: 實時查看和調整虛擬機的各項參數
jinfo -flag UseSerialGC 線程號 (查看使用了什麼類型的垃圾回收器 「+」表明使用 「—」表明沒有使用)
-XX:[+/-]option
-XX:option=value
jmap:
jmap -dump:format=b,file=c:\a.bin 線程號
jhat: 能夠用來分析jmap生成的文件,可是這個過程當中對cpu的佔有率很是的高
提供一個內置的http服務器用來分析數據,能夠根據提供的端口號直接訪問web頁面,查看具體的類生成信息
在web界面的最下面,show heap histogram能夠展現每一個java實例佔用內存的大小及生成的數量,能夠用來進行程序分許
Execute Object Query Language (OQL) query :能夠用查詢sql的形式來進行查詢對象
select s from java.lang.String s where a.value.length > 1000 --輸入的sql以下
jstack:用於生成虛擬機當前時刻的線程快照,線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的目的就是分析線程長時間出現停頓的緣由
在定時任務處理問題時,當程序正在運行時,是沒法定位到線程出現卡頓的具體緣由的,可是經過jstack這個工具能夠查看到當前線程的一些信息,對於死鎖、死循環等問題的查看。
能夠分析出某個線程是出於中止狀態仍是正在等待狀態。
在啓動線程的時候給線程起一個名稱,這樣就能用虛擬機監控工具監控線程的具體執行狀況
jConsole: 命令: JConsole
jConsole:內存監控
jConsole:線程監控
線程監控源碼:
package ThreadMonitor; import java.util.Scanner; public class Test1 { public static void main(String[] args) { Scanner sc = new Scanner(System.in); sc.next(); new Thread(new Runnable() { @Override public void run() { while (true) { } } },"while true").start(); sc.next(); testwait(new Object()); System.out.println("Thread starting ...."); } private static void testwait(Object obj) { new Thread(new Runnable() { @Override public void run() { synchronized (obj) { try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } },"wait").start(); } }
jConsole:死鎖監控
死鎖測試代碼:
死鎖類:
package ThreadMonitor; import java.security.KeyStore.PrivateKeyEntry; public class DeadLock implements Runnable { private Object obj1; private Object obj2; public DeadLock(Object obj1,Object obj2) { this.obj1 = obj1; this.obj2 = obj2; } @Override public void run() { synchronized (obj1) { try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized (obj2) { System.out.println("hello ....."); } } } }
測試類:
package ThreadMonitor; public class Main { public static void main(String[] args) { Object obj1 = new Object(); Object obj2 = new Object(); new Thread(new DeadLock(obj1, obj2)).start(); new Thread(new DeadLock(obj2, obj1)).start(); } }
虛擬機的可視化工具: VisualVM
插件地址: http://visualvm.github.io/pluginscenters.html
首頁地址:http://visualvm.github.io/index.html
能夠用來監控eclipse和jvm的運行具體信息
安裝以後還須要下載安裝相應的插件,在工具tools中就能夠進行下載安裝
虛擬機的性能調優:
一、知識 二、 工具 三、數據 四、經驗
虛擬機性能調優案例:
案例1: 場景: 績效考覈系統,會針對每一個考覈員工,生成一個各考覈點的考覈結果,造成Excel文檔,供用戶下載。文檔中包含用戶提交的考覈點信息以及分析結果。Excel文檔用於在用戶請求的時候生成,下載,並保存到內存服務器一份
環境: 2核 64G內存 CentOS Tomcat7.0 JDK1.7
問題: 常常有客戶反映長時間出現卡頓的現象
處理思路:
·優化sql
·監控內存 :常常會發生FULL GC 並且每次發生full GC 都會經歷很長的時間 20-30s
`問題出現緣由分析:因爲將大量的數據分裝到一個對象中,生成了一個大對象,而大對象會直接的進入老年代,而後垃圾回收的時候觸發Full GC,但因爲堆內存設置的過大,致使老年代垃圾回收的時間漫長,系統產生卡頓。
·解決方案:減少堆內存大小的設置,可是這樣會浪費物理空間。
最終解決方案: 部署多個web容器,每一個web容器的堆內存指定爲4G,搭建tomcat集羣
總結經驗:正常狀況使用單個容器的效率要比使用多個容器的效率高,由於若是部署多個容器,每一個容器部署的時候還要消耗相應的內存空間,從而使總體能夠用來處理用戶請求的內存空間減小,見底了系統的效率。可是對於上述狀況,明顯使用多個容器比使用單一容器更加高效。因此具體的狀況,還要按照具體代碼邏輯的設計去分析解決。
案例2: 場景:簡單的數據抓取系統,抓取網站上的一些數據,分發到其餘的應用。
環境:。。。。
出現問題:不按期出現內存溢出,把堆內存加大也無濟於事。導出堆轉儲快照信息,沒有任何信息。內存監控正常。
緣由分析:系統內存過小,致使觸發堆外內存的時候,將系統內存撐爆,出現了內存溢出的現象
處理思路:擴大系統內存
CLASS文件的結構: class文件是一組以8位字節爲基礎單位的二進制字節流,各個數據項目嚴格按照順序緊湊的排列在class文件中,中間沒有添加任何分割符,整個class文件中存儲的內容幾乎所有是程序運行的必要數據,沒有空隙存在。
當遇到8位字節以上的空間的數據項時,則會按照高位在前的方式分割成若干個8位字節進行存儲。
class文件中有兩種數據類型,分別是無符號數和表。
class文件結構: ·魔數 ·class文件版本 ·常量池 ·訪問標誌 ·類索引,父類索引,接口索引集合 ·字段表集合 ·方法表集合 ·屬性表集合
在cmd窗口的查詢命令:
javap -verbose class文件名稱 : 查看具體的編譯信息
魔數後面四位數字表示jdk的版本號:jdk 1.8 =52 jdk1.7=51 jdk 1.6=50 jdk1.5 = 49 jdk 1.4 = 48
字段表:用於描述接口或者類中申明的變量
方法表:用於描述方法的概要信息,可是具體的方法體信息是存儲在屬性表中的
屬性表:用於描述方法的屬性信心。
字節碼指令:java虛擬機的指令由一個字節長度的,表明着某種特定操做含義的數字,稱之爲操做碼,以及跟隨其後的零至多個表明此操做所需參數的操做數而構成
操做碼的長度爲1個字節,所以最大隻有256條
是基於棧的指令集架構
字節碼指令和數據類型: 在虛擬機的指令集中,大多數的指令都包含了其操做所對應的數據類型信息。
大多數指令是包含類型信息的,也有不包含類型信息的:goto與類型無關,arraylength操做數組類型
類型多,指令少
加載與存儲指令: 加載和存儲指令用於將數據在棧幀中的局部變量和操做數之間來回傳輸
將局部變量表加載到操做數棧:iload lload fload dload aload
將一個數值從操做數棧存儲到局部表量表: istore ifda
將一個常量加載到操做數棧: bipush sipush ldc ldc_w ldc2_w aconst_null iconst_m1 iconst
擴充局部變量表的訪問索引指令:wide
運算指令: 運算或者算數指令用於對兩個操做數棧上的值進行某種特定的運算,並把結果存儲到操做數棧頂
加法指令:add 減法指令:sub 乘法指令:mul 除法指令:div 取餘指令:rem 取反指令:neg
類型轉換指令
類型轉換指令能夠將兩種不一樣的數值類型進行相互轉換,這些轉換操做通常用於實現用戶代碼中的顯示類型轉換操做以及用來處理字節碼指令集中數據類型相關指令沒法與數據類型一一對應的問題。
寬化類型處理和窄化類型處理
對象建立與訪問指令: 建立類實例的指令:new 建立數組的指令:newarray anewarray multianewarray
訪問字段: getfield putfield getstatic putstatic
把數組元素加載到操做數棧的指令:baload csllfda
將操做數棧的值存儲到數組元素:astore
取數組長度的指令:arraylength
檢查實例類型的指令:instanceof checkcast
操做數棧管理指令:
操做數棧指令用於直接操做操做數棧。
將操做數棧的一個或兩個元素出棧: pop pop2
複製棧頂一個或兩個數值並將複製或雙份複製值從新壓入棧頂:dup dup2 dup_x1 dup_x2
將棧頂的兩個數值替換:swap
控制轉移指令:控制轉移指令可讓java虛擬機有條件或無條件的從指定的位置指令而不是控制轉移指令的下一條指令繼續執行程序。能夠認爲控制轉移指令就是在修改pc寄存器的值。
條件分支:ifeq iflt ifle ifne ifgt ifnull ifcmple
複合條件分支:tablewitch lookupswitch
無條件分支:goto goto_w jsr jsr_w ret
方法調用指令: invokevirtual指令用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是java語言中最多見的方法分派方式。
invokeinterface指令用於調用接口方法,它會在運行時搜索一個實現了這個接口方法的對象,找出合適的方法進行調用
invokespecial指令用於調用一些須要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法
invokestatic指令用於調用類方法(static方法)
方法的返回指令:方法的調用指令和數據類型無關,而方法的返回指令則根據返回值的類型進行區分的,包括有ireturn(當返回值是boolean、byte、char、short和int類型時使用)、ireturn、freturn、dreturn、和areturn,另外還有一條return指令共申明void方法、實例初始化方法、類和接口的類初始化方法使用。
同步指令:java虛擬機能夠支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管城(monitor)來支持的。
虛擬機的類加載機制:虛擬機把描述類的數據從class文件加載到內存,並對數據進行校驗,解析和初始化,最終造成能夠被虛擬機直接使用的java類型,這就是虛擬機的類加載機制。
加載類的策略: 懶加載 及時加載
類加載的時機: 加載-->鏈接-->初始化-->使用-->卸載
初始化:1、遇到new、getstatic、putstatic、或invokestatic這4條字節碼指令的時候,若是類沒有進行過初始化,則須要先觸發其初始化。生成這四條指令最多見的java代碼場景是:使用new關鍵字實例化對象的時候、讀取或者設置一個類的靜態字段的(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
2、使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先出發其初始化。
3、當初始化一個類的時候,若是發現其父類尚未進行初始化,則須要先觸發其父類的初始化。
4、當虛擬機啓動的時候,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
不被初始化的例子:
經過子類引用父類的靜態字段 ,子類不會被初始化
經過數組定義來引用類
調用類的常量
類加載的過程: 加載-->驗證-->準備-->解析-->初始化
加載: 經過一個類的全限定名來獲取定義此類的二進制流
將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
在內存中生成一個表明這個類的class對象,做爲這個類的各類數據的訪問入口
驗證: 驗證是鏈接的第一步,這一階段的目的是爲了確保class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
文件的格式驗證 、 元數據驗證 、 字節碼驗證、 符號引用驗證
準備: 準備階段正式爲類變量分配內存並設置變量的初始值。這些變量使用的內存都將在方法區中進行分配。
解析階段是虛擬機將常量池中的符號引用替換爲直接引用的過程。
類或者接口的解析 ·字段解析 ·類方法解析 ·接口方法解析
注: <clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊中只能訪問定義在靜態語句塊以前的的變量,定義在他以後的變量,在前面的語句塊中能夠被賦值,可是不能被訪問。
若是多個線程同時初始化一個類,只有一個線程會執行這個類的<clinit>()方法,其餘線程等待執行完畢。若是方法的執行時間過長,那麼就會在成多個線程的阻塞。
類加載器: 虛擬機設計團隊把類加載階段中的「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做放到java虛擬機外部去實現,以便讓應用程序本身去決定如何去獲取所須要的類。實現這個動做的代碼塊稱做爲類加載器。
只有被同一個類加載器加載的類纔可能會相等,相同的字節碼被不一樣的類加載器加載的類不相等。
package ClassLoader; import java.io.IOException; import java.io.InputStream; public class ClassLoaderDemo { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { ClassLoader mycl = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { String fileName = name.substring(name.lastIndexOf(".")+1)+".class"; InputStream ins = getClass().getResourceAsStream(fileName); if(ins ==null) { return super.loadClass(name); } try { byte[] buff = new byte[ins.available()]; ins.read(buff); return defineClass(name, buff, 0,buff.length); } catch (Exception e) { throw new ClassNotFoundException(); } } }; Object c = mycl.loadClass("ClassLoader.ClassLoaderDemo").newInstance(); System.out.println(c.getClass()); System.out.println(c instanceof ClassLoaderDemo); } }
類加載器的分類:
啓動類加載器 : 由c++實現,是虛擬機的一部分,用於加載javahome下的lib目錄下的類
擴展類加載器: 加載javahome下/lib/ext目錄中的類
應用程序類加載器: 加載用戶類路徑上的所制定的類庫
自定義類加載器
多種類加載器之間協同工做的模式:雙親委派模式
從jdk1.2開始,java虛擬機規範推薦開發者使用雙親委派模式(parentsDelagation Modle)進行類加載,其加載過程以下:
(1)若是一個類加載器收到了類加載請求,它首先不會本身去嘗試加載這個類,而是把類加載請求委派給父類加載器去完成
(2)每一層的類加載器都把類加載請求委派給父類加載器,直到全部的類加載請求都應該傳遞給頂層的啓動類加載器
(3)若是頂層的啓動類加載器沒法完成加載請求,子類加載器嘗試去加載,若是連最初發起請求類加載請求的類加載器也沒法完成加載請求的時候,將會拋出classNotFountException,而再也不調用其子類加載器去進行類加載
雙親委派模式的類加載機制的優勢是java類它的類加載器一塊兒具有了一種帶優先級的層次關係,越是接觸的類,越是被上層的類加載器加載,保證了程序運行的穩定性。
局部變量在使用過程當中必須先給他賦一個初始值,不然是不能直接去使用的。
slot: 當一個變量的pc寄存器的值大於slot的做用域的時候,slot是能夠進行復用的
方法的調用---靜態分派:
靜態分派: 在編譯階段就能夠進行肯定對方法的調用
package ClassLoader; import org.omg.Messaging.SyncScopeHelper; public class Test1 { static class Parent {}; static class Child1 extends Parent { } static class Child2 extends Parent{} public void sayhello(Child1 c) { System.out.println("c1 is call"); } public void sayhello(Child2 c) { System.out.println("c2 is call"); } public void sayhello(Parent p) { System.out.println("p is call"); } public static void main(String[] args) { Parent p1 = new Child1();//p1稱爲變量的靜態類型,Child1稱爲變量的實際類型 Parent p2 = new Child2(); //方法的調用經過靜態類型進行選擇的過程稱爲靜態調用 Test1 t = new Test1(); t.sayhello(p1); t.sayhello(p2); //實際類型發生變化 Parent p = new Child1(); p = new Child2(); //靜態類型發生變化 t.sayhello((Child1)p1); } }
在方法的執行過程當中會選擇一個最早匹配的方法進行方法的調用
package ClassLoader; public class Test2 { /*public void sayhello(int a) { System.out.println("int"); }*/ /*public void sayhello(double a) { System.out.println("double"); }*/ /*public void sayhello(char a) { System.out.println("char"); }*/ /*public void sayhello(char ... a) { System.out.println("char ..."); } */ /*public void sayhello(long a) { System.out.println("long"); }*/ public void sayhello(Object a) { System.out.println("object"); } public void sayhello(boolean a) { System.out.println("boolecn"); } public static void main(String[] args) { //在方法調用中會選擇一個最早匹配的進行調用 new Test2().sayhello('a'); } }
動態分派調用: 找到操做數棧頂的第一個元素所指向的對象的實際類型
若是在實際類型中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,若是經過則直接返回這個方法的直接引用,查找過程結束,若是不經過,拋出異常。
按照繼承關係從上往下依次對實際類型的個父類進行搜索與驗證
若是始終沒有找到,則拋出AbstractMethordError
注: 方法的靜態調用主要是針對方法的重載,動態調用主要是針對方法的重寫 。
動態類型語言支持:
靜態類型的語言在非運行階段,變量的類型是能夠肯定的。也就是說變量是由類型的
動態類型的語言在非運行階段,變量的類型是沒法進行肯定的,也就是說變量是沒有類型的,可是值是由類型的,也就是運行期間能夠肯定變量的值得類型