Java JVM Prearrangement Knowledge

Ergonomics

Ergonomics is the process by which the Java Virtual Machine (JVM) and garbage collection tuning, such as behavior-based(基于行为) tuning(调节,调整), improve application performance.

The JVM provides platform-dependent default selections for the garbage collector, heap size, and runtime compiler. These selections match the needs of different types of applications while requiring less command-line tuning.

In addition, behavior-based tuning dynamically tunes the sizes of the heap to meet a specified behavior of the application.

This section describes these default selections and behavior-based tuning. Use these defaults first before using the more detailed controls described in subsequent sections.

Behavior-Based Tuning

Maximum Pause Time Goal

The pause time is the duration during which the garbage collector stops the application and recovers space that is no longer in use. The intent of the maximum pause time goal is to limit the longest of these pauses.

An average time for pauses and a variance(方差) on that average is maintained by the garbage collector. The average is taken from the start of the execution but is weighted so that more recent pauses count more heavily. If the average plus the variance of the pause times is greater than the maximum pause time goal, then the garbage collector considers that the goal is not being met.

The maximum pause time goal is specified with the command-line option -XX:MaxGCPauseMillis=. This is interpreted(可理解的) as a hint to the garbage collector that pause times of milliseconds or less are desired. The garbage collector will adjust the Java heap size and other parameters related to garbage collection in an attempt to keep garbage collection pauses shorter than milliseconds.

By default there is no maximum pause time goal. These adjustments may cause garbage collector to occur more frequently, reducing the overall throughput of the application. The garbage collector tries to meet any pause time goal before the throughput goal. In some cases, though, the desired pause time goal cannot be met.

Throughput Goal

The throughput goal is measured in terms of the time spent collecting garbage and the time spent outside of garbage collection (referred to as application time).

The goal is specified by the command-line option -XX:GCTimeRatio=. The ratio of garbage collection time to application time is 1 / (1 + ). For example, -XX:GCTimeRatio=19 sets a goal of 1/20th or 5% of the total time for garbage collection.

The time spent in garbage collection is the total time for both the young generation and old generation collections combined. If the throughput goal is not being met, then the sizes of the generations are increased in an effort to increase the time that the application can run between collections.

Footprint Goal

If the throughput and maximum pause time goals have been met, then the garbage collector reduces the size of the heap until one of the goals (invariably(adv.总是) the throughput goal) cannot be met. The goal that is not being met is then addressed(处理,解决).


Tuning Strategy

Do not choose a maximum value for the heap unless you know that you need a heap greater than the default maximum heap size. Choose a throughput goal that is sufficient for your application.

The heap will grow or shrink to a size that will support the chosen throughput goal. A change in the application’s behavior can cause the heap to grow or shrink. For example, if the application starts allocating at a higher rate, the heap will grow to maintain the same throughput.

If the heap grows to its maximum size and the throughput goal is not being met, the maximum heap size is too small for the throughput goal. Set the maximum heap size to a value that is close to the total physical memory on the platform but which does not cause swapping of the application. Execute the application again. If the throughput goal is still not met, then the goal for the application time is too high for the available memory on the platform.

If the throughput goal can be met, but there are pauses that are too long, then select a maximum pause time goal. Choosing a maximum pause time goal may mean that your throughput goal will not be met, so choose values that are an acceptable compromise for the application.

It is typical that the size of the heap will oscillate(波动) as the garbage collector tries to satisfy competing goals. This is true even if the application has reached a steady state. The pressure to achieve a throughput goal (which may require a larger heap) competes with the goals for a maximum pause time and a minimum footprint (which both may require a small heap).


Arithmetic

对象存活判定算法

引用计数算法

引用计数算法是在JVM中被摒弃的一种对象存活判定算法,不过它也有一些知名的应用场景(如Python、FlashPlayer)

用引用计数器判断对象是否存活的过程是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。

引用计数算法的实现简单,判定效率也很高,大部分情况下是一个不错的算法。它没有被JVM采用的原因是它很难解决对象之间循环引用的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** * testGC()方法执行后,objA和objB会不会被GC呢? */
public class ReferenceCountingGC {
public Object instance = null;

private static final int _1MB = 1024 * 1024;

/** * 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过 */
private byte[] bigSize = new byte[2 * _1MB];

public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;

objA = null;
objB = null;

// 假设在这行发生GC,objA和objB是否能被回收?
System.gc();
}
}

