5、垃圾回收机制
JVM的垃圾回收机制主要涉及三个方面的问题:
1JVM有哪些垃圾回收算法?各自有什么优势?
2CMS垃圾回收器是如何工作的?有哪些阶段?
3服务卡顿的元凶到底是什么?
Java不用程序来管理内存的回收,但这些内存是如何回收的?
其实,JVM有专门的线程在做这件事情。当内容空间达到一定条件时,会自动触发,这个过程就叫GC,负责GC的组件被称为垃圾回收器。JVM规范没有规定垃圾回收器怎么实现,它只需要保证不要把正在使用的对象回收掉就可以。在现在的服务器环境中,经常被使用的垃圾回收器有CMS和G1,但JVM还有其它几个常见的垃圾回收器。
GC的过程是先找到活跃的对象,然后把其他不活跃的对象判定为垃圾,然后删除,所以GC只与活跃的对象有关,和堆的大小无关。
接下来学习下分代垃圾回收的内存划分和GC过程,再有就是常见的垃圾回收器。
这篇比较重要,因为几乎所有的垃圾回收器都是在这些基本思想上演化出来的。
GC的第一步就是找出活跃的对象,根据GC Roots遍历所有的可达对象,这个过程就叫作标记。
如上图所示,圆圈代表对象,绿色的代表GC Roots,红色的代表可以追溯到的对象,标记后,有多个灰色的圆圈,代表都是可被回收的对象。
清除阶段就是把未被标记的对象回收掉。
这种方式有一个明显的问题,会产生碎片空间。
比如申请了1k、2k、3k、4k、5k的内存
由于某些原因,2k和4k的内存不再使用,交给垃圾回收器回收。
解决碎片问题,就需要进行内存整理。
有一个思路就是提送一个对等的内存空间,将存活的对象复制过去,然后清除员内存空间。
在程序设计时,一般遇到扩缩容或者碎片整理问题时,复制算法都是非常有效的。比如:HashMap的扩容使用的是同样的思路,Redis的rehash也是如此。
整个过程如下图
这种方式看似完美,解决了碎片问题,但是弊端也非常明显,它浪费了一半的内存空间来做这个事情,如果原本资源就有限,这就是一种无法容忍的浪费。
不用分配一个对等的空间也是可以完成内存的整理工作。
可以把内存想象成一个非常大的数组,根据随机的index删除了一些数据,那么对数组的清理不需要另外一个数组来进行支持的,使用程序就可以。
主要思路是移动所有的存活对象,且按照内存地址顺序依次排列,然后将末端内存地址以后的内存全部收回。
对象的引用关系一般是非常复杂的,从效率上来说,一般整理算法是要低于复制算法的。
JVM的垃圾回收器,都是对以上几种朴素算法的结合使用,简单看一下它们的特点:
效率一般,缺点是回造成内存碎片的问题。
复制算法是所有算法里面效率最高的,缺点是造成一定的空间浪费。
效率比前两者要差,但没有空间浪费,也消除了内存碎片问题。
所以没有最优的算法,只有最合适的算法。
JVM是计算节点,而不是存储节点。最理想的情况就是对象使用完成之后,它的生命周期立马就结束了,而那些被频繁访问的资源,我们希望它能够常驻在内存里。
对象大致可以分为两类:
1大部分对象的生命周期都很短
2其他对象则很可能会存活很长时间
现在的垃圾回收器都会在物理上或者逻辑上,把这两类对象进行分区。我们把死的快的对象所占的区域叫年轻代(Young Generation)。把其他活的长的对象所占的区域叫作老年代(Old Generation),老年代在有时候会叫作Tenured Generation。
年轻代使用的垃圾回收算法是复制算法,因为年轻代发生GC后,会有非常少的对象存活,复制这部分对象是非常高效的
年轻代的内部分区
如图所示,年轻代分为:一个伊甸园空间(Eden),两个幸存者空间(Survivor)。
当年轻代中的Eden区分配满的时候,就会触发年轻代的GC(Minor GC),具体过程如下
1在Eden区执行了第一次GC之后,存活的对象会被移动到其中一个Suvivor分区(from);
2Eden区再次GC,这是会采用复制算法,将Eden和from区一起清理,存活的对象会被复制到to区;接下来只需要清空from区就可以了
在整个过程中总会有一个Survivor分区是空置的。Eden、from、to的默认比例是8:1:1,所以只会造成10%的空间浪费。
这个比例是由参数-XX:SurvivorRatio进行配置的(默认为8)。
补充下不常提到的TLAB。TLAB全称是Thread Local Allocation Buffer,JVM默认给每个线程开辟一个buffer区域,用来加速对象分配。这个buffer就放在Eden区中。
这个道理和Java语言中的ThreadLocal类似,避免了对公共区的操作,以及一些锁竞争。
老年代一般使用"标记-清除"、"标记-整理"算法。因为老年代的对象存活率一般是比较高的,空间又比较大,拷贝起来并不划算,不如采取就地收集的方式。
对象进入老年代的途径分类
如果对象够老,会通过"提升"进入老年代。关于对象老不老,是通过它的年龄来判断的。每发生一次Minor GC,存活下来的对象年龄都会加1,直到达到一定的阀值,就会提升到老年代,
这些对象如果变的不可达,直到老年代发生GC的时候才会被清理掉。
这个阀值可以通过参数 -XX:+MaxTenuringThreshold进行配置,最大值是15,因为它是用4bit存储的(所以把这个值调的很大的文章,是没有什么根据的)。
每次存活的对象,都会放入其中一个幸存区,这个区域默认比例是10%,但无法保证每次存活的对象都小于10%,当Survivor空间不够,就需要依赖其它内存(老年代)进行分配担保。这个时候,对象也会直接在老年代上分配。
超出某个大小的对象直接在老年代分配,通过参数设置-XX:PretenureSizeThreshold进行配置的,默认为0,默认全部在Eden区进行分配。
有的垃圾回收算法,并不要求age必须达到15才能晋升到老年代,它会使用一些动态的计算方法。比如,如果幸存区中相同年龄对象大小的和,大于幸存区的一半,大于或者等于age的对象将会直接进入老年代。
这些动态判定一半不受外部控制
对象的引用关系时一个巨大的网状,有的对象在Eden区,有的可能在老年代,那么这种跨代的引用是如何处理的呢?由于Minor GC是单独发生的,如果一个老年代的对象引用了它,如何确保能够让年轻代的对象存活呢?
对于是、否的判断,我们通常都会用到Bitmap(位图)和布隆过滤器来加快搜索的速度,需要另外再学习下(如果不知道这两个概念的话)
JVM也是用了类似的方法。其实,老年代是被分成众多的卡页(Card Page)的(一般数量是2的次幂)
卡表(Card Table)就是用于标记卡页状态的一个集合,每个卡表对应一个卡页。
如果年轻代有对象分配,而且老年代有对象指向这个新对象,那么这个老年代对象所对应内存的卡页就会被标识为dirty,卡表只需要非常小的存储空间就可以保留这些状态,垃圾回收时,就可以先读这个卡表,进行快速的判断。
接下来学习HotSpot的几个垃圾回收器,每种回收器都有各自的特点。在平常的GC优化时,一定要清楚现在用的是那种垃圾回收器。
下图包含了年轻代和老年代的划分,方便接下来的学习参考
处理GC的只有一条线程,并且在垃圾回收的过程中暂停一切用户线程。
这是最简单的垃圾回收器,虽然简单,但十分高效,通常用在客户端应用上。因为客户端应用不会频繁创建很多对象,用户也不会感觉出明显的卡顿。相反,它使用的资源更少,也更轻量级。
ParNew是Serial的多线程版本,由多条GC线程并行地进行垃圾清理。清理过程依然要停止用户线程。追求低停顿时间,与Serial唯一区别就是使用了多线程进行垃圾回收,在多CPU环境下性能比Serial会有一定程度的提升;但线程切换需要额外的开销,因此在单CPU环境中表现不如Serial。
另一个多线程版本的垃圾回收器。但与ParNew是有区别的
1Parallel Scavenge:追求CPU吞吐量,能够在较短时间内完成指定任务,适合没有交互的后台计算,弱交互强计算。
2ParNew:追求降低用户停顿时间,适合交互式应用,强交互弱计算。
与年轻代的Serial垃圾回收器对应,都是单线程版本,同样适合客户端使用。
年轻代Serial,使用复制算法。
老年代的Old Serial,使用标记-整理算法。
Parallel Old回收器是Parallel Scavenge 的老年代版本,追求CPU吞吐量。
CMS(Concurrent Mark Sweep)回收器是以获取最短GC停顿时间为目标的收集器,它在垃圾回收时使得用户线程和GC线程能够并发执行,因此在垃圾回收过程中用户也不会感到明显的卡顿。
长期看来,CMS垃圾回收器,是要被G1等垃圾回收器替换掉的,在Java8之后,使用它将会抛出一个警告!
除了上面几个垃圾回收器,我们还有G1、ZGC等更加高级的垃圾回收器,它们都有专门的配置参数来使其生效。
通过-XX:PrintCommandLineFlags参数,可以查看当前Java版本默认使用的垃圾回收器。在Java13中,默认的回收器就是G1。
以下是一些配置参数:
1-XX:+UseSerialGC 年轻代和年老代回收器
2-XX:+UseParNewGC 年轻代使用ParNew,老年代使用Serial Old。
3-XX:+UseParallelOldGC 年轻代和老年代哦都市用并行回收器。
4-XX:+UseConcMarkSweepGC 表示年轻代使用ParNew,老年代使用CMS。
5-XX:+UseG1GC 使用G1垃圾回收器
6-XX:+UseZGC 使用ZGC垃圾回收器
这些垃圾回收器的关系还是比较复杂的,请看下图
目前Java8还是主流使用版本,从Java8升级到高版本的Java体系是有一定成本的,所以CMS垃圾回收器还会持续一段时间
抛个问题,如果在垃圾回收的时候,又有新的对象进入怎么办?
为了保住程序不乱套,最好的办法就是暂停用户的一切线程,也就是在这段时间,是不能new对象的,只能等待,表象是在JVM上就是短暂的卡顿,什么都干不了,这个现象叫作Stop The World。
标记阶段,大多数是要STW的。如果不暂停用户进程,在标记对象的时候,有可能有其它用户线程会产生一些新的对象和引用,造成混乱。
现在的垃圾回收器,都会尽量去减少这个过程。但即使最先进的ZGC回收器,也会有短暂的STW过程。我们要做的就是在现有基础设施上,尽量减少GC停顿。
举例说明下
某个高并发服务的峰值流量是10万次/秒,后面有10台负载均衡的机器,那么每台机器平均下来需要1w/s。假如某台机器在这段时间内发生了STW,持续了一秒,那么至少需要10ms就可以返回的1万个请求,需要至少等待1秒。
在用户那里的表现就是系统发生了卡顿。如果我们的GC非常的频繁。这种卡顿就会特别的明显,严重影响用户体验。
虽然说Java为我们提供了非常棒的自动内存管理机制,但也不能滥用,因为它是有STW硬伤的。
介绍了堆的具体分区,年轻代和老年代。介绍了多个常用的垃圾回收器,不同的垃圾回收器有不同的特点。各种垃圾回收器都是为了解决头疼的STW问题,让GC时间更短,停顿更短,吞吐量更大。
接触了很多名词,总结如下
1Mark
2Sweep
3Copy
4Compact
1Young generation
2Survivor
3Eden
4Old Generation |Tenured Generation
5GC
--1Minor GC
--2Major GC
1weak generational hypothesis
2分配担保
3提升
4卡片标记
5STW
主板:分为家用和军用(服务器)家用主板电压较小主板面积较小只有硬盘接口和内存条接口以及CPU接口其他另算而军用主板特别的大用处理频率可以达到100ghz接口繁多光CPU接口就有几个或者几十个甚至几百个。
内存条:家用最高才16g开个双通道32g为目前最好的一个内存条频率才1666MB而服务器则有1024g一条的可以插十条不止使用频率特别高。再就是服务器不接显示器
Microsoft SQL Server 2000 的内存管理组件消除了对 SQL Server 可用的内存进行手工管理的需要。SQL Server 在启动时根据操作系统和其它应用程序当前正在使用的内存量,动态确定应分配的内存量。当计算机和SQL Server 上的负荷更改时,分配的内存也随之更改。有关更多信息,请参见内存构架。
下列服务器配置选项可用于配置内存使用并影响服务器性能:
min server memory
max server memory
max worker threads
index create memory
min memory per query
min server memory 服务器配置选项可用于确保 SQL Server 至少以最小的分配内存量启动,并且内存低于该值时不会释放内存。可以基于 SQL Server 的大小及活动将该配置选项设置为特定的值。始终将 min server memory 服务器配置选项设置为某个合理的值,以确保操作系统不向 SQL Server 请求太多内存而影响 SQL Server 性能。
max server memory 服务器配置选项可用于:在 SQL Server 启动及运行时,指定 SQL Server 可以分配的最大内存量。如果知道有多个应用程序与 SQL Server 同时运行,而且想保障这些应用程序有足够的内存运行,可以将该配置选项设置为特定的值。如果这些其它应用程序(如 Web 服务器或电子邮件服务器)只根据需要请求内存,则 SQL Server 将根据需要给它们释放内存,因此不要设置 max server memory 服务器配置选项。然而,应用程序通常在启动时不假选择地使用可用内存,而如果需要更多内存也不请求。如果有这种行为方式的应用程序与 SQL Server 同时运行在相同的计算机上,则将 max server memory 服务器配置选项设置为特定的值,以保障应用程序所需的内存不由 SQL Server 分配出。
不要将 min server memory 和 max server memory 服务器配置选项设置为相同的值,这样做会使分配给 SQL Server 的内存量固定。动态内存分配可以随时间提供最佳的总体性能。有关更多信息,请参见服务器内存选项。
max worker threads 服务器配置选项可用于指定为用户连接到 SQL Server 提供支持的线程数。255 这一默认设置对一些配置可能稍微偏高,这要具体取决于并发用户数。由于每个工作线程都已分配,因此即使线程没有正在使用(因为并发连接比分配的工作线程少),可由其它操作(如高速缓冲存储器)更好地利用的内存资源也可能是未使用的。一般情况下,应将该配置值设置为并发连接数,但不能超过 1,024。有关更多信息,请参见max worker threads 选项。
说明当 SQL Server 运行在 Microsoft Windowsreg; 95 或 Microsoft Windows 98 上时,最大工作线程服务器配置选项不起作用。
index create memory 服务器配置选项控制创建索引时排序操作所使用的内存量。在生产系统上创建索引通常是不常执行的任务,通常调度为在非峰值时间执行的作业。因此,不常创建索引且在非峰值时间时,增加该值可提高索引创建的性能。不过,最好将 min memory per query 配置选项保持在一个较低的值,这样即使所有请求的内存都不可用,索引创建作业仍能开始。有关更多信息,请参见 index create memory 选项。
min memory per query 服务器配置选项可用于指定分配给查询执行的最小内存量。当系统内有许多查询并发执行时,增大 min memory per query 的值有助于提高消耗大量内存的查询(如大型排序和哈希操作)的性能。不过,不要将 min memory per query 服务器配置选项设置得太高,尤其是在很忙的系统上,因为查询将不得不等到能确保占有请求的最小内存、或等到超过 query wait 服务器配置选项内所指定的值。如果可用内存比执行查询所需的指定最小内存多,则只要查询能对多出的内存加以有效的利用,就可以使用多出的内存。
附实例一篇:
最近,为了能在数据库服务器中运行其他应用程序,在保持数据库操作系统版本不变的前提下对数据库服务器进行了软、硬件上的升级。在软件上,将操作系统从Windows 2000升级到Windows Server 2003;在硬件上,将服务器中的内存由原来的512MB增加到1GB(1024MB)。
在升级后的开始几个星期之内,服务器在使用中表现良好。但是不久后就发现,在服务器上同时运行的其他应用程序却出现了异常,不时地报出内存分配不足的警告。经过几次跟踪后发现,原来是SQL Server吞去了大部分内存所致。被SQL Server占用的内存由升级前的不到400MB一下子增加到现在的900MB,并且有不断增长的趋势。
通过查找原因才知道这是SQL Server 缓冲池的预期行为。默认情况下,在启动 SQL Server之后,SQL Server会根据操作系统报告的物理内存数来动态增大或缩小高速缓冲存储器的容量。只要可用物理内存大小保持在4MB到10MB之间,SQL Server 缓冲池就会继续增大(保留可用物理内存在4MB到10MB之间是为了避免操作系统因为缺少内存而频繁地换页)。如果物理可用内存变得较少的时候,则SQL Server会将一些内存释放给操作系统。
为了使运行在服务器上的应用程序都能达到比较满意的效果,同时也为了能给其他应用程序分配足够的内存,需要采取措施限制SQL Server 的内存使用量。我们可以通过设置SQL Server 数据库引擎使用的内存的上下限来达到此目的。其具体步骤是:
1打开企业管理器,展开服务器组。
2单击该服务器,点击鼠标右键,单击属性菜单。
3在弹出的对话框中单击内存选项卡。
内存设置方法有两种:
1设置min server memory和max server memory 在一个范围段内。
比如,我们将它设置成最小0MB,最大255MB。这种方法在为一台服务器中运行多个应用程序分配内存时非常有用。
2设置 min server memory 和 max server memory 为同一数值。
比如,可以将它最大和最小值都设置成255MB。这样的设置方法与窗口中的另一个选项“使用固定的内存大小” 相一致。
虽然内存最小值和最大值设置是高级选项,但在设置完毕之后,最好还是先将SQL Server服务停止后再重新运行,以便SQL Server能更好地对内存进行合理安排。
不会。内存和硬盘是两种不同的配件。内存上的数据一关机就会消失,它不存储长期数据。因此,如果只是更换了内存而不是硬盘,那么原来的数据并不会被保留。在更换内存条时,确保选择了与电脑兼容的内存条。频率、型号和容量都与原装内存条相匹配。如果打算出售或回收电脑,最好清除所有的个人数据,并确保操作系统和应用程序都已完全卸载。
0条评论