深入理解JVM&G1GC(txt+pdf+epub+mobi电子书下载)


发布时间:2021-03-12 14:21:48

点击下载

作者:周明耀

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

深入理解JVM&G1GC

深入理解JVM&G1GC试读:

前言

7岁那年,当我合上《上下五千年》一套三册全书时,我对自己说,我想当个作家。这一晃27年了,等待了27年,我的第一本书《大话Java性能优化》在2016年4月正式面世,2016年8月第二次印刷,感谢读者的厚爱。第一次印刷时出现一些错别字,请原谅编辑小姑娘,99万字对她来说确实太多了,这是我的责任,未来一定尽全力避免。《深入理解JVM & G1 GC》是我的第二本书,也即将面世。对于我的每一本书,我都怀着忐忑、惊喜的心情,就像第一次面对我的女儿“小顽子”,给她取这个小名,是希望她顽强到底,因为我相信,你若顽强到底,一切皆有可能。

我喜欢看书,每年购买的书接近100本,也喜欢技术积累。一直没有出书的想法,直到遇到了电子工业出版社的董老师,在深圳南湖的一席畅谈后,我决定做一位业余的技术作家,致力于中国软件开发行业的技术推广、普及、推动。

每年都要面试很多学生,我感觉中国的大学不太注重实际项目开发能力的培养,较为教条,这也是我的系统丛书首先从Java技术开始的原因,它更加接地气。本书主要为学习Java语言的学生、初级程序员提供JVM和GC的使用和优化建议及经验,力求做到知识的综合传播,而不是仅仅针对Java虚拟机调优进行讲解。本书具体包括以下几方面:JVM基础知识、GC基础知识、G1 GC的深入介绍、G1 GC调优建议、JDK自带工具使用介绍等。

本书基于JDK8,总的来说,没有一招鲜式的性能调优秘籍或包罗万象的性能百科,能让你摇身一变成为老练的GC性能调优专家。相当数量的GC性能问题还需要专门的知识技能才能解决。性能调优在很大程度上是一门艺术。解决的GC性能问题越多,技艺才会越精湛。我们不只要关心GC的持续演进,也要积极地去了解它的设计原理和设计目标。

最后,自我介绍一下,我叫周明耀,研究生学历,12年工作经验,IBM开发者论坛专家作者。我是一名IT技术狂热爱好者,一名九三学社社员,一名顽强到底的工程师。我推崇技术创新、思维创新,对于新技术非常的热爱。

感谢我的家人,和谐的家庭帮助我完成了这本书。我的妻子,她美丽、细心、博学、偶尔不那么温柔,但是我很爱她。我的小顽子,她天生性格很像我,希望她能够踏踏实实做人,保持创新精神,平平安安、健健康康地生活下去。感谢我妻子的父母、我的父母,他们帮我照顾小孩,我才有时间编写此书。感谢浙江省特级教师、杭州高级化学老师郑克良老师,郑老师的一句永远不要放弃,推动着我多年的发展。感谢数学老师张老师在公开场合对我智商的褒奖,第一次收获这样的赞赏,对我这样内向的孩子是多么的重要,谢谢。

这本书献给我记忆中的爷爷奶奶、外公外婆,你们给我的都是最美的回忆。

我相信这本书不是终点,它是麦克叔叔此生一系列技术书籍的一员,咱们下一本书见。

轻松注册成为博文视点社区用户(www.broadview.com.cn),扫码直达本书页面。

·下载资源:本书所提供的示例代码及资源文件均可在【下载资源】处下载。

·提交勘误:您对书中内容的修改意见可在【提交勘误】处提交,若被采纳,将获赠博文视点社区积分(在您购买电子书时,积分可用来抵扣相应金额)。

·与作者交流:在页面下方【读者评论】处留下您的疑问或观点,与作者和其他读者一同学习交流。

页面入口:http://www.broadview.com.cn/31468第1章JVM & GC基础知识

单细胞生物在地球上经历了几十亿年的进化过程,又过了几十亿年,细胞与周围的细胞相互接触,从而形成了活的球形有机体,单细胞生物才进化为多细胞生物。在最初的时候,球形是多细胞生命能够生存的唯一形状,因为细胞之间只有相互接近才能互相协调功能。又过了十亿年,生命终于进化出了第一个神经元细胞,它是一种纤细的条状细胞,能够使两个细胞即使相隔一段距离仍能够通信。正是这项激活创新的诞生,各种各样的生命开始繁荣发展。有了神经元,生命不再局限于球状,细胞可以组成任何形状、大小和功能。蝴蝶、兰花和袋鼠,各种各样的生命形态都变成了可能。生命瞬间拓展出了成千上万种可能性,繁荣到令人惊叹,直到美丽的生命无处不在。程序设计的学习就好像探索生命起源一样,你不了解起源,就不可能找到正确的方法。