在上面这段代码中,对象objA 和对象objB都有字段instance,赋值令objA.instance = objB;、objB.instance = objA;,除此之外,这两个对象再无引用。如果JVM采用引用计数算法来管理内存,这两个对象不可能再被访问,但是他们互相引用着对方,导致它们引用计数不为0,所以引用计数器无法通知GC收集器回收它们。

可达性分析算法

在主流商用程序语言的实现中,都是通过可达性分析(tracing GC)来判定对象是否存活的。

此算法的基本思路是:通过一系列的称为“GC Roots”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是GC Roots到这个对象不可达)时,则证明此对象时不可用的。

对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

可以看到,GC Roots在对象图之外,是特别定义的“起点”,不可能被对象图内的对象所引用。

准确地说,GC Roots其实不是一组对象,而通常是一组特别管理的指向引用类型对象的指针,这些指针是tracing GC的trace的起点。它们不是对象图里的对象,对象也不可能引用到这些“外部”的指针,这也是tracing GC算法不会出现循环引用问题的基本保证。因此也容易得出,只有引用类型的变量才被认为是Roots,值类型的变量永远不被认为是Roots。只有深刻理解引用类型和值类型的内存分配和管理的不同,才能知道为什么root只能是引用类型。

在Java中,可作为GC Roots的对象包括以下几种:

  • 虚拟机栈(栈帧中的局部变量表,Local Variable Table)中引用的对象。

  • 方法区中类静态属性引用的对象。

  • 方法区中常量引用的对象。

  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

可以概括得出,可作为GC Roots的节点主要在全局性的引用与执行上下文中。要明确的是,tracing gc必须以当前存活的对象集为Roots,因此必须选取确定存活的引用类型对象。

GC管理的区域是Java堆,虚拟机栈、方法区和本地方法栈不被GC所管理,因此选用这些区域内引用的对象作为GC Roots,是不会被GC所回收的。

其中虚拟机栈和本地方法栈都是线程私有的内存区域,只要线程没有终止,就能确保它们中引用的对象的存活。

而方法区中类静态属性引用的对象是显然存活的。常量引用的对象在当前可能存活,因此,也可能是GC roots的一部分。

两次标记与finalize()方法

即使在可达性分析算法中不可达的对象,也不是一定会死亡的,它们暂时都处于“缓刑”阶段,要真正宣告一个对象“死亡”,至少要经历两次标记过程:

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finaliza()方法。

当对象没有覆盖finaliza()方法或者finaliza()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finaliza()方法,那么此对象将会放置在一个叫做 F-Queue 的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发此方法,但并不承诺会等待它运行结束,原因是:如果一个对象在finaliza()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能导致F-Queue 队列中的其它对象永久处于等待,甚至导致整个内存回收系统崩溃。

finaliza()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue 队列中的对象进行第二次小规模的标记。如果对象想在finaliza()方法中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可,例如把自己(this关键字)赋值给某个类变量或者对象的成员变量,这样在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,基本上它就真的被回收了

需要说明的是,使用finalize()方法来“拯救”对象是不值得提倡的,因为它不是C/C++中的析构函数,而是Java刚诞生时为了使C/C++程序员更容易接受它所做的一个妥协。它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。finalize() 能做的工作,使用try-finally或者其它方法都更适合、及时,所以笔者建议大家可以忘掉此方法存在。


GC Arithmetic

Mark-Sweep - 标记-清除

这是最基础的收集算法,顾名思义,算法分为两个阶段:

  • 标记:标记处所有需要回收的对象。
  • 清除:统一回收所有被标记的对象。

算法缺陷:

  • 效率问题,标记和清除两个过程的效率不高。

    因为内存碎片的存在,操作会变得更加费时,因为查找下一个可用空闲块已不再是一个简单操作。

  • 空间问题,标记清除之后会产生大量不连续的内存碎片。

    过多的内存碎片可能会造成后续在分配比较大的对象时,无法找到足够的连续内存而不得不提前触发另一次GC。

Copying - 复制

该算法的出现是为了解决Mark-Sweep的效率问题。

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块内存上面,然后再把已使用过的内存空间一次性清理掉。

