从Java虚拟机小测的结果来看JVM

前言

在去年年初我为了测试一个出题平台,随手打了几道题目,水了一篇Java虚拟机测试题。没想到今天一看竟然有人回答!热泪盈眶啊……好,那既然你肯回答,我就肯改主观题。截止目前,答题人数为24(有一次是我自己测试)。平均正确率是36%,平均答题时间竟然高达42:37!好感动(难道不是因为有人挂了很久么)。那时隔一年,我们来好好分析下这个测试,并看看这个测试能体现出什么。如果你想先测试下自己的实力,可以点击上面的链接去答题页面。

答题状况分析

那么,接着让我们来分析一下题目与答题状况:

第一题

截止JDK1.8,以属于Java的并行GC的是:

A.ParNew
B.Serial
C.Serial Old
D.Parallel Scavange
E.G1

第一题考察的内容很基本,就是对几大垃圾收集器的了解。

Serial收集器是最古老的收集器,在JDK1.3.1之前甚至是新生代收集的唯一选择。不过,这是一个单线程且采用复制算法,仅对新生代作用的收集器。由于比较旧,触发GC时就会采用“The World”。

你的线程都停下来了,那可怎么办呢JOJO

所有在运行的东西都会停下来!

Serial Old并不是旧版本的Serial收集器,而是Serial收集器的老年代版本。它使用标记-整理算法,同样是多线程的,也同样具有厉害的“The World”技能。

ParNew是Serial收集器的升级版本,也即多线程版本。其他的都一样,该时停的照样得停。

Parallel Scanvenge也是使用了复制算法的,作用于新生代的收集器。较之Serial,它跟追求增加用户代码运行时间的比重,也就是说,不管单次收集时间有多长,它着眼于减少收集时间的比重。而且它还有自动调节参数的功能。

Parallel Old就是适用于老年代的Parallel Scanvenge收集器,和Serial Old类似,它也是使用标记-整理算法的。其他的不同和Serial与Parallel Scanvenge的不同类似。

虽然题干中没提到CMS,但我还是想提及这个收集器。它全称Concurrent Mark Sweep,很明显这是标记-整理算法的收集器。而且,由于并发(Concurrent),CMS的垃圾清理并不需要停止整个程序。但是还CMS还是要时停的……因为初始标记和重新标记两步为了保证一致性,并不能并发的完成。

并发吗……不错的力量和速度。我Serial生活的那个时代,只有单线程可以用啊……

不仅如此,CMS还有很多其他的缺点。不过这里空白太小,我写不下。而且,虽然CMS+ParNew依然是很常用的一种搭配,CMS也将步入历史的尘埃了。JEP 291[1]提到了在未来Java版本中去除CMS收集器的计划,而且该JEP已经被列入JDK 9[2]中。现在你使用CMS收集器的话将会得到一句警告。而废弃CMS的原因很幽默,就是现任维护者Christoph Engelbert看不懂原先CMS复杂晦涩的代码(参见邮件[3])。(话说我在领英上看到,CMS的自适应freelist是JEP的提出者Jon Masamitsu制作的,你为啥不接锅?)不过这个JEP也激起了广泛的讨论。CMS如今在一些内存较小的设备(如树莓派)中有非常不错的性能,而作为替代的G1收集器(后文介绍)在这种情况下的性能并不好。而且由于CMS的侧重点不同,CMS的收集时间比G1要更少。不过随着G1的逐渐改善,相信这种差距会逐渐缩小,甚至于G1完全替代CMS。不过其实除了早期Sun实验室的吹笔论文[4],似乎也没有完整的针对JDK 9的CMS与G1对比,我在StackOverflow和Reddit上看了很多讨论,基本上都是嘴巴压测。(而且就算是Sun的论文,CMS也是有优势。不过,我这里倒是找到一篇简单对比的[5],可惜比较早)嘛,我在此号召各位有志青年与各路大神快去阅读OpenJDK,然后发起#Save CMS#。好在另一份邮件[6]提到移除CMS是一个漫长的过程,所以暂时似乎还能用着CMS过日子?

