首页 > 其他分享 >《深入理解JVM虚拟机》

《深入理解JVM虚拟机》

时间:2023-03-14 20:47:36浏览次数:44  
标签:Java 收集器 对象 虚拟机 深入 内存 JVM 线程

目录

相关书籍:

  • 《Java虚拟机规范》
  • 《Java语言规范》
  • 《垃圾回收算法手册:自动内存管理的艺术》
  • 《Virtual Machines:Versatile Platforms for Systems and Processes》
  • 《Java性能优化权威指南》,该系列中最出名的《Effective Java》许多人都读

历史笔记参考:https://www.cnblogs.com/bingmous/p/15643697.html

走进Java

HotSpot本身会有一定的内存消耗,约几十MB,这对最低也从几GB内存起步的大型单体应用来说并不算什么,但在微服务
下就是一笔不可忽视的成本。

编译jdk

jdk源码编译、调试参考1.6章节

Java内存区域与内存溢出异常

运行时数据区域

ava虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:

  • 方法区(所有线程共享)
  • 堆(所有线程共享)
  • 虚拟机栈(线程隔离)
  • 本地方法栈(线程隔离)
  • 程序计数器(线程隔离)

程序计数器:可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈:虚拟机栈描述的是Java方法执行的线程内存模型,:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)

本地方法栈:为本地方法服务

Java堆:此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是所有的对象实例以及数组都应当在堆上分配。Java堆是垃圾收集器管理的内存区域。

如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。

方法区:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。在《Java虚拟机规范》中描述为堆的一个逻辑部分。为了与堆区分也叫非堆。

jdk7时将静态变量、字符串常量池由永久代改到了堆(原来在运行时常量池中),jdk8时将永久代剩余的类型信息、域信息、方法信息、JIT代码缓存、运行时常量池都改到了本地内存,并改名为元空间。

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了类的版本、字段、方法、接口等描述信息,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行期间也可以将新的变量放入常量池,比如String的intern()方法。

直接内存:jdk1.4新加入了NIO(New Input/Output)引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。

HotSpot虚拟机对象探秘

对象的创建(普通对象,不包括数组、Class):Java虚拟机遇到new指令时,首先检查指令的参数是否能在运行时常量池中定位到一个类的符号引用,并且检查符号引用代表的类是否已被加载、解析和初始化过。如果没有进行类加载。类加载检查通过后,接下来虚拟机为新生对象分配内存(分配方式有指针碰撞、空闲列表,分配方式由Java堆是否规整决定,Java堆是否规整由采用的垃圾收集器是否带有空间压缩整理能力决定)。

对象创建的线程安全问题:一种是对象分配空间的动作进行同步处理(实际上虚拟机采用CAS+重试方式保证原子性),一种是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地缓冲区TLAB,本地缓冲区用完了分配新的缓冲区时才需要同步。虚拟机是否使用TLAB,可以通过+/-UseTLAB设置。

内存分配完成后虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。之后执行init方法初始化

总结:类加载检查 -> 分配内存 -> zero初始化 -> init方法

对象的内存布局:对象头(运行时元数据、类型指针)、实例数据、对齐填充。因为HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,因此任何对象的大小都必须是8字节的整数倍。对象头已经被设计成8字节的1倍或2倍(动态定义的)

对象的访问定位:主流的访问方式主要有使用句柄和直接指针两种

OOM异常

Java堆溢出
-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存异常的时候dump出当前的堆转储快照以便事后分析,如Eclipse Memory Analyzer,首先判断是内存泄露还是内存溢出。

内存泄露:查看泄露对象到GCRoots的引用链,找到泄露对象通过怎样的引用路径、与哪些GCRoots关联,才导致垃圾收集器无法回收,根据类型信息及GCRoots引用链的信息一般可以准确的定位这些对象的创建位置,进而找出产生内存泄露的代码的具体位置。

内存溢出:也就是内存中的对象都必须是存活的,那么就需要检查Java虚拟机的堆参数是否有上调的空间,检查代码上是否某些对象生命周期过长、持有状态时间过长、存储结构不合理等情况,尽量减少运行期的内存消耗。

详见虚拟机性能监控、故障处理工具章节。

