java并发编程学习整理-字节码执行引擎、垃圾回收、垃圾收集算法、垃圾收集器、GC性能-——概念性总结 weir 2018-04-22 10:52:28.0 java,并发 2403 这部分多是概念性问题总结,大家需要了解慢慢理解这里面的游戏规则,其实学习计算机语言就是这样在游戏规则里面玩才能玩的流。 你说他难吧就是太抽象了或者说我们队计算机底层了解不够,再或者说现代计算机在硬件没有质的飞跃的情况下也是绞尽脑汁的想提高计算机性能出来各种语言呀算法呀把一个简单的问题复杂化了。扯得有点远,这也是我发现越想了解底层的东西越发现在计算机世界里科技发展真的不快,至少我感觉没有满足现在我们对计算的需求。 字节码执行引擎 概述 JVM的字节码执行引擎,功能基本就是输入字节码文件,然后对字节码进行解析并处 理,最后输出执行的结果。 其实现方式可能有通过解释器直接解释执行,或者是通过即时编译器产生本地代 码,也就是编译执行,当然也可能两者皆有。 1:解释运行 以解释方式运行字节码 解释执行的意思是:读一句执行一句 2:编译运行(JIT) 将字节码编译成机器码 直接执行机器码 运行时编译,编译后性能有数量级的提升 n 栈帧 栈帧是用于支持JVM进行方法调用和方法执行的数据结构,栈帧随着方法调用而创 建,随着方法结束而销毁。里面存储了方法的局部变量、操作数栈、动态连接、方法返回 地址等信息。如下图示: 栈帧概念结构 局部变量表 用来存放方法参数和方法内部定义的局部变量的存储空间。 1:以变量槽slot为单位,目前一个slot存放32位以内的数据类型 2:对于64位的数据占2个slot 3:对于实例方法,第0位slot存放的是this,然后从1到n,依次分配给参数列表 4:然后根据方法体内部定义的变量顺序和作用域来分配slot 5:slot是复用的,以节省栈帧的空间,这种设计可能会影响到系统的垃圾收集行为 n 操作数栈 用来存放方法运行期间,各个指令操作的数据。方法执行的过程中,会有各种字节 码指令向操作数栈中写入和获取数据,也就是入栈和出栈。 1:操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配 2:在概念模型上,栈帧作为JVM栈的元素,应该是完全相互独立的。但虚拟机在实现的时候 可能会做一些优化,让两个栈帧出现部分重叠区域,以存放公用的数据,避免额外的参数 复制传递过程。 你觉得bbs内存会回收么? 加了这句bbs会回收么? 或者这样明确值null,bbs内存会回收么? 大家思考测试完这几种情况应该很容易得出结论了。 一个对象用完了主动明确的告诉jvm这个对象用完了你可以在合适的时候回收了。 动态连接 每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过 程的动态连接。 1:静态解析:类加载的时候,符号引用就转化成直接引用 2:动态连接:运行期间转换为直接引用 n 方法返回地址 方法执行后,要么正常完成并退出,要么发生异常导致退出。不管哪种方式,都需 要返回到方法被调用的位置,程序才能继续执行,这个位置就是方法返回地址。 n 方法调用概述 方法调用就是确定具体调用那一个方法,并不涉及方法内部的执行过程。 1:部分方法是直接在类加载的解析阶段,就确定了直接引用关系,包括:静态方法、私有方 法、实例构造器、父类方法这几种 2:但是对于实例方法,也称虚方法,因为重载和多态,需要运行期动态委派(dispatch) 分派 又分成静态分派和动态分派 1:静态分派:所有依赖静态类型来定位方法执行版本的分派方式,比如:重载方法 2:动态分派:根据运行期的实际类型来定位方法执行版本的分派方式,比如:覆盖方法 n 单分派和多分派 就是按照分派思考的纬度,多余一个的就算多分派,只有一个的称为单分派。 n 如何执行方法中的字节码指令 JVM通过基于栈的字节码解释执行引擎来执行指令,JVM的指令集也是基于栈的。 cmd运行 javap: D:\git\springbootdubbo\suanfa\target\classes\weir\jvm\execute>javap -v E1.class Classfile /D:/git/springbootdubbo/suanfa/target/classes/weir/jvm/execute/E1.class Last modified 2018-4-21; size 875 bytes MD5 checksum f2251b526f54fdacf772dc65159bd080 Compiled from "E1.java" public class weir.jvm.execute.E1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Class #2 // weir/jvm/execute/E1 #2 = Utf8 weir/jvm/execute/E1 #3 = Class #4 // java/lang/Object #4 = Utf8 java/lang/Object #5 = Utf8#6 = Utf8 ()V #7 = Utf8 Code #8 = Methodref #3.#9 // java/lang/Object."":()V #9 = NameAndType #5:#6 // "":()V #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lweir/jvm/execute/E1; #14 = Utf8 bb #15 = Utf8 (II)I #16 = Utf8 a #17 = Utf8 I #18 = Utf8 b #19 = Utf8 c #20 = Utf8 main #21 = Utf8 ([Ljava/lang/String;)V #22 = Methodref #1.#9 // weir/jvm/execute/E1."":()V #23 = Methodref #1.#24 // weir/jvm/execute/E1.bb:(II)I #24 = NameAndType #14:#15 // bb:(II)I #25 = Fieldref #26.#28 // java/lang/System.out:Ljava/io/PrintStream; #26 = Class #27 // java/lang/System #27 = Utf8 java/lang/System #28 = NameAndType #29:#30 // out:Ljava/io/PrintStream; #29 = Utf8 out #30 = Utf8 Ljava/io/PrintStream; #31 = Class #32 // java/lang/StringBuilder #32 = Utf8 java/lang/StringBuilder #33 = String #34 // bb=== #34 = Utf8 bb=== #35 = Methodref #31.#36 // java/lang/StringBuilder."":(Ljava/lang/String;)V #36 = NameAndType #5:#37 // "":(Ljava/lang/String;)V #37 = Utf8 (Ljava/lang/String;)V #38 = Methodref #31.#39 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; #39 = NameAndType #40:#41 // append:(I)Ljava/lang/StringBuilder; #40 = Utf8 append #41 = Utf8 (I)Ljava/lang/StringBuilder; #42 = Methodref #31.#43 // java/lang/StringBuilder.toString:()Ljava/lang/String; #43 = NameAndType #44:#45 // toString:()Ljava/lang/String; #44 = Utf8 toString #45 = Utf8 ()Ljava/lang/String; #46 = Methodref #47.#49 // java/io/PrintStream.println:(Ljava/lang/String;)V #47 = Class #48 // java/io/PrintStream #48 = Utf8 java/io/PrintStream #49 = NameAndType #50:#37 // println:(Ljava/lang/String;)V #50 = Utf8 println #51 = Utf8 args #52 = Utf8 [Ljava/lang/String; #53 = Utf8 ee #54 = Utf8 SourceFile #55 = Utf8 E1.java { public weir.jvm.execute.E1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #8 // Method java/lang/Object."":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lweir/jvm/execute/E1; public int bb(int, int); descriptor: (II)I flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=3 0: iload_1 1: iload_2 2: iadd 3: istore_3 4: iload_1 5: iload_3 6: iadd 7: ireturn LineNumberTable: line 6: 0 line 7: 4 LocalVariableTable: Start Length Slot Name Signature 0 8 0 this Lweir/jvm/execute/E1; 0 8 1 a I 0 8 2 b I 4 4 3 c I public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=3, args_size=1 0: new #1 // class weir/jvm/execute/E1 3: dup 4: invokespecial #22 // Method "":()V 7: astore_1 8: aload_1 9: iconst_3 10: bipush 7 12: invokevirtual #23 // Method bb:(II)I 15: istore_2 16: getstatic #25 // Field java/lang/System.out:Ljava/io/PrintStream; 19: new #31 // class java/lang/StringBuilder 22: dup 23: ldc #33 // String bb=== 25: invokespecial #35 // Method java/lang/StringBuilder."":(Ljava/lang/String;)V 28: iload_2 29: invokevirtual #38 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 32: invokevirtual #42 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 35: invokevirtual #46 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 38: return LineNumberTable: line 21: 0 line 22: 8 line 23: 16 line 26: 38 LocalVariableTable: Start Length Slot Name Signature 0 39 0 args [Ljava/lang/String; 8 31 1 ee Lweir/jvm/execute/E1; 16 23 2 bb I } SourceFile: "E1.java" D:\git\springbootdubbo\suanfa\target\classes\weir\jvm\execute> 看到这些不要蒙稍微耐心一点也不用太上心,稍微理解看看 大家可以试着分析局部变量表和操作数栈之间怎么数据交互(这里我就不啰嗦了,需要认知的栈的数据结构,一直往下压数据,出的话一个一个出,也就是只有一头可以进可以出) 今天刚好看到一篇文章可以结合学习一下( Java字节码的介绍 ) 垃圾回收 什么是垃圾 简单的说就是内存中已经不再被使用到的空间就是垃圾 1引用计数法 1:给对象添加一个引用计数器,有访问就加1,引用失效就减1。 2:优点:实现简单、效率高 3:缺点:不能解决对象之间循环引用的问题 1根搜索算法(可达性分析算法) 1:从根(GC Roots)节点向下搜索对象节点,搜索走过的路经称为引用链,当一个对象到根 之间没有连通的话,则该对象不可用。 2:可作为GC Roots的对象包括: 虚拟机栈(栈帧局部变量)中引用的对象 方法区类静态属性引用的对象 方法区中常量引用的对象 本地方法栈中JNI引用的对象 3:HotSpot使用了一组叫做OopMap的数据结构达到准确式GC的目的:在类加载完成的时候,JVM会计算出当 前对象在哪个偏移位置上会有什么引用,这样GC扫描的时候可以很快得到引用的信息。 4:在OopMap的协助下,JVM可以很快的做完GC Roots枚举。但是JVM并没有为每一条指令生成一个OopMap, 否则会需要非常多的额外空间,反而会增加GC回收成本。因此只在特定的位置才会记录这些信息,这些 “特定的位置”被称为安全点(SafePoint),即当前线程执行到安全点后才允许暂停进行GC。 5:如果一段代码中,对象引用关系不会发生变化,这个区域中任何地方开始GC都是安全的,那么这个区域 称为安全区域(Safe Region)。安全区域可看做是扩展了的安全点。 n 引用分类 1:强引用:类似于Object a = new A()这样的,不会被回收 2:软引用:还有用但并不必须的对象。这些对象会当作回收的第二梯队,如果回收这些对象后内存还是不 够,才发生内存溢出。用SoftReference来实现软引用。 3:弱引用:非必须对象,比软引用还要弱,垃圾回收时会回收掉。用WeakReference来实现弱引用。 4:虚引用:也称为幽灵引用或幻影引用,是最弱的引用。垃圾回收时会回收掉。用PhantomReference来实 现弱引用。 n 判断类无用的条件 1:JVM中该类的所有实例都已经被回收 2:加载该类的ClassLoader已经被回收 3:没有任何地方引用该类的Class对象 4:无法在任何地方通过反射访问这个类 n 判断是否垃圾的步骤 1:根搜索算法判断不可用 2:看是否有必要执行finalize方法,因为这个方法还可以让对象重新被使用。当对象没有覆 盖finalize方法,或者是这个方法已经被JVM调用过,就属于没有必要执行finalize。 两个步骤走完后对象仍然没有人使用,那么就属于垃圾,可被回收。 n GC类型 1:MinorGC:发生在新生代的收集动作 2:MajorGC / Full GC:发生在老年代的GC,通常伴随至少一次的MinorGC n Stop-The-World STW是Java中一种全局暂停的现象,多半由于GC引起。所谓全局停顿,就是所有Java 代码停止运行,native代码可以执行,但不能和JVM交互。 其危害是长时间服务停止,没有响应;对于HA系统,可能引起主备切换,严重危害 生产环境。 n 串行、并行、并发收集 1:串行收集:GC单线程内存回收、会暂停所有的用户线程 2:并行收集:多个GC线程并发工作,此时用户线程是暂停的 3:并发收集:用户线程和GC线程同时执行(不一定是并行,可能交替执行),不需要停顿用 户线程 4:Serial是串行的,Parallel是并行的,CMS是并发的 垃圾收集算法 n 垃圾收集算法——标记清除法(Mark-Sweep) 算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象。形如: 1:优点是简单 2:缺点是: 效率不高,标记和清除的效率都不高 标记清除后会产生大量不连续的内存碎片,从而导致在分配大对象时触发GC 这个JVM基本不会用 n 垃圾收集算法——复制算法(Copying) 把内存分成两块完全相同的区域,每次使用其中一块,当一块使用完了,就把这块上还存活的对 象拷贝到另外一块,然后把这块清除掉。形如: 1:优点是:实现简单,运行高效,不用考虑内存碎片问题 2:缺点是:内存浪费大,只能使用一半 3:JVM实际实现中,是将内存分为一块较大的Eden区和两块较小的Survivor空间,每次使用Eden和一块 Survivor,回收时,把存活的对象复制到另一块Survivor。 4:HotSpot默认的Eden和Survivor比是8:1,也就是每次能用90%的新生代空间 5:如果Survivor空间不够,就要依赖老年代进行分配担保,把放不下的对象直接进入老年代 n 分配担保 分配担保是:当新生代进行垃圾回收后,新生代的存活区放置不下,那么需要把这 些对象放置到老年代去的策略,也就是老年代为新生代的GC做空间分配担保。 1:在发生MinorGC前,JVM会检查老年代的最大可用的连续空间,是否大于新生代所有对象的 总空间,如果大于,可以确保MinorGC是安全的 2:如果小于,那么JVM会检查是否设置了允许担保失败,如果允许,则继续检查老年代最大可 用的连续空间,是否大于历次晋升到老年代对象的平均大小 3:如果大于,则尝试进行一次MinorGC 4:如果不大于,则改做一次Full GC n 垃圾收集算法——标记整理算法(Mark-Compact) 由于复制算法在存活对象比较多的时候,效率较低,且有空间浪费,因此老年代一 般不会选用复制算法,老年代多选用标记整理算法。 标记过程跟标记清除一样,但后续不是直接清除可回收对象,而是让所有存活对象 都向一端移动,然后直接清除边界以外的内存。形如: 垃圾收集器 概述 前面讨论的垃圾收集算法只是内存回收的方法,垃圾收集器就来具体实现这些这些 算法并实现内存回收。不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含的收集 器如下图所示: Serial(串行)收集器/Serial Old收集器 1:是一个单线程的收集器,在垃圾收集时,会Stop-the-World 2:优点是简单,对于单cpu,由于没有多线程的交互开销,可能更高效,是默认的Client模式 下的新生代收集器 3:使用-XX:+UseSerialGC来开启 会使用:Serial + Serial Old 的收集器组合 4:新生代使用复制算法,老年代使用标记-整理算法 n ParNew(并行)收集器 1:使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World 2:在并发能力好的CPU环境里,它停顿的时间要比串行收集器短;但对于单cpu或并发能力较 弱的CPU,由于多线程的交互开销,可能比串行回收器更差 3:是Server模式下首选的新生代收集器,且能和CMS收集器配合使用 4:使用-XX:+UseParNewGC来开启 会使用:ParNew + Serial Old的收集器组合 5:-XX:ParallelGCThreads:指定线程数,最好与CPU数量一致 6:新生代使用复制算法,老年代采用标记-整理算法 n 新生代Parallel Scavenge收集器/Parallel Old收集器 是一个应用于新生代的、使用复制算法的、并行的收集器,跟ParNew很类似,但更关注吞吐量 (CPU吞吐量就是运行应用代码的时间/总运行时间,这种收集器能最高效率的利用CPU,适合运行后台 应用)。 1:使用-XX:+UseParallelGC来开启新生代Parallel Scavenge收集器 2:使用-XX:+UseParallelOldGC来开启老年代使用Parallel Old收集器,使用Parallel Scavenge + Parallel Old的收集器组合 3:-XX:GCTimeRatio:指定运行应用代码的时间占总时间的比例,默认99,即1%的时间用来进行垃圾收集 4:-XX:MaxGCPauseMillis:设置GC的最大停顿时间 5:新生代使用复制算法,老年代使用标记-整理算法 n CMS(并发标记清除)收集器 1:分为四个阶段 初始标记:只标记GC Roots能直接关联到的对象 并发标记:进行GC Roots Tracing的过程 重新标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象 并发清除:并发回收垃圾对象 2:在初始标记和重新标记两个阶段还是会发生Stop-the-World 3:使用标记清除算法,也是一个使用多线程并发收集的垃圾收集器 4:最后的重置线程,指的是清空跟收集相关的数据并重置,为下一次收集做准备 5:优点:低停顿、并发执行 6:缺点: (1)并发执行,对CPU资源压力大 (2)无法处理在处理过程中产生的垃圾,可能导致FGC (3)采用的标记清除算法会导致大量碎片,从而在分配大对象是可能触发FGC 7:可设置的参数有: -XX:UseConcMarkSweepGC:使用ParNew + CMS + Serial Old的收集器组合,Serial Old将作为CMS出错 的后备收集器 -XX:ParallelCMSThreads:设定CMS的线程数量,默认是(ParallelGCThreads+3)/4 -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发回收,默认68% -XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的 整理,默认是开启的 -XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾收集后,进行一次内存整理 -XX:+CMSClassUnloadingEnabled:允许对类元数据进行收集 -XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS收集 n G1(Garbage-First)收集器 G1是一款面向服务端应用的收集器,jdk1.9开始就默认了,与其它收集器相比,具有如下特点: 1:G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW 2:G1仍然采用分代的思想,对存活时间较长,经过多次GC仍存活的对象,有不同的处理方 式,以获取更好的收集效果 3:G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。 4:G1把内存划分成多个独立的区域(Region),保留了新生代和老年代,但它们不再是物理隔 离的,而是一部分Region的集合,且不需要Region是连续的 5:G1的停顿可预测,能明确制定在一个时间段内,消耗在垃圾收集上的时间不能超过多长时 间。 6:G1跟踪各个Redion里面垃圾堆的价值大小,在后台维护一个优先列表,每次根据允许的时 间来回收价值最大的区域,从而保证在有限时间内的高效收集 7:跟CMS类似,也分为四个阶段 初始标记:只标记GC Roots能直接关联到的对象 并发标记:进行GC Roots Tracing的过程 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象 筛选回收:根据时间来进行价值最大化的回收 形如: n 使用和配置G1 1:-XX:+UseG1GC:开启G1 2:-XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停 顿小于这个时间 3:-XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45 4:-XX:NewRatio=n:默认为2 5:-XX:SurvivorRatio=n:默认为8 6:-XX:MaxTenuringThreshold=n:新生代到老年代的岁数,默认是15 7:-XX:ParallelGCThreads=n:并行GC的线程数,默认值会根据平台不同而不同 8:-XX:ConcGCThreads=n:并发GC使用的线程数 9:-XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的 风险,默认值是10% 10:-XX:G1HeapRegionSize=n:设置的G1区域的大小。值是2的幂,范围是1MB到32MB。目标是 根据最小的Java 堆大小划分出约2048 个区域。 GC性能 n GC性能指标 1:吞吐量= 应用代码执行的时间/运行的总时间 2:GC负荷,与吞吐量相反,是GC时间/运行的总时间 3:暂停时间,就是发生Stop-the-World的总时间 4:GC频率,就是GC在一个时间段发生的次数 5:反映速度,就是从对象成为垃圾到被回收的时间 6:交互式应用通常希望暂停时间越少越好 n JVM内存配置原则 1:新生代尽可能设置大点,如果太小会导致: YGC次数更加频繁 可能导致YGC后的对象进入老年代,如果此时老年代满了,会触发FGC 2:老年代 (1)针对响应时间优先的应用:由于老年代通常采用并发收集器,因此其大小要综合考虑并 发量和并发持续时间等参数,如果堆设置小了,可能会造成内存碎片,高回收频率会导致 应用暂停;如果堆设置大了,会需要较长的回收时间 (2)针对吞吐量优先的应用:通常设置较大的新生代和较小的老年代,这样可以尽可能回收 大部分短期对象,减少中期对象,而老年代尽量存放长期存活的对象 3:依据对象的存活周期进行分类,对象优先在新生代分配,长时间存活的对象进入老年代 4:根据不同代的特点,选取合适的收集算法 少量对象存活,适合复制算法大量对象存活,适合标记清除或者标记整理