JAVA垃圾回收机制

第一章 运行时数据区

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。如图所示:

程序计数器

程序计数器可以看做是当前线程所执行的字节码的行号指示器,是线程私有的。如果线程正在执行的是一个java方法,那么计数器记录的就是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个本地Native方法,这个计数器就为空(undefined)。此内存区域是唯一一个没有规定任何OutOfMemoryError情况的区域。

JAVA虚拟机栈

也是线程私有的,生命周期与线程相同。栈中存放的数据是栈帧,每个方法被执行的时候,虚拟机都会创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。方法被调用直至结束,就对应着一个栈帧的入栈和出栈。虚拟机栈可能出现两种异常:StackOverFlowErrorOutOfMemoryError

局部变量表主要存储三种数据类型:

数据类型描述
编译器可知的基本数据类型boolean、byte、char、short、int、long、float、double
对象引用reference类型,比如执行对象起始地址的指针或者指向对象相关位置的句柄
returnAddress类型指向了一条字节码指令的地址

局部变量表的存储空间单位是,其中long和double两种数据类型占用两个槽,其余数据类型都占一个槽,具体一个槽占用多大内存空间,是由每种虚拟机自己定义。局部变量表所需的内存空间在编译器就完全确定,运行期间不会改变大小。

本地方法栈

与虚拟机栈类似,不同在于java虚拟机栈是服务于java方法,而本地方法栈是服务于执行Native方法。

虚拟机所管理的内存中最大的一块,线程共享,但是Java堆中可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换等优化手段,“几乎”所有的对象实例都在这里分配内存。堆内存可能出现:OutOfMemoryError异常。

方法区

用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,线程共享。在JDK1.8之前,使用的是永久代(和堆共用内存管理方法)实现方法区,JDK1.8之后使用的是元空间(使用本地内存)。方法区也需要进行垃圾收集,其内存回收目标主要是针对常量池的回收和对类型的卸载

运行时常量池(Runtime Constant Pool)是方法区的一部分,Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。Java语言并不要求常量 一定只有编译期才能产生,运行期间也可以将新的常量放入池中,比如String类的 intern()方法。可能出现:OutOfMemoryError异常。

第二章 HotSpot虚拟机中对象的探究

一 对象的创建过程
  1. 检查是否能在常量池中定位到该类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程

  2. 为对象分配内存空间。如果使用Serial、ParNew等带压缩整理过程的收集器(内存规整),系统使用指针碰撞算法分配内存;如果使用CMS这种基于清除 (Sweep)算法的收集器(内存不规整),系统使用空闲列表算法分配内存。解决分配内存过程中的并发问题一般有两种方法:一是采用CAS配上失败 重试的方式保证更新操作的原子性;二是把内存分配的动作按照线程划分在不同的空间之中进行,即使用TLAB

  3. 将分配到的内存空间初始化为零值

  4. 设置对象头

  5. 执行构造方法初始化

二 对象的内存布局

对象在堆内存中的存储布局可以划分为三个部分:对象头、实例数据和对齐填充。

对象头

主要包括两部分内容:第一类是用于存储对象自身的运行时数据,称为"MarkWord",如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。第二类是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例,并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。

实例数据

在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。

对齐填充

任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍,因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

三 对象的访问定位

主要两种方式:句柄、直接指针。HotSpot使用直接指针。

使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference本身不需要被修改。

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销。

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

一 对象存活判断
引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。JAVA虚拟机不采用这种方式,因为无法解决循环引用的问题。

可达性分析算法

是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连, 则证明此对象是不可能再被使用的。

可作为GC Roots的对象:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。

  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

  • 在方法区中常量引用的对象,譬如字符串常量池里的引用。

  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

  • 所有被同步锁(synchronized关键字)持有的对象。

  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它也不会被立刻标记为待回收,而是尝试去执行一次finalize()方法,如果在该方法中完成了自救,那么就不会被回收了。finalize方法整个生命周期只能执行一次。

引用的分类
  • 强引用:强引用的对象永远不会被垃圾回收器回收,即使内存溢出。

  • 软引用:只有在发生内存溢出前,才会将软引用的对象回收。

  • 弱引用:弱引用的对象只能生存到下一次垃圾收集器工作之前,一旦垃圾收集器开始工作,必被回收。

  • 虚引用:一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

二 方法区的回收

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

废弃的常量比如常量池中没有引用关系的字符串。