JVM是Java语言可以跨平台、保持高发展的根本,没有了JVM,Java语言将失去运行环境。针对Java程序的性能优化一定不可能避免针对JVM的调优,随着JVM的不断发展,我们的应对措施也在不断地跟随、变化,内存的使用逐渐变得越来越复杂。所有高级语言都需要垃圾回收机制的保护,所以GC就是这么重要。

本章主要介绍和解决以下问题,这些也是全书的基础。

·为什么我们需要了解JVM和GC,这是您阅读本书的依据。

·了解GC的基础常用术语知识,作者和读者需要对术语定义进行统一。

·了解JVM的基础知识,包括堆、栈、方法区等。

·为深入了解JVM和GC做好知识储备。1.1引言

还记得机器猫吗?他和康夫有一张书桌,书桌的抽屉其实是一个时空穿梭通道,让我们操作机器猫的时空机器,回到1998年。那年的12月8日,第二代Java平台的企业版J2EE正式对外发布。为了配合企业级应用落地,1999年4月27日,Java程序的舞台—Java HotSpot Virtual Machine(以下简称HotSpot)正式对外发布,并从这之后发布的JDK1.3版本开始,HotSpot成为Sun JDK的默认虚拟机。

既然有了虚拟机,那一定需要收集垃圾的机制,这就是Garbage Collection,对应的产品我们称为Garbage Collector,垃圾回收器。1999年伴随JDK1.3.1一起来的是串行方式的Serial GC,它是第一款GC,并且这只是起点。此后,JDK1.4和J2SE1.3相继发布。2002年2月26日,J2SE1.4发布,Parallel GC和Concurrent Mark Sweep(CMS)GC跟随JDK1.4.2一起发布,并且Parallel GC在JDK6之后成为HotSpot默认GC。

HotSpot有这么多的垃圾回收器,那么如果有人问,Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC有什么不同呢?

·如果你想要最小化地使用内存和并行开销,请选Serial GC。

·如果你想要最大化应用程序的吞吐量,请选Parallel GC。

·如果你想要最小化GC的中断或停顿时间,请选CMS GC。

那么问题来了,既然我们已经有了上面三个强大的GC,为什么还要发布Garbage First(G1)GC呢?我只能说:“人类的追求是无限的,就像女人永远追求更美丽。”

为什么名字叫作Garbage First(G1)呢?故事背景稍微讲解一下,具体的内容请读者看后续章节。因为G1是一个并行回收器,它把堆内存分割为很多不相关的区间(Region),每个区间可以属于老年代或者年轻代,并且每个年龄代区间可以是物理上不连续的。老年代区间这个设计理念本身是为了服务于并行后台线程,这些线程的主要工作是寻找未被引用的对象,而这样就会产生一种现象,即某些区间的垃圾(未被引用对象)多于其他的区间。我们后面会介绍,垃圾回收时都是需要停下应用程序的,不然就没有办法防止应用程序的干扰,然后G1 GC可以集中精力在垃圾最多的区间上,并且只费一点点时间就可以清空这些区间里的垃圾,腾出完全空闲的区间。绕来绕去终于明白了,由于这种方式的侧重点在于处理垃圾最多的区间,所以我们给G1一个名字:垃圾优先(Garbage First)。

G1内部主要有四个操作阶段,这四个阶段也是第4章和第5章的主要内容,即:

·年轻代回收(A Young Collection);

·运行在后台的并行循环(A Background,Concurrent Cycle);

·混合回收(A Mixed Collection);

·全量回收(A Full GC)。1.2基本术语

结合我上一本书出版后的反馈信息以及个人阅读专业书籍的经验,由于计算机程序语言全部都是由国外创造、开发的,所以很多单词是经中国的技术专家由英文单词翻译的,这样可能的结果是专业术语按照自己的理解翻译,版本会较多,就如同各地方言一样。我觉得本书的专业性较强,很多英文单词容易出现歧义,所以我觉得有必要在这里统一一下对专业术语的中英文对照,以及对应的解释。我翻译和理解得不一定很到位,所以事先统一中英文对照还是很有必要的。

另外,需要提前说明的是,为了让每一个章节相对独立,有利于读者跳过一些章节,所以一些知识可能会重复介绍,这不是失误,是为了实现我对于我所有出版读物的规划,让技术以实践为目标,让使用更简单、更易落地。1.2.1 Java相关术语

1.2.1.1 Millisecond