虚拟机栈和本地方法栈溢出
Hotspot虚拟机不区分虚拟机栈和本地方法栈(且不支持扩展),-Xoss设置本地方法栈大小实际上没有任何效果,栈容量只能由-Xss参数确定。《Java虚拟机规范》允许Java虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机的选择是不支持扩展,所以除非在线程申请内存时就出现OOM,否则线程运行时是不会OOM的,只会因栈容量不足导致StackOverflowError。

操作系统分配给每个进程的内存是有限制的(如32位windows是2G),减去最大堆、方法区,剩下的就由虚拟机栈和本地方法栈来分配了,每个线程分配的栈内存越大,可以建立的线程数量就越小,建立线程时就越容易把剩下的内存耗尽。

虚拟机线程实现方面的内容参考Java内存模型与线程章节。

方法区和运行时常量池溢出
在jdk7及之后,字符串常量池放在了堆中,调用intern()方法会把首次出现的字符串在堆中的引用放在常量池,返回的也是堆中的地址。如果常量池中已经存在了这个字符串,那么intern()返回的是常量池中的地址。intern()方法的原则是将首次遇到的字符串放在常量池。

jdk8之后,永久代被元空间代替,使用本地内存,-XX:MetaspaceSize起始大小,-XX:MaxMetaspaceSize最大元空间大小,-XX:MinMetaspaceFreeRatio,-XX:MaxMetaspaceFreeRatio最小最大元空间的比例,用于控制垃圾回收方法区的频率。

本机直接内存溢出
直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致。

由直接内存导致的内存溢出堆dump文件会很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。

总结

java程序总内存=堆内存+直接内存(元空间+自定义使用的)+每个线程占用的内存(虚拟机栈、本地方法栈、程序计数器)+jvm本身占用

垃圾收集器与内存分配策略

对象已死?

引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。Java虚拟机没有采用这种算法,因为有很多例外情况要考虑,必须配合大量额外处理才能保证正常工作,比如对象之间的相互循环引用。

可达性分析算法
通过一系列GCRoots的根对象作为起始节点集,从这些起点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到GCRoots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

可作为GCRoots的对象包括以下几种:

  • 虚拟机栈中(栈帧的本地变量表)引用的对象,各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。只针对某一块区域进行垃圾回收时,该区域的对象完全有可能被其他区域的对象引用,这时候需要将这些关联的对象也加入到GCRoots中,才能保证可达性分析算法的正确性。

再谈引用

  • 强引用是指在程序代码之中普遍存在的引用赋值
  • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
  • 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

生存还是死亡
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的。要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

即如果重写了finalize()方法,在垃圾回收第一次标记后,进入F-Queue,稍后由虚拟机启动一个线程执行队列中的所有对象的finalize()方法(并不承诺一定会执行结束,因为它有可能执行很慢或出现了死循环导致其他对象无法执行finalize()),稍后会对队列中的对象进行第二次小规模的标记,如果还没有自救,那么就会被回收,如果自救成功,那么下一次垃圾回收时就不会在执行finalize()方法了,直接回收。

回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型

常量:如字符串常量,其他类、方法、字段的符号引用类似,都是使用可达性分析,不可达而且垃圾收集器判断有必要的话即可回收。

类型回收条件比较苛刻:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。HotSpot虚拟机提供了一些列参数进行控制。

垃圾收集算法

相关算法细节参考《垃圾回收算法手册》2-4章

分代收集理论
分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
3)跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

标记-清除算法
分为两个阶段,标记和清除,首先标记出要回收的对象,标记完成后统一回收掉所有被标记的对象,也可以反过来。

缺点主要有两个:

  • 执行效率不稳定,如果Java堆中有大量需要被回收的对象,这时需要大量的标记和清除动作,导致标记和清除两个过程的执行效率都随对象数量的增长而降低。
  • 内存空间的碎片化问题,标记清除之后导致存在大量不连续的内存碎片,会导致程序以后在分配大对象时无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。

标记-复制算法
解决标记-清除算法中的有大量需要被回收的对象效率低的问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当一块的内存用完了,就将还活着的对象赋值到另一块上面,在把已使用过的内存空间一次性清理掉。

缺点:

  • 可用内存缩小一半,浪费了一般空间。
  • 在对象存活率较高时,要进行比较多的复制操作,效率降低。更关键的是如果不想浪费空间,就需要额外的空间分配担保,所以在老年代中一般不能直接采用这种算法。

标记-整理算法
标记过程与标记-清除算法一样,但随后不直接回收对象,而是让所有存活对象都向空间一段移动,然后直接清理掉边界以外的内存。