G1收集器是全新的,也是最新的收集器,在JDK 7u4之后已经完全开放商用了。它是标记-整理算法与复制算法的一种结合,而这种结合建立在将内存分为一个个Region的基础上。G1能同时作用于新生代和老年代,而且进一步的讲,G1收集器直接作用于整个Java堆!从上面那些邮件中,开发者吹爆G1的态度可以看出,众开发者可是对G1寄予厚望的。不过我看到吐槽G1的声音依然不少(甚至有说“Use G1 when you have tons of memory and don’t care about burning CPU… 😉
”哈哈哈),可见G1虽然从04年第一篇论文开始到12年商用花了8年之久,它要走的优化之路还很漫长。

这题说难不难,只要看看带Parallel的就能选出A、D,剩下的E看自己对JVM的了解。在答题过程中,很多人漏选了ParNew。

第二题

关于以下GC日志的说法,正确的是:

13.112: [GC [DefNew: 3324K->152K(3712K), 0.0023122 secs] 3324K->152K(11904K), 0.0035311 secs]

A.13.112代表自上一条log记录时过了13.112秒
B.JVM当前的GC收集器为Serial
C.3324K->152K(3712K)代表清理前占用内存3324K,清理后占用152K,该区域剩余内存3712K
D.本次操作总用时为0.0023122+0.0035311秒

这题没啥好说的,考验的就是对日志的阅读能力。这里说明下这段日志的读法。最开始的时间是自虚拟机启动后过去的时间。GC是这里垃圾收集的类型,即是否为Full GC。随后的是“GC前区域内存->GC后区域内存(区域总内存)”,然后再是GC的执行时间。这两个时间是包含而不是叠加关系。

这里要注意的是,DefNew可能会和ParNew搞混。其实DefNew就是指默认新生代收集器,也就是Serial收集器。所以答案明显应选择B。这题选错的人不多,错项集中在C。

第三题

以下关于Java虚拟机的说法错误的是:

A.类对象不能被回收
B.Java堆不一定能扩展
C.一般来说,大对象的回收周期较小对象长
D.方法区溢出的错误提示信息是PermGen Space

这一题的考察点比较杂。

首先是类对象。因为是对象,Class对象就存在于Java堆中……对吗?答案是否定的,Class对象并没有明确规定存在于Java堆。而事实上,Hotspot中,Class对象存放在方法区中。那么Class对象是不是就不会被回收了呢?也不是,JDK8之前方法区的实现是永久代,所以虽然条件苛刻,但是Class对象也是可以被回收的。而JDK8之后永久代被废弃,并引入了元空间。虽然元空间并不会单独回收一个类,不过,当元空间的大小达到MaxMetaspaceSize时,依旧会触发回收。嘛,不管怎样,类对象都是可以被回收的。

只要设置-Xms于-Xmx一样Java堆就不会自动扩展了。

大对象的回收周期理论上就应该更长,因为回收大对象比回收小对象更加耗时。在JVM中,大对象会直接进入老年代,而老年代的回收周期较新生代要短的很多。

方法区在JDK8之前是以永久代的形式存在的,所以那时溢出的错误提示信息是PermGen Space。而JDK8开始引入元空间之后,错误提示信息也就随之变为Metaspace了。

正确答案无疑是A,不过由于JDK8开始移除了永久代,选择D也是有道理的。这题每个选项都有人选,我比较懵逼。

第四题

以下为GC Root的对象是

A.gc链链顶对象
B.JNI方法的local对象
C.方法的基本类型局部变量
D.类静态字段的对象
E.所有线程

第四题都没啥好说的了。A选项已经明显的不能在明显了,而所有人也基本都选择了这项。和local有关的基本都是GC Root。C选项甚至都不是对象!最后E选项可能会错选,注意,只有活着的线程才能作为GC Root。所以答案就是A、B、D,没有什么特别大的问题。