GC内部的动作(某一个过程)一般都是在毫秒级完成。毫秒(ms)是一种较为微小的时间单位,是一秒的千分之一。除了毫秒以外,还有皮秒、纳秒、微秒,计算公式分别如下:

·皮秒,符号ps(英语:picosecond,1皮秒等于一万亿分之一秒-12(10秒)

1,000 皮秒 = 1纳秒;1,000,000 皮秒 = 1微秒;1,000,000,000 皮秒 = 1毫秒;1,000,000,000,000 皮秒 = 1秒。

·纳秒,符号ns(英语:nanosecond )-9

1纳秒等于十亿分之一秒(10秒);1 纳秒 = 1000皮秒;1,000 纳秒 = 1微秒;1,000,000纳秒 = 1毫秒;1,000,000,000 纳秒 = 1秒。

·微秒,符号μs(英语:microsecond )-6

1微秒等于一百万分之一秒(10秒);0.000 001 微秒 = 1皮秒;0.001 微秒 = 1纳秒;1,000 微秒 = 1毫秒;1,000,000 微秒 = 1秒。

·毫秒,符号ms(英语:millisecond )-3

1毫秒等于一千分之一秒(10秒);0.000 000 001 毫秒 = 1皮秒;0.000 001 毫秒 = 1纳秒;0.001 毫秒 = 1微秒;1000 毫秒 = 1秒。

1.2.1.2 Megabyte

GC的区间划分基数一般采用兆级别,所以这里需要解释MB。

MB(全称MByte):计算机中的一种储存单位,读作“兆”。数据单位MB与Mb(注意B字母的大小写)常被误认为是一个意思,其实MByte含义是“兆字节”,Mbit的含义是“兆比特”。MByte是指字节数量,Mbit是指比特位数。MByte中的“Byte”虽然与Mbit中的“bit”翻译一样,都是比特,也都是数据量度单位,但二者是完全不同的。Byte是“字节数”,bit是“位数”,在计算机中每八位为一字节,也就是1Byte=8bit,是1∶8的对应关系。因此在书写单位时一定要注意B字母的大小写和含义。

1.2.1.3 Java应用程序如何配置JVM参数

这个实践方式需要贯穿整本书,毕竟只有学会配置参数(JVM选项),才能查看GC日志,最终才能主导讨论GC优化方法。

这里不是说如何配置正确的参数,仅仅只是演示一下如何在Eclipse和Linux这两个普遍运行Java程序的场景如何配置JVM参数(后面都统一称为选项)。

Eclipse工具的使用方法如下:

选择Eclipse→Run→Run Configurations→Arguments→VM arguments,在左边Java Application栏选中要设置的 Project 运行类,在VM arguments中填入 -Xms64m -Xmx256m。

这里-Xms是设置内存初始化的大小(如上面的64m),而-Xmx是设置最大能够使用内存的大小(如上面的256m,最好不要超过物理内存)。配置方式如图1-1所示,演示代码如代码清单1-1所示,运行输出请见代码清单1-2和代码清单1-3。图1-1 Eclipse配置参数演示图

代码清单1-1 演示代码

代码清单1-2 代码清单1-1运行输出(设置参数)

如果不配置选项,让JVM自己根据机器配置使用默认选项,输出如代码清单1-3所示。

代码清单1-3 代码清单1-1运行输出(未设置参数)

下面介绍Linux场景。

举一个例子,Tomcat在Linux环境下运行时,可以通过改变它的启动脚本来添加JVM参数,如代码清单1-4所示。自己编写Java应用程序的时候也可以按照这样的思路,编写一个脚本,可以是Shell、Python、Perl,然后在里面加入一个变量,在运行Java关键字的时候将这个关键字加为运行属性。

代码清单1-4 Linux配置参数示例

再次声明,以后都用选项代替参数。

1.2.1.4 并行计算

服务端程序与一般的用户终端程序相比,服务端程序需要承受很重的用户访问压力。根据淘宝的数据,“双11”一天支付宝核心数据库集群处理了41亿个事务,执行了285亿次SQL,生成了15TB日志,访问了1931亿次内存数据块、13亿个物理读。如此密集的访问,恐怕任何一台单机都难以胜任,因此,并行程序也就自然成了一个出路。

面对复杂业务模型,并行程序会比串行程序更容易适应业务需求,更容易模拟我们的现实世界。毕竟,我们的世界本质上是并行的。比如,当你开开心心去上学的时候,妈妈可能在家里忙着家务,爸爸在外打工赚钱,一家人其乐融融。如果有一天,你需要使用你的计算机来模拟这个场景,你会怎么做呢?如果你在一个线程里,既做了你自己,又做了妈妈,又做了爸爸,显然这不是一种好的解决方案。但如果你使用三个线程,分别模拟这三个人,一切看起来又是那么自然,而且容易被人理解。

虚拟机除了要执行main函数主线程外,还需要做JIT编译,需要做垃圾回收。无论是main函数、JIT编译还是垃圾回收,在虚拟机内部都实现为单独的一个线程。是什么使得虚拟机的研发人员这么设计呢?显然,这是因为业务的需要。因为这里的每一个任务都是相对独立的,不应该将没有关联的业务代码拼凑在一起,分离为不同的线程更容易理解和维护。因此,使用并行也不完全出自性能的考虑,而有时候,我们会很自然地那么做。

注意,并发和并行是两个非常容易被混淆的概念。它们都可以表示两个或者多个任务一起执行,但是偏重点有些不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。而并行是真正意义上的“同时执行”。

GC里面有很多并行操作,所以我在这里介绍了关于并行计算的基础概念。

1.2.1.5 进程和线程

计算机内部每个正在系统上运行的程序都是一个进程,每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行,也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。

线程是程序中一个单一的顺序控制流程,在单个程序中同时运行多个线程完成不同的工作称为多线程。

线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。多线程主要是为了节约CPU时间,对于如何发挥利用,需要根据具体情况而定。注意,线程在运行中需要使用计算机的内存资源和CPU。

1.2.1.6 多线程(multithreading)

这个知识点也会贯穿整个GC知识点,毕竟多线程是所有高级语言都支持的特性。

经常会听到公司的HR说,“我们不需要纯管理人员”。我以前也有一段时间是这样认为的,后来我发现,其实我们是需要管理人员的。我们需要那种具备技术能力,理解技术、人文、产品、沟通,具有多任务同时调度能力的管理人员。也就是说,还是需要管理人员的,否则就无法有效、高效地调动研发团队,研发团队的目标是既能够做出很好的产品、符合市场的要求,也能够快速解决市场反馈的缺陷。这和多线程管理方式类同。

多线程,这是一种从软件或者硬件上实现多个线程并发执行的技术,一般高级语言,特别是面向对象语言都具有该特性。具有多线程能力的计算机因为有硬件支持而能够在同一时间执行多于一个线程,从而提升整体处理的性能。具有这种能力的处理器包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level Multithreading)或同时多线程(Simultaneous Multithreading)处理器。