与标记-清除算法的本质差异在于前者是一种非移动式的回收算法,后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的决策。

  • 如果移动,尤其对于老年代这种每次回收都有大量存活对象的区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。
  • 如果跟标记-清除算法那样完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。内存的访问是用户程序最频繁的操作,甚至都没有之一,假如在这个环节上增加了额外的负担,势必会直接影响应用程序的吞吐量。
    基于以上两点,是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。

HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的,这也从侧面印证这点。CMS并不是每次回收都直接进行清除,大多数情况下是清除,当内存空间的碎片化程度已经影响到对象分配时,采用标记-整理算法收集一次。

HotSpot的算法细节实现

根节点枚举:必须停止用户线程,保证枚举结果的准确性。用户线程停止后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当有办法直接得到哪些地方存放着对象引用的。在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。

一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译(见第11章)过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。

普通对象指针(Ordinary Object Pointer,OOP)

安全点
导致OopMap内容变化的指令非常多,为了保证GCRoots不变化,只有线程程序到达安全点时才能进行GC。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的,“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。

让所有线程都跑到安全点有两种方式:抢占式中断和主动式中断,前者为虚拟机停止所有用户线程,如果没有到达安全点,则恢复执行到安全点;后者为线程在每一个安全点轮询一个标志位,如果标志位为真则主动中断挂起。轮询操作会在代码中频繁出现,必须保证足够高效HotSpot使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度。

轮询标志的地方和安全点时重合的,,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。??

安全区域
安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。对于这种情况,就必须引入安全区域(Safe Region)来解决。

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。

记忆集与卡表
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。

卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。卡表最简单的形式可以只是一个字节数组,而HotSpot虚拟机确实也是这样做的,字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。

写屏障
在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面。应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外的开销,不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多的。

卡表在高并发场景下还面临着“伪共享”(False Sharing)问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。

并发的可达性分析
GC Roots相比起整个Java堆中全部的对象毕竟还算是极少数,且在各种优化技巧(如OopMap)的加持下,它带来的停顿已经是非常短暂且相对固定(不随堆容量而增长)的了。从GC Roots再继续往下遍历对象图,这一步骤的停顿时间就必定会与Java堆容量直接成正比例关系了:堆越大,存储的对象越多,对象图结构越复杂,要标记更多对象而产生的停顿时间自然就更长。

用户线程与收集器并发工作,使用三色标记推导:

  • 白色:收集器未访问过
  • 灰色:访问过,但至少还要一个没有被扫描过
  • 黑色:访问过,且它的所有引用都扫描过(都是存活)

有可能出现的问题是,原本收集器标记为存活的对象,用户线程修改后已经死亡了(问题不大,下一次清理即可)。另一个问题是,原本收集器标记位死亡的对象,用户线程修改后为存活对象了,这个问题是不允许的。

只有当满足以下两个条件时才会出现第二个问题:1,赋值器插入了一条或多条从黑色对象到白色对象的新引用。2,赋值器删除了全部从灰色对象到白色对象的直接或简介引用。

对应有两种解决方案:1,增量更新,赋值器插入黑色到白色对象的引用时,记录下来,并发扫描结束之后,重新扫描这些黑色对象(变成了灰色,保证不误删)。2,原始快照,当灰色对象要删除指向白色对象的引用时,将删除的引用记录下来,并发扫描结束之后,重新扫描这些灰色对象(无论用户线程删不删 都重新扫描了 保证不误删)

引用记录的插入和删除时基于写屏障实现的。CMS(Concurrent Mark Sweep)是基于增量更新的,G1(Garbage First)、Shenandoah是基于原始快照的。

经典垃圾收集器

JDK9之后:

  • Serial + Serial Old
  • ParNew + CMS
  • Parallel Scavenge + Serial Old 或者 Parallel Old
  • G1
**Serial收集器**: 简单高效(与其他收集器的单线程相比),消耗额外内存小,没有线程交互的开销。

ParNew收集器
实质上是Serial收集器的多线程并行版本,jdk9之后只有它能与CMS配合。ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果。它默认开启的收集线程数与处理器核心数量相同,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程。

Parallel Scavenge收集器
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数(新生代大小、伊甸园区与幸存者区比例、晋升老年代对象大小等)以提供最合适的停顿时间或者最大的吞吐量。

Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法

Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法。