第五题

以下是一段java代码:

StringBuilder sBuilder = new StringBuilder("ja");
String java = sBuilder.append("va").toString();
System.out.println(java.intern() == java);
String drink = sBuilder.append("hothothothot").toString();
System.out.println(drink.intern() == drink);

资料:Java SE 7 Features and Enhancements。通过此段代码与资料,请以JDK 6、JDK 7为例简单阐述JDK 7更新的特性。(提示:请分别给出两版本的运行结果,指出带来此变化的更新并分析)

这是一道考察同时考察文档阅读与JVM知识的题目,曾经被我当作入群题(因为题目难度过于友好所以仅存活了数小时)。其实说难真的不难,这段代码从头到尾都在强调String::intern方法,所以很明显关键就是这个。对应的更新就是“JDK-6962931 : move interned strings out of the perm gen(RFE6962931)”。其次就是对String::intern方法的理解了。首先要注意的是,如果字符串由toString方法创建,那么字符串会出现在Java堆中。

在JDK 6中,intern方法会将第一次出现的字符串复制进永久代,然后返回其引用。而JDK 7中intern方法不再复制,而是改为记录改字符串的引用。也就是说,对于新的字符串,JDK 6的intern会再复制进常量池,然后返回其在常量池的引用;而JDK 7的intern会把原字符串的引用复制进常量池,并依旧返回原字符串的引用。不过这里比较坑的是,在编译期字符串“java”就已经被加入到常量池中了,所以intern会直接返回其在常量池中的引用。综上,正确的答案是:

在JDK 6中:

false
false

在JDK 7中:

false
true

答对的人还是有的,不过我注意到还有一位把JDK 6和JDK 7的情况写反了,只差一点点啊23333

后记

没想到随手一出的题还有那么多dalao愿意回答,真的很感动。而由于没有指定环境,还出现了第三题那样的双选,真是很不严谨。而且熟悉的dalao应该能发现,这基本就是《深入理解Java虚拟机》这本书的练习题!嘛,还是感谢一下24位dalao的捧场。其中,有三位dalao是全对的:

  • KAAAsS(不要脸)
  • 你猜F
  • jangz

看来这种活动以后可以多多搞搞23333

Reference

  1. Jon, Masamitsu. JEP 291: Deprecate the Concurrent Mark Sweep (CMS) Garbage Collector[EB/OL]. http://openjdk.java.net/jeps/291, 2017-06-15
  2. Oracle. JDK 9[EB/OL]. http://openjdk.java.net/projects/jdk9/, 2016-05-26
  3. Christoph, Engelbert. JEP 291: Deprecate the Concurrent Mark Sweep (CMS) Garbage Collector[EB/OL]. http://mail.openjdk.java.net/pipermail/jdk9-dev/2017-April/005734.html, 2017-04-05
  4. Detlefs D, Flood C, Heller S, et al. Garbage-first garbage collection[J]. 2008:37-48.
  5. Nikita, Salnikov-Tarnovski. G1 vs CMS vs Parallel GC[EB/OL]. https://plumbr.io/blog/garbage-collection/g1-vs-cms-vs-parallel-gc, 2013-11-28
  6. Thomas, Schatzl. JEP 291: Deprecate the Concurrent Mark Sweep (CMS) Garbage Collector[EB/OL]. http://mail.openjdk.java.net/pipermail/jdk9-dev/2017-April/005748.html, 2017-04-11
分享到

KAAAsS

喜欢二次元的程序员,喜欢发发教程,或者偶尔开坑。(←然而并不打算填)

相关日志

  1. 没有图片
  2. 没有图片
  3. 没有图片

评论

  1. 开发者头条 2018.07.30 1:40下午

    感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/jz5pfc 欢迎点赞支持!
    使用开发者头条 App 搜索 69380 即可订阅《KAAAsS Blog》

在此评论中不能使用 HTML 标签。