不再使用的类的判定条件,以下必须同时满足:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。

  • 加载该类的类加载器已经被回收

  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

三 垃圾收集算法
分代收集理论

大多数垃圾收集器都是基于分代收集理论的,因此这里只关注分代收集的算法。

分代收集理论建立在三个分代假说之上:

1)绝大多数对象都是朝生夕死的

2)熬过越多次垃圾收集的对象就越难以消亡

基于以上两个假说,可以建立新生代和老年代的概念。但是这样简单地划分还存在一个问题:在进行minorGC的时候,老年代的对象可能会引用新生代的对象,要确定新生代的对象是否应该被清除,需要在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,这无疑是成本很大的。

3)跨代引用相比于同代引用仅占极少一部分

针对上述问题,引入该假说。实际上如果存在跨代引用,由于老年代的对象很难回收,也导致其引用的新生代对象上升到老年代中。在该假说下,只需在新生代上建立一个全局的数据结构(该结构被称 为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。

标记-清除算法

是最基础的垃圾收集算法,缺点:效率慢、存在空间碎片

标记-复制算法

半区复制,实际容量只有内存区域的一半。此外,如果一次垃圾收集之后,有大量的对象都存活了下来,需要进行很多次对象的复制操作,因此这种算法适用于新生代,而且有统计表明新生代的对象有98%的对象都会在一次MinorGC中被清除,所以实际的内存比例不用保证1:1。

HotSpot虚拟机把新生代分成Eden区和两个Survivor区,比例是8:1:1,每次垃圾收集时,把Eden区和其中一个Survivor区存活的对象放入另一个Survivor区。如果放不下,就会启用分配担保,直接将这部分对象放入老年代。

标记-整理算法

标记-复制算法存在着多次复制存活对象的开销,在老年代不适用。因此,标记-整理算法适用于老年代,在该过程中,由于需要移动对象在内存中的位置并更新其引用,所以会产生STW(STOP THE WORLD)。另外,也有垃圾收集器在老年代使用标记-清除算法,尽管会产生内存碎片。

基于标记-清除算法的垃圾收集器比如CMS更关注延迟,基于标记-整理算法的垃圾收集器比如Parallel Scanvenge更关注吞吐量。

四 HotSpot的算法细节实现
根节点枚举

这个步骤查找所有的根节点(GC ROOT),所有的垃圾收集器都需要STW。

在HotSpot 中,一旦类加载动作完成的时候, HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用,以上称为OopMap的数据结构。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找。

安全点

根节点枚举,并不是在程序执行的所有指令中都产生OopMap数据结构,而是在特定的位置,这些位置称为安全点。也就是说,只有在安全点,才可以进行垃圾收集。方法调用、循环跳转、异常跳转等都属于指令序列复用,只有具有这些功能的指令才会产生安全点。

当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最 近的安全点上主动中断挂起。

安全区域

对于正在运行的线程(获得了CPU时间),可以通过安全点使其中断。但是对于阻塞状态的线程,就要引入安全区域来解决。

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

记忆集和卡表

为了解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围。事实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题。

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。记忆集中的记录精度可以有不同的实现方式:

  • 字长精度:每个记录精确到一个机器字长(32位或64位),该字包含跨代指针。

  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。

  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。(这种实现方式称为卡表

不同实现方式之间的差异在于其占用的空间及维护的成本。卡表实现占用的空间是最少的。

卡表实际上就是一个数组(G1使用的是哈希表),每一个元素对应一个内存块的起始地址,每个内存块的大小由具体垃圾收集器实现,这一块内存被称作一个卡页。只要卡页中存在一个对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1(称为把这个元素变脏),在垃圾收集发生时,把这部分对象加入GC Roots中一并扫描。

写屏障

写屏障主要是为了维护卡表(即当发生对象的跨代引用时,更新脏化卡表)。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环绕通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值 后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障,但直至G1收集器出现之前,其他收集器都只用到了写后屏障。

卡表在高并发场景下还面临着“伪共享”问题。现代中央处理器的缓存系统中是以缓存行(Cache Line) 为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏。

并发的可达性分析

三色标记法:把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。

  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。

  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

正常标记与并发标记

当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;

  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用

解决上述对象消失的问题,有两种方法:

增量更新:破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次

原始快照:破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描 一次。

以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。在 HotSpot虚拟机中,增量更新和原始快照这两种解决方案都有实际应用,譬如,CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。

五 经典垃圾收集器
Serial/Serial Old收集器

 Serial新生代:标记复制算法,Serial Old老年代:标记整理算法

Serial Old可以作为CMS收集器的备用,当CMS失败时,可以使用Serial Old

缺点:STW

优点:简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

ParNew收集器

是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。除了Serial收集器外,目前只有它能与CMS 收集器配合工作。

Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。CMS更关注低延迟,Parallel Scavenge更关注吞吐量,所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。

Parallel Old收集器

CMS收集器

关注低延迟(停顿时间短),基于标记-清除算法。分为四个步骤:

1)初始标记。标记GC Roots直接关联的对象,需要STW。

2)并发标记。从GC Roots直接关联的对象开始遍历对象图(三色标记),不需要STW

