JVM优化

1、我们为什么要对jvm做优化?

在本地开发环境中我们很少会遇到需要对jvm进行优化的需求,但是到了生产环境,我们 可能将有下面的需求:

运行的应用“卡住了”,日志不输出,程序没有反应,服务器的CPU负载突然升高,在多线程应用下,如何分配线程的数量?

2、jvm的运行参数

在jvm中有很多的参数可以进行设置,这样可以让jvm在各种环境中都能够高效的运行。 绝大部分的参数保持默认

即可。

2.1、三种参数类型

jvm的参数类型分为三类,分别是:

标准参数

-help

-version

-X参数

-Xint

-Xcomp

-XX参数(使用率较高)

-XX:newSize

-XX:+UseSerialGC

2.2、标准参数

jvm的标准参数,一般都是很稳定的,在未来的JVM版本中不会改变,可以使用java -help检索出所有的标准参数。

2.2.1、实例

实例1:查看jvm版本

[root@node01 ~]# java -version
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)
# -showversion参数是表示,先打印版本信息,再执行后面的命令,在调试时非常有用,
后面会使用到。

实例2:通过-D设置系统属性参数

public class TestJVM {
    public static void main(String[] args) {
        String str = System.getProperty("str"); 
        if (str == null) {
            System.out.println("orange");
        } else {
            System.out.println(str);
        }
    }
}

进行编译、测试:

#编译
[root@node01 test]# javac TestJVM.java
#测试
[root@node01 test]# java TestJVM orange
[root@node01 test]# java -Dstr=123 TestJVM
123

2.3、-X参数

jvm的-X参数是非标准参数,在不同版本的jvm中,参数可能会有所不同,可以通过java - X查看非标准参数。

[root@node01 test]# java -X
-Xmixed	混合模式执行 (默认)
-Xint	仅解释模式执行
-Xbootclasspath:<用 : 分隔的目录和 zip/jar 文件>
设置搜索路径以引导类和资源
-Xbootclasspath/a:<用 : 分隔的目录和 zip/jar 文件>
附加在引导类路径末尾
-Xbootclasspath/p:<用 : 分隔的目录和 zip/jar 文件>
置于引导类路径之前
-Xdiag	显示附加诊断消息
-Xnoclassgc	禁用类垃圾收集
-Xincgc	启用增量垃圾收集
-Xloggc:<file>	将 GC 状态记录在文件中 (带时间戳)
-Xbatch	禁用后台编译
-Xms<size>	设置初始 Java 堆大小
-Xmx<size>	设置最大 Java 堆大小
-Xss<size>	设置 Java 线程堆栈大小
-Xprof	输出 cpu 配置文件数据
-Xfuture	启用最严格的检查, 预期将来的默认值
-Xrs	减少 Java/VM 对操作系统信号的使用 (请参阅文档)
-Xcheck:jni	对 JNI 函数执行其他检查
-Xshare:off	不尝试使用共享类数据
-Xshare:auto	在可能的情况下使用共享类数据 (默认)
-Xshare:on	要求使用共享类数据, 否则将失败。
-XshowSettings	显示所有设置并继续
-XshowSettings:all
显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
显示所有属性设置并继续
-XshowSettings:locale
显示所有与区域设置相关的设置并继续

-X 选项是非标准选项, 如有更改, 恕不另行通知。

2.3.1、-Xint 、-Xcomp 、-Xmixed

JIT (just In Time Compiler)即时编译器 。

JIT工作原理:当JIT编译启用时(默认是启用的),JVM读入.class文件解释后,将其发给JIT编译器。JIT编译器将字节码编译成本机机器代码。

在解释模式(interpreted mode)下,-Xint标记会强制JVM执行所有的字节码,当然这会降低运行速度,通常低10倍或更多。

-Xcomp参数与它(-Xint)正好相反,JVM在第一次使用时会把所有的字节码编译成本地代码,从而带来最大程度的优化。

然而,很多应用在使用-Xcomp也会有一些性能损失,当然这比使用-Xint损失的少,原因是-xcomp没有让JVM启用JIT编译器的全部功能。JIT编译器可以对是否需要编译做判断,如果所有代码都进行编译的话,对于一些只执行一次的代码就没有意义了。

-Xmixed是混合模式,将解释模式与编译模式进行混合使用,由jvm自己决定,这是jvm默认的模式,也是推荐使用的模式。

示例:强制设置运行模式

#强制设置为解释模式
[root@node01 test]# java -showversion -Xint TestJVM 
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, interpreted mode) helloworld

#强制设置为编译模式
[root@node01 test]# java -showversion -Xcomp TestJVM
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, compiled mode)

helloworld
#注意:编译模式下,第一次执行会比解释模式下执行慢一些,注意观察。


#默认的混合模式
[root@node01 test]# java -showversion TestJVM
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode) helloworld

2.3.1、-Xms与-Xmx参数

Xms与-Xmx分别是设置jvm的堆内存的初始大小和最大大小。

-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。

-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。适当的调整jvm的内存大小,可以充分利用服务器资源,让程序跑的更快。示例:[root@node01 test]# java -Xms512m -Xmx2048m TestJVM helloworld

[root@node01 test]# java -Xms512m -Xmx2048m TestJVM orange

2.5、-XX参数

-XX参数也是非标准参数,主要用于jvm的调优和debug操作。

-XX参数的使用有2种方式,一种是boolean类型,一种是非boolean类型:

boolean类型

  • 格式:-XX:[±]

  • 如:-XX:+DisableExplicitGC 表示禁用手动调用gc操作,也就是说调用System.gc()无效

非boolean类型

  • 格式:-XX:

  • 如:-XX:NewRatio=1 表示新生代和老年代的比值用法:

[root@node01 test]# java -showversion -XX:+DisableExplicitGC  TestJVM
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)
orange

2.6、查看jvm的运行参数

有些时候我们需要查看jvm的运行参数,这个需求可能会存在2种情况:

第一,运行java命令时打印出运行参数;

第二,查看正在运行的java进程的参数;

2.6.1、运行java命令时打印参数

运行java命令时打印参数,需要添加-XX:+PrintFlagsFinal参数即可。

[root@node01 test]# java -XX:+PrintFlagsFinal -version 
[Global flags]
uintx AdaptiveSizeDecrementScaleFactor	= 4
{product}
uintx AdaptiveSizeMajorGCDecayTimeScale	= 10
{product}
uintx AdaptiveSizePausePolicy	= 0
{product}
uintx AdaptiveSizePolicyCollectionCostMargin	= 50
{product}
uintx AdaptiveSizePolicyInitializingSteps	= 20
{product}
uintx AdaptiveSizePolicyOutputInterval	= 0
{product}
uintx AdaptiveSizePolicyWeight	= 10
{product}
uintx AdaptiveSizeThroughPutPolicy	= 0
{product}
uintx AdaptiveTimeWeight	= 25
{product}
bool AdjustConcurrency	= false
{product}
bool AggressiveOpts	= false
{product}
intx AliasLevel	= 3
{C2 product}
bool AlignVector	= true
{C2 product}
intx AllocateInstancePrefetchLines	= 1
{product}
intx AllocatePrefetchDistance	= 256
{product}
intx AllocatePrefetchInstr	= 0
{product}

…………………………略…………………………………………





bool UseXmmI2D	= false
{C1 product}
intx ValueSearchLimit	= 1000
{C2 product}
bool VerifyMergedCPBytecodes	= true
{product}
bool VerifySharedSpaces	= false
{product}
intx WorkAroundNPTLTimedWaitHang	= 1
{product}
uintx YoungGenerationSizeIncrement	= 20
{product}
uintx YoungGenerationSizeSupplement	= 80
{product}
uintx YoungGenerationSizeSupplementDecay	= 8
{product}
uintx YoungPLABSize	= 4096
{product}
bool ZeroTLAB	= false
{product}
intx hashCode	= 5
{product}          java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)

由上述的信息可以看出,参数有boolean类型和数字类型,值的操作符是=或:=,分别代 表默认值和被修改的值。

示例:

java -XX:+PrintFlagsFinal -XX:+VerifySharedSpaces -version

intx ValueMapInitialSize	= 11
{C1 product}
intx ValueMapMaxLoopSize	= 8
{C1 product}
intx ValueSearchLimit	= 1000
{C2 product}
bool VerifyMergedCPBytecodes	= true
{product}
bool VerifySharedSpaces	:= true
{product}
intx WorkAroundNPTLTimedWaitHang	= 1
{product}
uintx YoungGenerationSizeIncrement	= 20
{product}
uintx YoungGenerationSizeSupplement	= 80
{product}
uintx YoungGenerationSizeSupplementDecay	= 8
{product}
uintx YoungPLABSize	= 4096
{product}
bool ZeroTLAB	= false
{product}
intx hashCode	= 5
{product}          java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode) 
#可以看到VerifySharedSpaces这个参数已经被修改了。