这样每次都只是对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。缺点是可用的内存空间缩小了一半。

Mark-Compact - 标记-整理

Copying算法在对象存活率较高时就要进行比较多的复制操作,效率将会降低。

更重要的是,如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,提出了Mark-Compact算法,标记过程与Mark-Sweep算法一样,但是清理过程不一样。

Mark-Compact算法的清理过程是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存

Generational Collection - 分代收集

这种算法并没有什么新的思想,只是根据对象存活周期的不同进行划分内存块。一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用适当的收集算法。

在新生代中,每次GC时都发现有大批对象死去,只有少量对象存活下来,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成回收。

在老年代中,因为对象的存活率高,没有额外空间对它进行分配担保,就必须使用Mark-Sweep或者Mark-Compact算法进行回收。

Minor GC与复制算法

现在的商业虚拟机都使用复制算法来回收新生代。新生代的GC又叫“Minor GC”,IBM公司的专门研究表明:新生代中的对象98%是“朝生夕死”的,所以Minor GC非常频繁,一般回收速度也比较快,同时“朝生夕死”的特性也使得Minor GC使用复制算法时不需要按照1:1的比例来划分新生代内存空间。

Minor GC过程

事实上,新生代将内存分为一块较大的Eden空间和两块较小的Survivor空间(From Survivor和To Survivor),每次Minor GC都使用Eden和From Survivor,当回收时,将Eden和From Survivor中还存活着的对象都一次性地复制到另外一块To Survivor空间上,最后清理掉Eden和刚使用的Survivor空间。

一次Minor GC结束的时候,Eden空间和From Survivor空间都是空的,而To Survivor空间里面存储着存活的对象。在下次MinorGC的时候,两个Survivor空间交换他们的标签,现在是空的“From” Survivor标记成为“To”,“To” Survivor标记为“From”。因此,在MinorGC结束的时候,Eden空间是空的,两个Survivor空间中的一个是空的,而另一个存储着存活的对象。

HotSpot虚拟机默认的 Eden:Survivor 的比例是 8:1,由于一共有两块Survivor,所以每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的容量会被“浪费”。

分配担保

上文说的98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代内存进行分配担保(Handle Promotion)。如果另外一块Survivor上没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。


GC Collectors

GC需要完成的三件事:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

如果说收集算法是内存回收的方法论,那么垃圾回收器就是内存回收的具体实现。

  • 两个收集器之间存在连线:它们之间可以搭配使用。
  • 虚拟机所处的区域:它属于新生代收集器还是老年代收集器。

Serial

Serial是最基本的,发展历史最悠久的Collector,在JDK 1.3.1之前,是虚拟机收集新生代的唯一选择。最适用于内存使用小于100MB的应用程序,这时候不论是CMS或G1都发挥不了太大的作用。

它是一个单线程的Collector,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条线程去完成GC,更重要的是在它进行GC时,必须暂停其他所有的工作线程(应用程序的线程),直到它GC结束。

单线程是它的劣势,也是它的优势。

  • 简单而高效
  • 对于限定单个CPU的环境来说,Serial没有线程交互开销,专心做GC工作自然可以获得最高的单线程GC效率。
  • 在桌面应用场景中,Serial是JVM运行在Client模式下的一个很好的选择。

Serial Old

Serial Old是Serial的老年代版本,同样以单线程GC。它主要用于Client模式。

如果用于Sever模式,一种用途是在JDK 1.5之前版本中与Parallel Scavenge搭配使用,另一种用途就是作为CMS的后备预案,在并发收集发生Concurrent Mode Failure时使用。

ParNew

ParNew其实是Serial的多线程版本,除了以多线程GC外,其余行为和设置与Serial是一样的,比如控制参数(-XX:SuvivorRatio)、收集算法、STW对象分配规则、回收策略等等。

ParNew除了多线程GC之外,其他与Serial相比并没有太多创新之处,但它却是许多运行在Server模式下JVM首选的新生代Collector,其中还有一个与性能无关但很重要的原因是,除了Serial外,目前只有它可以与CMS配合使用。

Parallel Scavenge

Parallel Scavenge是一个新生代的Collector,它使用的收集算法是Copying,以多线程进行GC。

它的目标是达到一个可控制的吞吐量(Throughput),CMS是尽可能地缩短在GC时用户线程的停顿时间。

吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + GC时间)。例如,虚拟机运行100分钟,其中GC花费了1分钟,那么吞吐量就是99%。

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

JVM调优关键参数:

  • -XX:MaxGCPauseMillis:尽可能地保证GC的时间不超过该值。
  • -XX:GCTimeRatio:范围是0~100,GC时间占总时间的比率。例如,设置为19,即GC时间的最大占比为5%(1 / (1+19)),默认值为99,即GC最大占比为1%(1 / (1+99))
  • -XX:+UseAdaptiveSizePolicy:不需要手工指定新生代的大小、Eden与Survivor比例、晋升老年代对象年龄等等细节参数,JVM根据当前系统的运行情况进行动态调整这些参数以提供最适合的停顿时间和最大的吞吐量(GC自适应的调节策略 GC Ergonomics)。

Parallel Old

Parallel Old是Parallel Scavenger的老年代版本,以多线程GC,使用Mark-Compact算法。

CMS - Concurrent Mark Sweep

Concurrent Low Pause Collector

CMS是一种期望得到最短GC时间为目标的Collector。CMS是基于Mark-Sweep实现的,整个过程分为以下几个步骤:

  • 初始标记(STW) - CMS initial mark,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
  • 并发标记 - CMS concurrent mark,进行GC Roots Tracing的过程。
  • 重新标记(STW) - CMS remark,为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,一般停顿时间比初始标记长一些,但远比并发标记短。
  • 并发清除 - CMS concurrent sweep

整个过程中最耗时的并发标记和并发清除过程都可以与用户线程一起运行。所以,从总体上来说,CMS内存回收过程与用户线程一起并发执行的。

与CMS配合使用的Collector:

  • Young Generation:Serial、ParNew
  • Tenured Generation:SerialOld

CMS缺点:

  • 对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用一部分CPU资源而导致应用程序慢,总吞吐量会降低。CMS默认启动的GC线程数为(CPU + 3) / 4。
  • 无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。
  • 空间碎片,因为它是基于Mark-Sweep算法实现的。

G1

G1将堆空间划分成了互相独立的区块。每块区域既有可能属于O区、也有可能是Y区,且每类区域空间可以是不连续的(对比CMS的O区和Y区都必须是连续的)。这种将O区划分成多块的理念源于:当并发后台线程寻找可回收的对象时、有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程、但可以用相对较少的时间优先回收包含垃圾最多区块。这也是为什么G1命名为Garbage First的原因:第一时间处理垃圾最多的区块。

G1相对于CMS的区别在:

  • G1在压缩空间方面有优势;
  • G1通过将内存空间分成区域(Region)的方式避免内存碎片问题;
  • Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活;
  • G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象;
  • G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做;
  • G1会在Young GC中使用,而CMS只能在O区使用。

Memory

Heap

Java堆(Heap)是Java虚拟机所管理的内存中最大的一块,它被所有线程共享的,在虚拟机启动时创建。此内存区域唯一的目的是存放对象实例,几乎所有的对象实例都在这里分配内存,且每次分配的空间是不定长的。

在Heap中分配一定的内存来保存对象实例,实际上只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中),在Heap中分配一定的内存保存对象实例和对象的序列化比较类似。

对象实例在Heap中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap中的位置,便于找到该对象实例。

Java虚拟机规范中描述道:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都在堆上分配的定论也并不“绝对”了。

Java堆是垃圾收集器管理的主要区域,因此也被称为”GC堆(Garbage Collected Heap)”。从内存回收的角度看内存空间可如下划分:

  • 新生代(Young)

新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。

新生代又可细分为Eden空间、From Survivor空间、To Survivor空间,默认比例为8:1:1。

  • 老年代(Tenured/Old)

在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。

  • 永久代(Perm)

永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。

新生代和老年代组成了Java堆的全部内存区域,而永久代不属于堆空间,它在JDK 1.8以前被Sun HotSpot虚拟机用作方法区的实现

Direct Memory

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但这部分内存也被频繁运用,而却可能导致OutOfMemoryError异常出现,所以这里放到一起讲解。

以NIO(New Input/Output)类为例,NIO引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能避免在Java堆和Native堆中来回复制数据,在一些场景里显著提高性能。

