JVM知识点总结(一)

本文内容仅包括JVM区域、JMM、GC。其中HotSpot的算法细节暂跳过,Shenandoah GC和ZGC较为省略。后续开坑,并且还有关于类的加载,程序编译等也会陆续补上。

Java运行时数据区域

Java运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。根据《Java虚拟机规范 SE8》可以划分为以下区域:

程序计数器(pc register)

程序计数器(ProgramCounterRegister)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域

堆(Heap)

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。用来存放对象实例。

  • 从内存回收角度看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:年轻代和老年代。年轻代还划分为3个内存池新生代(Eden space)和存活区(Survivor space)
  • 从内存分配角度看,线程共享的Java堆中能划分出多个线程私有的分配缓冲区(TLAB)。
    Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。实现时可固定大小也可扩展大小(-Xmx,-Xms)。如果堆中没有内存完成实例分配,且堆也无法再扩展,会抛出OOM异常。

专用服务器上需要保持-Xms和-Xmx一致,否则应用刚启动可能就有好几个FullGC。当两者配置不一致时,堆内存扩容可能会导致性能抖动

栈(Stack)

每启动一个线程,JVM就会在栈空间栈分配对应的线程栈,比如1MB的空间(-Xss1m)。它是线程私有的,生命周期与线程相同。这里可以分为两个部分,虚拟机栈以及本地方法栈。

Java Virtual Machine Stacks

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

常说的堆和栈中的栈其实指的是Java虚拟机栈或说是虚拟机栈中的局部变量表部分。局部变量表存放了编译期可知的基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)

Java虚拟机规范中,对这个区域规定了两种异常情况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  • 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;

Native Method Stacks

本地方法栈与虚拟机栈作用类似,区别是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native方法服务(JNI)。
与虚拟机栈一样,本地方法中也会抛出StackOverflowError和OOM异常

方法中使用的原生数据类型和对象引用地址在栈上存储;对象、对象成员与类定义、静态变量在堆上

方法区(Method Area) / 非堆(Non-Heap)

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java 虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与 Java 堆区分开来。
它本质上还是Heap,只是一般不归GC管理,里面划分为3个内存池。

  • Metaspace,以前叫持久代(永久代, Permanent generation),Java8换了个名字叫Metaspace。
    • 运行时常量池: 运行时常量池是方法区的一部分。用于存放编译期生成的各种字面量和符号引用。当常量池无法再请到内存时会抛出OOM异常。
  • CCS,Compressed Class Space,存放class信息的,和Metaspace有交叉
  • Code Cache,存放JIT编译器编译后的本地机器代码。

直接内存(Direct Memory)

直接内存不是虚拟机运行时数据区的一部分,不是Java虚拟机规范中定义的内存区域。但这部分内存也被频繁使用,也可能导致OOM。
本机直接内存的分配不会受到Java堆大小的限制,但还是会受到本机总内存大小以及处理器寻址空间的限制。
可通过 -XX: MaxDirectMemorySize=size 进行配置

Java内存模型(JMM)

JMM规范明确定义了不同的线程之间,通过哪些方式,在什么时候可以看见其他线程保存到共享变量中的值;以及在必要时,如何对共享变量的访问进行同步。这样的好处是屏蔽各种硬件平台和操作系统之间的内存访问差异,实现了Java并发程序真正的跨平台。

  • 所有的对象(包括内部的实例成员变量),static变量,以及数组,都必须存放到堆内存中
  • 局部变量,方法的形参、入参,异常处理语句的入参不允许在线程之间共享,所以不受内存模型的影响。
  • 多个线程同时对一个变量访问时【读取、写入】,这时候只要有某个线程执行的是写操作,那么这种现象就称之为”冲突“。
  • 可以被其他线程影响或感知的操作,成为线程间的交互行为,可分为:读取、写入、同步操作、外部操作等等。其中同步操作包括:对volatile变量的读写,对管程(monitor)的锁定与解锁,线程的起始操作与结尾操作,线程启动和结束等等。外部操作则是值对线程执行环境之外的操作,比如停止其他线程等等。
  • JMM规范的是线程间的交互操作,而不管线程内部对局部变量进行的操作。

