JVM虚拟机---常用JVM配置参数
常用JVM配置参数
常用JVM配置参数主要有:Trace跟踪参数、堆的分配参数、栈的分配参数。
一、Trace跟踪参数
跟踪参数用于跟踪监控JVM,对于开发人员来讲用于JVM调优以及故障排查的。
1、打印GC的简要信息
-verbose:gc -XX:+PrintGC
这两个配置参数效果是一样的,都是在发生GC时打印出简要的信息,例如执行代码
public static void main(String[] args) { byte[] bytes = null; for (int i = 0; i < 100; i++) { bytes = new byte[1 * 1024 * 1024]; } }
这个程序连续创建了100个1M的数组对象,使用-XX:+PrintGC或-verbose:gc参数执行该程序,即可查看到GC情况:
我们可以看到程序执行了2次GC(minor GC),这两次GC都是新生代的GC。
33038K表示回收前对象占用空间。1760K表示回收后对象占用空间。125952K表示还有多少空间可用。0.0044838 secs表示这次垃圾回收花的时间。
2、打印GC的详细信息以及堆使用详细信息
--打印GC详细信息 -XX:+PrintGCDetails --打印CG发生的时间戳 -XX:+PrintGCTimeStamps
使用-XX:+PrintGCDetails参数,查看控制台打印结果:
通过打印GC详细信息可以得出以下结论:
(1)Heap(堆)主要分为三大块:堆分为(PSYoungGen)新生代、(ParOldGen)老年代、(Metaspace)元空间。
注意这里没有永久区了,永久区在java8已经移除,原来放在永久区的常量、字符串静态变量都移到了元空间,并使用本地内存。
(2)(PSYoungGen)新生代又分为:伊甸区(eden)和幸存区(from和to)。
思考:上面total总的为38400K,它不是应该eden+from+to=total?这是因为新生代的垃圾回收算法是采用复制算法,简单的说就是在from和to之间来回复制(复制过程中再把不可达的对象回收掉),
所以必须保证其中一个区是空的,这样才能有预留空间存放复制过来的数据,所以新生代的总大小其实等于eden+from(或to)=33280K+5120K=38400k。
思考:不同对象一般放在哪个区呢?
(1)新生代主要存放的是哪些很快就会被GC回收掉的或者不是特别大的对象。
(2)老年代则是存放那些在程序中经历了好几次回收仍然还活着或者特别大的对象。
(3)元空间在这里都是放着一些用于存储类的信息、常量池、方法数据、方法代码等。这个区中的东西比老年代和新生代更不容易回收。
3、使用外部文件记录GC的日志
-Xloggc:gc.log
我在eclipse.ini配置
-XX:+PrintGC
-Xloggc:gc.log
运行eclipse会有一个gc.log记录日志文件
二、堆的分配参数
1、-Xmx –Xms:指定最大堆和最小堆
我的eclipse.ini中这两个的配置是:
-Xms256m
-Xmx1024m
然后我们在程序中运行如下代码:
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); //系统的最大空间 System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); //系统的空闲空间 System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); //当前可用的总空间
运行效果:
2、-Xmn、-XX:NewRatio、-XX:SurvivorRatio
-Xmn
设置新生代大小
-XX:NewRatio
新生代(eden+2*s)和老年代(不包含元空间)的比值
例如:4,表示新生代:老年代=1:4,即新生代占整个堆的1/5
-XX:SurvivorRatio(幸存代)
设置两个Survivor区和eden的比值
例如:8,表示两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10
举例:
-Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=3
代表最大最小堆内存都是20M,新生代:老年代为1:1。两个Survivor区和eden的比值为2:3
3、-XX:PermSize -XX:MaxPermSize
--设置元数据区的初始空间和最大空间
有关优化总结:
(1)官方推荐新生代占堆的3/8
(2)幸存代占新生代的1/10
三、栈大小分配
1、-Xss
通常只有几百K
决定了函数调用的深度
每个线程都有独立的栈空间
局部变量、参数 分配在栈上
GC 算法与种类
对于垃圾收集(GC), 我们需要考虑三件事情:哪些内存需要回收?如何判断是垃圾对象?垃圾回收算法有哪些?
一、GC的工作区域
1、不是GC的工作区域
(1)程序计数器、虚拟机栈和本地方法栈三个区域是线程私有的,随线程生而生,随线程灭而灭;
(2)栈中的栈帧随着方法的进入和退出而进行入栈和出栈操作,每个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具有确定性。
在这几个区域不需要过多考虑回收的问题,因为方法结束或线程结束时,内存自然就跟随着回收了。
2、GC的工作区域(哪些内存需要GC回收?)
(1)垃圾回收重点关注的是堆和方法区部分的内存。
因为一个接口中的多个实现类需要的内存可能不一样,一个方法的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分
配和回收都是动态的,所以垃圾回收器所关注的主要是这部分的内存。
二、垃圾对象的判定
Java堆中存放着几乎所有的对象实例,垃圾收集器对堆中的对象进行回收前,要先确定这些对象是否还有用,哪些还活着。对象死去的时候才需要回收。
1、引用计数法
引用计数法的逻辑是:在堆中存储对象时,在对象头处维护一个counter计数器,如果一个对象增加了一个引用与之相连,则将counter++。
如果一个引用关系失效则counter–。如果一个对象的counter变为0,则说明该对象已经被废弃,不处于存活状态。
优点
1)可即刻回收垃圾,每个对象都知道自己的被引用数,当counter为0时,对象就会把自己作为空闲空间连接到空闲链表,也就是在对象变成垃圾的同时就会被回收.
2)最大暂停时间短,每次通过指向mutator生成垃圾时,这部分垃圾都会被回收,大幅削减了mutator的最大暂停时间。
缺点
1)引用和去引用伴随加法和减法,影响性能
2)很难处理循环引用
2、可达性分析算法
这种算法的基本思路是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
Java语言是通过可达性分析算法来判断对象是否存活的。
在Java语言里,可作为GC Roots的对象包括下面几种:
(1)虚拟机栈(栈帧中的本地变量表)中引用的对象。
(2)方法区中的类静态属性引用的对象。
(3)方法区中的常量引用的对象。
(4)本地方法栈中JNI(Native方法)的引用对象。
三、垃圾回收算法
1、标记-清除算法
简单来说有两个步骤:标记、清除。
(1). 标记阶段:找到所有可访问的对象,做个标记
(2). 清除阶段:遍历堆,把未被标记的对象回收
缺 点
(1)因为涉及大量的内存遍历工作,所以执行性能较低,这也会导致“stop the world”时间较长,java程序吞吐量降低;
(2)对象被清除之后,被清除的对象留下内存的空缺位置会造成内存不连续,空间浪费。
2、标记整理(压缩)算法
标记-整理算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。
(1)、标记阶段:它的第一个阶段与标记/清除算法是一模一样的。
(2)、整理阶段:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。
上图中可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲
列表显然少了许多开销。
优点
标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。
缺点
标记/整理算法唯一的缺点就是效率也不高。不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。
3、复制算法
复制算法简单来说就是把内存一分为二,但只使用其中一份,在垃圾回收时,将正在使用的那份内存中存活的对象复制到另一份空白的内存中,最后将正在使用的内存空间的对象清除,完成垃圾回收。
优点
复制算法使得每次都只对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点
复制算法的代价是将内存缩小为原来的一半,这个太要命了。
注意(重要)
现在的虚拟机使用复制算法来进行新生代的内存回收。因为在新生代中绝大多数的对象都是“朝生夕亡”,所以不需要将整个内存分为两个部分,而是分为三个部分,一块为Eden(伊面区)和两块较小的
Survivor(幸存区)空间(默认比例->8:1:1)。每次使用Eden和其中的一块Survivor,垃圾回收时候将上述两块中存活的对象复制到另外一块Survivor上,同时清理上述Eden和Survivor。所以每次新生代就可以使用90%
的内存。只有10%的内存是浪费的。(不能保证每次新生代都少于10%的对象存活,当在垃圾回收复制时候如果一块Survivor不够时候,需要老年代来分担,大对象直接进入老年代)
总的来讲:复制算法不适用于存活对象较多的场合,如老年代(复制算法适合做新生代的GC)
4、三种算法总结
相同点
(1)三个算法都基于根搜索算法去判断一个对象是否应该被回收,而支撑根搜索算法可以正常工作的理论依据,就是语法中变量作用域的相关内容。
(2)在GC线程开启时,或者说GC过程开始时,它们都要暂停应用程序(stop the world)。
区别
三种算法比较:
效率:复制算法>标记-整理算法>标记-清除算法;
内存整齐度:复制算法=标记-整理算法>标记-清除算法
内存利用率:标记-整理算法=标记-清除算法>复制算法
5、分代收集算法
首先这不是一种新算法,它是一种思想。现在使用的Java虚拟机并不是只是使用一种内存回收机制,而是分代收集的算法。就是将内存根据对象存活的周期划分为几块。一般是把堆分为新生代、和老年代。短命对
象存放在新生代中,长命对象放在老年代中。
这个图是我拷贝来的,但要记住java8以后,已经没有永久区了,之前永久区存放的东西基本上放到了元空间中。
对于不同的代,采用不同的收集算法:
新生代:由于存活的对象相对比较少,因此可以采用复制算法该算法效率比较快。
老年代:由于存活的对象比较多哈,可以采用标记-清除算法或是标记-整理算法。
垃圾回收器
收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。收集器主要分三类:串行收集器、并行收集器以及并发收集器。
一、基础概念
1、并发和并行
a:并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
b:并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
2、新生代 GC 和老年代GC
a:新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
b:老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的)。Major GC的速度一般会比Minor GC慢10倍以上。
3、吞吐量
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
二垃圾收集器
1、Serial收集器
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)
参数控制:-XX:+UseSerialGC 串行收集器
2、ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩
参数控制:-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制线程数量
Serial收集器 VS ParNew收集器
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。
然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。
3、Parallel Scavenge收集器
是一个新生代收集器,他也是使用复制算法的收集器,又是并行的多线程收集器。Parallel收集器更关注系统的吞吐量。
也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩
参数控制:-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行
Parallel Scavenge收集器 VS CMS等收集器
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。
Parallel Scavenge收集器 VS ParNew收集器
Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。
GC自适应的调节策略
Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小。
Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调
节策略(GC Ergonomics)。
4、Serial Old收集器
它是Serial收集器的老年代版,它同样是一个单线程收集器,使用“标记--整理”算法。
5、Parallel Old 收集器
是Parallel Scavenge收集器的老年代版,使用多线程与“标记--整理”算法。
6、CMS收集器
是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
CMS收集器就非常符合这类应用的需求。
CMS收集器是基于“标记--清除”算法实现的, 他的运作过程相对于前几种收集器来说更复杂一些,整个过程分为4个步骤:
a、初始标记(CMS inital mark):需要“stop the world”,但只标记一下GC Roots能直接关联的对象,速度很快。
b、并发标记(CMS concurrent mark):是GC Roots Tracing的过程,花费时间长
c、重新标记(CMS remark):是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
d、并发清除(CMS concurrent sweep):是并发清除无用对象。
(
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。
缺点
(1)CMS收集器对CPU资源非常敏感
在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
(2)CMS收集器无法处理浮动垃圾
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃
圾就称为“浮动垃圾”。
(3)CMS收集器会产生大量空间碎片
CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。
空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
7、G1收集器
G1(Garbage-First)是一款面向服务端应用的垃圾收集器。
与CMS收集器相比有以下优点:
(1)没有大量空间碎片
与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。
(2)可预测的停顿
这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的
时间不得超过N毫秒。
G1收集器执行过程
a、初始标记(Initial Marking)
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
b、并发标记(Concurrent Marking)
并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
c、最终标记(Final Marking)
最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set
Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
d、筛选回收(Live Data Counting and Evacuation)
筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制
的,而且停顿用户线程将大幅提高收集效率。
三、常用的收集器组合
虽然我们是在对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。
常用的收集器组合
新生代GC策略 | 年老代GC策略 | 说明 | |
组合1 | Serial | Serial Old |
Serial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。
|
组合2 | Serial | CMS+Serial Old | CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。 |
组合3 |
ParNew
|
CMS |
使用-XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。
如果指定了选项-XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。
|
组合4 |
ParNew
|
Serial Old | 使用-XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。 |
组合5 |
Parallel Scavenge
|
Serial Old |
Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。
|
组合6 |
Parallel Scavenge
|
Parallel Old |
Parallel Old是Serial Old的并行版本
|
组合7 |
G1GC
|
G1GC |
-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启
-XX:MaxGCPauseMillis =50 #暂停时间目标 -XX:GCPauseIntervalMillis =200 #暂停间隔目标 -XX:+G1YoungGenSize=512m #年轻代大小 -XX:SurvivorRatio=6 #幸存区比例 |
图形展示
图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
JVM性能调优
一、调优策略
对于GC的性能主要有2个方面的指标:吞吐量throughput(工作时间不算gc的时间占总的时间比)和暂停pause(gc发生时app对外显示的无法响应)。
1、调优的目的
调优的最终目的当然增大吞吐量,减少暂停时间咯,映射到GC层面主要关心下面这两点:
(1)将转移到老年代的对象数量降低到最小。
(2)减少full GC的执行时间。(尽量减少GC的次数)
那什么情况对象会转移到老年代,主要有这四种:
(1)新生代对象每经历依次minor gc,年龄会加一,当达到年龄阀值会直接进入老年代。阀值大小一般为15。
(2)Survivor空间中所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而无需等到年龄阀值。
(3)大对象直接进入老年代。
(4)新生代复制算法需要一个survivor区进行轮换备份,如果出现大量对象在minor gc后仍然存活的情况时,就需要老年代进行分配担保,让survivor无法容纳的对象直接进入老年代。
再来分析为什么说要减少full GC时间次数,那得先看GC的两大分类
Partial GC:并不收集整个GC堆的模式
Young GC:只收集young gen的GC
Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
Full GC:针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。这里就明白为什么要减少Full GC的次数了。
一般Full GC所花费的时间是Young GC的十倍。
2、哪些方面可以考虑调优?
为了达到上面的目的,一般地,你可以考虑调优的有:
(1)减少使用全局变量和大对象。
(2)新生代和老年代的大小是否合适。
(3)新生代和老年代所占的比例是否合适。
(4)幸存者区和新生区所占的比例到是否合适。
(5)选择合适的GC收集器。
3、什么情况说明GC已经不错了呢?
此外,如果GC执行时间满足下列所有条件,就没有必要进行GC优化了:
Minor GC执行非常迅速(50ms以内)
Minor GC没有频繁执行(大约10s执行一次)
Full GC执行非常迅速(1s以内)
Full GC没有频繁执行(大约10min执行一次)
括号中的数字并不是绝对的,它们也随着服务的状态而变化。
二、调优经验(规则)
这些规则,一般是大家比较建议的,可以作为初始配置的时候进行配置建议,当然具体的还得通过JVM工具监测来具体分析。
(1) -Xmx 和-Xms 一般设置为一样大小。这样能稍微提高GC的运行效率,因为他/她不再需要估算堆是否需要调整大小了。
(2)官方推荐新生代占堆的3/8。
(3)幸存代占新生代的1/10。
(4)垃圾收集器如果内存比较大建议G1收集器,当然也可以用CMS收集器。
(5)-XX:+DisableExplicitGC禁止System.gc(),免得程序员误调用gc方法影响性能;
(6)吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象.
(7)采用并发回收时,年轻代小一点,年老代要大,因为年老大用的是并发回收,即使时间长点也不会影响其他程序继续运行,网站不会停顿.
(8)使用CMS的好处是用尽量少的新生代, 然后老生代利用CMS并行收集, 这样能保证系统低延迟的吞吐效率。
下面附上进行JVM的优化的一些参数:
-Xmx300m 最大堆大小 -Xms300m 初始堆大小 -Xmn100m 年轻代大小 -XX:SurvivorRatio=8 Eden区与Survivor区的大小比值,设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 -XX:+UseG1GC 使用 G1 (Garbage First) 垃圾收集器 -XX:MaxTenuringThreshold=14 提升年老代的最大临界值(tenuring threshold). 默认值为 15[每次GC,增加1岁,到15岁如果还要存活,放入Old区] -XX:ParallelGCThreads=8 设置垃圾收集器在并行阶段使用的线程数[一般设置为本机CPU线程数相等,即本机同时可以处理的个数,设置过大也没有用] -XX:ConcGCThreads=8 并发垃圾收集器使用的线程数量 -XX:+DisableExplicitGC 禁止在启动期间显式调用System.gc() -XX:+HeapDumpOnOutOfMemoryError OOM时导出堆到文件 -XX:HeapDumpPath=d:/a.dump 导出OOM的路径 -XX:+PrintGCDetails 打印GC详细信息 -XX:+PrintGCTimeStamps 打印CG发生的时间戳 -XX:+PrintHeapAtGC 每一次GC前和GC后,都打印堆信息 -XX:+TraceClassLoading 监控类的加载 -XX:+PrintClassHistogram 按下Ctrl+Break后,打印类的信息
一、概念
1、jvm生命周期
启动
:当启动一个java程序时,一个jvm实例就诞生了,任何一个拥有main
方法的class都可以作为jvm实例运行的起点。
运行
:main()函数作为程序初始线程起点,其它线程由该线程启动,包括守护线程(daemon)和non-daemon(普通线程)。守护线程是JVM自己使用的线程比如GC线程就是个守护线程,只要这个jvm实例还有普通线程执行,就不会停止,但是可以用exit()强制终止程序。
消亡
:所有非守护线程退出时,JVM实例结束生命,若安全管理器允许,程序也可以使用java.lang.Runtime类或者System.exit(0)来退出。实际上exit也是用到Runtime类来退出,Runtime是个神奇的类,它还可以用于启动和关闭非java进程。
2、JVM与Class文件
我们一直说java虚拟机实现的与语言是无关的
,java虚拟机不和包含java在内的任何语言绑定,它只和与class文件
这种特殊的二进制文件格式所关联,class文件中包含了java虚拟机指令集
和符号表
以及若干其他辅助信息。基于安全方面的考虑, Java 虑拟机规范要求在 Class 文件中使用许多强制性的语法和结构化约束
,但任一门功能性语言都可以表示为一个能被 Java 虚拟机所接受的有效的 Class 文件。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将 Java 虚拟机作为语言的产品交付媒介。例如,使用 Java 编译器可以把 Java 代码编译为存储字节码的 Class 文件,使用 JRuby 等其他语言的编译器同样可以把程序代码编译成 Class 文件,虚拟机并不关心Class 的来源是何种语言,如图。
3、什么是Class文件
Java字节码类文件(.class)是Java编译器编译Java源文件(.java)产生的“目标文件”。它是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间(方便于网络的传输)。
class文件是一组以8位字节为基础单位的二进制流。
class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。
4、什么是魔数
当我们把class文件转成16进制,我们可以看到文件的头四个字节是cafe babe
,这个就称为魔数
。,它唯一作用就告诉虚拟机当前的文件就是class文件。
使用魔数而不是用扩展名来进行识别主要是基于安全考虑,因为扩展名我们可以随意通过重命名等方式改动。而通过魔数就算你把结尾改成.clss。但它同样还能在JVM运行,因为它的头部还是cafe babe
没变。
很多文件存储标准中都用魔数进行身份标识,如图片gif,jpeg都在文件头部中存储着魔数。
5、jvm常量池
我先讲下概念,接下来我会将class文件转为16进制流后,在举例说明。
常量池中每一项常量都是一个表,jdk1.8有14种结构不同的表结构,这14个表有个共同特点,就是表开始的第一位都是一个u1类型的标志位,JVM根据这个标志位[tag]来确定某个常量池项表示什么类型的字面量,比如tag为1就是指CONSTANT_utf8_info
再看常量池类型表:
这14种常量项结构还有一个特点是,其中13表占用得字节固定,只有CONSTANT_Utf8_info占用字节不固定
,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,
这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,无法确定大小不固定,编译后,通过utf-8编码,就可以知道其长度。
在看每一项常量表对应的说明:
二、16进制class文件解析
先看java代码
package com.jincou.demo.domain;
public class XiaoXiao {
private String father;
public String fatherName() {
return "小小她爹";
}
}
通过命令自动生成class文件(会在同一目录生成)
javac XiaoXiao.java
在将class文件拖入文本编辑器里,显示自然就是16进制流了,如下:
上面的表其实可以划分为以下七个部分,.class 字节码文件包括:
- 魔数与class文件版本
- 常量池
- 访问标志
- 类索引、父类索引、接口索引
- 字段表集合
- 方法表集合
- 属性表集合
这篇博客只讲到常量池,其它的下篇讲,接下来我们一行一行解释,首先是:
cafe babe
:上面说过了这个是魔数,告诉JVM虚拟机我就是class文件。
0000 0034
:次版本号组成u2+主版本号u2。共占4个字节。0034转10进制为52,代表当前JDK版本为1.8。
0013
:说明有19-1即18个常量。
上面这些位置是固定的。接下来就是说明每一个常量:
0a
:这就是tag代表一个标志,0a代表10,去找常量池列表。
得知它是一个接口中方法的符号引用,然后去找CONSTANT_Methodref_info
对应常量列表描述:
从常量列表我们可以知道该类型一共占了5u,即0a00 0400 0f
,那么下一个tag就是08
代表字符串类型常量,以此类推就可以知道一共18个常量的信息。
三、class反编译
通过上面看16进制的却太麻烦了,现在我们可以通过JDK自带反编译工具查看会更加清晰。
javap -verbose 文件名
通过反编译看去就很直观,比如第一个字符常量很明显告诉你是CONSTANT_Methodref_info
,而且对于的就是4和15和上面完美对应。
最后思考,到底哪些会放到常量池?
1.常量池可以理解为class文件中的资源仓库,有很多种类型,主要存放两大常量
①.字面量
字面量就是通俗理解的java常量,如文本字符串,8大基本数据类型,final修饰的常量值等
②.符号引用
符号引用属于编译原理的概念,主要包含以下三种
1)类和接口的全限定名
2)字段的名称和描述符
3)方法的名称和描述符
上一篇博客讲【JVM虚拟机】(5)---深入理解JVM-Class中常量池
我们知道一个class文件正常可以分为7个部分:
- 魔数与class文件版本
- 常量池
访问标志
类索引、父类索引、接口索引
- 字段表集合
- 方法表集合
- 属性表集合
那么这篇博客主要讲有关 访问标志 和 类索引、父类索引、接口索引 相关的理解和代码示例。
先通俗的说下这两个的作用:
访问标志
: 告知该类是一个什么类型的类,是普通类?还是接口?还是枚举?或者其它类,是用什么修饰符修饰该类的。
类索引、父类索引、接口索引
: 告知该类全限名的常量池地址,有继承的话父类全限名的常量池地址,实现接口的话接口全限名的常量池地址(接口可以多个)。
一、概述
先对上篇博客做个补充:上篇博客虽然说了常量池但对class整体文件结构并没有说清楚,其实一个class文件即
.class
文件本质上就是一张表
,由下表所示的数据项构成。
上图也就是一开始所讲的7个部分组成。
二、访问标志
有关访问标志
找了很多资料,也看了《深入了解java虚拟机》书中第六章给的有关访问标志的信息,网上几乎讲访问标志都是下面这张图,然后写个pulic class 类 一测试,果然是0021 代表 ACC_PUBLIC+ACC_SUPER
这样一看是没毛病。但是都没有再写一个接口来验证的,如果自己写个接口就会发现下面我圈红的地方说,JDK1.2后该处必须为真 是不对的
。先看图。
1、访问标志转为16进制解释
思考
:就好比为什么ACC_PUBLIC是00 01?如何产生的呢。
访问标志
实际上就是一系列组合,因为有16位所以共有16个标志可以使用,但是目前就定义了8个,剩下的估计是给jdk9和10......预留的吧。这8个如图所示。
讲完理论,接下来我们进行代码测试,为了校验更佳准确我写个普通类和接口分别测试:
2、public class修饰类
public class XiaoXiao {
}
在同一目录生成成class文件
javac XiaoXiao.java
在看反编译class文件
javap -v XiaoXiao.class
我们发现这里flags为: ACC_PUBLIC, ACC_SUPER
,那这么推算那么十六进制应该是0021。
那我们再来查看XiaoXiao.class的十六进制数据
完美吻合。
3、接口校验
public interface DaDa {
}
同样先生成class文件在反编译class文件
看图我们可以发现flags值为:ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
,这个也很好理解接口本身就是抽象类。那么加起来就是0601
。但这里和上图有点不符的地方就是图中说ACC_SUPER只要是JDK1.2必须为真,而这里明显不为真,所以有关这点并不准确。
那我们再将class文件转位16进制验证。
这么一来验证也是通过的。
有关ACC_SUPER不准确的问题,应该是ACC_SUPER不会
是在JDK1.2以后必须为真,应该如下描述:
三、类索引、父类索引、接口索引
1、概念
在 .class 文件中由这三项数据来确定这个类
的继承关系。
1、类索引
:u2 数据类型,用于确定这个类
的全限定名。
2、父类索引
:u2 数据类型,用于确定这个类的父类
的全限定名。
3、接口索引
:u2 数据类型的集合,用于描述类实现了哪些接口
,这些被实现的接口将按照 implements 语句后的顺序从左至右排列在接口索引集合中。
接口索引集合分为两部分,第一部分表示接口计数器
(interfaces_count),是一个 u2 类型的数据,第二部分是接口索引表
表示接口信息,紧跟在接口计数器之后。
若一个类实现的接口为 0,则接口计数器的值为 0,接口索引表不占用任何字节。
同样这里测试写两个测试类来测试。
1、普通类测试
public class XiaoXiao {
}
同样生成class文件,然后查看16进制数据
我们看到该类的类索引在常量池0002位置 ,父类索引在常量池0003位置,接口为0000代表该类没有实现任何接口。
然后我们在反编译XiaoXiao.class文件,方便我们查找常量池。
真的是一目了然,常量池0002就是当前类,0003父类为默认继承了老祖宗Object。
完美!
2、实现接口测试
为了更加深刻理解,这里再写一个类实现两个接口的类,在来查看。
//接口
public interface DaDa {
}
//接口
public interface LaLa {
}
//类实现上面两个接口
public class XiaoXiao implements DaDa ,LaLa{
}
说明
:这里不能通过 javac XiaoXiao.java
生成XiaoXiao.class文件了,因为会报错。我分析原因是因为你手动编译是无法找到DaDa ,LaLa编译信息。
所以我们可以把整个项目启动后,到target目录下去找该class文件就可以。
在打开16进制文件。
之前有关class文件已经写了两篇博客:
1、【JVM虚拟机】(5)---深入理解JVM-Class中常量池
2、【JVM虚拟机】(6)---深入理解Class中访问标志、类索引、父类索引、接口索引
那么这篇博客主要讲有关 字段表集合 相关的理解和代码示例。
字段表
:用于描述接口或者类中声明的变量,字段包括类级(static修饰)变量
以及实例级变量
,但是不包括局部变量(方法内部变量)。
一、概念
字段表集合:包括了字段计数器
和字段数据区
如图:
Field_info: 依次包含访问标志
(access_flags)、名称索引
(name_index)、描述符索引
(descriptor_index)、属性表集合
(attributes)几项。
字段修饰符
放在access_flags项目中,它与类中的access_flags项目是非常相似的,都是一个u2的数据类型.
跟随access_flags标志的是两项索引值:name_index
和descriptor_index
,它们都是对常量池的引用,分别代表着字段的简单名称
以及字段方法和方法的描述符
。
描述符的作用
:是用来描述字段的数据类型,方法的参数列表(包括数量,类型以及顺序)和返回值。
描述符规则
: 基本数据类型以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符加L加对象名的全限定名来表示:
数组类型
:每一维度将使用一个前置的"["字符来描述.如一个定义为"java.lang.Stirng[ ]"类型的二维数组,将被记录为:"[[Ljava/lang/Stirng",一个整型数组"int[]"将被记录为"[I".
用描述符来描述方法
: 按照先参数列表,后返回值的顺序来描述,参数列表按照参数的严格顺序放在一组小括号"()"之内。
字段表集合中不会列出从父类或者父接口中继承而来的字段。
二、属性表集合-----静态field字段的初始化
在定义属性字段
的过程中,我们有时候会很自然地对 属性字段
直接赋值,如下所示:
public static final int MAX=100;
public int count=0;
对于虚拟机而言,上述的两个属性字段
赋值的时机是不同的:
-
对于非静态(即无static修饰)的属性字段的赋值将会出现在实例构造方法()中
-
对于静态的属性字段,有两个选择:1、在静态构造方法()中进行;2 、使用ConstantValue属性进行赋值。
Sun javac编译器对于 静态属性字段 的初始化赋值策略
1)、如果使用final和static同时修饰一个属性字段,并且这个字段是基本类型或者String类型的,那么编译器在编译这个字段的时候,会在对应的field_info结构体中
增加一个ConstantValue
类型的结构体,在赋值的时候使用这个ConstantValue
进行赋值。
2)、如果该属性字段并没有被final修饰,或者不是基本类型或者String类型,那么将在类构造方法()中赋值。
对于上述的public static final init MAX=100; javac编译器在编译此属性字段构建field_info结构体时,除了访问标志、名称索引、描述符索引外,会增加一个ConstantValue
类型的属性表。
三、示例
1、先来个网上的例子,图片解释很好
public class Simple {
private transient static final String str ="This is a test";
}
说明
1、字段计数器中的值为0x0001,表示这个类就定义了一个属性字段
2、 字段的访问标志是0x009A,这个字段的标志符有:ACC_TRANSIENT、ACC_FINAL、ACC_STATIC、ACC_PRIVATE;
3、 名称索引中的值为0x0005,指向了常量池中的第5项,为“str”,表明这个属性字段的名称是str;
4、描述索引中的值为0x0006,指向了常量池中的第6项,为"Ljava/lang/String;",表明这个field字段的数据类型是java.lang.String类型;
5、属性表计数器中的值为0x0001,表明field_info还有一个属性表;
6、属性表名称索引中的值为0x0007,指向常量池中的第7项,为“ConstantValue”,表明这个属性表的名称是ConstantValue,即属性表的类型是ConstantValue类型的;
7、属性长度中的值为0x0002,因为此属性表是ConstantValue类型,它的值固定为2;
8、常量值索引 中的值为0x0008,指向了常量池中的第8项,为CONSTANT_String_info类型的项,表示“This is a test” 的常量。在对此field赋值时,会使用此常量对field赋值。
2、自测
package com.jincou.demo.domain;
public class XiaoXiao {
public String name = "小小";
private Integer age = 3;
public static final String sex = "女";
}
接下来看16进制文件和class反编译文件
//1、这里直接截取到访问标志服后的16进制数据,从|开始代表字段集合相关16进制
00
2100 0600 0700 00|00 0300 0100 0800 0900
0000 0200 0a00 0b00 0000 1900 0c00 0900
0100 0d00 0000 0200 0e00 0100 0100 0f00
1000 0100 1100 0000 3300 0200 0100 0000
132a b700 012a 1202 b500 032a 06b8 0004
b500 05b1 0000 0001 0012 0000 000e 0003
0000 0003 0004 0004 000a 0005 0001 0013
0000 0002 0014
//2、查看 XiaoXiao.class反编译数据信息
Constant pool:
#1 = Methodref #7.#21 // java/lang/Object."<init>":()V
#2 = String #22 // 小小
#3 = Fieldref #6.#23 // com/jincou/demo/domain/XiaoXiao.name:Ljava/lang/String;
#4 = Methodref #24.#25 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#5 = Fieldref #6.#26 // com/jincou/demo/domain/XiaoXiao.age:Ljava/lang/Integer;
#6 = Class #27 // com/jincou/demo/domain/XiaoXiao
#7 = Class #28 // java/lang/Object
#8 = Utf8 name
#9 = Utf8 Ljava/lang/String;
#10 = Utf8 age
#11 = Utf8 Ljava/lang/Integer;
#12 = Utf8 sex
#13 = Utf8 ConstantValue
#14 = String #29 // 女
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 SourceFile
#20 = Utf8 XiaoXiao.java
#21 = NameAndType #15:#16 // "<init>":()V
#22 = Utf8 小小
#23 = NameAndType #8:#9 // name:Ljava/lang/String;
#24 = Class #30 // java/lang/Integer
#25 = NameAndType #31:#32 // valueOf:(I)Ljava/lang/Integer;
#26 = NameAndType #10:#11 // age:Ljava/lang/Integer;
#27 = Utf8 com/jincou/demo/domain/XiaoXiao
#28 = Utf8 java/lang/Object
#29 = Utf8 女
#30 = Utf8 java/lang/Integer
#31 = Utf8 valueOf
#32 = Utf8 (I)Ljava/lang/Integer;
接下来我们来分析从00 03开始。
//00 03 代表示成员变量的个数,此处为3个。
1)00 01 结合上表代表第一个变量的修饰符为 public
2)00 08 找常量池第8个 name
3)00 09 找常量池第9个 String
4)00 00 用来描述该变量的属性,因为这个变量没有附加属性,所以attributes_count为0,attribute_info为空。
//接下来直接分析第三个
1)00 19 结合上表 ACC_PUBLIC+ACC_STATIC+ACC_FINAL 刚好19
2)00 0c 找常量池第12个 sex
3)00 09 找常量池第9个 String
4)00 01 代表这个变量有一个附加属性
5)00 0d 找常量池第13个 ConstantValue
6)00 0000 02 属性长度
7)00 0e 找常量池第14个 女
通过这个例子我们注意到:
1)、name = "小小"
中的小小
并没有出现,这就是上一个例子所说的,因为它不是静态变量所以不属于类,而是属于对象,所以在创建对象的时候,才会出现。
2)、sex = "女" 中的女
出现了,因为它是静态属性字段,属于类级别的所以出现。
之前有关class文件已经写了两篇博客:
1、【JVM虚拟机】(5)---深入理解JVM-Class中常量池
2、【JVM虚拟机】(6)---深入理解Class中访问标志、类索引、父类索引、接口索引
3、【JVM虚拟机】(7)---深入理解Class中-属性集合
那么这篇博客主要讲有关 方法表集合 相关的理解和代码示例。
方法表集合
: 告知该方法是什么修饰符修饰?是否有方法值?返回类型是什么?方法名称,方法参数,还有就是方法内的一些信息。
一、方法集合概念
1、概念
方法表集合
:方法表集合和属性表集合其实很相似,都是由一个计数器(方法)
和若干个方法表
构成,只不过方法表的结构相对复杂很多。
方法表的结构体
:访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表(attribute_info)集合组成。
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
1)、访问标志
不多说了,和属性中的其实差不多,只是有些修饰符不一样。
2)、名称索引
就是指这个方法的名称。如:'public void getXX()'中,getXX就是名称索引。名称索引占两个字节,这个方法的名称以UTF-8格式的字符串存储在这个常量池项中。
3)、描述索引
指这个方法的返回值,方法内参数信息。一个方法的描述包含若干个参数的数据类型和返回值的数据类型。
4)、属性表(attribute_info)集合
下面讲
二、属性表集合
1、概述
在Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。
在方法表中, 属性表集合记录了某个方法的一些属性信息,这些信息包括:
- 这个方法的代码实现,即
方法的可执行的机器指令
- 这个方法声明的要
抛出的异常信息
- 这个方法是否
被@deprecated注解表示
- 这个方法是否是
编译器自动生成的
属性表(attribute_info)结构体的一般结构如下所示:
属性表占着非常大的一部分且定义了众多属性,上面只列举了4个,查看完成的:JDK1.7版本中21项属性表集合简要介绍
下面介绍两个重要的属性
1、Code属性
code属性比较复杂,它是经过编译器编译成字节码指令之后的数据。就是说java程序中的方法体经过javac编译器处理后,最终变成字节码存储在Code属性内
。
并非所有方法表都有这个属性,接口和抽象类就没有【没有方法体】。
Code属性是Class文件中最重要的一个属性,在Class文件中,Code属性用于描述代码,所有的其它数据项目都用来描述元数据,了解code属性对了解字
节码执行引擎来说是必要基础。
Code属性表的组成部分:
机器指令——code
目前的JVM使用一个字节表示机器操作码,即对JVM底层而言,它能表示的机器操作码不多于2的 8 次方,即 256个。class文件中的机器指令部分是class文件中最重要的部分,并且非常复杂。
异常处理跳转信息
如果代码中出现了try{}catch{}
块,那么try{}块内的机器指令的地址范围记录下来,并且记录对应的catch{}块中的起始机器指令地址,当运行时在try块中有异常抛出的话,JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转;
Java源码行号和机器指令的对应关系
---LineNumberTable属性表
编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,如果抛出异常,JVM可以根据这个对应关系,抛出异常信息,告诉我们我们的源码的多少行有问题,方便我们定位问题。
Code属性表结构体:
1、attribute_name_index
: 属性名称索引,占有2个字节,其内的值指向了常量池中的某一项,该项表示字符. 串“Code”;
2、attribute_length
: 属性长度,占有 4个字节,其内的值表示后面有多少个字节是属于此Code属性表的;
3、max_stack
: 操作数栈深度的最大值,占有 2 个字节,在方法执行的任意时刻,操作数栈都不应该超过这个值,虚拟机的运行的时候,会根据这个值来设置该方法对应的栈帧(Stack Frame)中的操作数栈的深度;
4、max_locals
最大局部变量数目,占有 2个字节,其内的值表示局部变量表所需要的存储空间大小;
5、code_length
: 机器指令长度,占有 4 个字节,表示跟在其后的多少个字节表示的是机器指令;
6、code
机器指令区域,该区域占有的字节数目由 code_length中的值决定。JVM最底层的要执行的机器指令就存储在这里;
7、exception_table_length
: 显式异常表长度,占有2个字节,如果在方法代码中出现了try{} catch()形式的结构,该值不会为空,紧跟其后会跟着若干个exception_table结构体,以表示异常捕获情况;
8、exception_table
: 显式异常表,占有8 个字节,start_pc,end_pc,handler_pc中的值都表示的是PC计数器中的指令地址。exception_table表示的意思是:如果字节码从第start_pc行到第end_pc行之间出现了catch_type所描述的异常类型,那么将跳转到handler_pc行继续处理。
9、attribute_count
: 属性计数器,占有 2 个字节,表示Code属性表的其他属性的数目
10、attribute_info
: 表示Code属性表具有的属性表,它主要分为两个类型的属性表:“LineNumberTable”类型和“LocalVariableTable”类型。“LineNumberTable”类型的属性表记录着Java源码和机器指令之间的对应关系“LocalVariableTable”类型的属性表记录着局部变量描述
2、ConstantValue属性
之所以学习这个,是因为后面类加载机制有联系到这个属性
这个属性的作用是通知虚拟机为静态变量赋值,只要被static修饰的变量才有这个属性,【有该属性的字段必须有ACC_STATIC访问标志,反过来不一定】。
对于 "int x = 123" 和 "static int x =123"这类代码在日常编写中很常见,但虚拟机对这两种变量赋值的时刻却不同。
对于非static变量[实例变量],是在实例构造器<init>进行
对于类变量,有两种方式选择
①在类构造器<clinit>方法中赋值
②使用ConstantValue属性初始化
目前Sun javac编译器是这么做的【具体咋做不知道 = =】,如果同时使用final和static修饰一个变量[这种修饰就相当于个常量],并且是String或基本类型,就使用②,
如果没有被final修饰或不是基本类型和String,就选择①在<clinit>方法中初始化
//有关这点我在上篇博客举过例子,最后几句话也对这个解释的很清楚。
三、示例
有关方法的代码示例,我就不亲自测了,因为有位博主写的已经很清晰啦,我自己写也没那么清晰。
1、访问标志
public static synchronized final void greeting(){
}
greeting()方法的修饰符有:public、static、synchronized、final 这几个修饰符修饰,那么相对地,
greeting()方法的访问标志中的ACC_PUBLIC
、ACC_STATIC
、ACC_SYNCHRONIZED
、ACC_FINAL
标志位都应该是1
从上面第一张图可以得出,该访问标志的值应该是十六进制0x0039
。
2、名称索引和描述符索引
紧接着访问标志(access_flags)后面的两个字节,叫做名称索引(name_index),这两个字节中的值是指向了常量池中某个常量池项的索引,该常量池项表示这这个方法名称的字符串。
方法描述符索引(descrptor_index)是紧跟在名称索引后面的两个字节,这两个字节中的值跟名称索引中的值性质一样,都是指向了常量池中的某个常量池项。这两个字节中的指向的常量池项,是表示了方法描述符的字符串。
3、代码示例
package com.louis.jvm;
public class Simple {
public static synchronized final void greeting(){
int a = 10;
}
}
1)、 Simple.class文件如下所示
注意
:方法表集合的头两个字节,即方法表计数器(method_count)的值是0x0002,它表示该类中有2 个方法。注意到,我们的Simple.java中就定义了一个greeting()方法,为什么class文件中会显示有两个方法呢?
原因
:如果我们在类中没有定义实例化构造方法,JVM编译器在将源码编译成class文件时,会自动地为这个类添加一个不带参数的实例化构造方法,这种添加是字节码级别的,JVM对所有的类实例化构造方法名采用了相同的名称:“”。如果我们显式地如下定义Simple()构造函数,这个类编译出来的class文件和上面的不带Simple构造方法的Simple类生成的class文件是完全相同的。
2)、Simple.class 中的<init>() 方法
解释:
1、方法访问标志(access_flags)
: 占有 2个字节,值为0x0001,即标志位的第 16 位为 1,所以该<init>()方法的修饰符是:ACC_PUBLIC;
2、 名称索引(name_index)
: 占有 2 个字节,值为 0x0004,指向常量池的第 4项,该项表示字符串'<init>',即该方法的名称是'<init>';
3、描述符索引(descriptor_index)
: 占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,并且无返回值(构造函数确实也没有返回值);
4、属性计数器(attribute_count)
: 占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;
5、属性表的名称索引(attribute_name_index)
:占有 2 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
6、 属性长度(attribute_length)
:占有4个字节,值为0x0000 0011,即十进制的 17,表明后续的 17 个字节可以表示这个Code属性表的属性信息;
7、 操作数栈的最大深度(max_stack)
:占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1;
8、局部变量表的最大容量(max_variable)
:占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
9、 机器指令数目(code_length)
:占有4个字节,值为0x0000 0005,表示后续的5 个字节 0x2A 、0xB7、 0x00、0x01、0xB1表示机器指令;
10、机器指令集(code[code_length])
:这里共有 5个字节,值为0x2A 、0xB7、 0x00、0x01、0xB1;
11、显式异常表集合(exception_table_count)
: 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;
12、Code属性表的属性表集合(attribute_count)
: 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,因为我们使用了-g:none 禁止编译器生成Code****属性表的 LineNumberTable 和LocalVariableTable;
解释下机器指令集:
第一个字节 **0x2A
:查询Java 虚拟机规范中关于操作码的解释,0x2A 对应的操作是"aload_0",作用是将第一个引用类型局部变量推送至栈顶;
第二个字节 0xB7
:0xB7 对应的操作是:"invokespecial",作用是调用超类构造方法、实例初始化方法或私有方法;它****带有2个字节的参数,即后面的 0x00、0x01 是它的参数,这个参数是某个常量池中的索引,指向了常量池的第一项,该项表示一个方法引用项CONSTANT_Methodref_info结构体,表示java.lang.Object 类中的<init>()方法,即 java/lang/Object."<init>"😦)V。这条指令的意思就是调用父类Object的构造方法<init>();
第5个字符是0xB1
: 对应操作是:“Ireturn”,作用是表示无返回值的方法返回,结束方法调用,这条语句放在方法的机器码最后,表示方法结束调用,返回。
我们可以使用javap -v Simple > Simple.txt,查看反编译信息是怎样显示这一信息的:
3)Simple.class 中的greeting() 方法
解释:
1、方法访问标志(access_flags)
: 占有 2个字节,值为 0x0039 ,即二进制的00000000 00111001,即标志位的第11、12、13、16位为1,根据上面讲的方法标志位的表示,可以得到该greeting()方法的修饰符有:ACC_SYNCHRONIZED、ACC_FINAL、ACC_STATIC、ACC_PUBLIC;
2、 名称索引(name_index)
: 占有 2 个字节,值为 0x0007,指向常量池的第 7 项,该项表示字符串“greeting”,即该方法的名称是“greeting”;
3、描述符索引(descriptor_index)
: 占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,并且无返回值;
4、属性计数器(attribute_count)
: 占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;
5、属性表的名称索引(attribute_name_index)
:占有 2 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
6、属性长度(attribute_length)
:占有4个字节,值为0x0000 0010,即十进制的16,表明后续的16个字节可以表示这个Code属性表的属性信息;
7、操作数栈的最大深度(max_stack)
:占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1;
8、 局部变量表的最大容量(max_variable)
:占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
9、器指令数目(code_length)
:占有4 个字节,值为0x0000 0004,表示后续的4个字节0x10、 0x0A、 0x3B、0xB1的是表示机器指令;
10、机器指令集(code[code_length])
:这里共有4 个字节,值为0x10、 0x0A、 0x3B、0xB1 ;
11、显式异常表集合(exception_table_count)
: 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;
12 Code属性表的属性表集合(attribute_count)
: 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,因为我们使用了-g:none 禁止编译器生成Code****属性表的 LineNumberTable 和LocalVariableTable;
指令集解释
第一个字节 0x10
: 查询Java虚拟机规范中关于操作码的解释,0x10 对应的操作是"bipush"," 作用是将单字节的常量值(-128~127) 推送至栈顶,它要求一个参数,后面的 0x0A 即是需要推送到栈顶的单字节,注意这里的 0x0A 是16进制,就是我们在代码里写的"a=10"中的10。
第三个字节"3B"
: “3B”对应的操作是:"istore_0",作用是将栈顶int 型数值存入第一个局部变量。我们在greeting() 方法中就声明了一个局部变量a,JVM的运行的时候,将这个局部变量a解析,并放置到局部变量表中的第一个位置;上述的0x10 0x0A 指令已经将0x0A 推送到了栈顶了,然后 0x3B指令便将栈顶的0x0A 取出,赋值给局部变量表中的第一个参数,即局部变量a,这样就完成了对局部变量a的赋值;
第4个字符是0xB1
: 对应操作是:“Ireturn”,作用是表示无返回值的方法返回,结束方法调用,这条语句放在方法的机器码最后,表示方法结束调用,返回。
我们可以使用javap -v Simple > Simple.txt,查看反编译信息是怎样显示这一信息的:
上篇博客我们简单说过异常信息是存放在属性表集合中的Code属性表里
,那么这篇博客就单独讲Code属性表中的exception_table。
在讲之前我们先思考两个问题?
1、为什么捕获异常会较大的性能消耗?
2、为什么finally中的代码会永远执行?
接下来会从JVM虚拟机的角度来解答这两个问题。
一、概念
1、JVM是如何捕获异常的?
1、编译而成的字节码中,每个方法都附带一个异常表
。
2、异常表中每一个条目代表一个异常处理器
3、触发异常时,JVM会遍历异常表,比较触发异常的字节码的索引值是否在异常处理器的from指针到to指针
的范围内。
4、范围匹配后,会去比较异常类型和异常处理器中的type是否相同
。
5、类型匹配后,会跳转到target指针所指向的字节码
(catch代码块的开始位置)
6、如果没有匹配到异常处理器,会弹出当前方法对应的Java栈帧
,并对调用者重复上述操作。
2、什么是异常表?
1. 每个方法都附带一个异常表
2. 异常表中每一个条目, 就是一个异常处理器
异常表如下:
3、什么是异常处理器?其组成部分有哪些?
1、异常处理器由from指针、to指针、target指针,以及所捕获的异常类型所构成(type)。
2、这些指针的数值就是字节码的索引(bytecode index, bci),可以直接去定位字节码。
3、from指针和to指针,标识了该异常处理器所监控的返回
4、target指针,指向异常处理器的起始位置。如catch代码块的起始位置
5、type:捕获的异常类型,如Exception
4、如果在方法的异常表中没有匹配到异常处理器,会怎么样?
1、会弹出当前方法对应的Java栈帧
2、在调用者上重复异常匹配的流程。
3、最坏情况下,JVM需要编译当前线程Java栈上所有方法的异常表
二、代码演示
1、try-catch
public static void main(String[] args) {
try {
mayThrowException();
} catch (Exception e) {
e.printStackTrace();
}
}
// 对应的 Java 字节码
public static void main(java.lang.String[]);
Code:
0: invokestatic mayThrowException:()V
3: goto 11
6: astore_1
7: aload_1
8: invokevirtual java.lang.Exception.printStackTrace
11: return
Exception table:
from to target type
0 3 6 Class java/lang/Exception // 异常表条目
上面Code中的字节码自己也没有仔细研究过,我们可以具体看下面的Exception table表,来进行分析。
1、from和to
: 指是try和catch之间的代码的索引位置。from=0,to=3,代表从字节索引0的位置到3(不包括3)。
2、target
: 代表catch后代码运行的起始位置。
3、type
: 指的是异常类型,这里是指Exception异常。
当程序触发异常时,java虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常
和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目target 指针指向的字节码。
如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的Java 栈帧
,并且在调用者(caller)中重复上述操作。在最坏情况下,
Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。
2、try-catch-finally
finally 代码块的编译比较复杂。当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中
。
代码示例
public static void XiaoXiao() {
try {
dada();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("Finally");
}
}
//通过javap 反编译
public static void XiaoXiao();
Code:
0: invokestatic #3 // Method dada:()V
3: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
6: ldc #7 // String Finally
8: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
11: goto 41
14: astore_0
15: aload_0
16: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
19: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
22: ldc #7 // String Finally
24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: goto 41
30: astore_1
31: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
34: ldc #7 // String Finally
36: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
39: aload_1
40: athrow
41: return
Exception table:
from to target type
0 3 14 Class java/lang/Exception
0 3 30 any
14 19 30 any
和之前有所不同,这次
1、异常表中,有三条数据,而我们仅仅捕获了一个Exception
2、异常表的后两个item的type为any
上面的三条异常表item的意思为
1、如果0到3之间,发生了Exception类型的异常,调用14位置的异常处理者。
2、 如果0到3之间,无论发生什么异常,都调用30位置的处理者。
3、 如果14到19之间(即catch部分),不论发生什么异常,都调用30位置的处理者。
`问题`:通过上面那幅图和javap反编译代码就可以很好的解释为什么finally里面的代码永远会执行?
原因:因为当前版本Java编译器的做法,是复制finally代码块的内容,分别放到所有正常执行路径,以及异常执行路径的出口中
。
这三份finally代码块都放在什么位置:
第一份位于try代码后 : 若果try中代码正常执行,没有异常那么finally代码就在这里执行。
第二份位于catch代码后 : 如果try中有异常同时被catch捕获,那么finally代码就在这里执行。
第三份位于异常执行路径 : 如果如果try中有异常但没有被catch捕获,或者catch又抛异常,那么就执行最终的finally代码。问题
:为什么捕获异常会较大的性能消耗?
因为构造异常的实例比较耗性能
。这从代码层面很难理解,不过站在JVM的角度来看就简单了,因为JVM在构造异常实例时需要生成该异常的栈轨迹
。这个操作会逐一访问当前
线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息。虽然具体不清楚JVM的实现细节,但
是看描述这件事情也是比较费时费力的。