0%

什么是SPI

SPI全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制,比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。我们经常遇到的就是java.sql.Driver接口,不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。
1635dec2151e31e
类图中,接口是定义的抽象SPI接口;实现方实现SPI接口;调用方依赖SPI接口。
SPI接口的定义在调用方,在概念上更依赖调用方;组织上位于调用方所在的包中;实现位于独立的包中。
当接口属于实现方的情况,实现方提供了接口和实现,这个用法很常见,属于API调用。我们可以引用接口来达到调用某实现类的功能。

阅读全文 »

最近在学习使用latex,需要一些windows下面特有的字体,因此需要安装这些字体到ubuntu下面。本篇文章将主要记录我在ubuntu中安装windows中的字体过程。也适用于安装其他的字体。另外这个安装过程适用于以Debian为基础的系统。

linux系统的字体文件放在/usr/share/fonts/目录以及用户的~/.fonts~/.local/share/fonts目录下,第一个位置为系统所用用户共享,将字体安装到这个目录需要管理员权限;后面两个位置则为当前登陆用户所有,安装字体到这个目录不需要管理员权限。

下面来讲解我的安装过程。

安装字体到/usr/share/fonts

1
2
3
4
5
6
7
8
9
10
11
12
## 准备安装的字体,这里是我从windows下面拷贝过来的,目录名称font

## 最好自己在/usr/share/fonts 目录下面创建一个子目录放置自己需要安装的字体
sudo mkdir -p /usr/share/fonts/windows
sudo mv font /usr/lshare/fonts/windows

## 生成核心字体,下面俩个命令是可选
sudo mkfontscale
sudo mkfontdir

## 刷新字体缓存
sudo fc-cache -fv

上面已经成功安装字体到系统中,但是如何确定安装是否成功呢,下面是我自己想的办法,如果你有好的办法,可以留言给我。
下面这个命令可以查看系统中的所有字体

1
2
fc-list  # 查看所有的字体
fc-list :lang=zh # 查看所有的中文字体

我通过下面这个命令来查看字体是否安装成功

1
fc-list | grep "替换成自己安装的字体名"

参考

  1. ubuntu查看支持的字体库
  2. ubuntu安装新字体

简介