吞吐量优先收集器终于有了比较名副其实的搭配组合,Parallel Scavenge + Parallel Old 之前前者只能与Serial Old配合使用。

CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

  • 初始标记:初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快
  • 并发标记:遍历整个对象图,可以与用户线程一起工作(并发可达性分析,三色标记)
  • 重新标记:修正并发期间因用户线程运行导致标记变动的那一部分对象的标记记录
  • 并发清除:清楚已经死亡的对象,不需要移动存活对象,可以与用户线程并发执行

CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集、低停顿。CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的缺点:

  • CMS收集器对处理器资源非常敏
  • 由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。因此,CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。(并发清楚阶段用户产生的新放在老年代的对象要有足够的空间),如果此时失败,会使用serial old重新进行老年代的垃圾收集。
  • 因为CMS是基于并发清除算法的,那么垃圾回收之后就会产生很多空间碎片,分配大对象如果分配不下就会提前出发Full gc,为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数,用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,该过程需要移动对象,无法并发,导致停顿时间变长。因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS收集器在执行过若干次不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理。

为什么并发清除是安全的?参考https://www.modb.pro/db/152967

Garbage First收集器
面向局部收集、基于Region的内存布局形式

JDK9开始,G1取代Parallel Scavenge + Parallel Old为服务端模式下默认的收集器。CMS被废弃。

是一款能够建立起“停顿时间模型”(Pause Prediction Model)的收集器,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。

每个region可以是Eden区、Survivo区、老年代区、Humongous区,超过一个Region一半的对象判定为大对象,每个Region的大小可以通过参数-XX:G1HeapRegionSize设置,取值范围1MB-32MB

将region作为最小的垃圾回收单元,这样可以有计划的避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器追踪各个region里面的垃圾堆积的价值大小(价值指回收所需空间大小以及所需时间的经验值),然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(-XX:MaxGCPauseMillis,默
认200毫秒),优先处理回收价值收益最大的那些region。这也是Garbage First名字的由来。

G1收集器至少有以下几个问题需要妥善解决:

  • 将Java堆划分为多个独立的region后,region里面存在的跨region的引用对象如何解决?解决思路:每个region维护自己的记忆集(记录了别的region指向自己的指针,并标记这些指针在哪些卡页范围之内),G1的记忆集在本质上是一个哈希表,key是别的region的起始地址,value是一个集合,里面存储的是卡表的索引号。由于region数量比传统收集器分代数量要多得多,因此G1收集器要比其他传统的垃圾收集器有着更高的内存占用。根据经验,G1至少要耗费大约相当于Java堆内存10%-20%的额外内存来维持收集器工作。
  • 并发标记阶段如何保证收集线程与用户线程互不干扰的进行?CMS采用增量更新算法实现,G1采用原始快照(STAB)实现。CMS老年代并发收集失败会进行Full gc,如果内存回收速度赶不上内存分配速度,G1也要被迫冻结用户线程,进行Full gc导致长时间的STW。
  • 怎样建立起可靠的停顿预测模型?用户通过-XX:MaxGCPauseMillis参数指定的停顿时间只意味着垃圾收集发生之前的期望值,但G1收集器要怎么做才能满足用户的期望呢?G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

如果我们不去计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作),G1收集器的运作过程大致可划分为以下四个步骤:

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  • 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象。
  • 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  • 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

低延迟垃圾收集器

Shenandoah收集器(细节比较多,后续再整理,对比G1)
这个项目的目标是实现一种能在任何堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的垃圾收集器

ZGC收集器:(后续整理)
ZGC和Shenandoah的目标是高度相似的,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。

选择合适的垃圾收集器

Epsilon收集器,不进行垃圾回收,

收集器的权衡

虚拟机及垃圾收集器日志

垃圾收集器参数总结

实战:内存分配与回收策略

Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存。前面介绍了垃圾收集器体系及运作原理,这一节介绍给对象分配内存。

  • 对象优先在Eden分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
  • 动态对象年龄判定:
    • 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
  • 空间分配担保:
    • 在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

虚拟机性能监控、故障处理工具

参考:https://www.cnblogs.com/bingmous/p/15787521.html

