写时复制(copy-on-write)是众多 UNIX 操作系统用到的内存优化的方法。比如在 Linux 系统中使用 fork() 函数复制进程时,大部分内存空间都不会被复制,只是复制进程,只有在内存中内容被改变时才会复制内存数据。 但是如果使用标记清除算法,这时内存会被设置标志位,就会频繁发生不应该发生的复制。
另外,关于标记清除的变形,还有一种叫做标记压缩(Mark and Compact)的算法,它不是将被标记的对象清除,而是将它们不断压缩。
HotSpot的正式发布名称为"Java HotSpot Performance Engine",是Java虚拟机的一个实现,包含了服务器版和桌面应用程序版。
G1 使用的是 SATB 标记算法,主要应用于垃圾收集的并发标记阶段,解决了CMS 垃圾收 集器重新标记阶段长时间 Stop The World(STW) 的潜在风险。其算法全称是 Snapshot At The Beginning,由字面理解,是垃圾回收器开始时活着的对象的一个快照。
它是通过 “根集合”穷举可达对象得到的,穷举过程中采用了三色标记法:
所以,漏标的情况只会发生在白色对象中,且满足以下任意一个条件:
SATB 利用 write barrier 将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根 Stop The World 地重新扫描一遍即可避免漏标问题。 因此 G1 Remark阶段 Stop The World 与 CMS 了的remark有一个本质上的区别,那就是这个暂停只需要扫描有 write barrier 所追中对象为根的对象, 而 CMS 的remark 需要重新扫描整个根 集合,因而CMS remark有可能会非常慢。
RSet全称是Remembered Set,是辅助GC过程的一种结构,典型的空间换时间工具,和Card Table有些类似。
G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用是使用Remembered Set来避免扫描全堆。
G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier(写屏障)暂时中断写操作(虽然写屏障使得应用线程增加了一些性能开销,但Minor GC变快了许多,整体的垃圾收集效率也提高了许多,通常应用的吞吐量也会有所改善),检查Reference引用的对象是否处于不同的Region之间(在分代中就是检查老年代中的对象是否引用了新生代的对象),如果是便通过CardTable(卡表)把相关引用信息记录到被引用对象所属的Region的Remembered Set中。
当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。
为了支持高频率的新生代的回收,虚拟机使用一种叫做卡表(Card Table)的数据结构,卡表作为一个比特位的集合,每一个比特位可以用来表示年老代的某一区域中的所有对象是否持有新生代对象的引用。当老年代中的某个区域持有了新生代对象的引用时,JVM就把这个区域对应的Card所在的位置标记为dirty(bit位设置为1)。
这样新生代在GC时,可以不用花大量的时间扫描所有年老代对象,来确定每一个对象的引用关系,而可以先扫描卡表,只有卡表的标记位为1时,才需要扫描给定区域的年老代对象。而卡表位为0的所在区域的年老代对象,一定不包含有对新生代的引用。这样子可以提高效率减少MinorGC的停顿时间。
如上图,卡表中每一个位表示年老代4K的空间,卡表记录未0的年老代区域没有任何对象指向新生代,只有卡表位为1的区域才有对象包含新生代引用,因此在新生代GC时,只需要扫描卡表位为1所在的年老代空间。使用这种方式,可以大大加快新生代的回收速度。
在JVM中,一个Card覆盖的默认大小是512字节,在多个线程并行收集时,JVM通过ParGCCardsPerStrideChunk参数设置每个线程每次扫描的Card数量,默认是256。
相当于是把老年代分成许多strides,每个线程每次扫描一个stride,每个stride大小为512*256 = 128K。
如果你的老年代大小为4G,那总共有4G/128K=32K个Strides。多线程在扫描这么多的strides时就涉及到调度和分配的问题,stride数量太多就会导致线程在stride之间切换的开销增加,进而导致GC暂停时间增长。
因此JVM提供了ParGCCardsPerStrideChunk这个参数来配置每个stride对应的card数量,这个数量要根据实际的业务场景进行调优,网上一般流传3个魔术数字:32768、4K和8K。
例如,配置每次扫描的Card数量:
(UnlockDiagnosticVMOptions:解锁任何额外的隐藏参数)
-XX:+UnlockDiagnosticVMOptions
-XX:ParGCCardsPerStrideChunk=4096
这个值不能设置的太大,因为GC线程需要扫描这个stride中老年代对象持有的新生代对象的引用,如果只有少量引用新生代的对象那就导致浪费了很多时间在根本不需要扫描的对象上。
(在JVM调优过程中,没有一个参数的值完美的,只有经过不断的调优过程,慢慢的摸索到适合自己应用的最佳参数范围,除非应用对YGC的耗时特别敏感,不到万不得已,不用优化该参数,默认的256也适合大部分情况。但是随着现在机器内存的扩大,适当的增大该参数值(4K),也是可以的)
虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后为新生对象分配内存。内存分配方式有两种:
指针碰撞的前提是Java堆是绝对规整的,有用的和空闲各自放在一边,中间放着一个指针作为分界点指示器,所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。在使用Serial,和ParNew等收集器时候使用的是指针碰撞。
如果Java堆不是规整的,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录了哪些是可用的内存,在分配的时候从列表中找到一块足够大的空间分配给对象实例,并更新列表上的记录。在使用CMS这种基于Mark-Sweep(标志-清除)算法的收集器时,通常采用空闲列表。
在minor gc过程中,survivor(幸存者)的剩余空间不足以容纳eden(伊甸园)及当前在用的survivor区间存活对象,只能将容纳不下的对象移到年老代(promotion),而此时年老代满了无法容纳更多对象,通常伴随full gc,因而导致的promotion failure。
这种情况通常需要增加年轻代大小,尽量让新生对象在年轻代的时候尽量清理掉。
如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;
这时JVM启用后备预案:临时启用Serail Old收集器(为什么不启用Parallel old?这样并行回收的停顿时间会更短。 https://stackoverflow.com/questions/39569649/why-is-cms-full-gc-single-threaded#39573243 ),而导致另一次Full GC的产生;
这样的代价是很大的,此时JVM将采用停顿的方式进行Full gc,整个gc时间会相当可观,完全违背了采用CMS GC的初衷,所以CMSInitiatingOccupancyFraction不能设置得太大。
出现该现象的原因主要是由于cms的无法处理浮动垃圾(Floating Garbage)引起的。
(出现此现象的原因主要有两个:一个是在年老代被用完之前不能完成对无引用对象的回收;一个是当新空间分配请求在年老代的剩余空间中无法得到满足(比如在年老代申请大空间对象))
这个跟cms的机制有关。cms的并发清理阶段,用户线程还在运行,因此不断有新的垃圾产生,而这些垃圾不在这次清理标记的范畴里头,cms无法在本次gc清除掉,这些就是浮动垃圾。
由于这种机制,cms年老代回收的阈值不能太高,否则就容易预留的内存空间很可能不够(因为本次gc同时还有浮动垃圾产生),从而导致concurrent mode failure发生。
要避免此现象,可以降低触发CMS的阀值,即参数-XX:CMSInitiatingOccupancyFraction的值,该值代表老年代堆空间的使用率,通常JDK默认值是68;可以选择调低到50或者以下(指设定CMS在对老年代占用率达到50%的时候开始GC),让CMS GC尽早执行,以保证有足够的空间
有一个需要注意的点,仅仅设置CMSInitiatingOccupancyFraction的值的值表示第一次CMS收集按照该比例收集,后面JVM将会自动进行调节。配置该参数使用的还有一个参数。
-XX:+UseCMSInitiatingOccupancyOnly可以设置true和false,默认为false。
将-XX+UseCMSInitiatingOccupancyOnly 值设置为true来命令 JVM 不基于运行时收集的数据来启动 CMS 垃圾收集周期。而是,当该标志被开启时,JVM 通过 CMSInitiatingOccupancyFraction 的值进行每一次 CMS 收集,而不仅仅是第一次。
然而,请记住大多数情况下,JVM 比我们自己能作出更好的垃圾收集决策。因此,只有当我们充足的理由 (比如测试) 并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。
不建议开启该属性值。
(1)、停顿时间
(2)、吞吐量
(3)、覆盖区(Footprint)
Jvm有client和server两个版本,分别针对桌面应用程序和服务端应用做了相应的优化,client版本加载速度较快,server版本加载速度较慢但运行起来较快。
简而言之:client版本启动快,server版本运行快。
由于服务器的CPU、内存和硬盘都比客户端机器强大,所以程序部署后,都应该以server模式启动,获取较好的性能。
小提示:可以通过运行:java -version来查看jvm默认工作在什么模式。
Minor GC和Major GC是俗称,在Hotspot JVM实现的Serial GC, Parallel GC, CMS, G1 GC中大致可以对应到某个Young GC和Old GC算法组合;
Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“Major GC”的时候一定要问清楚他想要指的是上面的Full GC还是Old GC。
程序中主动调用System.gc()强制执行的GC为Full GC。
如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。
如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。
在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。
这些线程是同时“存在”的——每个线程都处于执行过程中的某个状态。
如果程序能够并行执行,那么就一定是运行在多核处理器上。
此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。
可以得出结论:“并行”概念是“并发”概念的一个子集。 也就是说,编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码,只能称为并发。
看下图来进行理解:
(Erlang 之父 Joe Armstrong 用该图解释了并发与并行的区别)
并发是两个队列交替,但是只能使用一台咖啡机,并行是两个队列可以同时使用两台咖啡机
(任何属于冯诺依曼结构体系的计算机(经典计算机,目前应该除了实验中的量子计算机,都是属于该范畴的),其中的CPU(或者说核)必然是串行执行指令的。所以在任何单CPU机器上,是不存在严格意义上,或者说狭义上的并行的--在指令级别的严格意义上的并行,是指在一个足够小的时刻,可以允许大于一条的指令在执行。)
要很好的理解这个图,请记住下面几点:
一定要把图中的一个queues来对应一个任务,队列中的每一个人对应这个任务的步骤,并发和并行的共同前提肯定是有多个任务要处理(也就是图中的队列数量大于等于2)
再进一步区分:
并发强调了其实现的前提是:要处理的任务必须是可分步骤的(队列中的人数大于等于2);
并行强调了其实现的前提是:必须是多个处理器(咖啡机的数量大于等于2).
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
在这里,将吃饭和接电话理解为两个任务。