2.6.2、查看正在运行的jvm参数

如果想要查看正在运行的jvm就需要借助于jinfo命令查看。
首先,启动一个tomcat用于测试,来观察下运行的jvm参数。

cd /tmp/
rz 上传
tar -xvf apache-tomcat-7.0.57.tar.gz cd apache-tomcat-7.0.57
cd bin/
./startup.sh

#查看所有的参数,用法:jinfo -flags <进程id>

#通过jps 或者	jps -l 查看java进程
[root@node01 bin]# jps 
6346 Jps
25320 Bootstrap 
[root@node01 bin]# jps -l 6358 sun.tools.jps.Jps
25320 org.apache.catalina.startup.Bootstrap [root@node01 bin]#

[root@node01 bin]# jinfo -flags 1100 
Attaching to process ID 25320, please wait... Debugger attached successfully.
Server compiler detected. JVM version is 25.141-b15
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=31457280
-XX:MaxHeapSize=488636416 -XX:MaxNewSize=162529280 - XX:MinHeapDeltaBytes=524288 -XX:NewSize=10485760 -XX:OldSize=20971520 - XX:+UseCompressedClassPointers -XX:+UseCompressedOops - XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
Command line:	-Djava.util.logging.config.file=/tmp/apache-tomcat- 7.0.57/conf/logging.properties - Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager - Djava.endorsed.dirs=/tmp/apache-tomcat-7.0.57/endorsed - Dcatalina.base=/tmp/apache-tomcat-7.0.57 -Dcatalina.home=/tmp/apache- tomcat-7.0.57 -Djava.io.tmpdir=/tmp/apache-tomcat-7.0.57/temp

#查看某一参数的值,用法:jinfo -flag <参数名> <进程id> 
[root@node01 bin]# jinfo -flag MaxHeapSize 25320
-XX:MaxHeapSize=488636416

3、jvm的内存模型

JVM的内存结构大概分为:

堆(heap):线程共享,所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。

方法区(MEATHOD AREA):线程共享,存储类信息常量池静态变量、即时编译器编译后的代码。

PC寄存器(PC Register):线程私有。记录每个线程指向下一条要执行的指令。

方法栈(JVM Stack):线程私有、存储局部变量表操作栈、方法出口,对象指针。

本地方法栈(NATIVE METHOD STACK):线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。

3.1、jdk1.7的堆内存模型

Young 年轻区(代)

Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中, Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制 对象用,在Eden区间变的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动Tenured区间。

Tenured 老年区(代)**

Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young 复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。

Perm 永久区

Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以 解决问题。

Virtual区:

最大内存和初始内存的差值,就是Virtual区。

3.2、jdk1.8的堆内存模型

内存模型是由2部分组成,年轻代 + 年老代。

年轻代:Eden + 2*Survivor

年老代:OldGen

在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。

需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存 空间中,这也是与1.7的永久代最大的区别所在。

3.3、为什么要废弃1.7中的永久区?

官网给出了解释:http://openjdk.java.net/jeps/122

This is part of the JRockit and Hotspot convergence effort. JRockit
customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.

移除永久代是为融合HotSpot JVM与 JRockit JVM而做出的努力,因为JRockit没有永久代,不需要配置永久代。

现实使用中,由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen。

基于此,将永久区废弃,而改用元空间,改为了使用本地内存空间。

3.4、通过jstat命令进行查看堆内存使用情况

jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下: jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]

3.4.1、查看class加载统计

[root@node01 ~]# jps
7080 Jps
25320 Bootstrap
[root@node01 ~]# jstat -class 25320

Loaded	Bytes	Unloaded	Bytes	Time
3273	7122.3	0	0.0	3.98

说明:

Loaded:加载class的数量

Bytes:所占用空间大小

Unloaded:未加载数量

Bytes:未加载占用空间

Time:时间

3.4.2、查看编译统计

[root@node01 ~]# jstat -compiler 25320
Compiled Failed Invalid	Time	FailedType FailedMethod 
2376	1	0		8.04	1   org/apache/tomcat/util/IntrospectionUtils setProperty

说明:

Compiled:编译数量。
Failed:失败数量
Invalid:不可用数量
Time:时间
FailedType:失败类型
FailedMethod:失败的方法

3.4.3、垃圾回收统计

[root@node01 ~]# jstat -gc 25320
S0C	S1C	S0U	S1U	EC	EU	OC	OU	MC
MU	CCSC	CCSU	YGC	YGCT	FGC	FGCT	GCT	
9216.0 8704.0	0.0	6127.3 62976.0	3560.4	33792.0	20434.9
23808.0 23196.1 2560.0 2361.6	7	1.078	1	0.244	1.323

#也可以指定打印的间隔和次数,每1秒中打印一次,共打印5次

[root@node01 ~]# jstat -gc 25320 1000 5
S0C	S1C	S0U	S1U	EC		EU		OC		OU	MC        MU		CCSC		CCSU		YGC		YGCT	FGC	FGCT	GCT
9216.0 8704.0	0.0	6127.3 62976.0	3917.3	33792.0	20434.9
23808.0 23196.1 2560.0 2361.6	7	1.078	1	0.244	1.323
9216.0 8704.0	0.0	6127.3 62976.0	3917.3	33792.0	20434.9
23808.0 23196.1 2560.0 2361.6	7	1.078	1	0.244	1.323
9216.0 8704.0	0.0	6127.3 62976.0	3917.3	33792.0	20434.9
23808.0 23196.1 2560.0 2361.6	7	1.078	1	0.244	1.323
9216.0 8704.0	0.0	6127.3 62976.0	3917.3	33792.0	20434.9
23808.0 23196.1 2560.0 2361.6	7	1.078	1	0.244	1.323
9216.0 8704.0	0.0	6127.3 62976.0	3917.3	33792.0	20434.9
23808.0 23196.1 2560.0 2361.6	7	1.078	1	0.244	1.323

说明:

S0C:第一个Survivor区的大小(KB)
S1C:第二个Survivor区的大小(KB)
S0U:第一个Survivor区的使用大小(KB)
S1U:第二个Survivor区的使用大小(KB)
EC:Eden区的大小(KB)
EU:Eden区的使用大小(KB)
OC:Old 区 大 小 (KB)
OU:Old 使 用 大 小 (KB)
MC:方法区大小(KB)
MU:方法区使用大小(KB)
CCSC:压缩类空间大小(KB)
CCSU:压缩类空间使用大小(KB)
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间

4、jmap的使用以及内存溢出分析

前面通过jstat可以对jvm堆的内存进行统计分析,而jmap可以获取到更加详细的内容, 如:内存使用情况的汇总、对内存溢出的定位与分析。

4.1、查看内存使用情况