基础故障处理工具

  • jps(JVM Process Status Tool)列出正在运行的虚拟机进程
  • jstat(JVM Statistics Monitoring Tool)虚拟机统计信息监视工具,监视虚拟机各种运行状态信息的命令行工具
  • jinfo(Configuration Info for Java)Java配置信息工具,实时查看和调整虚拟机各项参数
  • jmap(Memory Map for Java)Java内存映像工具,用于生成堆转储快照,它还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等
  • jhat(JVM Heap Analysis Tool):虚拟机堆转储快照分析工具,JDK提供jhat命令与jmap搭配使用,来分析jmap生成的堆转储快照。(一般不用,一般拷贝快照到别的服务器使用工具进行分析)
  • jstack(Stack Trace for Java)Java堆栈跟踪工具,用于生成虚拟机当前时刻的线程快照。从JDK 5起,java.lang.Thread类新增了一个getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码完成jstack的大部分功能。

基础工具总结
基础工具:用于支持基本的程序创建和运行(见表4-5)

安全:用于程序签名、设置安全测试等(见表4-6)

国际化:用于创建本地语言文件(见表4-7)

远程方法调用:用于跨Web或网络的服务交互(见表4-8)

Java IDL与RMI-IIOP:在JDK 11中结束了十余年的CORBA支持,这些工具不再提供

部署工具:用于程序打包、发布和部署(见表4-10)

Java Web Start(见表4-11)

性能监控和故障处理:用于监控分析Java虚拟机运行信息,排查问题(见表4-12)

WebService工具:与CORBA一起在JDK 11中被移除(见表4-13)

REPL和脚本工具(见表4-14)

可视化故障处理工具

JDK中除了附带大量的命令行工具外,还提供了几个功能集成度更高的可视化工具,用户可以使用这些可视化工具以更加便捷的方式进行进程故障诊断和调试工作。这类工具主要包括JConsole、JHSDB、VisualVM和JMC四个

JHSDB:基于服务性代理的调试工具
JConsole:Java监视与管理控制台
VisualVM:多合-故障处理工具,插件地址::https://visualvm.github.io/pluginscenters.html

  • 生成、浏览堆转储快照
  • 分析程序性能
  • BTrace动态日志跟踪,基于Instrument实现,类似Arthas
    Java Mission Control:可持续在线的监控工具

HotSpot虚拟机插件及工具

HSDIS:JIT生成代码反汇编

调优案例分析与实战

案例分析

  • 大内存硬件上的程序部署策略
  • 集群间同步导致的内存溢出
  • 堆外内存导致的溢出错误:对外内存只有在fullgc的时候垃圾收集器会进行收集,否则直到oom也不会收集,需要捕捉异常手动执行System.gc()
  • 外部命令导致系统缓慢:通过Java的Runtime.getRuntime().exec()方法来调用脚本会fork进程执行,非常耗费资源
  • 服务器虚拟机进程崩溃:异步调用,socket几类太多,可以采用消息队列异步处理
  • 不恰当数据结构导致内存占用过大:使用HashMap<Long, Long>存储大量entry,存储效率低。
  • 由Windows虚拟内存导致的长时间停顿
  • 由安全点导致长时间停顿:比如for int里面有耗时较长的操作,导致垃圾回收等待线程进入安全点,耗时较长

实战:Eclipse运行速度调优

  • 升级jdk版本
  • 编译时间和类加载时间的优化
  • 调整内存设置控制垃圾收集频率
  • 选择收集器降低延迟

类文件结构

无关性的基石

基于安全方面的考虑,《Java虚拟机规范》中要求在Class文件必须应用许多强制性的语法和结构化约束,但图灵完备的字节码格式,保证了任意一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件

Class类文件的结构

Java技术能够一直保持着非常良好的向后兼容性,Class文件结构的稳定功不可没。

任何一个Class文件都对应着唯一的一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以动态生成,直接送入类加载器中)

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符。

,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表,

使用 javap -v TestClass.class 查看字节码

魔数与Class文件的版本
每个Class文件的头4个字节被称为魔数(0xCAFEBABE),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。紧接着魔数的4个字节存储的是Class文件的版本号,Java的版本号是从45开始的,第5/6个字节为此版本号,7/8个字节为主版本号,Java版本从45开始,每个大版本增加1。《Java虚拟机规范》明确要求拒绝执行超过其版本号的Class文件。

常量池
紧接着主、次版本号之后的是常量池入口,是一个u2类型的数据,代码常量池容量,从1开始计数,如22,表示共1-21个常量池,0表示不引用常量池。
常量池中主要存放字面量和符号引用。