如图1-2所示,描绘了一个Java线程的生命周期,从出生→开发→运行→死亡,中间还存在一些临时性状态,例如等待。图1-2 Java线程生命周期图

1.2.1.7 内存泄漏(Memory Leak)

这个概念会在后续GC抛出异常时需要介绍,较为常见的OutOfMemory异常是最基本的内存不足引起的异常,很多时候是因为应用程序造成的内存泄漏情况引起了该错误的发生。

有句话叫作“管理浮于表面”,指的是没有有效的管理措施,即便制定了,也没有真正落实,这样就容易出现人浮于事的情况,就会有很多烂摊子没有人去收拾。总的来说,抓细节永远是最难的,这和解决内存泄漏有得比。

内存泄漏也称作“存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未被释放,结果导致一直占据该内存单元,一直持续到程序结束,即所谓内存泄漏。

内存泄漏形象的比喻是“操作系统可提供给所有进程的存储空间正在被某个进程榨干”,最终结果是程序运行时间越长,占用存储空间越多,最终用尽全部内存空间,造成整个系统崩溃。所以“内存泄漏”是从操作系统的角度来看的。这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。由程序申请的一块内存,如果没有任何一个指针指向它,那么这块内存就泄漏了。

在Java程序里,如果发生内存泄漏,那么最后都会抛出OutOfMemoryError异常,如代码清单1-5所示,模拟了一个抛出OOM异常的程序。

代码清单1-5 内存泄漏演示代码

运行后会看到如代码清单1-6所示的错误日志输出。

代码清单1-6 内存泄漏演示代码运行输出

由于GC一直在发展,所以一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。

1.2.1.8 Soft、Weak、Phantom、Final和JNI References

这个知识点在第6章介绍老年代回收优化的时候会涉及,提前介绍引用类型,对于整本书也有一些意义。

Java中的引用有点像C++里面的指针,通过引用可以对堆中的对象进行操作。在Java程序中,最常见的引用类型是强引用,也是默认的引用类型。当在Java语言中使用New操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。