本机直接内存的分配不会受到Java堆大小的限制,但是既然是内存,还是会受到本机总内存(包括RAM以及SWAP区或分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统的限制),从而导致动态扩展时出现OutOfMemoryError异常。

方法区(Method Area)

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。

Object Class Data(类定义数据)是存储在方法区的,此外,常量、静态变量、JIT编译后的代码也存储在方法区。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 Non-Heap。

  • JDK 1.8以前的永久代(PermGen)

Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集,也就是说,Java虚拟机规范只是规定了方法区的概念和它的作用,并没有规定如何去实现它。

对于JDK 1.8之前的版本,HotSpot虚拟机设计团队选择把GC分代收集扩展至方法区,即用永久代来实现方法区,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他的虚拟机(如Oracle JRockit、IBM J9等)来说是不存在永久代的概念的。

如果运行时有大量的类产生,可能会导致方法区被填满,直至溢出。常见的应用场景如:

  1. Spring和ORM框架使用CGLib操纵字节码对类进行增强,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。

  2. 大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)。

  3. 基于OSGi的应用(即使是同一个类文件,被不同的类加载器加载也会视为不同的类)

这些都会导致方法区溢出,报出java.lang.OutOfMemoryError: PermGen space

  • JDK 1.8的元空间(Metaspace)

在JDK 1.8中,HotSpot虚拟机设计团队为了促进HotSpot与 JRockit的融合,修改了方法区的实现,移除了永久代,选择使用本地化的内存空间(而不是JVM的内存空间)存放类的元数据,这个空间叫做元空间(Metaspace)。

做了这个改动以后,java.lang.OutOfMemoryError: PermGen的空间问题将不复存在,并且不再需要调整和监控这个内存空间。

虚拟机需要为方法区设计额外的GC策略:如果类元数据的空间占用达到参数“MaxMetaspaceSize”设置的值,将会触发对死亡对象和类加载器的垃圾回收。

为了限制垃圾回收的频率和延迟,适当的监控和调优元空间是非常有必要的。元空间过多的垃圾收集可能表示类、类加载器内存泄漏或对你的应用程序来说空间太小了。

元空间的内存管理由元空间虚拟机来完成。先前,对于类的元数据我们需要不同的垃圾回收器进行处理,现在只需要执行元空间虚拟机的C++代码即可完成。在元空间中,类和其元数据的生命周期和其对应的类加载器是相同的。话句话说,只要类加载器存活,其加载的类的元数据也是存活的,因而不会被回收掉。

准确的来说,每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元空间。当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。

  • 运行时常量池(Runtime Constant Pool)

运行时常量池(Runtime Constant Pool)是方法区的一部分。

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

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译器才能产生,也就是并非置入Class文件中的常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,此特性被开发人员利用得比较多的便是String类的intern() 方法。


Performance

GC Arithmetic

Conditions:

  • -XX:+UseSerialGC, -XX:+UseParallelGC, -XX:+UseConcMarkSweepGC, -XX:ParallelCMSThreads=2, -XX:ParallelCMSThreads=4, -XX:+UseG1GC
  • 每次运行大概花55分钟
  • -Xmx2048M -server
  • OpenJDK version: 1.8.0_51
  • Software: Linux version 4.0.4-301.fc22.x86_64
  • Hardware: Intel® Core™ i7-4790 CPU @ 3.60GHz
  • 每次通过optaplanner解决13个问题,每个问题大概5分钟,并且前30秒的JVM预热时间不计算在内。
  • 解决问题时不会发生IO,运行过程中,单个CPU完全饱和,并且会一直创建很多生命周期很短的对象,然后GC负责收集它们。
  • 基准测试测量每毫秒能被计算的分数,越高表示越好。需要说明的是,计算一个分数可不是一件容易的事情,它涉及很多计算,有兴趣的话,可以去optaplanner查看它们的源码。

结果非常清晰,JDK8默认的ParallelGC是最快的,其他垃圾回收器相比默认的ParallelGC都会有不同程度的衰减,并且G1表现最差,是最慢的。


Reference

http://ifeve.com/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3g1%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8/
http://blog.jobbole.com/109170/
https://crowhawk.github.io/2017/08/15/jvm_3/
http://www.importnew.com/27793.html
https://mp.weixin.qq.com/s/2H2ce_n2NQXWxueImpKaMA
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html#sthref5