访问标志
紧跟常量池的两个字节,一共定义了9个。

类索引、父类索引与接口索引集合

总结:.java文件中的所有信息都按照一定的格式保存在class文件中

  • 魔数u4 - 次版本u2 - 主版本u2 - 常量池大小u2 - 常量池内容 - 访问标志u2 - 类索引u2 - 父类索引u2 - 接口索引集合大小u2 - 接口集合内容 - 字段表结构 - 方法表结构 - 属性表结构

字节码指令简介

公有设计,私有实现

Class文件结构的发展

虚拟机类加载机制

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。

类加载的时机

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。

被动引用(除了Java虚拟机规范定义的6中除外)不会出发类的初始化:

  • 通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化
  • 通过数组定义来引用类,不会触发此类的初始化
  • 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

类加载的过程

加载

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的

  • 如果组件类型是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将被标识在加载该组件类型的类加载器的类名称空间上
  • 如果数组的组件类型不是引用类型,Java虚拟机将会把数组C标记为与引导类加载器关联。
  • 数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的可访问性将默认为public,可被所有的类和接口访问到

类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的类型数据的外部接口。

验证
加载阶段与连接阶段的部分动作是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部分,这两个阶段的开始时间仍然保持着固定的先后顺序。

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证:

准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段(“通常情况”下是数据类型的零),如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值(static final字段)

解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
类或接口的解析、字段解析、方法解析、接口方法解析

初始化
在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。初始化阶段就是执行类构造器<clinit>()方法的过程。

父类中定义的静态语句块要优先于子类的变量赋值操作

类加载器

Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

双亲委派模型
启动类加载器、扩展类加载器、应用程序类加载器

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成

破坏双亲委派模型

  • jdk1.2双亲委派模型出现之前的代码
  • 第二次被破获是由于模型自身缺陷,如jndi服务
  • 由于用户对程序动态性的追求而导致的,如代码热更新、模块热部署

Java模块化系统

模块的兼容性
为了使可配置的封装隔离机制能够兼容传统的类路径查找机制,JDK 9提出了与“类路径”(ClassPath)相对应的“模块路径”(ModulePath)的概念。简单来说,就是某个类库到底是模块还是传统的JAR包,只取决于它存放在哪种路径上。

模块化下的类加载器

  • 扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代。
  • 平台类加载器和应用程序类加载器都不再派生自java.net.URLClassLoader

虚拟机字节码执行引擎

运行时栈帧结构

Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈的栈元素。

局部变量表:局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。

操作数栈:
操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈

动态连接:
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接

方法返回地址:正常调用完成、异常调用完成

方法调用:

方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程

解析:换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析(静态方法、私有方法、实例构造器、父类方法、final方法)

  • 静态分派(体现在重载方法上,如参数为int、long,调用时传入char)、
  • 动态分派(体现在重写方法上,在运行期根据实际类型确定方法执行版本的分派过程称为动态分派, invokevirtual)
  • 单分派与多分派
  • 虚拟机动态分派的实现:虚方法表、接口方法表

动态类型语言支持

动态类型语言:动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编
译期进行的

Java与动态类型

java.lang.invoke包

invokedynamic指令

实战:掌控方法分派规则

  • 通过java对动态类型语言的支持改变java默认的重载分派规则

基于栈的字节码解释执行引擎

解释执行
基于栈的指令集与基于寄存器的指令集
基于栈的解释器执行过程

类加载及执行子系统的案例与实战

案例分析

  • Tomcat:正统的类加载器架构
  • OSGi:灵活的类加载器架构
  • 字节码生成技术与动态代理的实现
  • Backport工具:Java的时光机器

实战:自己动手实现远程执行功能

前端编译与优化

Javac编译器
Java语法糖的味道

  • 泛型
  • 自动装箱、拆箱与遍历循环
  • 条件编译

实战:插入式注解处理器

  • lombok的原理与这个类似

后端编译与优化

即时编译器

把热点代码编译为本地机器码,提高代码执行效率

编译对象与触发条件

  • 编译对象是方法
  • 触发条件是根据方法调用计数和回边计数(字节码往回调用)

编译过程

实战:查看及分析即时编译结果

提前编译器

提前编译的优劣得失
实战:Jaotc的提前编译

