垃圾回收机制算法

 

1 垃圾回收机制概述

Java垃圾回收机制其实就是GC线程不定时的回收堆内存中的不可达对象。

由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。

1.1 垃圾回收过程

注意:不可达对象并不会马上就被直接回收,而是至少要经过两次标记的过程。

 第一次被标记过的对象,会检查该对象是否重写了finalize()方法。如果重写了该方法,则将其放入一个F-Query队列中,否则,直接将对象加入“即将回收”集合。在第二次标记之前,F-Query队列中的所有对象会逐个执行finalize()方法,但是不保证该队列中所有对象的finalize()方法都能被执行,这是因为JVM创建一个低优先级的线程去运行此队列中的方法,很可能在没有遍历完之前,就已经被剥夺了运行的权利。那么运行finalize()方法的意义何在呢?这是对象避免自己被清理的最后手段:如果在执行finalize()方法的过程中,使得此对象重新与GC Roots引用链相连,则会在第二次标记过程中将此对象从F-Query队列中清除,避免在这次回收中被清除,恢复成了一个“正常”的对象。但显然这种好事不能无限的发生,对于曾经执行过一次finalize()的对象来说,之后如果再被标记,则不会再执行finalize()方法,只能等待被清除的命运。 

之后,GC将对F-Queue中的对象进行第二次小规模的标记,将队列中重新与GC Roots引用链恢复连接的对象清除出“即将回收”集合。所有此集合中的内容将被回收。

1.2 finalize作用

Java技术使用finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

public class Test001 {
    public static void main(String[] args) {
        Test001 test001 = new Test001();
        test001=null;
        System.gc();
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("垃圾回收机制之前调用。。。");
    }
}

1.3 内存泄漏和内存溢出

内存溢出:内存不够应用程序使用。

内存泄漏:对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。

上面图中可以看出,里面有被引用对象和未被引用对象。未被引用对象会被垃圾回收器回收,而被引用的对象却不会。未被引用的对象当然是不再被使用的对象,因为没有对象再引用它。然而无用对象却不全是未被引用对象。其中还有被引用的。就是这种情况导致了内存泄漏。

如何防止内存泄露

下面是几条容易上手的建议,来帮助你防止内存泄漏的发生。

  • 特别注意一些像HashMap、ArrayList的集合对象,它们经常会引发内存泄漏。当它们被声明为static时,它们的生命周期就会和应用程序一样长。
  • 特别注意事件监听和回调函数。当一个监听器在使用的时候被注册,但不再使用之后却未被反注册。
  • “如果一个类自己管理内存,那开发人员就得小心内存泄漏问题了。” 通常一些成员变量引用其他对象,初始化的时候需要置空。

2 垃圾回收机制算法

2.1 引用计数法

每个对象添加一个引用计数器,默认是15,每当有引用指向该对象(对象可达),那么该值+1,否则-1。当gc检测到该计数器值为0时垃圾收集器回收对象使用的内存。

优点:引用计数器可以很快的执行,交织在程序运行中,对程序需要不被长时间打断的实时环境比较有利

缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.而且每次加减非常浪费内存。

2.2 复制算法

这篇中介绍,堆内存新生代分为eden、s0、s1三个区,其中S0和S1相等大小。而复制算法呢,每次只使用S0和S1其中的一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑碎片啊什么的,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。所以复制算法用于新生代垃圾回收。

当某个对象比如user1新创建时在eden,随后由于经常被使用,所以存放在到s0,又来一个对象user1同样存放到s0,此时垃圾回收,则仅仅扫描s0区(因为s1是空的嘛),如果发现有对象被引用(比如user1)就将其复制到s1区,然后清理s0区。之后如果又来新对象则添加至s1区,下次回收器扫描S1同理。

缺点:可使用内存降低为原来的一般

2.3 标记清除与标记压缩算法

标记-清除(Mark-Sweep)算法顾名思义,主要就是两个动作,一个是标记,另一个就是清除。

标记就是根据特定的算法(如:引用计数算法,可达性分析算法等)标出内存中哪些对象可以回收,哪些对象还要继续用。

标记指示回收,那就直接收掉;标记指示对象还能用,那就原地不动留下。

缺点:标记与清除没有连续性效率低,清除之后内存产生大量碎片

标记-压缩算法

标记压缩法在标记清除基础之上做了优化,把可回收的对象压缩到内存一端,而后进行垃圾清理(比如讲上图中蓝色全部压缩到左上角,这样直接回收这部分即可)。(java中老年代使用的就是标记压缩法)

2.4 分代算法

根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。

对于新生代和老年代来说,新生代回收频率很高,但是每次回收耗时很短,而老年代回收频率较低,但是耗时会相对较长,所以应该尽量减少老年代的GC.

垃圾回收时的停顿现象