GC相关

判断对象已死

引用计数算法

此算法已经不再使用。它是通过判断对象的引用数量, 通过判断对象的引用数量来决定对象是否可以被回收

  • 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
  • 任何引用计数为0的对象实例可以被当作垃圾收集
  • 执行效率高,程序执行受影响较小

缺点是无法检测出循环引用的情况,可能导致内存泄露。

可达性分析算法

当前使用的算法,基本思路是通过一系列称为GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链‘ (Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。
可以作为GC Root的对象包括以下几种

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

除了这些固定的GC Roots集合外,根据用户所选用的垃圾回收器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成GC Roots集合。比如分代收集和局部回收,如果只针对Java堆中某一块区域发起垃圾收集时,必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中取,才能保证可达性分析的正确性。

几种引用

Java中的引用概念包括四种,强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。

强引用

它是最传统的引用的定义 Object obj =new Object()。无论任何情况下,哪怕是oom,只要强引用关系还存在,垃圾收集器就永远不会回收掉的对象。

软引用

它描述对象处在有用但非必须的状态。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,再会抛出内存溢出异常。它可以用来实现高速缓冲。

弱引用

它描述非必须的对象,比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
使用场景比如ThreadLocalMap中使用弱引用存储ThreadLocal

虚引用(幽灵引用)

最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象示例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知.
幽灵引用及其队列的使用情况并不多见,主要用来实现比较精细的内存使用控制,这对于移动设备来说是很有意义的。程序可以在确定一个对象要被回收之后,再申请内存创建新的对象。通过这种方式可以使得程序所消耗的内存维持在一个相对较低的数量。

方法区的垃圾回收

《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载)。方法区垃圾收集的性价比较低:在Java堆中,尤其是新生代中,对常规应用进行一次垃圾手机通常可以回收70%到90%的空间,但方法区的回收成果远低于此。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
判断一个常量是否废弃就是只要常量池中的常量没有被任何地方引用,就可以被回收。而要判断一个类型是否属于不再被使用的类的条件比较苛刻,需要满足下面三个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGI、JSP的重加载等,否则通常是很难达成的。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    Java虚拟机被允许对上述三个条件的无用类进行回收,但这是被允许,不是一定会回收。通过参数-Xnoclassgc进行控制。
    在大量使用反射、动态代理、CGLib等字节码框架,动态生存JSP以及OSGI这类频繁自定义类加载器的场景,通常需要Java虚拟机具备类型加载的能力,以保证不会对方法区造成过大的内存压力。

垃圾收集算法

标记-清除算法

最早出现的也是最基础的垃圾算法标记-清除(Mark-Sweep)算法,分为标记和清除两个阶段。

  • 标记: 从根集合进行扫描,对存活的对象进行标记
  • 清除: 对堆内存从头到尾进行线性遍历,回收不可达对象内存
    缺点:标记和清除的两个过程效率不高,并且会产生大量不连续的内存碎片

标记-复制算法

也被简称为复制算法,它为了解决标记-清除算法面对大量回收对象时执行效率低的问题而生。
它将可用内存按容量划分为大小等等的两块,每次只使用其中的一块。当这块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
如果内存中多数对象都是存活的,这个算法会产生大量的内存间复制的开销,但对于大多数对象都是可以回收的情况,算法要复制的就是少量的存活对象,而且每次都是针对整个半区进行内存回收,分配内存也就不用考虑空间碎片的复杂情况。

现在的商用Java虚拟机大多都优先采用这种收集算法回收新生代。比如1989年Andrew Appel提出的半区复制分代策略,Appel式回收:将新生代分为一块较大的Eden空间和两块较小的Survivor空间S0和S1.每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另一块Survivor空间上,然后直接清理掉Eden和已使用过的那块Survivor空间。