[root@# jmap -heap 25320
Attaching to process ID 25320, please wait... Debugger attached successfully.
Server compiler detected.
JVM version is 25.141-b15

using thread-local object allocation. Parallel GC with 2 thread(s)

Heap Configuration: #堆内存配置信息
MinHeapFreeRatio	= 0
MaxHeapFreeRatio	= 100
MaxHeapSize	= 488636416 (466.0MB)
NewSize	= 10485760 (10.0MB)
MaxNewSize	= 162529280 (155.0MB)
OldSize	= 20971520 (20.0MB)
NewRatio	= 2
SurvivorRatio	= 8
MetaspaceSize	= 21807104 (20.796875MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize	= 17592186044415 MB
G1HeapRegionSize	= 0 (0.0MB)

Heap Usage: # 堆内存的使用情况
PS Young Generation #年轻代
Eden Space:
    capacity = 123731968 (118.0MB)
    used	= 1384736 (1.320587158203125MB) 
    free	= 122347232 (116.67941284179688MB)
    1.1191416594941737% used
From Space:
    capacity = 9437184 (9.0MB) 
    used	= 0 (0.0MB)
    free	= 9437184 (9.0MB)
    0.0% used 
To Space:
    capacity = 9437184 (9.0MB) 
    used	= 0 (0.0MB)
    free	= 9437184 (9.0MB)
    0.0% used
PS Old Generation #年老代
    capacity = 28311552 (27.0MB)
    used = 13698672 (13.064071655273438MB) 
    free = 14612880 (13.935928344726562MB) 
    48.38545057508681% used
13648 interned Strings occupying 1866368 bytes.

4.2、查看内存中对象数量及大小

#查看所有对象,包括活跃以及非活跃的jmap -histo <pid> | more

#查看活跃对象
jmap -histo:live <pid> | more
[root@node01 ~]# jmap -histo:live 25320 | more 
num	#instances	#bytes	class name
----------------------------------------------
1:	37437	7914608	[C
2:	34916	837984	java.lang.String
3:	884	654848	[B
4:	17188	550016	java.util.HashMap$Node
5:	3674	424968	java.lang.Class
6:	6322	395512	[Ljava.lang.Object;
7:	3738	328944	java.lang.reflect.Method
8:	1028	208048	[Ljava.util.HashMap$Node;
9:	2247	144264	[I
10:	4305	137760
java.util.concurrent.ConcurrentHashMap$Node
11:	1270	109080	[Ljava.lang.String;
12:	64	84128
[Ljava.util.concurrent.ConcurrentHashMap$Node;
13:	1714	82272	java.util.HashMap
14:	3285	70072	[Ljava.lang.Class;
15:	2888	69312	java.util.ArrayList
16:	3983	63728	java.lang.Object
17:	1271	61008
org.apache.tomcat.util.digester.CallMethodRule
18:	1518	60720	java.util.LinkedHashMap$Entry
19:	1671	53472
com.sun.org.apache.xerces.internal.xni.QName
20:	88	50880	[Ljava.util.WeakHashMap$Entry;
21:	618	49440	java.lang.reflect.Constructor
22:	1545	49440	java.util.Hashtable$Entry
23:	1027	41080	java.util.TreeMap$Entry
24:	846	40608
org.apache.tomcat.util.modeler.AttributeInfo 25:	142	38032	[S
26:	946	37840	java.lang.ref.SoftReference
27:	226	36816	[[C
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

#对象说明

B byte
C char
D double
F float
I int
J long
Z boolean
[ 数组,如[I表示int[]
[L+类名 其他对象

4.3、将内存使用情况dump到文件中

有些时候我们需要将jvm当前内存中的情况dump到文件中,然后对它进行分析,jmap也 是支持dump到文件中的。

#用法:
jmap -dump:format=b,file=dumpFileName <pid>
#示例
jmap -dump:format=b,file=/tmp/dump.dat 25320

可以看到已经在/tmp下生成了dump.dat的文件。

4.4、通过MAT工具对dump文件进行分析

4.5.1、MAT工具介绍
MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止 了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。
官网地址:https://www.eclipse.org/mat/

4.5.2、下载安装

下载地址:https://www.eclipse.org/mat/downloads.php

将下载得到的MemoryAnalyzer-1.8.0.20180604-win32.win32.x86_64.zip进行解压

5、实例:内存溢出的定位与分析

内存溢出在实际的生产环境中经常会遇到,比如,不断的将数据写入到一个集合中,出现了死循环,读取超大的文件等等,都可能会造成内存溢出。
如果出现了内存溢出,首先我们需要定位到发生内存溢出的环节,并且进行分析,是正常还是非正常情况,如果是正常的需求,就应该考虑加大内存的设置,如果是非正常需求,那么就要对代码进行修改,修复这个bug。
首先,我们得先学会如何定位问题,然后再进行分析。如何定位问题呢,我们需要借助于jmap与MAT工具进行定位分析。
接下来,我们模拟内存溢出的场景。

5.1、模拟内存溢出

编写代码,向List集合中添加100万个字符串,每个字符串由1000个UUID组成。如果程 序能够正常执行,最后打印ok。

package com.yaorange.jvm;
import java.util.ArrayList;
import java.util.List; 
import java.util.UUID;

public class TestJvmOutOfMemory {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>(); 
        for (int i = 0; i < 10000000; i++) {
            String str = "";
            for (int j = 0; j < 1000; j++) {
                str += UUID.randomUUID().toString();
            }
            list.add(str);
        }
        System.out.println("ok");
    }
}

为了演示效果,我们将设置执行的参数,这里使用的是Idea编辑器。

#参数如下:
-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

5.2、运行测试

测试结果如下:

java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid5348.hprof ...
Heap dump file created [8137186 bytes in 0.032 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuil der.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.yaorange.jvm.TestJvmOutOfMemory.main(TestJvmOutOfMemory.java:14) Process finished with exit code 1

可以看到,当发生内存溢出时,会dump文件到java_pid5348.hprof。

5.3、导入到MAT工具中进行分析

可以看到,有91.03%的内存由Object[]数组占有,所以比较可疑。
分析:这个可疑是正确的,因为已经有超过90%的内存都被它占有,这是非常有可能出现内存溢出的。

查看详情:

可以看到集合中存储了大量的uuid字符串。

6、jstack的使用

有些时候我们需要查看下jvm中的线程执行情况,比如,发现服务器的CPU的负载突然增高了、出现了死锁、死循环等,我们该如何分析呢?

由于程序是正常运行的,没有任何的输出,从日志方面也看不出什么问题,所以就需要 看下jvm的内部线程的执行情况,然后再进行分析查找出原因。

这个时候,就需要借助于jstack命令了,jstack的作用是将正在运行的jvm的线程情况进 行快照,并且打印出来:

#用法:jstack <pid>

[root@node01 bin]# jstack 25320
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.141-b15 mixed mode):

"Attach Listener" #24 daemon prio=9 os_prio=0 tid=0x00007fabb4001000 nid=0x906 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"http-bio-8080-exec-5" #23 daemon prio=5 os_prio=0 tid=0x00007fabb057c000 nid=0x8e1 waiting on condition [0x00007fabd05b8000]
java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method)
- parking to wait for	<0x00000000f8508360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awa it(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:44 2)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32) at
java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1 074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java
:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.jav a:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread
.java:61)
at java.lang.Thread.run(Thread.java:748)



"http-bio-8080-exec-4" #22 daemon prio=5 os_prio=0 tid=0x00007fab9c113800
nid=0x8e0 waiting on condition [0x00007fabd06b9000] java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for	<0x00000000f8508360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awa it(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:44 2)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32) at
java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1 074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java
:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.jav a:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread
.java:61)
at java.lang.Thread.run(Thread.java:748)

"http-bio-8080-exec-3" #21 daemon prio=5 os_prio=0 tid=0x0000000001aeb800 nid=0x8df waiting on condition [0x00007fabd09ba000]
java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method)
- parking to wait for	<0x00000000f8508360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awa it(AbstractQueuedSynchronizer.java:2039)



at
java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:44 2)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32) at
java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1 074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java
:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.jav a:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread
.java:61)
at java.lang.Thread.run(Thread.java:748)

"http-bio-8080-exec-2" #20 daemon prio=5 os_prio=0 tid=0x0000000001aea000 nid=0x8de waiting on condition [0x00007fabd0abb000]
java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method)
- parking to wait for	<0x00000000f8508360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awa it(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:44 2)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32) at
java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1 074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java
:1134)


at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.jav a:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread
.java:61)
at java.lang.Thread.run(Thread.java:748)

"http-bio-8080-exec-1" #19 daemon prio=5 os_prio=0 tid=0x0000000001ae8800 nid=0x8dd waiting on condition [0x00007fabd0bbc000]
java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method)
- parking to wait for	<0x00000000f8508360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awa it(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:44 2)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32) at
java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1 074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java
:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.jav a:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread
.java:61)
at java.lang.Thread.run(Thread.java:748)

"ajp-bio-8009-AsyncTimeout" #17 daemon prio=5 os_prio=0 tid=0x00007fabe8128000 nid=0x8d0 waiting on condition [0x00007fabd0ece000]
java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method)
at org.apache.tomcat.util.net.JIoEndpoint$AsyncTimeout.run(JIoEndpoint.java: 152)
at java.lang.Thread.run(Thread.java:748)

"ajp-bio-8009-Acceptor-0" #16 daemon prio=5 os_prio=0 tid=0x00007fabe82d4000 nid=0x8cf runnable [0x00007fabd0fcf000]
java.lang.Thread.State: RUNNABLE
at java.net.PlainSocketImpl.socketAccept(Native Method) at
java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409) at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513) at
org.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(Defaul tServerSocketFactory.java:60)
at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:220)
at java.lang.Thread.run(Thread.java:748)

"http-bio-8080-AsyncTimeout" #15 daemon prio=5 os_prio=0 tid=0x00007fabe82d1800 nid=0x8ce waiting on condition [0x00007fabd10d0000]
java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method)
at org.apache.tomcat.util.net.JIoEndpoint$AsyncTimeout.run(JIoEndpoint.java: 152)
at java.lang.Thread.run(Thread.java:748)