垃圾回收的任务是识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以更高效的执行,大部分情况下,会要求系统进入一个停顿的状态。停顿的目的是为了暂停所有的应用线程,只有这样的系统才不会有新垃圾的产生。同时停顿保证了系统状态在某一个瞬间的一致性,也有利于更好的标记垃圾对象。因此在垃圾回收时,都会产生应用程序的停顿。因此要减少垃圾回收次数

3 垃圾收集器

Java垃圾回收器是Java虚拟机(JVM)的三个重要模块(另外两个是解释器和多线程机制)之一,为应用程序提供内存的自动分配(Memory Allocation)、自动回收(Garbage Collect)功能,这两个操作都发生在Java堆上(一段内存快)。某一个时点,一个对象如果有一个以上的引用(Rreference)指向它,那么该对象就为活着的(Live),否则死亡(Dead),视为垃圾,可被垃圾回收器回收再利用。垃圾回收操作需要消耗CPU、线程、时间等资源,所以容易理解的是垃圾回收操作不是实时的发生(对象死亡马上释放),当内存消耗完或者是达到某一个指标(Threshold,使用内存占总内存的比列,比如0.75)时,触发垃圾回收操作。有一个对象死亡的例外,java.lang.Thread类型的对象即使没有引用,只要线程还在运行,就不会被回收。

3.1 串行回收器(Serial Collector)

单线程执行回收操作,回收期间暂停所有应用线程的执行,client模式下的默认回收器,通过-XX:+UseSerialGC命令可选项强制指定。参数可以设置使用新生代串行和老年代串行回收器

串行收集器就是使用单线程进行垃圾回收。对新生代的回收使用复制算法,对老年代使用标记压缩算法回收时间较长,但其稳定性是优于其他回收器

执行垃圾回收时,应用程序线程暂停,GC线程开始(开始垃圾回收),垃圾回收完成后,应用程序线程继续执行。注意:在GC线程运行过程中使用单线程进行串行回收。

3.2 并行回收

3.2.1 并行回收器(ParNew回收器)

并行回收器在串行回收器基础上做了改进,他可以使用多个线程同时进行垃圾回收,对于计算能力强的计算机而言,可以有效的缩短垃圾回收所需的尖际时间。ParNew回收器是一个工作在新生代的垃圾收集器,这个回收器只针对新生代进行并发回收,老年代依然使用串行回收。回收算法依然和串行回收一样,新生代使用复制算法,老年代使用标记压缩算法。在多核条件下,它的性能显然优于串行回收器,如果要使用这种回收器,可以在启动参数中配置:-XX:+UseParNewGC如果要进一步指定并发的线程数,可以配置一下参数:-XX:ParallelGCThreads

进行垃圾回收时应用程序线程依然被暂停,GC线程并行开始执行垃圾回收,垃圾回收完成后,应用程序线程继续执行

3.2.2并行回收集器(ParallelGC)

这种回收器有两种配置,一种类似于ParNEW:新生代使用并行回收、老年代使用串行回收。它与ParNew的不同在于它在设计目标上更重视吞吐量,可以认为在相同的条件下它比ParNew更优。要使用这种回收器可以在启动程序中配置:

-XX:+UseParallelGC

Parallel回收器另外一种配置则不同于ParNew,对于新生代和老年代均适应并行回收,要使用这种回收器可以在启动程序中配置:

XX:+UseParallelOldGC

3.2.3 CMS回收器

CMS回收器: Concurrent Mark Sweep,并发标记清除。注意这里注意两个词:并发、标记清除。

并发表示它可以与应用程序并发执行、交替执行;标记清除表示这种回收器不是使用的是标记压缩算法,这和前面介绍的串行回收器和并发回收器有所不同。需要注意的是CMS回收器是一种针对老年代的回收器,不对新生代产生作用。这种回收器优点在于减少了应用程序停顿的时间,因为它不需要应用程序完成暂定等待垃圾回收,而是与垃圾回收并发执行。要执行这种垃圾回收器可以在启动参数中配置:-XX:+UseConcMarkSweepGC

整个收集过程大致分为4个步骤:

①.初始标记(CMS initial mark)

②.并发标记(CMS concurrenr mark)

③.重新标记(CMS remark)

④.并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,但比并发标记阶段要短。

     由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,器主要有三个显著缺点:

CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。

CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full  GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,
即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低

最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full  GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full  GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full  GC之后,跟着来一次碎片整理过程。

clip_image010

3.2.4 G1回收器

G1回收器是jdk1.7以后推出的回收器,试图取代CMS回收器。

不同于其他的回收器、G1将堆空间划分成了互相独立的区块。每块区域既有可能属于老年代、也有可能是新生代,并且每类区域空间可以是不连续的(对比CMS的老年代和新生代都必须是连续的)。这种将老年代区划分成多块的理念源于:当并发后台线程寻找可回收的对象时、有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程、但可以用相对较少的时间优先回收包含垃圾最多区块。这也是为什么G1命名为Garbage First的原因:第一时间处理垃圾最多的区块。要使用G1回收器需要在启动是配置以下参数:

-XX:+UseG1GC

G1相对CMS回收器来说优点在于:

1、因为划分了很多区块,回收时减小了内存碎片的产生;

2、G1适用于新生代和老年代,而CMS只适用于老年代。

使用.XXX:+UseG1GC 应用G1收集器,Mills指定最大停顿时间使用-XX:MaxGCPausel。设置并行回收的线程数量使用-XX:ParallelGCThreads

4 使用Jmeter进行Tomcat配置调优测试

JMeter是一款在国外非常流行和受欢迎的开源性能测试工具,像LoadRunner 一样,它也提供了一个利用本地Proxy Server(代理服务器)来录制生成测试脚本的功能.。

4.1 准备工作

添加依赖:

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>8.5.31</version>
        </dependency>

web.xml:

<?xml version="1.0" encoding="utf-8"?>
<web-app version="3.0"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
	http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <display-name>test</display-name>


</web-app>

servlet:

@WebServlet("/index")
public class IndexServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("首页Index");
        resp.setCharacterEncoding("utf-8");
        resp.getWriter().write("这里是首页。。。");
    }
}

通过http://localhost:8080/index能访问并且显示

这里是首页。。。

并且命令行输出“首页Index”代表成功。

接下来使用Jmeter

1.添加线程组并且设置基本参数

2 添加http请求

3 点击http请求按钮后设置一些参数比如:

4 添加聚合报告

5 点击运行,如下图所示:此时可看到程序输出多个“首页Index”

10个字段,含义分别如下。

Label:每个 JMeter 的 element(例如 HTTP Request)都有一个 Name 属性,这里显示的就是 Name 属性的值

#Samples:表示你这次测试中一共发出了多少个请求,如果模拟10个用户,每个用户迭代10次,那么这里显示100

Average:平均响应时间——默认情况下是单个 Request 的平均响应时间,当使用了 Transaction Controller 时,也可以以Transaction 为单位显示平均响应时间

Median:中位数,也就是 50% 用户的响应时间

90% Line:90% 用户的响应时间

Note:关于 50% 和 90% 并发用户数的含义,请参考下文

Min:最小响应时间

Max:最大响应时间

Error%:本次测试中出现错误的请求的数量/请求的总数

Throughput:吞吐量——默认情况下表示每秒完成的请求数(Request per Second),当使用了 Transaction Controller 时,也可以表示类似 LoadRunner 的 Transaction per Second 数

KB/Sec:每秒从服务器端接收到的数据量,相当于LoadRunner中的Throughput/Sec

4.2 tomcate参数调优测试——串行回收

这里可能开发工具有所不同,设置参数的地方不同。

idea

第一步:

第二步:

设置以下参数(测试结果电脑配置不同可能会有不同),参数含义

(1)

-XX:+PrintGCDetails -Xmx32M -Xms32M

-XX:+HeapDumpOnOutOfMemoryError

-XX:+UseSerialGC

-XX:PermSize=32M     JVM初始分配的非堆内存 

我测试的是吞吐量为1392.4/sec,6次垃圾回收

(2)增加堆内存

-XX:+PrintGCDetails -Xmx512M -Xms32M

-XX:+HeapDumpOnOutOfMemoryError

-XX:+UseSerialGC

-XX:PermSize=32M     JVM初始分配的非堆内存 

我测试的吞吐量为1737.9/sec,6次垃圾回收(初始内存不变导致次数不变)

增大最大内存,吞吐量增加

(3)调整初始堆

-XX:+PrintGCDetails -Xmx512M -Xms512M

-XX:+HeapDumpOnOutOfMemoryError

-XX:+UseSerialGC

-XX:PermSize=32M 

我测试的吞吐量为吞吐量为1740.6/sec,0次垃圾回收

增大初始内存,垃圾回收次数减少

(3)并行回收

-XX:+PrintGCDetails -Xmx512M -Xms512M

-XX:+HeapDumpOnOutOfMemoryError

-XX:+UseParNewGC

-XX:PermSize=32M 

我测试的吞吐量为吞吐量为1771.2/sec,0次垃圾回收

没什么变化,可能我的机器问题,听说应该要高点

(4)并行合并回收(UseParallelGC)

-XX:+PrintGCDetails -Xmx512M -Xms512M

-XX:+HeapDumpOnOutOfMemoryError

-XX:+UseParallelGC

-XX:+UseParallelOldGC

-XX:ParallelGCThreads=8

-XX:PermSize=32M

我测试的吞吐量为吞吐量为2050.4/sec,0次垃圾回收

总结:初始堆值和最大堆内存内存越大,吞吐量就越高。最好使用并行收集器,因为并行收集器速度比串行吞吐量高,速度快。设置堆内存新生代的比例和老年代的比例最好为1:2或者1:3。减少GC对老年代的回收。

  • 3
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值