当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际大多就是老年代)进行分配担保

标记-整理算法

复制算法在对象存活率较高的情况下效率较低。而且如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象被100%存活的情况,所以老年代一般不适用复制算法。
标记整理(Mark-Compact)算法的标记过程和标记-清除算法一样,但后续步骤是让所有存活对象都向内存空间一端进行移动,然后直接清理边界外的内存。

  • 标记: 从根集合进行扫描,对存活的对象进行标记
  • 清除: 移动所有存活的对象,且按照内存地址次序依次排列,然后将末端地址以后的内存全部回收
    移动存活对象,尤其是老年代这种每次回收都有大量存活对象的区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作会造成STW(标记-清除也会,只是时间较短)。但是不移动对象会造成内存碎片,因此是否移动对象都有弊端。
    即使不移动对象会使得收集器的效率提升一些,但因内存分配和访问相比垃圾收集频率要高得多,这部分的耗时增加,总吞吐量仍然是下降的。Parallel Scavenge收集器就是基于标记-整理的。
    另一种“和稀泥”式的解决方案是让虚拟机平时多数时间采用标记-清除算法,容忍碎片的存在,直到内存空间的碎片化程度已经影响到对象分配时,再采用标记-整理算法收集一次。基于标记-清除算法的CMS收集器面临空间碎片过多的时候就采用这种处理方法。

不同垃圾回收器

垃圾回收器

串行GC(Serial GC)/ ParNewGC

-XX: +UseSerialGC
串行GC对年轻代使用复制算法,对老年代使用标记整理算法
两者都是单线程的垃圾收集器,不能进行并行处理,所以都会触发全线暂停,停止所有的应用线程。
因此这种算法不能充分利用多核CPU。不管有多少CPU,JVM在垃圾收集时都只能使用单个核心。
CPU利用率高,暂停时间长。简单粗暴,就像老式电脑,容易卡死。该选项只适合几百MB堆内存的JVM,而且是单核CPU时比较有用

-XX: +UseParNewGC
改进版的SerialGC, 可以配合CMS使用,就是Serial GC的多线程并行版本。

并行GC(Parallel GC)

-XX: +UseParallelGC
-XX: +UseParallelOldGC
-XX: +UseParallelGC -XX: +UseParallelOldGC

年轻代和老年代的垃圾回收都会触发STW。 在年轻代使用复制算法,老年代使用标记整理算法。
-XX: ParallelGCThreads=N来指定GC线程数,其默认值为CPU核心数。
并行垃圾收集器适用于多核服务器,主要目标是增加吞吐量。因为对系统资源的有效利用,能达到更高的吞吐量(运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)):

  • 在GC期间,所有CPU内核都在并行清理垃圾,所以总暂停时间更短;
  • 在两次GC周期的间隔期,没有GC线程在运行,不会消耗任何系统资源。

有两个参数用于精确控制吞吐量:

  • -XX: MaxGCPauseMills 最大垃圾收集停顿时间
  • -XX: GCTimeRatio 吞吐量的大小

CMS GC