"http-bio-8080-Acceptor-0" #14 daemon prio=5 os_prio=0 tid=0x00007fabe82d0000 nid=0x8cd runnable [0x00007fabd11d1000]
java.lang.Thread.State: RUNNABLE
at java.net.PlainSocketImpl.socketAccept(Native Method) at
java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409) at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)



at
org.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(Defaul tServerSocketFactory.java:60)
at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:220)
at java.lang.Thread.run(Thread.java:748)

"ContainerBackgroundProcessor[StandardEngine[Catalina]]" #13 daemon prio=5 os_prio=0 tid=0x00007fabe82ce000 nid=0x8cc waiting on condition [0x00007fabd12d2000]
java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(C ontainerBase.java:1513)
at java.lang.Thread.run(Thread.java:748)

"GC Daemon" #10 daemon prio=2 os_prio=0 tid=0x00007fabe83b4000 nid=0x8b3 in Object.wait() [0x00007fabd1c2f000]
java.lang.Thread.State: TIMED_WAITING (on object monitor) at java.lang.Object.wait(Native Method)
-waiting on <0x00000000e315c2d0> (a sun.misc.GC$LatencyLock) at sun.misc.GC$Daemon.run(GC.java:117)
-locked <0x00000000e315c2d0> (a sun.misc.GC$LatencyLock)

"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fabe80c3800 nid=0x8a5 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007fabe80b6800 nid=0x8a4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007fabe80b3800 nid=0x8a3 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007fabe80b2000 nid=0x8a2 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE


"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007fabe807f000 nid=0x8a1
in Object.wait() [0x00007fabd2a67000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method)
- waiting on <0x00000000e3162918> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x00000000e3162918> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007fabe807a800 nid=0x8a0 in Object.wait() [0x00007fabd2b68000]
java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method)
-waiting on <0x00000000e3162958> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
-locked <0x00000000e3162958> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"main" #1 prio=5 os_prio=0 tid=0x00007fabe8009000 nid=0x89c runnable [0x00007fabed210000]
java.lang.Thread.State: RUNNABLE
at java.net.PlainSocketImpl.socketAccept(Native Method) at
java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409) at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513) at
org.apache.catalina.core.StandardServer.await(StandardServer.java:453) at org.apache.catalina.startup.Catalina.await(Catalina.java:777) at org.apache.catalina.startup.Catalina.start(Catalina.java:723) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java
:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorI mpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)

at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:321)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:455)

"VM Thread" os_prio=0 tid=0x00007fabe8073000 nid=0x89f runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007fabe801e000
nid=0x89d runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007fabe8020000
nid=0x89e runnable
"VM Periodic Task Thread" os_prio=0 tid=0x00007fabe80d6800 nid=0x8a6
waiting on condition
JNI global references: 43

6.1、线程的状态

被分成6种:
初始态(NEW)

  • 创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。

运行态(RUNNABLE),在Java中,运行态包括 就绪态 和 运行态。

  • 就绪态
    该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。
    所有就绪态的线程存放在就绪队列中。

  • 运行态
    获得CPU执行权,正在执行的线程。
    由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条
    运行态的线程。

阻塞态(BLOCKED)

  • 当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。

  • 而在Java中,阻塞态专指请求锁失败时进入的状态。

  • 由一个阻塞队列存放所有阻塞态的线程。

  • 处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。

等待态(WAITING)

  • 当前线程中调用wait、join、park函数时,当前线程就会进入等待态。

  • 也有一个等待队列存放所有等待态的线程。

  • 线程处于等待态表示它需要等待其他线程的指示才能继续运行。

  • 进入等待态的线程会释放CPU执行权,并释放资源(如:锁)

超时等待态(TIMED_WAITING)

  • 当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态;

  • 它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其他线程唤醒;

  • 进入该状态后释放CPU执行权 和 占有的资源。

  • 与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。

终止态(TERMINATED)

  • 线程执行结束后的状态。

6.2、实例:死锁问题

如果在生产环境发生了死锁,我们将看到的是部署的程序没有任何反应了,这个时候我 们可以借助jstack进行分析,下面我们实例下查找死锁的原因。

6.2.1、构造死锁

编写代码,启动2个线程,Thread1拿到了obj1锁,准备去拿obj2锁时,obj2已经被Thread2锁定,所以发送了死锁。

public class TestDeadLock {

    private static Object obj1 = new Object();
    private static Object obj2 = new Object();

    public static void main(String[] args) { 
        new Thread(new Thread1()).start(); 
        new Thread(new Thread2()).start();
    }

    private static class Thread1 implements Runnable{ 
        @Override
        public void run() { 
            synchronized (obj1){
                System.out.println("Thread1 拿到了 obj1 的锁!");

                try {
                    // 停顿2秒的意义在于,让Thread2线程拿到obj2的锁
                    Thread.sleep(2000);
                } catch (InterruptedException e) { 
                    e.printStackTrace();
                }

                synchronized (obj2){
                    System.out.println("Thread1 拿到了 obj2 的锁!");
                }
            }
        }
    }


    private static class Thread2 implements Runnable{ 
        @Override
        public void run() { 
            synchronized (obj2){
                System.out.println("Thread2 拿到了 obj2 的锁!");

                try {
                    // 停顿2秒的意义在于,让Thread1线程拿到obj1的锁
                    Thread.sleep(2000);
                } catch (InterruptedException e) { 
                    e.printStackTrace();
                }
                synchronized (obj1){
                    System.out.println("Thread2 拿到了 obj1 的锁!");
                }
            }
        }
    }
}

6.2.2、在linux上运行

[root@node01 test]# javac TestDeadLock.java
[root@node01 test]# ll
总用量 28
-rw-r--r--.	1	root	root	184 9月	11	10:39	TestDeadLock$1.class
-rw-r--r--.	1	root	root	843 9月	11	10:39	TestDeadLock.class
-rw-r--r--.	1	root	root	1567 9月	11	10:39	TestDeadLock.java
-rw-r--r--.	1	root	root	1078 9月	11	10:39	TestDeadLock$Thread1.class
-rw-r--r--.	1	root	root	1078 9月	11	10:39	TestDeadLock$Thread2.class
-rw-r--r--.	1	root	root	573 9月	9	10:21	TestJVM.class
-rw-r--r--.	1	root	root	261 9月	9	10:21	TestJVM.java

[root@node01 test]# java TestDeadLock
Thread1 拿到了 obj1 的锁!
Thread2 拿到了 obj2 的锁!
#这里发生了死锁,程序一直将等待下去

6.2.3、使用jstack进行分析

[root@node01 ~]# jstack 3256
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.141-b15 mixed mode):

"Attach Listener" #11 daemon prio=9 os_prio=0 tid=0x00007f5bfc001000 nid=0xcff waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" #10 prio=5 os_prio=0 tid=0x00007f5c2c008800 nid=0xcb9 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Thread-1" #9 prio=5 os_prio=0 tid=0x00007f5c2c0e9000 nid=0xcc5 waiting for monitor entry [0x00007f5c1c7f6000]
java.lang.Thread.State: BLOCKED (on object monitor) at TestDeadLock$Thread2.run(TestDeadLock.java:47)
-waiting to lock <0x00000000f655dc40> (a java.lang.Object)
-locked <0x00000000f655dc50> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748)

"Thread-0" #8 prio=5 os_prio=0 tid=0x00007f5c2c0e7000 nid=0xcc4 waiting for monitor entry [0x00007f5c1c8f7000]
java.lang.Thread.State: BLOCKED (on object monitor) at TestDeadLock$Thread1.run(TestDeadLock.java:27)
-waiting to lock <0x00000000f655dc50> (a java.lang.Object)
-locked <0x00000000f655dc40> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748)

"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007f5c2c0d3000 nid=0xcc2 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f5c2c0b6000 nid=0xcc1 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f5c2c0b3000 nid=0xcc0 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f5c2c0b1800 nid=0xcbf runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f5c2c07e800 nid=0xcbe in Object.wait() [0x00007f5c1cdfc000]
java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method)
- waiting on <0x00000000f6508ec8> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x00000000f6508ec8> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f5c2c07a000 nid=0xcbd in Object.wait() [0x00007f5c1cefd000]
java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method)
-waiting on <0x00000000f6506b68> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
-locked <0x00000000f6506b68> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153) "VM Thread" os_prio=0 tid=0x00007f5c2c072800 nid=0xcbc runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f5c2c01d800 nid=0xcba runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f5c2c01f800 nid=0xcbb runnable

"VM Periodic Task Thread" os_prio=0 tid=0x00007f5c2c0d6800 nid=0xcc3 waiting on condition

JNI global references: 6



Found one Java-level deadlock:

=============================
"Thread-1":
waiting to lock monitor 0x00007f5c080062c8 (object 0x00000000f655dc40, a java.lang.Object),
which is held by "Thread-0" "Thread-0":
waiting to lock monitor 0x00007f5c08004e28 (object 0x00000000f655dc50, a java.lang.Object),
which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
at TestDeadLock$Thread2.run(TestDeadLock.java:47)
-waiting to lock <0x00000000f655dc40> (a java.lang.Object)
-locked <0x00000000f655dc50> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at TestDeadLock$Thread1.run(TestDeadLock.java:27)
-waiting to lock <0x00000000f655dc50> (a java.lang.Object)
-locked <0x00000000f655dc40> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

在输出的信息中,已经看到,发现了1个死锁,关键看这个:

"Thread-1":
at TestDeadLock$Thread2.run(TestDeadLock.java:47)
-waiting to lock <0x00000000f655dc40> (a java.lang.Object)
-locked <0x00000000f655dc50> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at TestDeadLock$Thread1.run(TestDeadLock.java:27)
-waiting to lock <0x00000000f655dc50> (a java.lang.Object)
-locked <0x00000000f655dc40> (a java.lang.Object) at java.lang.Thread.run(Thread.java:748)

可以清晰的看到:

Thread2获取了 <0x00000000f655dc50> 的锁,等待获取 <0x00000000f655dc40>
这个锁
Thread1获取了 <0x00000000f655dc40> 的锁,等待获取 <0x00000000f655dc50>
这个锁
由此可见,发生了死锁

7、VisualVM工具的使用

VisualVM,能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。
VisualVM使用简单,几乎0配置,功能还是比较丰富的,几乎囊括了其它JDK自带命令的所有功能。

内存信息线程信息
Dump堆(本地进程)
Dump线程(本地进程)
打开堆Dump。堆Dump可以用jmap来生成。打开线程Dump
生成应用快照(包含内存信息、线程信息等等)
性能分析。CPU分析(各个方法调用时间,检查哪些方法耗时多),内存分析(各类对象占用的内存,检查哪些类占用内存多)
……

7.1、启动

在jdk的安装目录的bin目录下,找到jvisualvm.exe,双击打开即可。

7.2、查看本地进程

7.3、查看CPU、内存、类、线程运行信息

7.4、查看线程详情

也可以点击右上角Dump按钮,将线程的信息导出,其实就是执行的jstack命令。

显示的内容是一样的。

7.5、抽样器

抽样器可以对CPU、内存在一段时间内进行抽样,以供分析。

8、jProfiler 调试工具

以上大部分工具都是jdk自带的,在生产环境的话需要掌握,如果是开发阶段的话,建议用jProfiler 配合 idea中的插件。

9、类加载器

9.1、类加载器的基本概念

顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。

9.2、类与类加载器

对于任意一个类,都需要加载它的类加载器和这个类本身来确定这个类在Java虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间。也就是说,如果比较两个类是否是同一个类,除了这比较这两个类本身的全限定名是否相同之外,还要比较这两个类是否是同一个类加载器加载的。即使同一个类文件两次加载到同一个虚拟机中,但如果是由两个不同的类加载器加载的,那这两个类仍然不是同一个类。
这个相等性比较会影响一些方法,比如Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法等,还有instanceof关键字做对象所属关系判定等。下面的代码演示了不同的类加载器对instanceof关键字的影响:

package temp;

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception{
        ClassLoader loader=new ClassLoader() {
            @Override
            public Class<?> loadClass(String name)throws ClassNotFoundException{
                try{
                    String filename=name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream is=getClass().getResourceAsStream(filename);
                    if(is==null){
                        return super.loadClass(name);
                    }
                    byte[] b=new byte[is.available()];
                    is.read(b);
                    return defineClass(name,b,0,b.length);
                }catch(IOException e){
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object obj=loader.loadClass("temp.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj.getClass().getClassLoader());
        System.out.println(ClassLoaderTest.class.getClassLoader());
        System.out.println(obj instanceof temp.ClassLoaderTest);
    }

}

运行结果:

class temp.ClassLoaderTest
temp.ClassLoaderTest$1@e6ea0c6
sun.misc.Launcher$AppClassLoader@58644d46
false

这里构造了一个简单的类加载器,它可以加载与自己在同一个路径下的Class文件。然后使用这个类加载器去加载全限定名是temp.ClassLoaderTest的类,并实例化了这个类的对象。从第一行输出可以看出,这个对象确实是temp.ClassLoaderTest类的一个实例,我们打印了一下对象obj的类加载器和ClassLoaderTest的类加载器,发现确实是不同的两个不同的类加载器,最后输出表明在做instanceof检查时出现了false,这是因为这时虚拟机中有两个temp.ClassLoaderTest类,一个是系统应用程序类加载器加载的,另一个是自定义的类加载器加载的,这两个类虽然来自同一个Class文件,但是加载它们的类加载器不同,导致类型检查时结果是false。

9.3、双亲委派模型

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:

启动类加载器:负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。

扩展类加载器:这个加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用程序类加载器:这个类加载器是由sun.misc.Launcher$AppClassLoader实现的。由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以也叫系统类加载器。它负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
用户的应用程序就是在这三个类加载器的配合下加载的。不过,用户还可以加入自己的类加载器,这些类加载器的关系如下图:

这种类加载的层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。不过这个父子关系不是通过继承实现的,而是使用组合关系来复用父加载器的代码。

双亲委派模型的工作过程如下:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围内没找到这个类)时,自加载器才会尝试自己加载。

双亲委派模型是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过双亲委派模型,对于 Java 核心库的类的加载工作由启动类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。

10、什么是垃圾回收?

程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存资源,最终将导致内存溢出,所以对内存资源的管理是非常重要了。

10.1、C/C++语言的垃圾回收

在C/C++语言中,没有自动垃圾回收机制,是通过new关键字申请内存资源,通过delete 关键字释放内存资源。

如果,程序员在某些位置没有写delete进行释放,那么申请的对象将一直占用内存资源, 最终可能会导致内存溢出。

10.2、Java语言的垃圾回收

为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC。

有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别 完成。

换句话说,自动的垃圾回收的算法就会变得非常重要了,如果因为算法的不合理,导致内存资源一直没有释放,同样也可能会导致内存溢出的。

当然,除了Java语言,C#、Python等语言也都有自动的垃圾回收机制。

11、垃圾回收的常见算法

自动化的管理内存资源,垃圾回收机制必须要有一套算法来进行计算,哪些是有效的对 象,哪些是无效的对象,对于无效的对象就要进行回收处理。

常见的垃圾回收算法有:引用计数法、标记清除法、标记压缩法、复制算法、分代算法 等。

11.1、引用计数法

引用计数是历史最悠久的一种算法,最早George E. Collins在1960的时候首次提出,50年后的今天,该算法依然被很多编程语言使用。

11.1.1、原理

假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败 时,对象A的引用计数器就-1,如果对象A的计数器的值为0,就说明对象A没有引用了, 可以被回收。

11.1.2、优缺点

优点:

实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。

在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报outofmember 错误。

区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。

缺点:

每次对象被引用时,都需要去更新计数器,有一点时间开销。

浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。

无法解决循环引用问题。(最大的缺点)

什么是循环引用?

    class A{
        public B b;
    }
    class B{ 
        public A a;
    }
    public class Main{
        public static void main(String[] args){
            A a = new A();   
            B b = new B();   
            a.b=b;
            b.a=a;
            a = null;                           
            b = null;
    }
}

虽然a和b都为null,但是由于a和b存在循环引用,这样a和b永远都不会被回收。

11.2、标记清除法

标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。

  • 标记:从根节点开始标记引用的对象。

  • 清除:未被标记引用的对象就是垃圾对象,可以被清理。

11.2.1、原理

代表的是程序运行期间所有对象的状态,它们的标志位全部是0(也就是未标记, 以下默认0就是未标记,1为已标记),假设这会儿有效内存空间耗尽了,JVM将会停止应用程序的运行并开启GC线程,然后开始进行标记工作,

按照根搜索算法,标记完以后, 对象的状态如下图。

从root对象可达的对象就被标记为了存活的对象,此 时已经完成了第一阶段标记。接下来,就要执行第二阶段清除了,那么清除完以后,剩下的对象以及对象的状态如下图所示。

可以看到,没有被标记的对象将会回收清除掉,而被标记的对象将会留下,并且会将标记位重新归0。接下来就不用说了,唤醒停止的程序线程,让程序继续运行即可。

11.2.2、优缺点

可以看到,标记清除算法解决了引用计数算法中的循环引用的问题,没有从root节点引 用的对象都会被回收。

同样,标记清除算法也是有缺点的:

效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。

通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。

11.3、标记压缩算法

标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一 样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。

11.3.1、原理

11.3.2、优缺点

优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。

11.4、复制算法

复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。

如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。

11.4.1、JVM中年轻代内存空间

  • 1.在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor 区“To”是空的。

  • 2.紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍 存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过- XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对 象会被复制到“To”区域。

  • 3.经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他 们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。

  • 4.GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

11.4.2、优缺点

  • 优点:

在垃圾对象多的情况下,效率较高清理后,内存无碎片

  • 缺点:

在垃圾对象少的情况下,不适用,如:老年代内存分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低

11.5、分代算法

前面介绍了多种回收算法,每一种算法都有自己的优点也有缺点,谁都不能替代谁,所以根据垃圾回收对象的特点进行选择,才是明智的选择。

分代算法其实就是这样的,根据回收对象的特点进行选择,在jvm中,年轻代适合使用复制算法,老年代适合使用标记清除或标记压缩算法。

12、垃圾收集器以及内存分配

前面我们讲了垃圾回收的算法,还需要有具体的实现,在jvm中,实现了多种垃圾收集 器,包括:串行垃圾收集器、并行垃圾收集器、CMS(并发)垃圾收集器、G1垃圾收集器,接下来,我们一个个的了解学习。

12.1、串行垃圾收集器

串行垃圾收集器,是指使用单线程进行垃圾回收,垃圾回收时,只有一个线程在工作, 并且java应用中的所有线程都要暂停,等待垃圾回收的完成。这种现象称之为STW(Stop-The-World)。

对于交互性较强的应用而言,这种垃圾收集器是不能够接受的。一般Javaweb应用中是不会采用该收集器的。

12.2、并行垃圾收集器

并行垃圾收集器在串行垃圾收集器的基础之上做了改进,将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间。(这里是指,并行能力较强的机器)当然了,并行垃圾收集器在收集的过程中也会暂停应用程序,这个和串行垃圾回收器是 一样的,只是并行执行,速度更快些,暂停的时间更短一些。

12.3、CMS垃圾收集器

CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器, 该回收器是针对老年代垃圾回收的,通过参数-XX:+UseConcMarkSweepGC进行设置。

CMS垃圾回收器的执行过程如下:

  • 初始化标记(CMS-initial-mark) ,标记root,会导致stw;

  • 并发标记(CMS-concurrent-mark),与用户线程同时运行;

  • 预清理(CMS-concurrent-preclean),与用户线程同时运行;

  • 重新标记(CMS-remark) ,会导致stw;

  • 并发清除(CMS-concurrent-sweep),与用户线程同时运行;

  • 调整堆大小,设置CMS在清理之后进行内存压缩,目的是清理内存中的碎片;

  • 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;

stw: Stop-The-World

–Java中一种全局暂停的现象

–全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互

–多半由于GC引起

•Dump线程

•死锁检查

•堆Dump

#设置启动参数
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms16m -Xmx16m

#运行日志
[GC (Allocation Failure) [ParNew: 4926K->512K(4928K), 0.0041843 secs] 9424K->6736K(15872K), 0.0042168 secs] [Times: user=0.00 sys=0.00,
real=0.00 secs]

#第一步,初始标记
[GC (CMS Initial Mark) [1 CMS-initial-mark: 6224K(10944K)] 6824K(15872K), 0.0004209 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第二步,并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第三步,预处理
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第四步,重新标记
[GC (CMS Final Remark) [YG occupancy: 1657 K (4928 K)][Rescan (parallel)
, 0.0005811 secs][weak refs processing, 0.0000136 secs][class unloading, 0.0003671 secs][scrub symbol table, 0.0006813 secs][scrub string table, 0.0001216 secs][1 CMS-remark: 6224K(10944K)] 7881K(15872K), 0.0018324
secs] [Times: user=0.00 sys=0.00, real=0.00 secs] #第五步,并发清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.004/0.004 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第六步,重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

由以上日志信息,可以看出CMS执行的过程。

12.4、G1垃圾收集器(重点)

G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方计划在jdk9中将 G1变成默认的垃圾收集器,以替代CMS。

G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:

1.第一步,开启G1垃圾收集器

2.第二步,设置堆的最大内存

3.第三步,设置最大的停顿时间

G1中提供了三种模式垃圾回收模式,Young(Minor ) GC、Old(Major_ GC)、Mixed GC 和 Full GC,在不同的条件下被触发。

13 、JVM字节码

前面我们通过tomcat本身的参数以及jvm的参数对tomcat做了优化,其实要想将应用程序跑的更快、效率更高,除了对tomcat容器以及jvm优化外,应用程序代码本身如果写的效率不高的,那么也是不行的,所以,对于程序本身的优化也就很重要了。

对于程序本身的优化,可以借鉴很多前辈们的经验,但是有些时候,在从源码角度方面分析的话,不好鉴别出哪个效率高,如对字符串拼接的操作,是直接“+”号拼接效率高还是使用StringBuilder效率高?

这个时候,就需要通过查看编译好的class文件中字节码,就可以找到答案。我们都知道,java编写应用,需要先通过javac命令编译成class文件,再通过jvm执行,jvm执行时是需要将class文件中的字节码载入到jvm进行运行的。

13.1、通过javap命令查看class文件的字节码内容

首先,看一个简单的Test1类的代码:

package com.yaorange.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 > Test.txt
javap用法: javap <options> <classes>
其中, 可能的选项包括:
  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置

查看Test1.txt文件,内容如下:

Classfile /D:/ftp/java01/workspace/test/out/production/test/com/company/Test1.class
  Last modified 2019-10-25; size 573 bytes
  MD5 checksum 75ef9824d9df521c1960942adb7c0dff
  Compiled from "Test1.java"
public class com.company.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
   #4 = Class              #28            // com/company/Test1
   #5 = Class              #29            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/company/Test1;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               c
  #21 = Utf8               SourceFile
  #22 = Utf8               Test1.java
  #23 = NameAndType        #6:#7          // "<init>":()V
  #24 = Class              #30            // java/lang/System
  #25 = NameAndType        #31:#32        // out:Ljava/io/PrintStream;
  #26 = Class              #33            // java/io/PrintStream
  #27 = NameAndType        #34:#35        // println:(I)V
  #28 = Utf8               com/company/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 com.company.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 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/company/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 14: 0
        line 15: 2
        line 16: 4
        line 17: 8
        line 18: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [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方的信息。(这个是需要我们重点关注的)

13.2、常量池

官网文档:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4-140

13.3、描述符

13.3.1、字段描述符

官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2

13.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;

13.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的是args)
         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  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            2      14     1     a   I
            4      12     2     b   I
            8       8     3     c   I
}
SourceFile: "Test1.java"

13.4.1 、图解

13.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
    }
}

13.5.1、查看class字节码

  Classfile /F:/code/yaorange-jvm/yaorange-jvm-
test/target/classes/com/yaorange/jvm/Test2.class
  MD5 checksum 901660fc11c43b6daadd0942150960ed
  Compiled from "Test2.java"
public class com.yaorange.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            // com/yaorange/jvm/Test2
   #3 = Methodref          #2.#27         // com/yaorange/jvm/Test2."
<init>":()V
   #4 = Methodref          #2.#29         // com/yaorange/jvm/Test2.method1:
()V
   #5 = Methodref          #2.#30         // com/yaorange/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
   #8 = Class              #35            // java/lang/Object
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/yaorange/jvm/Test2;
  #16 = Utf8               main
  #17 = Utf8               ([Ljava/lang/String;)V
  #18 = Utf8               args
  #19 = Utf8               [Ljava/lang/String;
  #20 = Utf8               method1
  #21 = Utf8               i
  #22 = Utf8               I
  #23 = Utf8               a
  #24 = Utf8               method2
  #25 = Utf8               SourceFile
  #26 = Utf8               Test2.java
  #27 = NameAndType        #9:#10         // "<init>":()V
  #28 = Utf8               com/yaorange/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 com.yaorange.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   Lcom/yaorange/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
com/yaorange/jvm/Test2
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: invokevirtual #4                  // Method method1:()V
        10: new           #2                  // class
com/yaorange/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   Lcom/yaorange/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   Lcom/yaorange/jvm/Test2;
                        2      13     1     i   I
                        7       8     2     a   I
}
SourceFile: "Test2.java"

13.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

将本地变量中的数字做了相加,并且将数据压入到操作栈将操作栈中的数据,再次压入到本地变量中

小结:可以通过查看字节码的方式对代码的底层做研究,探究其原理。

