于封裝的原則,API 的設計者會將部分成員(屬性、字段、方法等)隱藏以保證健壯性。但總有需要直接訪問這些私有成員的情況。
為了訪問一個類型的私有成員,除了更改 API 還有就是使用反射技術:
public class MyApi
{
public MyApi()
{
_createdAt=DateTime.Now;
}
private DateTime _createdAt;
public int ShowTimes { get; private set; }
public void ShowCreateTime()
{
Console.WriteLine(_createdAt);
ShowTimes++;
}
}
void Main()
{
var api=new MyApi();
var field=api.GetType().GetField("_createdAt", BindingFlags.NonPublic | BindingFlags.Instance);
var value=field.GetValue(api);
Console.WriteLine(value);
}
這種寫法并不優雅:
筆者基于“動態類型技術”探索出了一種相對來說比較優雅的方案用于美化上述代碼,并為其命名為 ReflectionDynamicObject :
void Main()
{
var api=new MyApi();
dynamic wrapper=ReflectionDynamicObject.Wrap(api);
Console.WriteLine(wrapper._createdAt);
}
除了支持獲取值,ReflectionDynamicObject 還支持賦值:
void Main()
{
var api=new MyApi();
dynamic wrapper=ReflectionDynamicObject.Wrap(api);
wrapper._createdAt=new DateTime(2022, 2, 2, 22, 22, 22);
api.ShowCreateTime();
}
除了字段,當然也支持對屬性的操作:
void Main()
{
var api=new MyApi();
dynamic wrapper=ReflectionDynamicObject.Wrap(api);
wrapper.ShowTimes=100;
Console.WriteLine(wraper.ShowTimes);
}
在對屬性的支持上,ReflectionDynamicObject 使用了“快速反射”技術,將取值和復制操作生成了委托以優化性能。
ReflectionDynamicObject 派生自 DynamicObject ,其內部通過反射技術獲取到所有的屬性和字段并對其 getter 和 setter 方法進行存儲并通過 TryGetMember 和 TrySetMember 方法經運行時調用。
public sealed class ReflectionDynamicObject : DynamicObject
{
private readonly object _instance;
private readonly Accessor _accessor;
private ReflectionDynamicObject(object instance)
{
_instance=instance ?? throw new ArgumentNullException(nameof(instance));
_accessor=GetAccessor(instance.GetType());
}
public static ReflectionDynamicObject Wrap(Object value)
{
if (value==null) throw new ArgumentNullException(nameof(value));
return new ReflectionDynamicObject(value);
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (_accessor.TryFindGetter(binder.Name, out var getter))
{
result=getter.Get(_instance);
return true;
}
return base.TryGetMember(binder, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
if (_accessor.TryFindSetter(binder.Name, out var setter))
{
setter.Set(_instance, value);
return true;
}
return base.TrySetMember(binder, value);
}
#region 快速反射
private interface IGetter
{
object Get(object instance);
}
private interface ISetter
{
void Set(object instance, object value);
}
private class Getter : IGetter
{
private FieldInfo _field;
public Getter(FieldInfo field)
{
_field=field ?? throw new ArgumentNullException(nameof(field));
}
public object Get(object instance)
{
return _field.GetValue(instance);
}
}
private class Setter : ISetter
{
private FieldInfo _field;
public Setter(FieldInfo field)
{
_field=field ?? throw new ArgumentNullException(nameof(field));
}
public void Set(object instance, object value)
{
_field.SetValue(instance, value);
}
}
private class Getter<T1, T2> : IGetter
{
private readonly Func<T1, T2> _getter;
public Getter(Func<T1, T2> getter)
{
_getter=getter ?? throw new ArgumentNullException(nameof(getter));
}
public object Get(object instance)
{
return _getter((T1)instance);
}
}
private class Setter<T1, T2> : ISetter
{
private readonly Action<T1, T2> _setter;
public Setter(Action<T1, T2> setter)
{
this._setter=setter ?? throw new ArgumentNullException(nameof(setter));
}
public void Set(object instance, object value)
{
this._setter.Invoke((T1)instance, (T2)value);
}
}
private class Accessor
{
public Accessor(Type type)
{
this._type=type ?? throw new ArgumentNullException(nameof(_type));
var getter=new SortedDictionary<string, IGetter>();
var setter=new SortedDictionary<string, ISetter>();
var fields=_type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
foreach (var field in fields)
{
getter[field.Name]=new Getter(field);
setter[field.Name]=new Setter(field);
}
var props=_type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
foreach (var item in props)
{
if (item.CanRead)
{
var method=item.GetMethod;
var funcType=typeof(Func<,>).MakeGenericType(item.DeclaringType, item.PropertyType);
var func=method.CreateDelegate(funcType);
var getterType=typeof(Getter<,>).MakeGenericType(item.DeclaringType, item.PropertyType);
var get=(IGetter)Activator.CreateInstance(getterType, func);
getter[item.Name]=get;
}
if (item.CanWrite)
{
var method=item.SetMethod;
var actType=typeof(Action<,>).MakeGenericType(item.DeclaringType, item.PropertyType);
var act=method.CreateDelegate(actType);
var setterType=typeof(Setter<,>).MakeGenericType(item.DeclaringType, item.PropertyType);
var set=(ISetter)Activator.CreateInstance(setterType, act);
setter[item.Name]=set;
}
}
_getters=getter;
_setters=setter;
}
private readonly Type _type;
private readonly IReadOnlyDictionary<string, IGetter> _getters;
private readonly IReadOnlyDictionary<string, ISetter> _setters;
public bool TryFindGetter(string name, out IGetter getter)=> _getters.TryGetValue(name, out getter);
public bool TryFindSetter(string name, out ISetter setter)=> _setters.TryGetValue(name, out setter);
}
private static Dictionary<Type, Accessor> _accessors=new Dictionary<Type, Accessor>();
private static object _accessorsLock=new object();
private static Accessor GetAccessor(Type type)
{
if (_accessors.TryGetValue(type, out var accessor)) return accessor;
lock (_accessorsLock)
{
if (_accessors.TryGetValue(type, out accessor)) return accessor;
accessor=new Accessor(type);
var temp=new Dictionary<Type, Accessor>(_accessors);
temp[type]=new Accessor(type);
_accessors=temp;
return accessor;
}
}
#endregion
}
基于復雜度的考慮,ReflectionDynamicObject 并未添加對“方法”的支持。這也就意味著對方法的調用是缺失的。雖然動態行為讓程序擺脫了對字符串的依賴,但是該實現對“重構”的支持仍然不友好。
Liquid 主題引擎 是筆者根據 Liquid 語言和 Shopify 主題機制并采用 Fluid 模板引擎實現的一套 HTML 主題引擎。該引擎允許最終用戶自由地修改自己的主題模板而不會對宿主造成影響。最終目標是做到多語言、多主題、高擴展性以及所見即所得。
在編寫 Liquid 主題引擎 時,筆者需要重寫 Fluid 模板引擎的 render 標簽讓子視圖從 snippets 文件夾加載。在實現該標簽時,需要訪問 TemplateContext 的 LocalScope 和 RootScope 字段,不幸的是上述字段被標記為了 internal ,無法在外部程序集中訪問到。于是便有了 ReflectionDynamicObject ,幫助筆者完成對 LocalScope 和 RootScope 的訪問。
今日內容
Tomcat8的優化
看懂Java底層字節碼
編碼的優化建議
tomcat服務器在JavaEE項目中使用率非常高,所以在生產環境對tomcat的優化也變得非 常重要了。
對于tomcat的優化,主要是從2個方面入手,一是,tomcat自身的配置,另一個是
tomcat所運行的jvm虛擬機的調優。
下面我們將從這2個方面進行講解。
1.1、Tomcat配置優化1.1.1、部署安裝tomcat8 下載并安裝:
https://tomcat.apache.org/download-80.cgi
cd /tmp
wget http://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat‐ 8/v8.5.34/bin/apache‐tomcat‐8.5.34.tar.gz
tar ‐xvf apache‐tomcat‐8.5.34.tar.gz cd apache‐tomcat‐8.5.34/conf
#修改配置文件,配置tomcat的管理用戶
vim tomcat‐users.xml #寫入如下內容:
#保存退出
#如果是tomcat7,配置了tomcat用戶就可以登錄系統了,但是tomcat8中不行,還需要修改另一個配置文件,否則訪問不了,提示403
vim webapps/manager/META‐INF/context.xml
#將<Valve的內容注釋掉
<!‐‐ ‐‐>
#保存退出即可
#啟動tomcat
cd /tmp/apache‐tomcat‐8.5.34/bin/
./startup.sh && tail ‐f …/logs/catalina.out
#打開瀏覽器進行測試訪問http://192.168.40.133:8080/
點擊“Server Status”,輸入用戶名、密碼進行登錄,tomcat/tomcat
進入之后即可看到服務的信息。
1.1.2、禁用AJP連接
在服務狀態頁面中可以看到,默認狀態下會啟用AJP服務,并且占用8009端口。
什么是AJP呢?
AJP(Apache JServer Protocol)
AJPv13協議是面向包的。WEB服務器和Servlet容器通過TCP連接來交互;為了節省SOCKET創建的昂貴代價,WEB服務器會嘗試維護一個永久TCP連接到servlet容器,并且在多個請求和響應周期過程會重用連接。
我們一般是使用Nginx+tomcat的架構,所以用不著AJP協議,所以把AJP連接器禁用。修改conf下的server.xml文件,將AJP服務禁用掉即可。
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
重啟tomcat,查看效果。
可以看到AJP服務以及不存在了。
1.1.3、執行器(線程池)
在tomcat中每一個用戶請求都是一個線程,所以可以使用線程池提高性能。
修改server.xml文件:
<!‐‐將注釋打開‐‐> <Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐" maxThreads="500" minSpareThreads="50" prestartminSpareThreads="true" maxQueueSize="100"/> <!‐‐ 參數說明: maxThreads:最大并發數,默認設置 200,一般建議在 500 ~ 1000,根據硬件設施和業務來判斷 minSpareThreads:Tomcat 初 始 化 時 創 建 的 線 程 數 , 默 認 設 置 25 prestartminSpareThreads: 在 Tomcat 初始化的時候就初始化 minSpareThreads 的參數值,如果不等于 true,minSpareThreads 的值就沒啥效果了 maxQueueSize,最大的等待隊列數,超過則拒絕請求 ‐‐> <!‐‐在Connector中設置executor屬性指向上面的執行器‐‐> <Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
保存退出,重啟tomcat,查看效果。
在頁面中顯示最大線程數為-1,這個是正常的,僅僅是顯示的問題,實際使用的指定的值。
也有人遇到這樣的問
題:https://blog.csdn.net/weixin_38278878/article/details/80144397
1.1.4、3種運行模式
tomcat的運行模式有3種:
1.bio
默認的模式,性能非常低下,沒有經過任何優化處理和支持.
2.nio
nio(new I/O),是Java SE 1.4及后續版本提供的一種新的I/O操作方式(即java.nio包及其子包)。Java nio是一個基于緩沖區、并能提供非阻塞I/O操作的Java API,因此nio 也被看成是non-blocking I/O的縮寫。它擁有比傳統I/O操作(bio)更好的并發運行性能。
3.apr
安裝起來最困難,但是從操作系統級別來解決異步的IO問題,大幅度的提高性能.
推薦使用nio,不過,在tomcat8中有最新的nio2,速度更快,建議使用nio2. 設置nio2:
<Connector executor="tomcatThreadPool" port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" connectionTimeout="20000" redirectPort="8443" />
可以看到已經設置為nio2了。
1.2、部署測試用的java web項目
為了方便測試性能,我們將部署一個java web項目,這個項目本身和本套課程沒有什么關系,僅僅用于測試。
注意:這里在測試時,我們使用一個新的tomcat,進行測試,后面再對其進行優化 調整,再測試。
1.2.1、創建dashboard數據庫
在資料中找到sql腳本文件dashboard.sql,在linux服務器上執行。
cat dashboard.sql | mysql ‐uroot ‐proot
創建完成后,可以看到有3張表。
1.2.2、部署web應用
在資料中找到itcat-dashboard-web.war,上傳到linux服務器,進行部署安裝。
cd /tmp/apache‐tomcat‐8.5.34/webapps rm ‐rf * mkdir ROOT cd ROOT/ rz上傳war包 jar ‐xvf itcat‐dashboard‐web.war rm ‐rf itcat‐dashboard‐web.war #修改數據庫配置文件 cd /tmp/apache‐tomcat‐8.5.34/webapps/ROOT/WEB‐INF/classes vim jdbc.properties #這里根據自己的實際情況進行配置 jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://node01:3306/dashboard? useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueri es=true jdbc.username=root jdbc.password=root
重新啟動tomcat。
訪問首頁,查看是否已經啟動成功:http://192.168.40.133:8080/index
1.3、使用Apache JMeter進行測試
Apache Jmeter是開源的壓力測試工具,我們借助于此工具進行測試,將測試出tomcat
的吞吐量等信息。
1.3.1、下載安裝
下載地址:http://jmeter.apache.org/download_jmeter.cgi
安裝:直接將下載好的zip壓縮包進行解壓即可。
進入bin目錄,找到jmeter.bat文件,雙機打開即可啟動。
1.3.2、修改主題和語言
默認的主題是黑色風格的主題并且語言是英語,這樣不太方便使用,所以需要修改下主題和中文語言。
主題修改完成。
接下來設置語言為簡體中文。
1.3.3、創建首頁的測試用例
第一步:保存測試用例
第二步:添加線程組,使用線程模擬用戶的并發
1000個線程,每個線程循環10次,也就是tomcat會接收到10000個請求。
第三步:添加http請求
第四步:添加請求監控
1.3.4、啟動、進行測試
1.3.5、聚合報告
在聚合報告中,重點看吞吐量。
1.4、調整tomcat參數進行優化
通過上面測試可以看出,tomcat在不做任何調整時,吞吐量為73次/秒。
1.4.1、禁用AJP服務
可以看到,禁用AJP服務后,吞吐量會有所提升。
當然了,測試不一定準確,需要多測試幾次才能看出是否有提升。
1.4.2、設置線程池
通過設置線程池,調整線程池相關的參數進行測試tomcat的性能。
1.4.2.1、最大線程數為500,初始為50
<Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐" maxThreads="500" minSpareThreads="50" prestartminSpareThreads="true"/>
測試結果:
吞吐量為128次/秒,性能有所提升。
1.4.2.2、最大線程數為1000,初始為200
<Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐" maxThreads="1000" minSpareThreads="200" prestartminSpareThreads="true"/>
吞吐量為151,性能有所提升。
1.4.2.3、最大線程數為5000,初始為1000
是否是線程數最多,速度越快呢? 我們來測試下。
<Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐" maxThreads="5000" minSpareThreads="1000" prestartminSpareThreads="true"/>
可以看到,雖然最大線程已經設置到5000,但是實際測試效果并不理想,并且平均的響 應時間也邊長了,所以單純靠提升線程數量是不能一直得到性能提升的。
1.4.2.4、設置最大等待隊列數
默認情況下,請求發送到tomcat,如果tomcat正忙,那么該請求會一直等待。這樣雖然 可以保證每個請求都能請求到,但是請求時間就會邊長。
有些時候,我們也不一定要求請求一定等待,可以設置最大等待隊列大小,如果超過就不等待了。這樣雖然有些請求是失敗的,但是請求時間會雖短。典型的應用:12306。
<!‐‐最大等待數為100‐‐> <Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐" maxThreads="500" minSpareThreads="100" prestartminSpareThreads="true" maxQueueSize="100"/>
測試結果:
平均響應時間:3.1秒響應時間明顯縮短
錯誤率:49.88%
錯誤率提升到一半,也可以理解,最大線程為500,測試的并發為1000 吞吐量:238次/秒
吞吐量明顯提升
結論:響應時間、吞吐量這2個指標需要找到平衡才能達到更好的性能。
1.4.3、設置nio2的運行模式
將最大線程設置為500進行測試:
<Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐" maxThreads="500" minSpareThreads="50" prestartminSpareThreads="true"/> <!‐‐ 設置nio2 ‐‐> <Connector executor="tomcatThreadPool" port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" connectionTimeout="20000" redirectPort="8443" />
可以看到,平均響應時間有縮短,吞吐量有提升,可以得出結論:nio2的性能要高于
nio。
1.5、調整JVM參數進行優化
接下來,測試通過jvm參數進行優化,為了測試一致性,依然將最大線程數設置為500, 啟用nio2運行模式。
1.5.1、設置并行垃圾回收器
#年輕代、老年代均使用并行收集器,初始堆內存64M,最大堆內存512M JAVA_OPTS="‐XX:+UseParallelGC ‐XX:+UseParallelOldGC ‐Xms64m ‐Xmx512m ‐ XX:+PrintGCDetails ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCDateStamps ‐ XX:+PrintHeapAtGC ‐Xloggc:../logs/gc.log"
測試結果與默認的JVM參數結果接近。(執行了2次測試,結果是第二次測試的結果)
1.5.2、查看gc日志文件
將gc.log文件上傳到gceasy.io查看gc中是否存在問題。
在報告中顯示,在5次GC時,系統所消耗的時間大于用戶時間,這反應出的服務器的性能存在瓶頸,調度CPU等資源所消耗的時間要長一些。
問題二:
可以關鍵指標中可以看出,吞吐量表現不錯,但是gc時,線程的暫停時間稍有點長。
問題三:
通過GC的統計可以看出:
年輕代的gc有74次,次數稍有多,說明年輕代設置的大小不合適需要調整
FullGC有8次,說明堆內存的大小不合適,需要調整
問題四:
從GC原因的可以看出,年輕代大小設置不合理,導致了多次GC。
1.5.3、調整年輕代大小
JAVA_OPTS="‐XX:+UseParallelGC ‐XX:+UseParallelOldGC ‐Xms128m ‐Xmx1024m ‐ XX:NewSize=64m ‐XX:MaxNewSize=256m ‐XX:+PrintGCDetails ‐ XX:+PrintGCTimeStamps ‐XX:+PrintGCDateStamps ‐XX:+PrintHeapAtGC ‐ Xloggc:../logs/gc.log"
將初始堆大小設置為128m,最大為1024m
初始年輕代大小64m,年輕代最大256m
從測試結果來看,吞吐量以及響應時間均有提升。
查看gc日志:
可以看到GC次數要明顯減少,說明調整是有效的。
1.5.4、設置G1垃圾回收器
#設置了最大停頓時間100毫秒,初始堆內存128m,最大堆內存1024m JAVA_OPTS="‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=100 ‐Xms128m ‐Xmx1024m ‐ XX:+PrintGCDetails ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCDateStamps ‐ XX:+PrintHeapAtGC ‐Xloggc:../logs/gc.log"
測試結果:
可以看到,吞吐量有所提升,評價響應時間也有所縮短。
1.5.5、小結
通過上述的測試,可以總結出,對tomcat性能優化就是需要不斷的進行調整參數,然后 測試結果,可能會調優也可能會調差,這時就需要借助于gc的可視化工具來看gc的情 況。再幫我我們做出決策應該調整哪些參數。
前面我們通過tomcat本身的參數以及jvm的參數對tomcat做了優化,其實要想將應用程 序跑的更快、效率更高,除了對tomcat容器以及jvm優化外,應用程序代碼本身如果寫 的效率不高的,那么也是不行的,所以,對于程序本身的優化也就很重要了。
對于程序本身的優化,可以借鑒很多前輩們的經驗,但是有些時候,在從源碼角度方面 分析的話,不好鑒別出哪個效率高,如對字符串拼接的操作,是直接“+”號拼接效率高還 是使用StringBuilder效率高?
這個時候,就需要通過查看編譯好的class文件中字節碼,就可以找到答案。
我們都知道,java編寫應用,需要先通過javac命令編譯成class文件,再通過jvm執行,
jvm執行時是需要將class文件中的字節碼載入到jvm進行運行的。
2.1、通過javap命令查看class文件的字節碼內容
首先,看一個簡單的Test1類的代碼:
package cn.itcast.jvm; public class Test1 { public static void main(String[] args) { int a=2; int b=5; int c=b ‐ a; System.out.println(c); } }
通過javap命令查看class文件中的字節碼內容:
javap ‐v Test1.class > Test1.txt javap用法: javap <options> <classes> 其中, 可能的選項包括: ‐help ‐‐help ‐? ‐version ‐v ‐verbose ‐l ‐public ‐protected ‐package ‐p ‐private ‐c ‐s ‐sysinfo ‐constants ‐classpath <path> ‐cp <path> ‐bootclasspath <path>
查看Test1.txt文件,內容如下:
Classfile /F:/code/itcast‐jvm/itcast‐jvm‐ test/target/classes/cn/itcast/jvm/Test1.class Last modified 2018‐9‐27; size 577 bytes MD5 checksum 4214859db3543c0c783ec8a216a4795f Compiled from "Test1.java" public class cn.itcast.jvm.Test1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1=Methodref #5.#23 // java/lang/Object."<init>": ()V #2=Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream; #3=Methodref #26.#27 // java/io/PrintStream.println: (I)V #28=Utf8 cn/itcast/jvm/Test1 #29=Utf8 java/lang/Object #30=Utf8 java/lang/System #31=Utf8 out #32=Utf8 Ljava/io/PrintStream; #33=Utf8 java/io/PrintStream #34=Utf8 println #35=Utf8 (I)V { public cn.itcast.jvm.Test1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/itcast/jvm/Test1; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: iconst_2 1: istore_1 2: iconst_5 3: istore_2 4: iload_2 5: iload_1 6: isub 7: istore_3 8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 11: iload_3 12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 15: return LineNumberTable: line 6: 0 line 7: 2 line 8: 4 line 9: 8 line 10: 15 LocalVariableTable: Start 0 Length 16 Slot 0 Name args Signature [Ljava/lang/String; 2 14 1 a I 4 12 2 b I 8 8 3 c I } SourceFile: "Test1.java"
內容大致分為4個部分:
第一部分:顯示了生成這個class的java源文件、版本信息、生成時間等。第二部分:顯示了該類中所涉及到常量池,共35個常量。
第三部分:顯示該類的構造器,編譯器自動插入的。
第四部分:顯示了main方的信息。(這個是需要我們重點關注的)
2.2、常量池
官網文檔:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4-140
Constant Type Value 說明 CONSTANT_Class 7 類或接口的符號引用 CONSTANT_Fieldref 9 字段的符號引用 CONSTANT_Methodref 10 類中方法的符號引用 CONSTANT_InterfaceMethodref 11 接口中方法的符號引用 CONSTANT_String 8 字符串類型常量 CONSTANT_Integer 3 整形常量 CONSTANT_Float 4 浮點型常量 CONSTANT_Long 5 長整型常量 CONSTANT_Double 6 雙精度浮點型常量 CONSTANT_NameAndType 12 字段或方法的符號引用 CONSTANT_Utf8 1 UTF-8編碼的字符串 CONSTANT_MethodHandle 15 表示方法句柄 CONSTANT_MethodType 16 標志方法類型 CONSTANT_InvokeDynamic 18 表示一個動態方法調用點
2.3、描述符
2.3.1、字段描述符
官網:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2
FieldType term Type Interpretation B byte signed byte C char Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16 D double double-precision floating-point value F float single-precision floating-point value I int integer J long long integer LClassName; reference an instance of class ClassName S short signed short Z boolean true or false [ reference one array dimension
2.3.2、方法描述符
官網:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3
示例:
The method descriptor for the method:
Object m(int i, double d, Thread t) {...}
is:
(IDLjava/lang/Thread;)Ljava/lang/Object;
2.4、解讀方法字節碼
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V //方法描述,V表示該方法的放回值為void flags: ACC_PUBLIC, ACC_STATIC // 方法修飾符,public、static的 Code: // stack=2,操作棧的大小為2、locals=4,本地變量表大小,args_size=1, 參數 的個數 stack=2, locals=4, args_size=1 0: iconst_2 //將數字2值壓入操作棧,位于棧的最上面 1: istore_1 //從操作棧中彈出一個元素(數字2),放入到本地變量表中,位 于下標為1的位置(下標為0的是this) 2: iconst_5 //將數字5值壓入操作棧,位于棧的最上面 3: istore_2 //從操作棧中彈出一個元素(5),放入到本地變量表中,位于第下標為2個位置 4: iload_2 //將本地變量表中下標為2的位置元素壓入操作棧(5) 5: iload_1 //將本地變量表中下標為1的位置元素壓入操作棧(2) 6: isub //操作棧中的2個數字相減 7: istore_3 // 將相減的結果壓入到本地本地變量表中,位于下標為3的位置 // 通過#2號找到對應的常量,即可找到對應的引用 8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 11: iload_3 //將本地變量表中下標為3的位置元素壓入操作棧(3) // 通過#3號找到對應的常量,即可找到對應的引用,進行方法調用 12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 15: return //返回 LineNumberTable: //行號的列表 line 6: 0 line 7: 2 line 8: 4 line 9: 8 line 10: 15 LocalVariableTable: // 本地變量表 Start 0 Length 16 Slot 0 Name args Signature [Ljava/lang/String; 2 14 1 a I 4 12 2 b I 8 8 3 c I } SourceFile: "Test1.java"
2.4.1、圖解
2.5、研究 i++ 與 ++i 的不同
我們都知道,i++表示,先返回再+1,++i表示,先+1再返回。它的底層是怎么樣的呢? 我們一起探究下。
編寫測試代碼:
public class Test2 { public static void main(String[] args) { new Test2().method1(); new Test2().method2(); } public void method1(){ int i=1; int a=i++; System.out.println(a); //打印1 } public void method2(){ int i=1; int a=++i; System.out.println(a);//打印2 } }
2.5.1、查看class字節碼
Classfile /F:/code/itcast‐jvm/itcast‐jvm‐ test/target/classes/cn/itcast/jvm/Test2.class MD5 checksum 901660fc11c43b6daadd0942150960ed Compiled from "Test2.java" public class cn.itcast.jvm.Test2 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1=Methodref #8.#27 // java/lang/Object."<init>": ()V #2=Class #28 // cn/itcast/jvm/Test2 #3=Methodref #2.#27 // cn/itcast/jvm/Test2." <init>":()V #4=Methodref #2.#29 // cn/itcast/jvm/Test2.method1: ()V #5=Methodref #2.#30 // cn/itcast/jvm/Test2.method2: ()V #6=Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream; #7=Methodref #33.#34 // java/io/PrintStream.println: (I)V #26=Utf8 Test2.java #27=NameAndType #9:#10 // "<init>":()V #28=Utf8 cn/itcast/jvm/Test2 #29=NameAndType #20:#10 // method1:()V #30=NameAndType #24:#10 // method2:()V #31=Class #36 // java/lang/System #32=NameAndType #37:#38 // out:Ljava/io/PrintStream; #33=Class #39 // java/io/PrintStream #34=NameAndType #40:#41 // println:(I)V #35=Utf8 java/lang/Object #36=Utf8 java/lang/System #37=Utf8 out #38=Utf8 Ljava/io/PrintStream; #39=Utf8 java/io/PrintStream #40=Utf8 println #41=Utf8 (I)V { public cn.itcast.jvm.Test2(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/itcast/jvm/Test2; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: new #2 // class cn/itcast/jvm/Test2 3: dup 4: invokespecial #3 // Method "<init>":()V 7: invokevirtual #4 // Method method1:()V 10: new #2 // class cn/itcast/jvm/Test2 13: dup 14: invokespecial #3 // Method "<init>":()V 17: invokevirtual #5 // Method method2:()V 20: return LineNumberTable: line 6: 0 line 7: 10 line 8: 20 LocalVariableTable: Start Length Slot Name Signature 0 21 0 args [Ljava/lang/String; public void method1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: iconst_1 1: istore_1 2: iload_1 3: iinc 1, 1 6: istore_2 7: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 10: iload_2 11: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 14: return LineNumberTable: line 11: 0 line 12: 2 line 13: 7 line 14: 14 LocalVariableTable: Start Length Slot Name Signature 0 15 0 this Lcn/itcast/jvm/Test2; 2 13 1 i I 7 8 2 a I public void method2(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: iconst_1 1: istore_1 2: iinc 1, 1 5: iload_1 6: istore_2 7: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 10: iload_2 11: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 14: return LineNumberTable: line 17: 0 line 18: 2 line 19: 7 line 20: 14 LocalVariableTable: Start Length Slot Name Signature 0 15 0 this Lcn/itcast/jvm/Test2; 2 13 1 i I 7 8 2 a I } SourceFile: "Test2.java"
2.5.2、對比
i++:
0: iconst_1 //將數字1壓入到操作棧 1: istore_1 //將數字1從操作棧彈出,壓入到本地變量表中,下標為1 2: iload_1 //從本地變量表中獲取下標為1的數據,壓入到操作棧中 3: iinc 1, 1 // 將本地變量中的1,再+1 6: istore_2 // 將數字1從操作棧彈出,壓入到本地變量表中,下標為2 7: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 10: iload_2 //從本地變量表中獲取下標為2的數據,壓入到操作棧中 11: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 14: return
++i:
0: iconst_1 //將數字1壓入到操作棧 1: istore_1 //將數字1從操作棧彈出,壓入到本地變量表中,下標為1 2: iinc 1, 1// 將本地變量中的1,再+1 5: iload_1 //從本地變量表中獲取下標為1的數據(2),壓入到操作棧中 6: istore_2 //將數字2從操作棧彈出,壓入到本地變量表中,下標為2 7: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 10: iload_2 //從本地變量表中獲取下標為2的數據(2),壓入到操作棧中 11: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 14: return
區別:
i++
只是在本地變量中對數字做了相加,并沒有將數據壓入到操作棧
將前面拿到的數字1,再次從操作棧中拿到,壓入到本地變量中
++i
將本地變量中的數字做了相加,并且將數據壓入到操作棧將操作棧中的數據,再次壓入到本地變量中
小結:可以通過查看字節碼的方式對代碼的底層做研究,探究其原理。
2.6、字符串拼接
字符串的拼接在開發過程中使用是非常頻繁的,常用的方式有三種:
+號拼接: str+“456”
StringBuilder拼接
StringBuffer拼接
StringBuffer是保證線程安全的,效率是比較低的,我們更多的是使用場景是不會涉及到 線程安全的問題的,所以更多的時候會選擇StringBuilder,效率會高一些。
那么,問題來了,StringBuilder和“+”號拼接,哪個效率高呢?接下來我們通過字節碼的 方式進行探究。
首先,編寫個示例:
package cn.itcast.jvm; public class Test3 { public static void main(String[] args) { new Test3().m1(); new Test3().m2(); public void m1(){ String s1="123"; String s2="456"; String s3=s1 + s2; System.out.println(s3); } public void m2(){ String s1="123"; String s2="456"; StringBuilder sb=new StringBuilder(); sb.append(s1); sb.append(s2); String s3=sb.toString(); System.out.println(s3); } } }
從解字節碼中可以看出,m1()方法源碼中是使用+號拼接,但是在字節碼中也被編譯成了
StringBuilder方式。
所以,可以得出結論,字符串拼接,+號和StringBuilder是相等的,效率一樣。
接下來,我們再看一個案例:
package cn.itcast.jvm; public class Test4 { public static void main(String[] args) { new Test4().m1(); new Test4().m2(); } public void m1(){ String str=""; for (int i=0; i < 5; i++) { str=str + i; } System.out.println(str); } public void m2(){ StringBuilder sb=new StringBuilder(); for (int i=0; i < 5; i++) { sb.append(i); } System.out.println(sb.toString()); } }
m1() 與 m2() 哪個方法的效率高? 依然是通過字節碼的方式進行探究。
可以看到,m1()方法中的循環體內,每一次循環都會創建StringBuilder對象,效率低于
m2()方法。
2.7、小結
使用字節碼的方式可以很好查看代碼底層的執行,從而可以看出哪些實現效率高,哪些 實現效率低。可以更好的對我們的代碼做優化。讓程序執行效率更高。
優化,不僅僅是在運行環境進行優化,還需要在代碼本身做優化,如果代碼本身存在性 能問題,那么在其他方面再怎么優化也不可能達到效果最優的。
3.1、盡可能使用局部變量
調用方法時傳遞的參數以及在調用中創建的臨時變量都保存在棧中速度較快,其他變 量,如靜態變量、實例變量等,都在堆中創建,速度較慢。另外,棧中創建的變量,隨 著方法的運行結束,這些內容就沒了,不需要額外的垃圾回收。
3.2、盡量減少對變量的重復計算
明確一個概念,對方法的調用,即使方法中只有一句語句,也是有消耗的。所以例如下 面的操作:
for (int i=0; i < list.size(); i++) {...}
建議替換為:
int length=list.size();
for (int i=0,i < length; i++)
{...}
這樣,在list.size()很大的時候,就減少了很多的消耗。
3.3、盡量采用懶加載的策略,即在需要的時候才創建
String str="aaa"; if (i==1){ list.add(str); } //建議替換成 if (i==1){ String str="aaa"; list.add(str); }
3.4、異常不應該用來控制程序流程
異常對性能不利。拋出異常首先要創建一個新的對象,Throwable接口的構造函數調用 名為fillInStackTrace()的本地同步方 法,fillInStackTrace()方法檢查堆棧,收集調用跟蹤信息。只要有異常被拋出,Java虛擬機就必須調整調用堆棧,因為在處理過程中創建 了一個新的對象。異常只能用于錯誤處理,不應該用來控制程序流程。
3.5、不要將數組聲明為public static final
因為這毫無意義,這樣只是定義了引用為static final,數組的內容還是可以隨意改變的, 將數組聲明為public更是一個安全漏洞,這意味著這個數組可以被外部類所改變。
3.6、不要創建一些不使用的對象,不要導入一些不使用的類
這毫無意義,如果代碼中出現"The value of the local variable i is not used"、“The import java.util is never used”,那么請刪除這些無用的內容
3.7、程序運行過程中避免使用反射
反射是Java提供給用戶一個很強大的功能,功能強大往往意味著效率不高。不建議在程序運行過程中使用尤其是頻繁使用反射機制,特別是 Method的invoke方法。
如果確實有必要,一種建議性的做法是將那些需要通過反射加載的類在項目啟動的時候 通過反射實例化出一個對象并放入內存。
3.8、使用數據庫連接池和線程池
這兩個池都是用于重用對象的,前者可以避免頻繁地打開和關閉連接,后者可以避免頻 繁地創建和銷毀線程。
3.9、容器初始化時盡可能指定長度
容器初始化時盡可能指定長度,如:new ArrayList<>(10); new HashMap<>(32); 避免容器長度不足時,擴容帶來的性能損耗。
3.10、ArrayList隨機遍歷快,LinkedList添加刪除快
3.11、使用Entry遍歷Map
Map<String,String> map=new HashMap<>(); for (Map.Entry<String,String> entry : map.entrySet()) { String key=entry.getKey(); String value=entry.getValue(); }
避免使用這種方式:
Map<String,String> map=new HashMap<>(); for (String key : map.keySet()) { String value=map.get(key); }
3.12、不要手動調用System.gc();
3.13、String盡量少用正則表達式
正則表達式雖然功能強大,但是其效率較低,除非是有需要,否則盡可能少用。
replace() 不支持正則
replaceAll() 支持正則
如果僅僅是字符的替換建議使用replace()。
3.14、日志的輸出要注意級別
// 當 前 的 日 志 級 別 是 error LOGGER.info("保存出錯!" + user);
3.15、對資源的close()建議分開操作
者 | 靖凡無所畏懼
責編 | 屠敏
出品 | CSDN 博客
前言
相信現在很多人都是用Vue做過了各種項目,但是項目代碼做完和上線并不代表這結束,還有上線以后的優化也是很重要的一點,這次的博客就來給大家說一下如何優化一下Vue的項目,讓你的項目打包和運行速度更上一個臺階。
優化策略
生成打包報告
修改webpack默認配置
加載外部CDN資源
路由懶加載
開啟gizp配置
生成打包報告
生成Vue的打包報告目的在于讓我們先大概的了解到當前項目生成的各個文件的大小,讓我們心里有數,知道哪里需要優化。
通過命令行參數形式生成報告:
//通過 vue-cli的命令選項可以生成打包報告
// --report 選項可以生成 report.html 用以分析包內容
vue-cli-service build --report
通過可視化面板直接查看報告:
可以在UI面板中,通過控制臺和分析面板,可以方便的查看項目中所存在的問題。
上邊圖片中標注的就是我們需要注意的,依賴項很多,導致打的包最終體積非常的大,相信這項目如果上線那么前端會先請求這個無比巨大的js文件,估計甲方爸爸會錘死前端人員。
通過 vue.config.js修改webpack的默認配置
通過vue-cli 3,0 工具生成的項目,默認隱藏了所有的 webpack配置項,目的是為了屏幕項目的配置過程,讓程序員的工作重心放到具體的功能和邏輯的實現上。
如果程序員有修改 webpack默認配置的需求,可以在項目根目錄中 按需創建vue.config.js這個配置文件,從而對項目的打包發布過程做自定義配置,具體可以參考 https://cli.vuejs.org/zh/config/#vue-config-js
為開發模式和發布模式指定不同的打包入口:
默認情況下,Vue項目的開發模式和發布模式公用一個入口文件(src/main.js),為了將項目的開發過程與發布過程分離,我們可以設置兩種模式,各自指定打包入口文件。
開發模式入口文件:src/main-dev.js
發布模式入口文件:src/main-prod.js
通過chainWebpack自定義打包入口:
module.exports={
lintOnSave: false,
chainWebpack:config=>{
//生產環境
config.when(process.env.NODE_ENV==='production',config=>{
//注意main-prod.js是已經存在的文件
config.entry('app').clear.add('./src/main-prod.js')
})
//開發環境
config.when(process.env.NODE_ENV==='development',config=>{
//注意main-dev.js是已經存在的文件
config.entry('app').clear.add('./src/main-dev.js')
})
}
}
因為開發生產環境不同,所以在main.js文件中內容就可能不同,為了解決耦合,我們就把main.js 重新劃分成了兩個文件 分別是main-dev.js 和 main-prod.js
通過externals加載外部CDN資源:
默認情況下,通過import語法導入的第三方依賴包,最終會打包合并到同一個文件中,從而導致打包成功后,單文件體積過大的問題。
vue.config.js的配置externals節點:
module.exports={
lintOnSave: false,
chainWebpack:config=>{
//生產環境
config.when(process.env.NODE_ENV==='production',config=>{
config.entry('app').clear.add('./src/main-prod.js')
config.set('externals',{
vue:'Vue',
'vue-router':'VueRouter',
axios:'axios',
lodash:'_',
echarts:'echarts',
nprogress:'NProgress',
'vue-quill-editor':'VueQuillEditor'
})
})
//開發環境
config.when(process.env.NODE_ENV==='development',config=>{
config.entry('app').clear.add('./src/main-dev.js')
})
}
}
通過設置externals這個節點,指定哪些包直接去window上去查找使用,而不會將這些指定的包再次打包起來,這樣就可以減小最終生成的js的文件體積。
去除main.js中引入的第三方包的樣式表:
將這些樣式表也統統的從main.js文件中去除,統一由CDN進行引入。
在public/index.html中添加 css和js的CDN地址:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%=BASE_URL %>favicon.ico">
<title><%=htmlWebpackPlugin.options.title %></title>
<!-- nprogress 的樣式表文件 -->
<link rel="stylesheet" />
<!-- 富文本編輯器 的樣式表文件 -->
<link rel="stylesheet" />
<link rel="stylesheet" />
<link rel="stylesheet" />
<script src="https://cdn.staticfile.org/vue/2.6.11/vue.min.js"></script>
<script src="https://cdn.staticfile.org/vue-router/3.1.6/vue-router.min.js"></script>
<script src="https://cdn.staticfile.org/axios/0.19.2/axios.min.js"></script>
<script src="https://cdn.staticfile.org/lodash.js/4.17.15/lodash.min.js"></script>
<script src="https://cdn.staticfile.org/echarts/4.7.0/echarts.min.js"></script>
<script src="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.js"></script>
<!-- 富文本編輯器的 js 文件 -->
<script src="https://cdn.staticfile.org/quill/1.3.4/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-quill-editor@3.0.6/dist/vue-quill-editor.js"></script>
</head>
<body>
<noscript>
<strong>We're sorry but <%=htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
我們通過CDN去引用css樣式的話,我們項目生成的包中就不會存在這些css文件,可以減小項目的體積,并且,我們還將 指定的js文件也通過CDN的方式來引用,這樣這些js就會注冊到windows全局中,這樣就對應到了vue.config.js配置externals節點的內容了。
引入CDN效果對比:
未使用CDN:
使用CDN后:
甲方爸爸打人的力度輕了許多。
路由懶加載
當項目打包構建的時候,js包會變得非常巨大,影響頁面加載,如果我們能夠把不同路由對應的組件分割成不同的代碼塊,然后當路由被訪問的時候才加載對應的組件,這樣就更加高效了。
安裝@bable/plugin-syntax-dynamic-import包
在babel.config.js配置文件中聲明該插件
將路由改為按需加載的形式:
const Login==> import(/* webpackChunkName: "login_home_welcome" */ '../components/Login.vue')
const Home==> import(/* webpackChunkName: "login_home_welcome" */ '../components/Home.vue')
const Welcome==> import(/* webpackChunkName: "login_home_welcome" */ '../components/Welcome.vue')
其中webpackChunkName 指定了 打包的文件名字,讓 Login.vue 、Home.vue 、 Welcome.vue 統一打到 指定的包中。
開啟gizp配置:
這一項應該由后端小哥來做,使用gizp可以減少文件體積,使得傳輸速度更快,我們就拿Express來展示一下如何去做。
//安裝相應的包
npm install compression -D
//導入包
const compression=require('compression')
//啟用中間件
app.use(compression)
這樣我們請求包的體積就會變得更小,有助于提升我們項目運行的速度。
最終效果展示
最終在我們經過上述一系列的操作以后,我們最終生產環境打出來的包體積只有356.2KB,這樣我們項目在上線以后,就可以減輕首頁假死的問題,同時因為請求的文件小了很多,所以運行速度也會加快,大家趕快去優化一下自己的項目吧。
版權聲明:本文為CSDN博主「靖凡無所畏懼」的原創文章,遵循CC 4.0 BY-SA版權協議。原文:https://blog.csdn.net/wjf1997/article/details/106248699
*請認真填寫需求信息,我們會在24小時內與您取得聯系。