跨域资源共享(CORS)是一种机制,它使用额外的HTTP头来告诉浏览器让运行在一个origin(domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域HTTP请求。

比如,站点http://domain-a.com的某HTML页面通过img的src请求http://domain-b.com/image.jpg。网络上的许多页面都会加载来自不同域的CSS样式表,图像和脚本等资源。

出于安全原因,浏览器限制从脚本内发起的跨源HTTP请求。 例如,XMLHttpRequest和Fetch API遵循同源策略。这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。

阅读全文 »

本文主要讲解Java8中JVM的一个更新,就是持久代的移除。将会介绍为什么需要移除持久代以及它的替代者:元空间(Metaspace)。

持久代

绝大部分Java程序员应该都见过 “java.lang.OutOfMemoryError: PermGen space “这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是JVM规范,而后者则是JVM规范的一种实现,并且只有HotSpot才有 “PermGen space”,而对于其他类型的虚拟机,如JRockit(Oracle)、J9(IBM)并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在jsp页面比较多的情况,容易出现永久代内存溢出。我们现在通过动态生成类来模拟 “PermGen space”的内存溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 代码段1
public class Test {
}

// 持续创建新的class
public class PermGenOomMock{
public static void main(String[] args) {
URL url = null;
List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
try {
url = new File("/tmp").toURI().toURL();
URL[] urls = {url};
while (true){
ClassLoader loader = new URLClassLoader(urls);
classLoaderList.add(loader);
loader.loadClass("com.paddx.test.memory.Test");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行结果如下
820406-20160327005846979-1124627174
本例中使用的JDK版本是1.7,指定的PermGen区的大小为8M。通过每次生成不同URLClassLoader对象来加载Test类,从而生成不同的类对象,这样就能看到我们熟悉的 “java.lang.OutOfMemoryError: PermGen space “异常。这里之所以采用JDK 1.7,是因为在JDK1.8 中, HotSpot已经没有 “PermGen space”这个区间了,取而代之是一个叫做Metaspace(元空间)的东西。
其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。

java 6中JVM的内存结构如下:
java_memory_permGen

持久代中包含了虚拟机中所有可通过反射获取到的数据,比如Class和Method对象。不同的Java虚拟机之间可能会进行类共享,因此持久代又分为只读区和读写区。

JVM用于描述应用程序中用到的类和方法的元数据也存储在持久代中。JVM运行时会用到多少持久代的空间取决于应用程序用到了多少类。除此之外,Java SE库中的类和方法也都存储在这里。

如果JVM发现有的类已经不再需要了,它会去回收(卸载)这些类,将它们的空间释放出来给其它类使用。Full GC会进行持久代的回收。

  • Java堆中用到的JVM类的元数据。
  • Java类对应的HotSpot虚拟机中的内部表示也存储在这里。
  • 类的层级信息,字段,名字。
  • 方法的编译信息及字节码。
  • 变量
  • 常量池和符号解析

持久代的大小

  • 它的上限是MaxPermSize,默认是64M - 85M

  • Java堆中的连续区域:如果存储在非连续的堆空间中的话,要定位出老年代和新生代中的对象引用是非常复杂并且耗时。卡表(card table),是一种记忆集(Remembered Set),它用来记录某个内存代中普通对象指针(oops)的修改。

  • 持久代用完后,会抛出OutOfMemoryError “PermGen space”异常。解决方案:

    • 应用程序清理引用来触发类卸载;
    • 增加MaxPermSize的大小。
  • 需要多大的持久代空间取决于类的数量,方法的大小,以及常量池的大小。

为什么移除持久代

  • 它的大小是在启动时固定死的——很难进行调优。
    -XX:MaxPermSize

  • HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,同时它对应用不透明,且是非强类型,难以跟踪调试,还需要存储元数据的元数据信息(meta-metadata)。

  • 简化Full GC:每一个回收器有专门的元数据迭代器。

  • 可以在GC不进行暂停的情况下并发地释放类数据。

  • 使得原来受限于持久代的一些改进未来有可能实现

元空间

jvm_metapsace
持久代的空间被彻底地删除了,它被一个叫元空间的区域所替代了。持久代删除了之后,很明显,JVM会忽略PermSize和MaxPermSize这两个参数,还有就是你再也看不到java.lang.OutOfMemoryError: PermGen error的异常。

JDK 8的HotSpot JVM现在使用的是本地内存来表示类的元数据,这个区域就叫做元空间。

元空间的特点:

  • 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
  • 每个加载器有专门的存储空间
  • 只进行线性分配
  • 不会单独回收某个类(除了RedefineClasses和类加载失败)
  • 省掉了GC扫描及压缩的时间
  • 元空间里的对象的位置是固定的
  • 当类加载器被GC发现死亡时,进行集体回收

元空间的内存分配模型:

  • 绝大多数的类元数据的空间都从本地内存中分配
  • 用来描述类元数据的类也被删除了
  • 可以给元数据分配多个虚拟映射内存空间。
  • 给每个类加载器分配一个内存块的列表。
    1. 块的大小取决于类加载器的类型;
    2. sun/反射/代理对应的类加载器的块会小一些
  • 归还内存块,释放内存块列表
  • 一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
  • 减少碎片的策略

我们来看下JVM是如何给元数据分配虚拟内存的空间的
metaspace_allocation_java_latte

你可以看到虚拟内存空间是如何分配的(vs1,vs2,vs3) ,以及类加载器的内存块是如何分配的。CL是Class Loader的缩写。

理解_mark和_klass指针
要想理解下面这张图,你得搞清楚这些指针都是什么东西。
JVM中,每个对象都有一个指向它自身类的指针,不过这个指针只是指向具体的实现类,而不是接口或者抽象类。

对于32位的JVM:
_mark : 4字节常量
_klass: 指向类的4字节指针 对象的内存布局中的第二个字段( _klass,在32位JVM中,相对对象在内存中的位置的偏移量是4,64位的是8)指向的是内存中对象的类定义。

64位的JVM:
_mark : 8字节常量
_klass: 指向类的8字节的指针
开启了指针压缩的64位JVM: _mark : 8字节常量,_klass: 指向类的4字节的指针

java对象的内存布局

java_object_layout_java_latte
类指针压缩空间(Compressed Class Pointer Space)
只有是64位平台上启用了类指针压缩才会存在这个区域。对于64位平台,为了压缩JVM对象中的_klass指针的大小,引入了类指针压缩空间(Compressed Class Pointer Space)。
compressed_class_pointer_space_java_latte
类指针压缩空间(Compressed Class Pointer Space)
java_object_layout_compressed_java_latte
指针压缩概要
64位平台上默认打开。

  • 使用-XX:+UseCompressedOops压缩对象指针 “oops”指的是普通对象指针(“ordinary” object pointers)。 Java堆中对象指针会被压缩成32位。 使用堆基地址(如果堆在低26G内存中的话,基地址为0)
  • 使用-XX:+UseCompressedClassPointers选项来压缩类指针
  • 对象中指向类元数据的指针会被压缩成32位
  • 类指针压缩空间会有一个基地址

元空间和类指针压缩空间的区别

  • 类指针压缩空间只包含类的元数据,

    • 比如InstanceKlass, ArrayKlass
      • 仅当打开了UseCompressedClassPointers选项才生效
      • 为了提高性能,Java中的虚方法表也存放到这里
      • 这里到底存放哪些元数据的类型,目前仍在减少
  • 元空间包含类的其它比较大的元数据,比如方法,字节码,常量池等。

元空间的调优

使用-XX:MaxMetaspaceSize参数可以设置元空间的最大值,默认是没有上限的,也就是说你的系统内存上限是多少它就是多少。-XX:MetaspaceSize选项指定的是元空间的初始大小,如果没有指定的话,元空间会根据应用程序运行时的需要动态地调整大小。

MaxMetaspaceSize的调优

  • -XX:MaxMetaspaceSize={unlimited}
  • 元空间的大小受限于你机器的内存
  • 限制类的元数据使用的内存大小,以免出现虚拟内存切换以及本地内存分配失败。
    • 如果怀疑有类加载器出现泄露,应当使用这个参数;
    • 32位机器上,如果地址空间可能会被耗尽,也应当设置这个参数。
  • 元空间的初始大小是21M——这是GC的初始的高水位线,超过这个大小会进行Full GC来进行类的回收。
  • 如果启动后GC过于频繁,请将该值设置得大一些
  • 可以设置成和持久代一样的大小,以便推迟GC的执行时间

CompressedClassSpaceSize的调优

  • 只有当-XX:+UseCompressedClassPointers开启了才有效
  • -XX:CompressedClassSpaceSize=1G
  • 由于这个大小在启动的时候就固定了的,因此最好设置得大点。
  • 没有使用到的话不要进行设置
  • JVM后续可能会让这个区可以动态的增长。不需要是连续的区域,只要从基地址可达就行;可能会将更多的类元信息放回到元空间中;未来会基于PredictedLoadedClassCount的值来自动的设置该空间的大小

元空间的一些工具

  • jmap -permstat改成了jmap -clstats。它用来打印Java堆的类加载器的统计数据。对每一个类加载器,会输出它的名字,是否存活,地址,父类加载器,以及它已经加载的类的数量及大小。除此之外,驻留的字符串(intern)的数量及大小也会打印出来。
  • jstat -gc,这个命令输出的是元空间的信息而非持久代的
  • jcmd GC.class_stats提供类元数据大小的详细信息。使用这个功能启动程序时需要加上-XX:+UnlockDiagnosticVMOptions选项。

提高GC的性能
如果你理解了元空间的概念,很容易发现GC的性能得到了提升。

  • Full GC中,元数据指向元数据的那些指针都不用再扫描了。很多复杂的元数据扫描的代码(尤其是CMS里面的那些)都删除了。
  • 元空间只有少量的指针指向Java堆。这包括:类的元数据中指向java/lang/Class实例的指针;数组类的元数据中,指向java/lang/Class集合的指针。
  • 没有元数据压缩的开销
  • 减少了根对象的扫描(不再扫描虚拟机里面的已加载类的字典以及其它的内部哈希表)
  • 减少了Full GC的时间
  • G1回收器中,并发标记阶段完成后可以进行类的卸载

总结

  • Hotspot中的元数据现在存储到了元空间里。mmap中的内存块的生命周期与类加载器的一致。
  • 类指针压缩空间(Compressed class pointer space)目前仍然是固定大小的,但它的空间较大
  • 可以进行参数的调优,不过这不是必需的。
  • 未来可能会增加其它的优化及新特性。比如,应用程序类数据共享;新生代GC优化,G1回收器进行类的回收;减少元数据的大小,以及JVM内部对象的内存占用量。

参考

  1. Java 8的元空间
  2. Metaspace in Java 8
  3. Java虚拟机16:Metaspace
  4. java持久代

翻译自what-is-garbage-collection
本篇文章主要讨论什么是GC,为什么要有GC?

什么是GC(Garbage Collection)

乍一看,垃圾收集应该处理名称所暗示的 —-找到并扔掉垃圾。实际上它恰恰相反。垃圾收集器追踪仍在使用的所有对象,并将其余对象标记为垃圾。考虑到这一点,我们开始深入研究Java虚拟机是如何实现内存的自动回收,在Java中这个过程叫做GC。

这篇文章不会一开始就深入GC的细节,而是先介绍垃圾收集器的一般性质,然后介绍核心概念和方法。

免责声明:此内容侧重于Oracle Hotspot和OpenJDK。在其他JVM(例如jRockit或IBM J9)上,本文中涉及的某些方面可能表现不同。

手动内存管理

在我们开始介绍垃圾收集之前,让我们快速回顾一下您必须手动并明确地为数据分配和释放内存的日子。如果你忘了释放它,你将无法重复使用内存。内存被声明但没有使用。这种情况称为内存泄漏。

下面是一个C语言写的例子,手动管理内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int send_request() {
size_t n = read_size();
// 申请内存
int *elements = malloc(n * sizeof(int));

if(read_elements(n, elements) < n) {
// elements not freed!
return -1;
}

// …
// 释放内存
free(elements)
return 0;
}

我们可以看到,这是很容易忘记释放申请的内存。内存泄漏过问题在去是比现在更常见的问题。你只能通过修复代码来释放它们。因此,更好的方法是自动回收未使用的内存,完全消除人为错误的可能性。这种自动化称为垃圾收集(简称GC)。

智能指针

自动化的第一种方法之一是使用析构函数。例如,在C++中vector就是利用使用的这种方式,当它检测到对象不再使用范围时,析构函数将被自动调用:

1
2
3
4
5
6
7
8
9
10
int send_request() {
size_t n = read_size();
vector<int> elements = vector<int>(n);

if(read_elements(elements.size(), &elements[0]) < n) {
return -1;
}

return 0;
}

但是在更复杂的情况下,特别是在跨多个线程共享对象时,只有析构函数是不够的。最简单的垃圾收集形式:引用计数。对于每个对象,您只需知道它被引用的次数以及该计数何时达到零,就可以安全地回收该对象。一个众所周知的例子是C++的共享指针:

1
2
3
4
5
6
7
8
9
10
11
12
int send_request() {
size_t n = read_size();
auto elements = make_shared<vector<int>>();

// read elements

store_in_cache(elements);

// process elements further

return 0;
}

现在,为了避免在下次调用函数时读取元素,我们可能希望缓存它们。在这种情况下,当向量超出范围时销毁它不是一种选择。因此,我们使用shared_ptr。它会跟踪对它的引用数量。传递它时这个数字会增加,而当它离开范围时会减少。一旦引用数达到零,shared_ptr就会自动删除基础向量。

自动内存管理

在上面的C++代码中,我们仍然必须明确说明何时需要处理内存管理。但是,如果我们能够使所有对象都以这种方式运行呢?这将非常方便,因为开发人员不再需要考虑自己清理。运行时将自动了解某些内存不再使用并释放它。换句话说,它会自动回收对象,第一个垃圾收集器是在1959年为Lisp创建的,从那时起该技术才得以发展。

引用计数

我们用C++的共享指针演示的想法可以应用于所有对象。许多语言,如Perl,Python或PHP都采用这种方法。最好用图片说明:
引用计数
绿色云表示他们指向的对象仍然由程序使用。从技术上讲,这些可能是当前正在执行的方法中的局部变量或静态变量或其他内容。它可能因编程语言而异,因此我们不会在此处关注它。蓝色圆圈是内存中的活动对象,其中的数字表示其引用计数。最后,灰色圆圈是未从任何仍明确使用的对象引用的对象(这些对象由绿色云直接引用)。灰色对象因此是垃圾,可以由垃圾收集器清理。这一切看起来都很好,不是吗?嗯,确实如此,但整个方法都有很大的缺点。由于循环引用,它们的引用计数不为零,因此很容易最终得到一个分离的对象循环,这些对象都不在范围内。这是一个例子:
Java-GC-cyclical-dependencies

标记-清除

首先,JVM更具体地说明了对象的可达性。我们有一个非常具体和明确的对象集,称为GC ROOTS,而不是我们在前面章节中看到的模糊定义的绿云。

  • 局部变量
  • 活动线程
  • 静态字段
  • JNI引用

JVM用于跟踪所有可到达(实时)对象并确保不可访问对象声明的内存可以重用的方法称为标记和清除算法。它包括两个步骤:

  • 标记:正在遍历所有可到达的对象,从GC ROOTS开始并在本机内存中保留有关所有和GC ROOTS对象的关联的对象。
  • 扫描:确保下一次分配可以重用不可到达对象占用的内存地址。

JVM中的不同GC算法(例如并行清除,并行标记+复制或CMS)正在以稍微不同的方式实现这些阶段,但在概念级别,该过程仍然类似于上述两个步骤。关于这种方法的一个至关重要的事情是循环不再泄漏:
Java-GC-mark-and-sweep

不太好的事情是需要停止应用程序线程以便于收集无用对象的程序执行,因为如果它们一直在不断变化,你就无法真正计算引用。当应用程序暂时停止以便JVM可以专门来进行信息的收集,这种情况称为“stop the world”(STW)。它们可能由于多种原因而发生,但垃圾收集是迄今为止最受欢迎的一种。

本文主要讨论HTTP协议中的Transfer-Encoding。Transfer-Encoding,是一个 HTTP 头部字段,字面意思是「传输编码」。实际上,HTTP协议中还有另外一个头部与编码有关:Content-Encoding(内容编码)。Content-Encoding通常用于对实体内容进行压缩编码,目的是优化传输,例如用gzip压缩文本文件,能大幅减小体积。内容编码通常是选择性的,例如jpg/png这类文件一般不开启,因为图片格式已经是高度压缩过的,再压一遍没什么效果不说还浪费CPU。

而Transfer-Encoding则是用来改变报文格式(这个可能你现在还不理解,先看后面),它不但不会减少实体内容传输大小,甚至还会使传输变大,那它的作用是什么呢?本文接下来主要就是讲这个。我们先记住一点,Content-Encoding和Transfer-Encoding二者是相辅相成的,对于一个HTTP报文,很可能同时进行了内容编码和传输编码。

阅读全文 »

Accept-EncodingContent-Encoding是HTTP中用来对「采用何种编码格式传输正文」进行协定的一对头部字段。它的工作原理是这样:浏览器发送请求时,通过Accept-Encoding带上自己支持的内容编码格式列表;服务端从中挑选一种用来对正文进行编码,并通过Content-Encoding响应头指明选定的格式;浏览器拿到响应正文后,依据Content-Encoding进行解压。当然,服务端也可以返回未压缩的正文,但这种情况不允许返回Content-Encoding。这个过程就是HTTP的内容编码机制。

内容编码目的是优化传输内容大小,通俗地讲就是进行压缩。一般经过gzip压缩过的文本响应,只有原始大小的1/4。对于文本类响应是否开启了内容压缩,是我们做性能优化时首先要检查的重要项目;而对于JPG/PNG这类本身已经高度压缩过的二进制文件,不推荐开启内容压缩,效果微乎其微还浪费CPU。不过谷歌开源了一个新的JPG图片压缩算法guetzli,这个算法只有原来的1/3大小,有兴趣可以看一下。

阅读全文 »

本篇文章将要介绍的是ParNew和CMS搭配使用的垃圾回收器组合。CMS和其他老年代收集器不同的地点是,它使用的是标记-清除算法。

CMS收集器设计的目的是避免在老年代收集时长时间停顿。它通过两种方式实现这一目标。首先,它不是采用标记-拷贝来回收老年代,而是使用可被回收链表来管理回收空间。其次,整个垃圾回收器的执行期间,绝大部分是可以和应用程序并发执行。这意味着垃圾收器不会显式停止应用程序线程执行来执行。但是应该注意,它仍然与应用程序线程竞争CPU时间。默认情况下,此GC算法使用的线程数等于计算机物理核心数的1/4。

如果您的主要目标是延迟,这种组合在多核机器上是一个不错的选择。减少单个GC暂停的持续时间会直接影响终端用户使用应用程序的感受,从而使他们感觉应用程序响应更快。由于大多数时候GC消耗了一些CPU资源而没有执行应用程序的代码,因此CMS垃圾回收器通常比只运行Parallel GC的垃圾回收器的吞吐量要差一些。

正文

如果想要使用这中搭配方式,需要在运行java程序时使用下面参数:

1
-XX:+UseParNewGC -  -XX:+UseConcMarkSweepGC

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**************************************
* Author : zhangke
* Date : 2019-02-20 11:34
* email : [email protected]
* Desc : 年轻代使用ParNew 老年代使用CMS
*
* -verbose:gc -Xms20M -Xmx20M -Xmn10M
* -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
* -XX:MaxTenuringThreshold=1
* -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
*
***************************************/
public class CMS {
private static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws InterruptedException {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[4 * _1MB];
allocation2 = new byte[3 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[2 * _1MB];
allocation1[0] = 'd';
allocation2[1] = 'd';
// 防止GC日志输出不完,应用就结束
Thread.sleep(10);
}
}

输出日志信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2019-04-18T15:38:27.114+0800: 0.112: [GC (Allocation Failure) 2019-04-18T15:38:27.114+0800: 0.112: [ParNew: 5640K->376K(9216K), 0.0099638 secs] 5640K->4474K(19456K), 0.0100193 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 

2019-04-18T15:38:27.126+0800: 0.124: [GC (Allocation Failure) 2019-04-18T15:38:27.126+0800: 0.124: [ParNew (promotion failed): 7699K->7322K(9216K), 0.0095832 secs]2019-04-18T15:38:27.136+0800: 0.134: [CMS: 7496K->7487K(10240K), 0.0045668 secs] 11797K->11621K(19456K), [Metaspace: 3006K->3006K(1056768K)], 0.0142215 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]

2019-04-18T15:38:27.141+0800: 0.139: [GC (CMS Initial Mark) [1 CMS-initial-mark: 7487K(10240K)] 13833K(19456K), 0.0002405 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-04-18T15:38:27.141+0800: 0.139: [CMS-concurrent-mark-start]
2019-04-18T15:38:27.141+0800: 0.139: [CMS-concurrent-mark: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-04-18T15:38:27.141+0800: 0.139: [CMS-concurrent-preclean-start]
2019-04-18T15:38:27.142+0800: 0.139: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-04-18T15:38:27.142+0800: 0.139: [CMS-concurrent-abortable-preclean-start]
2019-04-18T15:38:27.142+0800: 0.139: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-04-18T15:38:27.142+0800: 0.140: [GC (CMS Final Remark) [YG occupancy: 6345 K (9216 K)]2019-04-18T15:38:27.142+0800: 0.140: [Rescan (parallel) , 0.0006864 secs]2019-04-18T15:38:27.142+0800: 0.140: [weak refs processing, 0.0001303 secs]2019-04-18T15:38:27.142+0800: 0.140: [class unloading, 0.0003487 secs]2019-04-18T15:38:27.143+0800: 0.141: [scrub symbol table, 0.0003745 secs]2019-04-18T15:38:27.143+0800: 0.141: [scrub string table, 0.0002689 secs][1 CMS-remark: 7487K(10240K)] 13833K(19456K), 0.0019541 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-sweep-start]
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-reset-start]
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

年轻代(Minor GC)

日志信息如下:

1
2019-04-18T15:38:27.114+0800: 0.112: [GC (Allocation Failure) 2019-04-18T15:38:27.114+0800: 0.112: [ParNew: 5640K->376K(9216K), 0.0099638 secs] 5640K->4474K(19456K), 0.0100193 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 

按照显示的顺序来进行介绍:

  1. 2019-04-18T15:38:27.114+0800:GC开始的时间
  2. 0.112:GC开始相对于JVM启动的开始时间,单位是秒
  3. GC:标记用于辨别是年轻代GC还是Full GC(回收年轻代和老年代)
  4. Allocation Failure:造成GC的原因。在这个案例里,GC被启动的原因是年轻代所剩的空间不能满足对象的分配。
  5. ParNew:GC回收器使用的名称。这里代表ParNew垃圾回收器。
  6. 5640K->376K(9216K): GC回收前年轻代使用的空间大小->GC回收后年轻代使用的空间大小(年轻代总空间大小)
  7. 0.0099638 secs:收集持续的时间,还没有最终的清理
  8. 5640K->4474K(19456K):GC前堆已被使用的空间大小->GC后堆中已被使用的空间大小(堆可被使用的总大小)
  9. 0.0100193 secs:整个GC持续的时间,标记和复制年轻代活着的对象所花费的时间(包括和老年代通信的开销、对象晋升到老年代开销、垃圾收集周期结束一些最后的清理对象等的花销)
  10. [Times: user=0.00 sys=0.00, real=0.00 secs] :参照前面的文章这里就不具体解释

老年代

下面是CMS垃圾回收器的日志,这些是按照CMS的不同阶段打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
2019-04-18T15:38:27.141+0800: 0.139: [GC (CMS Initial Mark) [1 CMS-initial-mark: 7487K(10240K)] 13833K(19456K), 0.0002405 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-04-18T15:38:27.141+0800: 0.139: [CMS-concurrent-mark-start]
2019-04-18T15:38:27.141+0800: 0.139: [CMS-concurrent-mark: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-04-18T15:38:27.141+0800: 0.139: [CMS-concurrent-preclean-start]
2019-04-18T15:38:27.142+0800: 0.139: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-04-18T15:38:27.142+0800: 0.139: [CMS-concurrent-abortable-preclean-start]
2019-04-18T15:38:27.142+0800: 0.139: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-04-18T15:38:27.142+0800: 0.140: [GC (CMS Final Remark) [YG occupancy: 6345 K (9216 K)]2019-04-18T15:38:27.142+0800: 0.140: [Rescan (parallel) , 0.0006864 secs]2019-04-18T15:38:27.142+0800: 0.140: [weak refs processing, 0.0001303 secs]2019-04-18T15:38:27.142+0800: 0.140: [class unloading, 0.0003487 secs]2019-04-18T15:38:27.143+0800: 0.141: [scrub symbol table, 0.0003745 secs]2019-04-18T15:38:27.143+0800: 0.141: [scrub string table, 0.0002689 secs][1 CMS-remark: 7487K(10240K)] 13833K(19456K), 0.0019541 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-sweep-start]
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-reset-start]
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

下面分别来介绍各个阶段的情况:

阶段1:Initial Mark

这个是 CMS 两次 stop-the-wolrd 事件的其中一次,这个阶段的目标是:标记那些直接被 GC root 引用或者被年轻代存活对象所引用的所有对象,标记后示例如下所示
cms-1
上述例子对应的日志信息为:

1
2019-04-18T15:38:27.141+0800: 0.139: [GC (CMS Initial Mark) [1 CMS-initial-mark: 7487K(10240K)] 13833K(19456K), 0.0002405 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

日志的意思如下:

  1. 2019-04-18T15:38:27.141+0800:0.139: GC 开始的时间,以及相对于 JVM 启动的相对时间(单位是秒,这里大概是4.33h),与前面 ParNew 类似,下面的分析中就直接跳过这个了;
  2. CMS-initial-mark:初始标记阶段,它会收集所有 GC Roots 以及其直接引用的对象;
  3. 7487K:当前老年代使用的容量;
  4. (10240k):老年代可用的最大容量,这里是 10M;
  5. 13833K:整个堆目前使用的容量;
  6. (19456K):堆可用的容量,这里是19M;
  7. 0.0002405 secs:这个阶段的持续时间;
  8. [Times: user=0.04 sys=0.00, real=0.04 secs]:与前面的类似,这里是相应 user、system and real 的时间统计。

阶段2 concurrent Mark

在这个阶段GC垃圾回收器会遍历老年代,然后标记所有存活的对象,它会根据上个阶段找到的 GC Roots 遍历查找。并发标记阶段,它会与用户的应用程序并发运行。并不是老年代所有的存活对象都会被标记,因为在标记期间用户的程序可能会改变一些引用,如下图所示:
g1-07-591x187
在上面的图中,与阶段1的图进行对比,就会发现有一个对象的引用已经发生了变化,这个阶段相应的日志信息如下:

1
2
2019-04-18T15:38:27.141+0800: 0.139: [CMS-concurrent-mark-start]
2019-04-18T15:38:27.141+0800: 0.139: [CMS-concurrent-mark: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  1. CMS-concurrent-mark:并发收集阶段,这个阶段会遍历老年代,并标记所有存活的对象;
  2. 0.000/0.000 secs:这个阶段的持续时间与时钟时间;
  3. [Times: user=0.00 sys=0.00, real=0.00 secs] :如前面所示,但是这部分的时间,其实意义不大,因为它是从并发标记的开始时间开始计算,这期间因为是并发进行,不仅仅包含 GC 线程的工作,还包括了应用线程并行的线程。

阶段3:concurrent Preclean

Concurrent Preclean:这也是一个并发阶段,与应用的线程并发运行,并不会stop应用的线程。上面一个阶段和应用并发运行的过程中,一些对象的引用可能会发生变化,但是这种情况发生时,JVM会将包含这个对象的区域(Card)标记为 Dirty,这也就是 Card Marking。
cms-3
在pre-clean阶段,那些能够从 Dirty 对象到达的对象也会被标记,这个标记做完之后,dirty card 标记就会被清除了
cms-4

1
2
2019-04-18T15:38:27.141+0800: 0.139: [CMS-concurrent-preclean-start]
2019-04-18T15:38:27.142+0800: 0.139: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  1. CMS-concurrent-preclean:Concurrent Preclean 阶段,对在前面并发标记阶段中引用发生变化的对象进行标记;
  2. 0.000/0.000 secs:这个阶段的持续时间与时钟时间;
  3. [Times: user=0.00 sys=0.00, real=0.00 secs]:同并发标记阶段中的含义。

阶段4:Concurrent Abortable Preclean

这也是一个并发阶段,同样不会影响影响用户的应用线程。这个阶段是为了尽量承担最终标记阶段的工作(这个阶段会STW)。这个阶段持续时间依赖于很多的因素。这个阶段是在重复做很多相同的工作,直到满足一些条件(比如:重复迭代的次数、完成的工作量或者时钟时间等)就会退出。这个阶段的日志信息如下:

1
2
2019-04-18T15:38:27.142+0800: 0.139: [CMS-concurrent-abortable-preclean-start]
2019-04-18T15:38:27.142+0800: 0.139: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  1. CMS-concurrent-abortable-preclean:Concurrent Abortable Preclean 阶段;
  2. 0.000/0.000 secs:这个阶段的持续时间与时钟时间,本质上,这里的 gc 线程会在 STW 之前做更多的工作,通常会持续 5s 左右;
  3. [Times: user=0.00 sys=0.00, real=0.00 secs]:同前面。

阶段5:Final Remark

这是第二个 STW 阶段,也是 CMS 中的最后一个,这个阶段的目标是标记所有老年代所有的存活对象,由于之前的阶段是并发执行的,gc线程可能跟不上应用程序的变化,为了完成标记老年代所有存活对象的目标,STW就非常有必要了。

通常 CMS的Final Remark 阶段会在年轻代尽可能干净的时候运行,目的是为了减少连续STW发生的可能性(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。这个阶段会比前面的几个阶段更复杂一些,相关日志如下:

1
2019-04-18T15:38:27.142+0800: 0.140: [GC (CMS Final Remark) [YG occupancy: 6345 K (9216 K)]2019-04-18T15:38:27.142+0800: 0.140: [Rescan (parallel) , 0.0006864 secs]2019-04-18T15:38:27.142+0800: 0.140: [weak refs processing, 0.0001303 secs]2019-04-18T15:38:27.142+0800: 0.140: [class unloading, 0.0003487 secs]2019-04-18T15:38:27.143+0800: 0.141: [scrub symbol table, 0.0003745 secs]2019-04-18T15:38:27.143+0800: 0.141: [scrub string table, 0.0002689 secs][1 CMS-remark: 7487K(10240K)] 13833K(19456K), 0.0019541 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
  1. 2019-04-18T15:38:27.142+0800: 0.140:阶段开始的时间,阶段开始的时间相对与JVM启动的时间,单位s
  2. [GC (CMS Final Remark):表示这是CMS的Final Remark阶段
  3. [YG occupancy: 6345 K (9216 K)]:年轻代当前占内存大小,及年轻代总的内存大小
  4. 2019-04-18T15:38:27.142+0800: 0.140: [Rescan (parallel) , 0.0006864 secs]:前面的时间表示的是这个阶段开始的时间,和相对JVM启动的时间。Rescan 是当应用暂停的情况下完成对所有存活对象的标记,这个阶段是并行处理的,这里花费了 0.0006864s;
  5. 2019-04-18T15:38:27.142+0800: 0.140: [weak refs processing, 0.0001303 secs]:前面的时间表示的是这个阶段开始的时间,和相对JVM启动的时间。第一个子阶段,它的工作是处理弱引用;后面是这个阶段花费的时间
  6. 2019-04-18T15:38:27.142+0800: 0.140: [class unloading, 0.0003487 secs]:前面的时间表示的是这个阶段开始的时间,和相对JVM启动的时间。第二个子阶段,它的工作是卸载为被使用的类,后面是这个阶段花费的时间
  7. 2019-04-18T15:38:27.143+0800: 0.141: [scrub symbol table, 0.0003745 secs]:前面时间表示的是这个阶段开始的时间和相对JVM启动的开始时间。第三个子阶段,清理符号表,包含了类级元数据。寿面时间表示这个阶段花费的时间。
  8. 2019-04-18T15:38:27.143+0800: 0.141: [scrub string table, 0.0002689 secs]:前面时间表示的是这个阶段开始的时间和相对JVM启动的开始时间,这是最后的子阶段,清理字符串表,内部化字符串。后面时间表示这个阶段花费的时间。还包括暂停的时钟时间。
  9. [1 CMS-remark: 7487K(10240K)] 13833K(19456K), 0.0019541 secs]这个阶段之后,老年代堆的使用情况(老年代总的内存大小)堆的使用量与总量(包括年轻代,年轻代在前面发生过 GC)
  10. [Times: user=0.01 sys=0.00, real=0.00 secs] :在不同态下执行的时间。

经历过这五个阶段之后,老年代所有存活的对象都被标记过了,现在可以通过清除算法去清理那些老年代不再使用的对象。
g1-10-591x187

阶段6:Concurrent Sweep

这里不需要STW,它是与用户的应用程序并发运行,这个阶段是:清除那些不再使用的对象,回收它们的占用空间为将来使用。如下图所示

1
2
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-sweep-start]
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  1. CMS-concurrent-sweep:这个阶段主要是清除那些没有被标记的对象,回收它们的占用空间;
  2. 0.000/0.000 secs:这个阶段的持续时间与时钟时间;
  3. [Times: user=0.00 sys=0.00, real=0.00 secs] :同前面;

阶段7:Concurrent Reset

这个阶段也是并发执行的,它会重设 CMS 内部的数据结构,为下次的 GC 做准备,对应的日志信息如下:

1
2
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-reset-start]
2019-04-18T15:38:27.144+0800: 0.142: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

日志详情分别如下:

  1. CMS-concurrent-reset :这个阶段的开始,目的如前面所述;
  2. 0.000/0.000 secs:这个阶段的持续时间与时钟时间;
  3. Times: user=0.15 sys=0.10, real=0.04 secs]:同前面。

总结

CMS通过将大量工作分散到并发处理阶段来在减少STW时间,在这块做得非常优秀,但是CMS也有一些其他的问题,具体的可以看这篇文章JVM垃圾回收器

  1. CMS 收集器无法处理浮动垃圾( Floating Garbage),可能出现 “Concurrnet Mode Failure” 失败而导致另一次 Full GC 的产生,可能引发串行Full GC;
  2. 空间碎片,导致无法分配大对象,CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认就是开启的),用于在 CMS 收集器顶不住要进行 Full GC 时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长;
  3. 对于堆比较大的应用上,GC 的时间难以预估。

参考

  1. http://matt33.com/2018/07/28/jvm-cms/
  2. GC Algorithms Implementations

前面写了一篇文章GC日志详解1之serial已经详细介绍了Serial和Serial Old搭配使用时输出日志的解析。这篇文章将详细介绍Parallel Scavenge 和 Parallel Old搭配使用时输出日志的解析。

Parallel Scavenge在年轻代使用,采用标记-复制的方法来进行垃圾回收,而Paralle Old是在老年代进行回收,采用的是标记-整理的方法来进行垃圾回收。俩个垃圾回收器都会暂停所用应用程序的线程,也就是STW现象。叫做Parallel是因为,这俩个垃圾回收器都会使用多线程来进行垃圾的回收,通过这种方式,GC执行的时间也因此减少。

GC在执行的时候,线程的数量是可以配置的。通过参数XX:ParallelGCThreads=NNN来进行配置,如果没有配置,默认的收集线程数等于机器的核心数。

如果你的目标是提高系统的吞吐量,并且运行应用的机器是多核机器,特别适用于这种搭配。能够提高吞吐量的原因是这种搭配能够更高效的使用系统资源:

  • 在GC执行的过程中,所有核心都在并行清理垃圾,从而缩短暂停时间
  • 在GC回收周期之间,也就是在应用执行的过程中,收集者都没有消耗任何资源。(这点我还没搞懂)

另一方面,由于GC收集器的所有阶段都必须在没有任何中断的情况下发生,因此这组收集器仍然容易受到长时间暂停的影响,在此期间应用程序线程将被停止。因此,如果延迟是您的主要目标,您可以选择我在上一篇文章里面提到的后俩种搭配。

阅读全文 »