week9 作业 1
提示:以下介绍内容来自于网络,不正确之处请多多指教
1.请简述 JVM 垃圾回收原理
什么是垃圾回收?
垃圾回收就是对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,释放占用的空间,防止内存泄露。
为什么需要垃圾回收?
在开发中,需要创建大量的对象,如果动态创建的对象没有得到及时回收,持续堆积,最后会导致内存被占满,造成溢出。因此Java 提供了一种垃圾回收机制,在后台创建一个守护进程。该进程会在内存紧张的时候自动跳出来,把内存的垃圾全部进行回收,从而保证程序的正常运行。
JVM是什么?
JVM又称为Java 虚拟机(Java virtual machine),是运行 Java 程序必不可少的机制。JVM实现了Java语言最重要的特征:即平台无关性。其实现原理是:编译后的 Java 程序指令并不直接在硬件系统的 CPU 上执行,而是由 JVM 执行。JVM屏蔽了与具体平台相关的信息,使Java语言编译程序只需要生成在JVM上运行的目标字节码(.class),就可以在多种平台上不加修改地运行。Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。因此实现java平台无关性。它是 Java 程序能在多平台间进行无缝移植的可靠保证,同时也是 Java 程序的安全检验引擎。
JVM 是 编译后的 Java 程序(.class文件)和硬件系统之间的接口 。
JVM垃圾回收:
说到垃圾回收,首先,需要确定哪些对象是垃圾,其次,回收垃圾有哪些方法。
前面已经介绍了垃圾回收都是基于内存回收的,因此先给出一个JVM运行时的内存结构图,如下图所示:
一、垃圾回收区域
从图中我们可以看出,Java内存运行时区域大致分为三个部分:PC寄存器、java虚拟机栈、本地方法栈,这3个区域是所有线程独有的一块区域,其生命周期随线程而生,随线程而灭,这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束,内存自然就跟随着回收了。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收是动态的,垃圾收集关注的就是这部分的内存。
二、垃圾对象
为了确定哪些对象是垃圾,jvm为我们提供了一些算法去判定。常见的判断是否存活有两种方法:引用计数法和可达性分析。
1、引用计数法
为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数。当该个数为零,意味着没有人再使用这个对象,可以认为“对象死亡”。每当有一个地方去引用它时候,引用计数器就增加1。但是,这种方案存在严重的问题,就是无法检测“循环引用”:当两个对象互相引用,它俩的计数都不为零,因此永远不会被回收。而实际上对于开发者而言,这两个对象已经完全没有用处了。鉴于这样的弊端,Java 里没有采用这样的方案来判定对象的“存活性”。
2、可达性分析
可达性分析基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。不能到达的则被可回收对象。
下面这张图就是可达性分析的描述:
我们发现,GC Roots 本身是一个出发点,也就是说我们每次进行可达性分析的时候都要从这个初始点出发。换句话说,初始点我们一定是可达的。那么,Java 里有哪些对象可以作为GC Roots呢?主要有以下四种:虚拟机栈(帧栈中的本地变量表)中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象。
三、垃圾回收算法
上面我们已经能够确定那些对象可以被视为垃圾了。下面我们可以分析一下,如何去回收这些垃圾,同样的,有一系列算法。首先我们定义一个规则确定那些是垃圾、存活对象、空白空间
1、标记-清理
第一步(标记),利用可达性遍历内存,把“存活”对象和“垃圾”对象进行标记。第二步(清理),我们再遍历一遍,把所有“垃圾”对象所占的空间直接 清空 即可。
结果如下:
特点:简单方便容易产生内存碎片
2、标记-整理
上面的方法我们发现会产生内存碎片,因此在这个方法中同样为两步:
第一步(标记):利用可达性遍历内存,把“存活”对象和“垃圾”对象进行标记。
第二步(整理):把所有存活对象堆到同一个地方,这样就没有内存碎片了。
结果如下:
特点:适合存活对象多,垃圾少的情况需要整理的过程
3、复制
将内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完了,就将还活着的对象复制到另一块上,然后再把使用过的内存空间一次性清理掉
过程如下:
特点:简单不会产生碎片内存利用率太低,只用了一半
四、堆和方法区的垃圾回收
上面我提到了三种方法来回收内存,下面要讲的分代回收算法是第四种。java中的垃圾回收大致在两部分,第一个就是堆、第二个就是方法区。为此先看方法区是如何进行垃圾回收的。
1、方法区的垃圾回收
方法区又叫做永久代。永久代的垃圾回收主要有两部分:废弃常量和无用的类。
首先是废弃常量垃圾回收的一般步骤:
第一步:判定一个常量是否是废弃常量:没有任何一个地方对这个常量进行引用就表示是废弃常量。
第二步:垃圾回收
然后是无用的类垃圾回收的一般步骤
第一步:判定一个类是否是“无用的类”:需要满足下面三个条件
Java堆中不存在该类的任何实例,也就是该类的所有实例都被回收,加载该类的ClassLoader已经被回收该类对应的Class对象在任何地方没有引用了,也不能通过反射访问该类的方法。满足上面三个条件就可以回收了,但不是强制的。
2、Java 堆的垃圾回收:分代回收算法
在讲回收算法前,我们先来看一下 Java 堆的结构。
Java 堆空间分成了三部分,这三部分用来存储三类数据:刚创建的对象、存活了一段时间的对象、永久存在的对象。也就是说,常规的 Java 堆至少包括了 新生代 和 老年代 两块内存区域,而且这两块区域有很明显的特征:新生代:存活对象少、垃圾多,老年代:存活对象多、垃圾少。针对这种特点,我们有如下的几种方案;
1.新生代-复制 回收机制
对于新生代区域,由于每次 GC 都会有大量新对象死去,只有少量存活。因此采用 复制 回收算法,GC 时把少量的存活对象复制过去即可。但是从上面我们可以看到,新生代也划分了三个部分比例:Eden:S1:S2=8:1:1。其中 Eden 意为伊甸园,形容有很多新生对象在里面创建;S1和S2中的S表示Survivor,为幸存者,即经历 GC 后仍然存活下来的对象。
工作原理如下:
首先,Eden对外提供堆内存。当 Eden区快要满了,触发垃圾回收机制,把存活对象放入 Survivor A 区,清空 Eden 区;Eden区被清空后,继续对外提供堆内存;当 Eden 区再次被填满,对 Eden区和 Survivor A 区同时进行垃圾回收,把存活对象放入 Survivor B区,同时清空 Eden 区和Survivor A 区;当某个 Survivor区被填满,把多余对象放到Old 区;当 Old 区也被填满时,进行 下一阶段的垃圾回收。下面看看老年代的垃圾回收。
2.老年代-标记整理 回收机制
老年代的特点是:存活对象多、垃圾少。因此,根据老年代的特点,这里仅仅通过少量地移动对象就能清理垃圾,而且不存在内存碎片化。也就是标记整理的回收机制。既然是标记整理算法,而且老年代内部也不存在着内存划分,所以只需要根据标记整理的具体步骤进行垃圾回收就好了。
至此,基本上对堆内存的分代回收机制都进行了描述。但是要有一个问题没有解决,那就是JVM提供的垃圾回收器。
五、垃圾回收器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
在了解 垃圾回收器之前,首先得了解一下垃圾回收器的几个名词。
1. 吞吐量
CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。
2. 停顿时间
停顿时间 指垃圾回收器正在运行时,应用程序 的 暂停时间。
3. GC的名词
新生代GC:Minor GC
老年代GC:Major GC
4. 并发与并行
(1)串行(Parallel)
垃圾回收线程 进行垃圾回收工作,但此时 用户线程 仍然处于 等待状态。
(2)并发(Concurrent)
这里的并发指 用户线程 与 垃圾回收线程 交替执行。
(3)并行(Parallel)
这里的并行指 用户线程 和多条 垃圾回收线程 分别在不同 CPU 上同时工作。
下面其中垃圾回收器是基于HotSpot虚拟机,如图所示:
在 JVM 中,具体实现有 Serial、ParNew、Parallel Scavenge、CMS、Serial Old(MSC)、Parallel Old、G1 等。在上图中,可以看到 不同垃圾回收器 适合于 不同的内存区域,如果两个垃圾回收器之间 存在连线,那么表示两者可以 配合使用。
第一种:Serial(单线程)
Serial 回收器是最基本的 新生代垃圾回收器,是单线程的垃圾回收器。采用的是 复制算法。垃圾清理时,Serial回收器不存在线程间的切换,因此,在单 CPU` 的环境下,垃圾清除效率比较高。
第二种:Serial Old(单线程)
Serial Old回收器是 Serial回收器的老生代版本,单线程回收器,使用 标记-整理算法。在 JDK1.5 及其以前,它常与Parallel Scavenge回收器配合使用,达到较好的吞吐量,另外它也是 CMS 回收器在Concurrent Mode Failure时的后备方案。
第三种:ParNew(多线程)
ParNew回收器是在Serial回收器的基础上演化而来的,属于Serial回收器的多线程版本,采用复制算法。运行在新生代区域。在实现上,两者共用很多代码。在不同运行环境下,根据CPU核数,开启不同的线程数,从而达到最优的垃圾回收效果。
第四种:Parallel Scavenge(多线程)
Parallel Scavenge回收器也是运行在新生代区域,属于多线程的回收器,采用复制算法。与ParNew不同的是,ParNew回收器是通过控制垃圾回收的线程数来进行参数调整,而Parallel Scavenge回收器更关心的是程序运行的吞吐量。即一段时间内用户代码运行时间占总运行时间的百分比。
第五种:Parallel Old(多线程)
Parallel Old回收器是Parallel Scavenge回收器的老生代版本,属于多线程回收器,采用标记-整理算法。Parallel Old回收器和Parallel Scavenge回收器同样考虑了吞吐量优先这一指标,非常适合那些注重吞吐量和CPU资源敏感的场合。
第六种:CMS(多线程回收)
CMS回收器是在最短回收停顿时间为前提的回收器,属于多线程回收器,采用标记-清除算法。
第七种:G1回收器
G1是 JDK 1.7中正式投入使用的用于取代CMS的压缩回收器。它虽然没有在物理上隔断新生代与老生代,但是仍然属于分代垃圾回收器。G1仍然会区分年轻代与老年代,年轻代依然分有Eden区与Survivor区。
G1首先将堆分为大小相等的 Region,避免全区域的垃圾回收。G1的分区示例如下图所示:
这种使用区域划分内存空间以及有优先级的区域回收方式,保证G1回收器在有限的时间内可以获得尽可能高的回收效率。
下面对这几种垃圾回收机制进行一个总结:
2.设计一个秒杀系统,主要的挑战和问题有哪些?核心的架构方案或者思路有哪些?
针对一个秒杀系统:主要时要解决并发读与并发写。保证在系统超大流量并发读写、高性能和高可用。
主要的挑战有如何保证系统面对百万级的请求流量不出故障,如何保证高并发情况下数据的一致性写。
设计一个秒杀系统,可以参考以下原则:
1. 数据要尽量少:首先是指用户请求的数据能少就少。请求的数据包括上传给系统的数据和系统返回给用户的数据。
为什么“数据要尽量少”?
首先,这些数据在网络上传输需要时间,其次不管是请求数据还是返回数据都需要服务器做处理,而服务器在写网络时通常都要做压缩和字符编码,这些都非常消耗 CPU,所以减少传输的数据量可以显著减少 CPU 的使用。
其次,“数据要尽量少”还要求系统依赖的数据能少就少,包括系统完成某些业务逻辑需要读取和保存的数据,这些数据一般是和后台服务以及数据库打交道的。调用其他服务会涉及数据的序列化和反序列化,而这也是 CPU 的一大杀手,同样也会增加延时。而且,数据库本身也容易成为一个瓶颈,所以和数据库打交道越少越好,数据越简单、越小则越好。
2. 请求数要尽量少:用户请求的页面返回后,浏览器渲染这个页面还要包含其他的额外请求,比如说,这个页面依赖的 CSS/JavaScript、图片,以及 Ajax 请求等等都定义为“额外请求”,这些额外请求应该尽量少。因为浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,有的时候有页面依赖或者连接数限制,一些请求(例如 JavaScript)还需要串行加载等。另外,如果不同请求的域名不一样的话,还涉及这些域名的 DNS 解析,可能会耗时更久。所以减少请求数可以显著减少以上这些因素导致的资源消耗。例如,减少请求数最常用的一个实践就是合并 CSS 和 JavaScript 文件,把多个 JavaScript 文件合并成一个文件,在 URL 中用逗号隔开各个文件,这种方式在服务端仍然是单个文件各自存放,只是服务端会有一个组件解析这个 URL,然后动态把这些文件合并起来一起返回。
3. 路径要尽量短:所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。通常,这些节点可以表示为一个系统或者一个新的 Socket 连接(比如代理服务器只是创建一个新的 Sock连接来转发请求)。每经过一个节点,一般都会产生一个新的 Socket 连接。然而,每增加一个连接都会增加新的不确定性。从概率统计上来说,假如一次请求经过 5 个节点,每个节点的可用性是 99.9% 的话,那么整个请求的可用性是:99.9% 的 5 次方,约等于 99.5%。所以缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)。要缩短访问路径有一种办法,就是多个相互强依赖的应用合并部署在一起,把远程过程调用(RPC)变成 JVM 内部之间的方法调用。
4. 依赖要尽量少:所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。举个例子,比如说你要展示秒杀页面,而这个页面必须强依赖商品信息、用户信息,还有其他如优惠券、成交列表等这些对秒杀不是非要不可的信息(弱依赖),这些弱依赖在紧急情况下就可以去掉。要减少依赖,我们可以给系统进行分级,比如 0 级系统、1 级系统、2 级系统、3 级系统,0 级系统如果是最重要的系统,那么 0 级系统强依赖的系统也同样是最重要的系统,以此类推。注意,0 级系统要尽量减少对 1 级系统的强依赖,防止重要的系统被不重要的系统拖垮。例如支付系统是 0 级系统,而优惠券是 1 级系统的话,在极端情况下可以把优惠券给降级,防止支付系统被优惠券这个 1 级系统给拖垮。
5. 不要有单点:系统中的单点可以说是系统架构上的一个大忌,因为单点意味着没有备份,风险不可控,我们设计分布式系统最重要的原则就是“消除单点”。那如何避免单点呢?关键点是避免将服务的状态和机器绑定,即把服务无状态化,这样服务就可以在机器中随意移动。如何那把服务的状态和机器解耦呢?这里也有很多实现方式。例如把和机器相关的配置动态化,这些参数可以通过配置中心来动态推送,在服务启动时动态拉取下来,我们在这些配置中心设置一些规则来方便地改变这些映射关系。应用无状态化是有效避免单点的一种方式,但是像存储服务本身很难无状态化,因为数据要存储在磁盘上,本身就要和机器绑定,那么这种场景一般要通过冗余多个备份的方式来解决单点问题。
设计一个秒杀系统,从架构上可以从以下几个方面考虑:
1.把秒杀系统独立出来单独打造一个系统,这样可以有针对性地做优化;
2.在系统部署上也独立做一个机器集群,这样秒杀的大流量就不会影响到正常的商品购买集群的机器负载;3.将热点数据(如库存数据)单独放到一个缓存系统中,以提高“读性能”;
4.增加秒杀答题环节,防止有秒杀器恶意抢单。
版权声明: 本文为 InfoQ 作者【倩】的原创文章。
原文链接:【http://xie.infoq.cn/article/d78dc9d0db37180fbf21ee1ae】。未经作者许可,禁止转载。
评论 (1 条评论)