编译器优化技术

  • 方法内联:编译器最重要的优化手段,就是把目标方法的代码原封不动地“复制”到发起调用的方法之中,避免发生真实的方法调用而已
    • 类型继承关系分析技术,这是整个应用程序范围内的类型分析技术,用于确定在目前已加载的类中,某个接口是否有多于一种的实现、某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息
  • 逃逸分析,类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。
    • 方法逃逸:当一个对象在方法里面被定义后,它可能被外部方法所引用
    • 线程逃逸:甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量
    • 如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低,则可能为这个对象实例采取不同程度的优化:
      • 栈上分配:栈上分配可以支持方法逃逸,但不能支持线程逃逸
      • 标量替换:如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。标量替换可以视作栈上分配的一种特例,实现更简单(不用考虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内
    • 同步消除:
  • 公共子表达式消除
  • 数组边界检查消除

实战:深入理解Graal编译器

Java内存模型与线程

《Java虚拟机规范》中曾试图定义一种Java内存模型,来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果

Java内存模型

Java与线程

Java与协程

线程安全与锁优化

线程安全

锁优化

  • 根据逃逸分析进行锁消除
  • 锁粗化:没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗,因此锁粗化会将多个小块的锁扩大为一个大块的锁
  • 轻量级锁:轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
    • 轻量级锁提升程序性能的依据是,对于绝大部分的锁,在整个同步周期内都是不存在竞争的,没有竞争,轻量级锁便可以通过cas避免互斥量的开销。但如果确实存锁竞争,除了互斥量本身的开销外,还额外发生了cas的开销,因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁要慢。

标签:Java,收集器,对象,虚拟机,深入,内存,JVM,线程
From: https://www.cnblogs.com/bingmous/p/17095742.html

相关文章

  • 保证DDS通信 ------ 配置主机和虚拟机在同一网段下
    参考博客:https://www.cnblogs.com/mrlayfolk/p/16155702.html1.设备情况主机:win10,有线连接PPPOE,校园网虚拟机:VM--centos72.主机配置打开控制面板→网络→网......
  • 基于jvm-sandbox的mock项目:iMock 实现过程遇到的问题及解决
    imock项目地址https://github.com/chufusheng/imock-web,作者很nice,中间沟通了很多次,感谢支持!!! 团队年度目标有一个搭建Mock平台任务,落到了我头上。1、首先明确团队的需......
  • JVM 三色标记法
    三色标记(Tri-Color-Marking)微信公众号:运维开发故事,作者:老郑垃圾收集器在并发标记的过程中,执行标记期间应用线程还在并行运行,对象间的引用关系时刻发生变化,垃圾收集器在标记......
  • SPECJVM2008的简单结果
    SPECJVM2008的简单结果摘要前面两台学习了简单使用.今天进行一下简单的数据采集.背景我这边先测试使用startup的测试脚本startup主要是单线程进行测试的.其......
  • SPECJVM2008 再学习
    SPECJVM2008再学习摘要昨天的太水了感觉今天有必要再水一点..存在的问题默认进行启动sunflow必定过不去.一般的解决办法要求进行重新编译但是我不知道怎么下......
  • JAVA重试机制多种方式深入浅出
    重试机制在分布式系统中,或者调用外部接口中,都是十分重要的。重试机制可以保护系统减少因网络波动、依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制。......
  • 深入理解ES6--用模块封装代码
    用模块封装代码在模块顶部创建的变量不会自动被添加到全局共享作用域(模块顶部this的值为undefined),必须导出后,外部代码才可访问。浏览器中使用模块​​<script>​​的type属......
  • 深入理解ES6--迭代器、生成器、代理、反射、Promise
    迭代器(Iterator)和生成器(Generator)for-of循环及展开运算符…都是针对迭代器的!!!不能使用箭头函数来创建生成器;ES6函数的简写方式可以(只需在函数名前加星号)可迭代对象具有Symbol......
  • 深入理解ES6--Set、Map及Symbol
    Set集合和Map集合Set集合是一种无重复元素的列表,通常用来检测给定的值在某个集合中是否存在;Map集合内含多组键值对,通常用来缓存频繁取用的数据。ES5中的问题varmap=Objec......
  • Docker设置JDK17的JVM启动参数踩坑
    背景需求中需要接入腾讯广告的SDK,在编写完代码进行自测时,直接报错,提示Unabletomakeprotectedfinaljava.lang.Classjava.lang.ClassLoader.defineClass堆栈信息如......