在前面提到过,判断一个对象是否存活的标准为是否存在指向这个对象的引用。在某函数中创建对象,例如:StringBuffer str = new StringBuffer(“Hello World”);,假设该代码是在函数体内运行的,那么局部变量str将被分配在栈内,而对象StringBuffer实例,被分配在Java堆上。局部变量str指向StringBuffer实例所在堆空间,通过str可以操作该实例,那么str就是StringBuffer的引用。此时,如果运行一个赋值语句:StringBuffer str1=str;,那么,str所指向的对象也将被str1所指向,同时在局部栈空间上会分配空间存放str1变量。此时,StringBuffer实例就有两个引用,而对引用使用“==”操作用于表示两个操作数所指向的堆空间地址是否相同,不表示两个操作数所指向的对象是否相等。

Java中提供了4个级别的引用,即强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)这4个级别。在这4个级别中只有强引用类是包内可见的,其他3种引用类型均为Public,可以在应用程序中直接使用,垃圾回收器会尝试回收只有弱引用的对象。

简要罗列一个GC对于不同引用类型的区别。

·强引用(Strong Reference):在一个线程内,无需引用直接可以使用的对象,除非引用不存在了,否则强引用不会被GC清理。我们平时声明变量使用的就是强引用,普通系统99%以上都是强引用,比如,String s = "Hello World"。

·软引用(Soft Reference):JVM抛出OOM之前,GC清理所有的软引用对象。垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue)。类似弱引用,只不过Java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。

·弱引用(Weak Reference):弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。虽然,GC在运行时一定回收弱引用对象,但是复杂关系的弱对象群常常需要好几次GC的运行才能完成。就像上面描述的场景,弱引用对象常常用于Map结构中,引用数据量较大的对象,一旦该对象的强引用为null时,GC能够快速地回收该对象空间。

·虚引用(Phantom Reference):又称为幽灵引用,主要目的是在一个对象所占的内存被实际回收之前得到通知,从而可以进行一些相关的清理工作。幽灵引用在使用方式上与之前介绍的三种引用类型有很大的不同。首先幽灵引用在创建时必须提供一个引用队列作为参数,其次幽灵引用对象的get方法总是返回null,因此无法通过幽灵引用来获取被引用的对象。

1.2.1.9 finalization机制

Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。Object类提供了finalize方法来添加自定义的销毁逻辑。如果一个类有特殊的销毁逻辑,可以覆写finalize方法。

从功能上来说,finalize方法与C++中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize方法在本质上不同于C++中的析构函数。当垃圾回收器发现没有引用指向一个对象时,会调用这个对象的finalize方法。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

由于finalize方法的存在,虚拟机中的对象一般处于三种可能的状态。第一种是可达状态,当有引用指向该对象时,该对象处于可达状态。根据引用类型的不同,有可能处于强引用可达、软引用可达或弱引用可达状态。第二种是可复活状态,如果对象的类覆写了finalize方法,则对象有可能处于该状态。虽然垃圾回收器是在对象没有引用的情况下才调用其finalize方法,但是在finalize方法的实现中可能为当前对象添加新的引用。因此在finalize方法运行完成之后,垃圾回收器需要重新检查该对象的引用。如果发现新的引用,那么对象会回到可达状态,相当于该对象被复活,否则对象会变成不可达状态。当对象从可复活状态变为可达状态之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可达状态,也就是说,一个对象的finalize方法只会被调用一次。第三种是不可达状态,在这个状态下,垃圾回收器可以自由地释放对象所占用的内存空间。

1.2.1.10 Serviceability Agent(SA)

这个工具在第6章会具体讲解其基本原理,也会重点介绍SA工具的使用方法及优缺点。

SA(Serviceability Agent)是JDK自带的不为广大Java程序员所熟悉的底层诊断工具。酒香不怕巷子深,SA提供了一套可以深入JVM内部进行探索的机制,对于Java Web应用、Java服务端应用的各种问题的诊断具有重要意义。

平时用来查看VM内部信息的常用的工具都在$JAVA_HOME/bin目录下,如图1-3所示,其中一些工具就是用Serviceability Agent开发的。

SA与目标进程是两个独立的进程,这个必须明确,所以两个进程之间通过进程间通信实现调试,并且SA不会影响目标进程的正常运行。SA依赖于操作系统提供的调试API,属于建立在一系列的Debug原语上的工具。

这里不多介绍,我们会在第6章详细介绍使用方案、方法、输出。图1-3 SA jar包位置图

1.2.1.11 Interned Strings

这个专业术语会在第3章讲解选项(参数)-XX:+UseStringDeplucation时提到,G1 GC专门针对这个问题提出了一个JVM选项。

在 Java 语言中有8种基本类型和一种比较特殊的类型String。这

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载