-XX: +UseConcMarkSweepGC
对年轻代采用并行STW方式的标记复制算法,对老年代主要使用并发标记清除算法。
CMC GC的设计目标是避免在老年代垃圾收集时出现长时间的卡顿,主要通过两种手段达到此目标:

  1. 不对老年代进行整理,而是使用空间列表(free-lists)来管理内存空间的回收
  2. 在标记清除阶段的大部分工作和应用线程一起并发执行
    也就是说,在这些阶段并没有明显的应用线程暂停。但值得注意的是,它仍然和应用线程争抢CPU。默认情况下,CMS使用的并发线程数等于CPU核心数的1/4。
    如果服务器是多核CPU,并且主要调优目标是降低GC停顿导致的系统延迟,那么使用CMS是个明智的选择。进行老年代的并发回收时,可能会伴随着多次年轻代的minor GC。
    六个阶段:
  3. 初始标记 (nitial mark):会STW,初始标记的目标是标记所有的根对象,包括根对象直接引用的对象,以及被年轻代中所有存活对象锁引用的对象(老年代单独回收)
  4. 并发标记(concurrent mark):在此阶段,CMS GC遍历老年代,标记所有的存活对象,从前一阶段找到的根对象开始算起。并发标记阶段,就是应用程序同时运行,不用暂停的阶段
  5. 并发预处理:此阶段同样是与应用程序并发执行的,不需要停止应用线程。因为前一阶段并发标记与程序并发运行,可能有一些引用关系已经发生了改变。如果在并发标记过程中引用关系发生了变化,JVM会通过Card的方式将发生了改变的区域标记为脏区,这就是所谓的卡片标记
  6. 重新标记(remark)/ 最终标记:会STW,最终标记阶段是此次GC时间中的第二次,也是最后一次STW停顿。本阶段的目标是完成老年代中所有存活对象的标记。因为之前的预清理阶段是并发执行的,有可能GC线程更不是应用程序的修改速度。所以需要一次STW暂停来处理各种复杂的情况。通常CMS会尝试在年轻代尽可能空的情况下执行Final Remark阶段,以免触发多次STW事件。
  7. 并发清除(concurrent sweep):此阶段与应用程序并发执行,不需要STW停顿。JVM在此阶段删除不再使用的对象,并回收他们占用的内存空间。
  8. 并发重置(concurrent reset):此阶段与应用程序并发执行,重置CMS算法相关的内部数据,为下一次GC循环做准备。
    CMS垃圾收集器在减少停顿时间上做了很多复杂而有用的工作,用于垃圾回收的并发线程执行的同时,并不需要暂停应用线程。当然CMS也有一些缺点,最大的问题就是老年代内存碎片问题,在某些情况下GC会造成不可预测的暂停时间,特别是堆内存特别大的情况下。
    CMS提供了一个-XX: +UseCMS-CompactAtFullCollection开关参数,默认是开启的,从JDK9开始废弃,用于在CMS收集器不得不进行Full GC开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,无法并发。这样空间碎片问题解决了,但停顿时间会变成。因此还有另一个参数-XX: CMSFullGCsBeforeCompaction(9开始废弃),这个参数的作用是要求CMS收集器在执行若干次不整理空间的Full GC之后,下一次进行Full GC前先进行碎片整理,默认是0,表示每次进行Full GC时都会进行碎片整理。

G1 GC

G1的全称是Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理它.
G1 GC最主要的设计目标是:将STW停顿的时间和分布,变成可预期且可配置的
事实上,G1 GC是一款软实时垃圾收集器,可以为其设置某项特定的性能指标。为了达成可预期停顿时间的指标,G1 GC有一些独特的实现。
首先,堆不再分成年轻代和老年代,而是划分为多个(通常是2048)可以存放对象的小块堆区域。每个小块,可能一会被定义成Eden区,一会被指定为Survivor区或者old区。在逻辑上,所有的Eden区和Survivor区合起来就是年轻代,所有的Old区拼一起就是老年代。
这样划分后,使得G1不必每次都去收集整个堆空间,而是以增量的方式来进行处理:每次只处理一部分内存块,成为此次GC的回收集。每次GC暂停都会收集所有年轻代的内存块,但一般只包含部分老年代的内存块。
G1的另一项创新是,在并发阶段估算每个小堆块存活对象的总数。构建回收集的原则是:垃圾最多的小块会被优先收集

G1 GC之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集

  • -XX:MaxGCPauseMills:预期 G1 每次执行 GC 操作的暂停时间,单位是毫秒,默认值是 200 毫秒,G1 会尽量保证控制在这个范围内