3)重新标记。增量更新解决并发三色标记中对象消失的问题,需要STW

4)并发清除

缺点:

1)并发过程中占用了处理器资源,影响吞吐量。

2)标记-清除算法会产生空间碎片,进而产生Full GC。

3)无法处理“浮动垃圾”。在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。因此垃圾收集过程中,还需要预留足够内存空间提供给用户线程使用,如果预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集, 但这样停顿时间就很长了。

G1垃圾收集器

开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。G1面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的 Region采用不同的策略去处理

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设 定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。

G1收集器能建立可预测的停顿时间模型,它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region。

G1中的记忆集:为了解决不同Region之间的跨代引用,需要在每一个Region都保留一个记忆集。G1的记忆集在存储结构的本质上是一 种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。

G1使用原始快照法解决并发标记过程中的对象消失问题。此外,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。

建立起可靠的停顿预测模型:在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

G1垃圾收集的步骤:

  • 1)初始标记,标记GC Roots直接关联的对象,需要STW。

  • 2)并发标记,从GC Roots直接关联的对象遍历三色标记,不需要STW。

  • 3)最终标记,原始快照解决并发标记可能产生的对象消失问题,需要STW。

  • 4)筛选回收,把需要回收的region中的存活对象复制到另一个region中,然后回收,需要STW。

G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的, 换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才能担当起“全功能收集器”的重任与期望。

G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。

G1相比CMS的缺点:

1)负载高。CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索 (SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。由于G1对写屏障的复杂操作 要比CMS消耗更多的运算资源,所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现为类似于消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理。

2)记忆集使用的内存空间大

低延迟垃圾收集器

在CMS和G1出现之前,所有垃圾收集器都是需要STW的。CMS和G1实现了并发的标记,但是由于在垃圾回收阶段,都是需要STW的。CMS使用的是标记-清除算法,不可避免地产生内存碎片;而G1虽然是按照更小粒度进行垃圾收集,局部来看不会产生内存碎片,但是如果出现大对象的分配,仍然可能面临局部内存碎片的问题。

低延迟处理器的目标是在堆空间可控的情况下,使得回收阶段也是可以和用户线程并发的,从而尽可能的降低延迟。

Shenandoah收集器

Shenandoah收集器与G1收集器内存布局类似,主要有以下几点不同:

  • G1的回收阶段不能与用户线程并发,而Shenandoah收集器可以(使用转发指针和读屏障)。

  • Shenandoah默认没有分代,也就是说region没有区分新生代和老年代。

  • 使用连接矩阵代替记忆集,降低了维护代价。所谓连接矩阵,就是如果region N有对象引用了region M,就在矩阵的N行M列打一个标记。

回收阶段很难做到与用户线程并行,因为一个存活的对象如果被垃圾回收线程移动到了新的地址,而用户线程访问到了旧的地址,就会出现错误。Shenandoah在每个对象头中增加了转发指针,如果没有进行过对象的移动,该指针指向该对象;一旦发生了对象的移动,该指针就指向新的地址,这样用户线程后续访问该对象就会通过转发指针访问到新的地址。

修改指针的操作也需要考虑到与用户线程并发的问题,如果对象已经被移动到了新的地址,转发指针还未更新,此时用户线程就来访问对象也会出错。这种场景Shenandoah使用的是CAS和读屏障解决。

ZGC

ZGC也是基于Regionde ,其特点:

Region具有动态性,动态创建和销毁,以及动态的区域容量大小。

使用染色指针,直接把对象的标记信息存储在对象引用的指针上(需要64位操作系统硬件支持)。

六 垃圾收集器的选择
  • 应用程序的主要关注点是什么,吞吐量 or 低延迟 or 内存占用

  • 使用的JDK的发行商是什么,版本号是多少

  • 29
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值