13.6、字符串拼接

字符串的拼接在开发过程中使用是非常频繁的,常用的方式有三种:

  • 号拼接: str+"456"

  • StringBuilder 拼接

  • StringBuffer拼接

StringBuffer是保证线程安全的,效率是比较低的,我们更多的是使用场景是不会涉及到线程安全的问题的,所以更多的时候会选择StringBuilder,效率会高一些。

那么,问题来了,StringBuilder和“+”号拼接,哪个效率高呢?接下来我们通过字节码的方式进行探究。

首先,编写个示例:

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);
    }
}

查看 Test3.class的字节码

Classfile /F:/code/yaorange-jvm/yaorange-jvm-
test/target/classes/com/yaorange/jvm/Test3.class
  MD5 checksum b3f7629e7e37768b9b5581be01df40d6
  Compiled from "Test3.java"
public class com.yaorange.jvm.Test3
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#36        // java/lang/Object."<init>":
()V
   #2 = Class              #37            // com/yaorange/jvm/Test3
   #3 = Methodref          #2.#36         // com/yaorange/jvm/Test3."
<init>":()V
   #4 = Methodref          #2.#38         // com/yaorange/jvm/Test3.m1:()V
   #5 = Methodref          #2.#39         // com/yaorange/jvm/Test3.m2:()V
   #6 = String             #40            // 123
   #7 = String             #41            // 456
   #8 = Class              #42            // java/lang/StringBuilder
   #9 = Methodref          #8.#36         // java/lang/StringBuilder."
<init>":()V
  #10 = Methodref          #8.#43         //