5个阶段:

  1. 初始标记,此阶段标记所有从GC根对象直接可达的对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以在这个阶段实际并没有产生额外的停顿。
  2. Root区扫描:此阶段标记所有从根区域可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域
  3. 并发标记:与CMS的并发标记阶段类似,只遍历对象图,并在一个特殊的位图中标记能访问到的对象
  4. 再次标记:会STW。短暂地停止应用线程,停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标记开始时未被标记的存活对象。
  5. 清理:会STW。为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块排序,以提升GC的效率,维护并发标记的内部状态。所有不包含存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的:例如空堆区的回收,还有大部分的存活率计算。

G1 GC的注意事项
特别需要注意的是,某些情况下 G1 触发了 Full GC,这时 G1 会退化使用 Serial 收集器来完成垃圾的清理工作,它仅仅使用单线程来完成 GC 工作,GC 暂停时间将达到秒级别的。

1.并发模式失败
G1 启动标记周期,但在 MⅸGC 之前,老年代就被填满,这时候 G1 会放弃标记周期。
解决办法:增加堆大小,或者调整周期(例如增加线程数-XX: ConcGCThreads 等)。

2. 晋升失败
没有足够的内存供存活对象或晋升对象使用,由此触发了 Full GC (to-space exhausted/to-space overflow)。解决办法:
a)增加-XX: G1 ReservePercent 选项的值(并相应增加总的堆大小)增加预留内存量。
b)通过减少-XX: InitiatingHeapOccupancyPercent 提前启动标记周期。
c)也可以通过增加-XX: ConcGCThreads 选项的值来增加并行标记线程的数目。

3.巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动 Full GC,来释放空间。
解决办法:增加内存或者增大 -XX: G1HeapRegionSize

Shenandoah GC

-XX: +UnlockExperimentalVMOptions
-XX: +UseShenandoahGC -Xmx16g
Shenandoah GC 立项比 ZGC 更早,设计为 GC 线程与应用线程并发执行的方式,通过实现垃圾回收过程的并发处理,改善停顿时间使得 GC 执行线程能够在业务处理线程运行过程中进行堆压缩、标记和整理,从而消除了绝大部分的暂停时间。

Shenandoah 团队对外宣称 Shenandoah GC 的暂停时间与堆大小无关,无论是 200 MB 还是 200GB 的堆内存,都可以保障具有很低的暂停时间(注意:并不像 ZGC 那样保证暂停时间在 10ms 以内)

ZGC

-XX: +UnlockExperimentalVMOptions -XX: +UseZGC -Xmx16g
ZGC 最主要的特点包括:

  1. GC 最大停顿时间不超过 10ms
  2. 堆内存支持范围广,小至几百 MB 的堆空间,大至 4TB 的超大堆内存(UDK13 升至 16TB)
  3. 与 G1 相比,应用吞吐量下降不超过 15%
  4. 当前只支持Linux/x64位平台,JDK15 后支持Mac0S 和 Windows系统

各个GC对比

各个GC对比

常用的GC组合

  1. Serial+Serial Old 实现单线程的低延迟垃圾回收机制;
  2. ParNew+CMS,实现多线程的低延迟垃圾回收机制;
  3. Parallel Scavenge和 Parallel Scavenge Old,实现多线程的高吞吐量垃圾回收机制。

选择正确的 GC 算法,唯一可行的方式就是去尝试,一般性的指导原则:

  1. 如果系统考虑吞吐优先,CPU 资源都用来最大程度处理业务,用 Parallel GC;
  2. 如果系统考虑低延迟有限,每次 GC 时间尽量短,用 CMS GC; 3. 如果系统内存堆较大,同时希望整体来看平均 GC 时间可控,使用 G1GC。对于内存大小的考量:
  3. 一般 4G 以上,算是比较大,用 G1 的性价比较高。
  4. 一般超过 8G,比如 16G-64G 内存,非常推荐使用 G1GC。

参考文献

《深入理解Java虚拟机 第三版》

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