java/lang/StringBuilder.append:
(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #11 = Methodref          #8.#44         //
java/lang/StringBuilder.toString:()Ljava/lang/String;
  #12 = Fieldref           #45.#46        //
java/lang/System.out:Ljava/io/PrintStream;
  #13 = Methodref          #47.#48        // java/io/PrintStream.println:
(Ljava/lang/String;)V
  #14 = Class              #49            // java/lang/Object
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/yaorange/jvm/Test3;
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
   #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               m1
  #27 = Utf8               s1
  #28 = Utf8               Ljava/lang/String;
  #29 = Utf8               s2
  #30 = Utf8               s3
  #31 = Utf8               m2
  #32 = Utf8               sb
  #33 = Utf8               Ljava/lang/StringBuilder;
  #34 = Utf8               SourceFile
  #35 = Utf8               Test3.java
  #36 = NameAndType        #15:#16        // "<init>":()V
  #37 = Utf8               com/yaorange/jvm/Test3
  #38 = NameAndType        #26:#16        // m1:()V
  #39 = NameAndType        #31:#16        // m2:()V
  #40 = Utf8               123
  #41 = Utf8               456
  #42 = Utf8               java/lang/StringBuilder
  #43 = NameAndType        #50:#51        // append:
(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #44 = NameAndType        #52:#53        // toString:
()Ljava/lang/String;
  #45 = Class              #54            // java/lang/System
  #46 = NameAndType        #55:#56        // out:Ljava/io/PrintStream;
  #47 = Class              #57            // java/io/PrintStream
  #48 = NameAndType        #58:#59        // println:
(Ljava/lang/String;)V
  #49 = Utf8               java/lang/Object
  #50 = Utf8               append
  #51 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #52 = Utf8               toString
  #53 = Utf8               ()Ljava/lang/String;
  #54 = Utf8               java/lang/System
  #55 = Utf8               out
  #56 = Utf8               Ljava/io/PrintStream;
  #57 = Utf8               java/io/PrintStream
  #58 = Utf8               println
  #59 = Utf8               (Ljava/lang/String;)V
{
  public com.yaorange.jvm.Test3();
  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   Lcom/yaorange/jvm/Test3;
  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
com/yaorange/jvm/Test3
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: invokevirtual #4                  // Method m1:()V
        10: new           #2                  // class
com/yaorange/jvm/Test3
        13: dup
        14: invokespecial #3                  // Method "<init>":()V
        17: invokevirtual #5                  // Method m2:()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 m1();
                descriptor: ()V
                flags: ACC_PUBLIC
                Code:
                  stack=2, locals=4, args_size=1
                     0: ldc           #6                  // String 123
                     2: astore_1
                     3: ldc           #7                  // String 456
                     5: astore_2
                     6: new           #8                  // class
            java/lang/StringBuilder
                     9: dup
                    10: invokespecial #9                  // Method
            java/lang/StringBuilder."<init>":()V  
                    13: aload_1
                    14: invokevirtual #10                 // Method
            java/lang/StringBuilder.append:
            (Ljava/lang/String;)Ljava/lang/StringBuilder;
                    17: aload_2
                    18: invokevirtual #10                 // Method
            java/lang/StringBuilder.append:
            (Ljava/lang/String;)Ljava/lang/StringBuilder;
                    21: invokevirtual #11                 // Method
            java/lang/StringBuilder.toString:()Ljava/lang/String;
                    24: astore_3
                    25: getstatic     #12                 // Field
            java/lang/System.out:Ljava/io/PrintStream;
                    28: aload_3
                    29: invokevirtual #13                 // Method
            java/io/PrintStream.println:(Ljava/lang/String;)V
                    32: return
                  LineNumberTable:
                    line 11: 0
                    line 12: 3
                    line 13: 6
                    line 14: 25
                    line 15: 32
                  LocalVariableTable:
                    Start  Length  Slot  Name   Signature
                        0      33     0  this   Lcom/yaorange/jvm/Test3;
                        3      30     1    s1   Ljava/lang/String;
                        6      27     2    s2   Ljava/lang/String;
                       25       8     3    s3   Ljava/lang/String;
           public void m2();
            descriptor: ()V
            flags: ACC_PUBLIC
            Code:
              stack=2, locals=5, args_size=1
                 0: ldc           #6                  // String 123
                 2: astore_1
                 3: ldc           #7                  // String 456
                 5: astore_2
                 6: new           #8                  // class
        java/lang/StringBuilder
                 9: dup
                10: invokespecial #9                  // Method
        java/lang/StringBuilder."<init>":()V
                13: astore_3
                14: aload_3
                15: aload_1
                16: invokevirtual #10                 // Method
        java/lang/StringBuilder.append:
        (Ljava/lang/String;)Ljava/lang/StringBuilder;
                19: pop
                20: aload_3
                21: aload_2
                22: invokevirtual #10                 // Method
        java/lang/StringBuilder.append:
        (Ljava/lang/String;)Ljava/lang/StringBuilder;
                25: pop
                26: aload_3
                27: invokevirtual #11                 // Method
        java/lang/StringBuilder.toString:()Ljava/lang/String;
                30: astore        4
                32: getstatic     #12                 // Field
        java/lang/System.out:Ljava/io/PrintStream;
                35: aload         4
                37: invokevirtual #13                 // Method
        java/io/PrintStream.println:(Ljava/lang/String;)V
                40: return
              LineNumberTable:
                line 18: 0
                line 19: 3
                line 20: 6
                line 21: 14
                line 22: 20
                line 23: 26
                line 24: 32
                line 25: 40
              LocalVariableTable:
                Start  Length  Slot  Name   Signature
                    0      41     0  this   Lcom/yaorange/jvm/Test3;
                    3      38     1    s1   Ljava/lang/String;
                    6      35     2    s2   Ljava/lang/String;
                   14      27     3    sb   Ljava/lang/StringBuilder;
                   32       9     4    s3   Ljava/lang/String;
}
SourceFile: "Test3.java"

从解字节码中可以看出, m1()方法源码中是使用+号拼接,但是在字节码中也被编译成了StringBuilder方式。所

以,可以得出结论,字符串拼接,+号和StringBuilder是相等的,效率一样。

接下来,我们再看一个案例:

package com.yaorange.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() 哪个方法的效率高?

依然是通过字节码的方式进行探究。

  Classfile /F:/code/yaorange-jvm/yaorange-jvm-
test/target/classes/com/yaorange/jvm/Test4.class
  MD5 checksum f87a55446b8b6cd88b6e54bd5edcc9dc
  Compiled from "Test4.java"
public class com.yaorange.jvm.Test4
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#39        // java/lang/Object."<init>":
()V
   #2 = Class              #40            // com/yaorange/jvm/Test4
   #3 = Methodref          #2.#39         // com/yaorange/jvm/Test4."
<init>":()V
   #4 = Methodref          #2.#41         // com/yaorange/jvm/Test4.m1:()V
   #5 = Methodref          #2.#42         // com/yaorange/jvm/Test4.m2:()V
   #6 = String             #43            //
   #7 = Class              #44            // java/lang/StringBuilder
   #8 = Methodref          #7.#39         // java/lang/StringBuilder."
<init>":()V
   #9 = Methodref          #7.#45         //
java/lang/StringBuilder.append:
(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #10 = Methodref          #7.#46         //
java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #11 = Methodref          #7.#47         //
java/lang/StringBuilder.toString:()Ljava/lang/String;
  #12 = Fieldref           #48.#49        //
java/lang/System.out:Ljava/io/PrintStream;
  #13 = Methodref          #50.#51        // java/io/PrintStream.println:
(Ljava/lang/String;)V
  #14 = Class              #52            // java/lang/Object
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/yaorange/jvm/Test4;
  #22 = Utf8               main
   #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               m1
  #27 = Utf8               i
  #28 = Utf8               I
  #29 = Utf8               str
  #30 = Utf8               Ljava/lang/String;
  #31 = Utf8               StackMapTable
  #32 = Class              #53            // java/lang/String
  #33 = Utf8               m2
  #34 = Utf8               sb
  #35 = Utf8               Ljava/lang/StringBuilder;
  #36 = Class              #44            // java/lang/StringBuilder
  #37 = Utf8               SourceFile
  #38 = Utf8               Test4.java
  #39 = NameAndType        #15:#16        // "<init>":()V
  #40 = Utf8               com/yaorange/jvm/Test4
  #41 = NameAndType        #26:#16        // m1:()V
  #42 = NameAndType        #33:#16        // m2:()V
  #43 = Utf8
  #44 = Utf8               java/lang/StringBuilder
  #45 = NameAndType        #54:#55        // append:
(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #46 = NameAndType        #54:#56        // append:
(I)Ljava/lang/StringBuilder;
  #47 = NameAndType        #57:#58        // toString:
()Ljava/lang/String;
  #48 = Class              #59            // java/lang/System
  #49 = NameAndType        #60:#61        // out:Ljava/io/PrintStream;
  #50 = Class              #62            // java/io/PrintStream
  #51 = NameAndType        #63:#64        // println:
(Ljava/lang/String;)V
  #52 = Utf8               java/lang/Object
  #53 = Utf8               java/lang/String
  #54 = Utf8               append
  #55 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #56 = Utf8               (I)Ljava/lang/StringBuilder;
  #57 = Utf8               toString
  #58 = Utf8               ()Ljava/lang/String;
  #59 = Utf8               java/lang/System
   #60 = Utf8               out
  #61 = Utf8               Ljava/io/PrintStream;
  #62 = Utf8               java/io/PrintStream
  #63 = Utf8               println
  #64 = Utf8               (Ljava/lang/String;)V
{
  public com.yaorange.jvm.Test4();
    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   Lcom/yaorange/jvm/Test4;
  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
com/yaorange/jvm/Test4
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: invokevirtual #4                  // Method m1:()V
        10: new           #2                  // class
com/yaorange/jvm/Test4
        13: dup
        14: invokespecial #3                  // Method "<init>":()V
        17: invokevirtual #5                  // Method m2:()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 m1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #6                  // String
         2: astore_1  // 将空字符串压入到本地变量表中的下标为1的位置
         3: iconst_0  // 将数字0压入操作栈顶
         4: istore_2  // 将栈顶数字0压入到本地变量表中的下标为2的位置
         5: iload_2  // 将本地变量中下标为2的数字0压入操作栈顶
         6: iconst_5 // 将数字5压入操作栈顶
         7: if_icmpge     35  //比较栈顶两int型数值大小,当结果大于等于0时跳
转到35
        10: new           #7                  // class
java/lang/StringBuilder
        13: dup  //复制栈顶数值并将复制值压入栈顶(数字5)
        14: invokespecial #8                  // Method
java/lang/StringBuilder."<init>":()V
        17: aload_1
        18: invokevirtual #9                  // Method
java/lang/StringBuilder.append:
(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: iload_2 //将本地变量中下标为2的数字0压入操作栈顶
        22: invokevirtual #10                 // Method
java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        25: invokevirtual #11                 // Method
java/lang/StringBuilder.toString:()Ljava/lang/String;
        28: astore_1
        29: iinc          2, 1
        32: goto          5
        35: getstatic     #12                 // Field
java/lang/System.out:Ljava/io/PrintStream;
        38: aload_1
        39: invokevirtual #13                 // Method
java/io/PrintStream.println:(Ljava/lang/String;)V
        42: return
      LineNumberTable:
      line 11: 0
        line 12: 3
        line 13: 10
        line 12: 29
        line 15: 35
        line 16: 42
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            5      30     2     i   I
            0      43     0  this   Lcom/yaorange/jvm/Test4;
            3      40     1   str   Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 5
          locals = [ class java/lang/String, int ]
        frame_type = 250 /* chop */
          offset_delta = 29
  public void m2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #7                  // class
java/lang/StringBuilder
         3: dup
         4: invokespecial #8                  // Method
java/lang/StringBuilder."<init>":()V
         7: astore_1
         8: iconst_0
         9: istore_2
        10: iload_2
        11: iconst_5
        12: if_icmpge     27
        15: aload_1
        16: iload_2
        17: invokevirtual #10                 // Method
java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        20: pop
        21: iinc          2, 1
        24: goto          10
         27: getstatic     #12                 // Field
java/lang/System.out:Ljava/io/PrintStream;
        30: aload_1
        31: invokevirtual #11                 // Method
java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: invokevirtual #13                 // Method
java/io/PrintStream.println:(Ljava/lang/String;)V
        37: return
      LineNumberTable:
        line 19: 0
        line 20: 8
        line 21: 15
        line 20: 21
        line 23: 27
        line 24: 37
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10      17     2     i   I
            0      38     0  this   Lcom/yaorange/jvm/Test4;
            8      30     1    sb   Ljava/lang/StringBuilder;
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 10
          locals = [ class java/lang/StringBuilder, int ]
        frame_type = 250 /* chop */
          offset_delta = 16
}
SourceFile: "Test4.java"

可以看到, m1()方法中的循环体内,每一次循环都会创建StringBuilder对象,效率低于m2()方法。

13.7、小结

使用字节码的方式可以很好查看代码底层的执行,从而可以看出哪些实现效率高,哪些实现效率低。可以更好的对我们的代码做优化。让程序执行效率更高。

14 、代码优化

优化,不仅仅是在运行环境进行优化,还需要在代码本身做优化,如果代码本身存在性能问题,那么在其他方面再怎么优化也不可能达到效果最优的。

14.1、尽可能使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。

14.2、尽量减少对变量的重复计算

明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的。所以例如下面的操作:

for (int i = 0; i < list.size(); i++)
{...}

建议替换为:

int length = list.size();
for (int i = 0,  i < length; i++)
{...}

这样,在list.size()很大的时候,就减少了很多的消耗。

14.3、尽量采用懒加载的策略,即在需要的时候才创建

String str = "aaa";
if (i == 1){
  list.add(str);
}

//建议替换成
if (i == 1){
  String str = "aaa";
  list.add(str);
}

14.4、异常不应该用来控制程序流程

异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方 法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建 了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。

14.5、不要将数组声明为public static final

因为这毫无意义,这样只是定义了引用为static final,数组的内容还是可以随意改变的,将数组声明为public更是一个安全漏洞,这意味着这个数组可以被外部类所改变。

14.6、不要创建一些不使用的对象,不要导入一些不使用的类

这毫无意义,如果代码中出现"The value of the local variable i is not used"、"Theimport java.util is never used",那么请删除这些无用的内容

14.7、程序运行过程中避免使用反射

反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是 Method的invoke方法。如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存。

14.8 、使用数据库连接池和线程池

这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程。

14.9、容器初始化时尽可能指定长度

容器初始化时尽可能指定长度,如:new ArrayList<>(10); new HashMap<>(32); 避免容器长度不足时,扩容带来的性能损耗。

14.10、ArrayList随机遍历快,LinkedList添加删除快

14.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);
}

14.12、不要手动调用System.gc();

14.13、String尽量少用正则表达式

正则表达式虽然功能强大,但是其效率较低,除非是有需要,否则尽可能少用。

replace() 不支持正则

replaceAll() 支持正则

如果仅仅是字符的替换建议使用replace()。

14.14、日志的输出要注意级别

// 当前的日志级别是error  
LOGGER.info("保存出错!" + user);

14.15、对资源的close()建议分开操作

try{
    XXX.close();
    YYY.close();
}
catch (Exception e){
    ...
}
// 建议改为
try{
    XXX.close();
}
catch (Exception e){
    ...
}
try{
    YYY.close();
}
catch (Exception e){
    ...
}