一篇文章了解JVM GC

内存管理

内存(Memory)作用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,内存的运行也决定了计算机的稳定运行。

内存管理是指软件运行时对计算机内存资源的分配和使用。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。

GC是什么

GC (Gabage Collection) 垃圾回收,主要作用是释放和回收内存资源

Java自动管理内存,不用像 C++ 需要手动管理内存, Java 程序员只管 New New New 即可,Java 会自动回收过期的对象。

程序计数器、Java栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的!
在这里插入图片描述
我们把Java对象的回收释放交给GC就万事大吉了吗!不,不良的开发习惯,还是会导致出现内存泄漏的问题。进一步了解Java GC的原理,知道其执行的过程,理解背后的思想。不光能够提高代码开发的质量,还有从中学习到宝贵的开发经验。

认识GC之前我们先看看一个案例,判断该代码算法是否会造成内存泄漏

public class Test {
	
    public Object obj;

    public void test(){
        obj = new Object();
        //...其他代码
    }
}

GC算法和方式

如果公司交给你一个需求,来实现自动垃圾回收,你会怎么开发。
首先你会想,哪些是垃圾对象,这些垃圾对象可以分类吗,这些垃圾对象分别在哪里,应该怎么回收这些垃圾对象。

如何判断垃圾对象

哪些是可以被GC回收的对象呢?

  • 引用计数判断法(reference-counting):每个对象都有个引用计数器,被引用一次时+1,引用失效时-1,当对象的引用计数器为0时,则是可以被GC回收的对象。
  • 可达性分析法(Readchablility Analysis):以一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链(不可达)时,则是可以被GC回收的对象(GC Roots必然活的对象,不会被回收的对象)。
    在这里插入图片描述
怎么回收垃圾对象

根据不同的垃圾对象判断条件来分类,可分为引用计数判断法-回收算法和可达性分析法-回收算法。引用计数回收算法就是根据引用计数判断法来回收垃圾对象。我们重点讲可达性分析法回收算法。Java目前已不推荐使用引用计数回收算法。

可达性分析法回收算法实现可分为两个阶段标记回收。标记都是以可达性作为标记,回收可分为直接回收、复制回收、移动(也叫整理)回收。

  • 标记算法:直接回收标记为不可达的对象。将红色框框直接回收,我们发现回收之后会很多内存碎片。
    在这里插入图片描述
  • 复制算法:该算法很好的解决了内存空间碎片的问题,它将内存分为大小相同的两个区域,运行区域,预留区域,所有创建的新对象都分配到运行区域,当运行区域内存不够时,将运作区域中存活对象全部复制到预留区域,然后再清空整个运行区域内存,这时两块区域的角色也发生了变化,每次存活的对象就像皮球一下在运行区域与预留区域踢来踢出,而垃圾对象会随着整个区域内存的清空而释放掉。虽然解决了内存碎片的问题,但是预留一半的内存区域未免有些浪费,并且如果内存中大量的是存活状态,只有少量的垃圾对象,收集器要执行更多次的复制操作才能释放少量的内存空间,得不偿失。
    在这里插入图片描述
  • 标记整理算法:回收之前,算法将存活的对象向内存空间的一端移动,然后将存活对象边界以外的空间全部清空。这样解决了内存碎片问题,也不存在空间的浪费问题。但是也存在移动的动作,当需要整理大量且都是存活的对象时,则需要移动大量的存活对象,且释放的内存也只有一丁点。
    在这里插入图片描述
垃圾对象回收方式

根据GC的运行方式和GC线程与用户线程的关系划分:

  • 串行:单线程运行,用户线程暂停,Stop The World。不适合服务器环境。
  • 并行:多线程运行,用户线程暂停,回收效率高于串行。适用于科学计算或者大数据分析等弱交互的场景。在这里插入图片描述
  • 并发:多线程运行,用户线程几乎不用暂停,可以与用户线程同时执行。适用于对响应时间有要求的场景。
    在这里插入图片描述
垃圾对象回收实现

我们知道了什么是垃圾对象,又有哪些算法可以回收这些垃圾对象。接下来就是开发实现垃圾回收功能。

在选择具体的回收算法时,可以不用拘于某个算法。对不同生命周期的对象的回收动作进行分类,分别采用不同的垃圾回收算法,以提高回收的效率。分析JVM中的对象生命周期,我们可以得出下面的结论:

  • 绝大多数的对象都是“朝生夕灭”的,既创建不久即可消亡;
  • 熬过越多此垃圾回收过程的对象就越难以消亡;

我们可以将“朝生夕灭”的分为新生代,“难以消亡”分为老年代。新生代(PSYoungGen)特点垃圾对象多,回收动作频繁,用标记复制算法回收效率高。老年代(ParOldGen)特点垃圾对象少,生存时间长,回收作动少,用标记清除、标记整理算法。上述回收方式可以称为分代收集算法
在这里插入图片描述
上面都是GC的设计思路和回收模型,JDK已有了对应的落地实现,我们要通过这些设计思想来理解和应用这些落地实现。
在这里插入图片描述
在这里插入图片描述

G1是JDK1.8之后的,这个本篇不作拓展。新生代(Young Gen)都是采用复制算法,老年代(Old Gen)都是采用标记清除、标记整理算法。这些垃圾收集器主要区别是回收方式的不同。

串行执行是最古老、最简单、效率高的一种回收方式。执行时,用户线程暂停,适用于单核服务器。

并行执行是串行的升级版,多线程执行垃圾回收,效率大大提高。执行时,用户线程暂停,适用于多核服务器。

并发执行是多线程与用户线程并发执行,目的是尽量减少对用户线程的暂停时间,实现最短的回收停顿时间。

  • Serial(新生代):串行执行
  • ParNew(新生代):并行执行
  • Parallel(新生代):并行执行,是ParNew的升级版
  • Serial Old(老生代):串行执行
  • Parallel Old(老生代):并行执行
  • CMS(老生代):并发执行(Concurrent Mark-Sweep),使用标记清除算法

GC应用配置

JVM设置垃圾回收器的配置参数,就是在上述的垃圾回收器之间选择,针对性配置垃圾回收器。

    //资源类测试
    public static void main(String[] args) throws Exception {
        System.out.println("Start...");
        for (int i = 1; i <= 3; i++) {
            System.out.println("i = " + i);
            //堆中创建10M的对象
            byte[] bytes = new byte[1024 * 1024 * 10];
            Thread.sleep(1000);
        }
    }

GC信息名称解释:
在这里插入图片描述

-XX:+UseSerialGC:Serial(新生代)+Serial Old(老生代)

新生代和老年代都使用单线程串行执行GC。

JVM参数配置:-Xms20m -Xmx20m -XX:+UseSerialGC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails

堆分为:def new generation(新生代)、tenured generation(老年代)、Metaspace(元空间)

Heap
 def new generation   total 6144K, used 165K [0x00000000fec00000, 0x00000000ff2a0000, 0x00000000ff2a0000)
  eden space 5504K,   3% used [0x00000000fec00000, 0x00000000fec29608, 0x00000000ff160000)
  from space 640K,   0% used [0x00000000ff160000, 0x00000000ff160000, 0x00000000ff200000)
  to   space 640K,   0% used [0x00000000ff200000, 0x00000000ff200000, 0x00000000ff2a0000)
 tenured generation   total 13696K, used 10811K [0x00000000ff2a0000, 0x0000000100000000, 0x0000000100000000)
   the space 13696K,  78% used [0x00000000ff2a0000, 0x00000000ffd2ee78, 0x00000000ffd2f000, 0x0000000100000000)
 Metaspace       used 2843K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K
-XX:+UseParNewGC:ParNew(新生代)+Serial Old(老生代)

新生代适用多线程并行执行GC,老年代使用单线程串行执行GC。

JVM参数配置:-Xms20m -Xmx20m -XX:+UseParNewGC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails

堆分为:par new generation(新生代)、tenured generation(老年代)、Metaspace(元空间)

Heap
 par new generation   total 6144K, used 165K [0x00000000fec00000, 0x00000000ff2a0000, 0x00000000ff2a0000)
  eden space 5504K,   3% used [0x00000000fec00000, 0x00000000fec29608, 0x00000000ff160000)
  from space 640K,   0% used [0x00000000ff160000, 0x00000000ff160000, 0x00000000ff200000)
  to   space 640K,   0% used [0x00000000ff200000, 0x00000000ff200000, 0x00000000ff2a0000)
 tenured generation   total 13696K, used 10811K [0x00000000ff2a0000, 0x0000000100000000, 0x0000000100000000)
   the space 13696K,  78% used [0x00000000ff2a0000, 0x00000000ffd2ee78, 0x00000000ffd2f000, 0x0000000100000000)
 Metaspace       used 2843K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

堆警告信息:Java HotSpot™ 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release(Java HotSpot(TM)64位服务器VM警告:不推荐将ParNew年轻收集器与Serial旧收集器一起使用,并且在将来的发行版中可能会删除它)

拓展:-XX:ParallelGCThreads=并行线程数(默认CPU数),可以自定义设置并行的线程数。

-XX:+UseParallelGC:Parallel(新生代)+Parallel Old(老生代)

新生代和老年代都使用多线程并行GC。

又称吞吐量收集器(吞吐量=用户线程执行时间/用户线程执行时间+GC线程运行时间),吞吐量越高,说明CPU执行效率高,最高100%执行用户线程,不执行GC。

JVM参数配置:-Xms20m -Xmx20m -XX:+UseParallelGC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails

堆分为:PSYoungGen(新生代)、ParOldGen(老年代)、Metaspace(元空间)

Heap
 PSYoungGen      total 6144K, used 169K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
  eden space 5632K, 3% used [0x00000000ff980000,0x00000000ff9aa558,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 13824K, used 10814K [0x00000000fec00000, 0x00000000ff980000, 0x00000000ff980000)
  object space 13824K, 78% used [0x00000000fec00000,0x00000000ff68f970,0x00000000ff980000)
 Metaspace       used 2843K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

拓展:-XX:MaxGCPauseMillis=毫秒数,可以自定义配置GC的暂停时间,采用自适应调节策略,是对ParNew的重大提升。JDK1.8默认配置,查看JVM默认配置

java -XX:+PrintCommandLineFlags -version

在这里插入图片描述

-XX:+UseParallelOldGC:Parallel(新生代)+Parallel Old(老生代)

新生代和老年代都使用多线程并行GC。与-XX:+UseParallelGC一样,可相互激活。JDK1.6之后有效,1.6之前还是Parallel(新生代)+Serial Old(老生代)。

-XX:+UseConcMarkSweepGC:ParNew(新生代)+CMS(老生代)

新生代使用多线程并发GC,老年代使用多线程并发GC,并以Serial Old(老生代)作为异常时备用。

并发标记执行流程:

  1. 初始标记(STW initial mark) ***暂停应用
  2. 并发标记(Concurrent marking)
  3. 并发预清理(Concurrent precleaning)
  4. 重新标记(STW remark) *** 暂停 应用
  5. 并发清理(Concurrent sweeping)
  6. 并发重置(Concurrent reset)
    在这里插入图片描述

JVM参数配置:-Xms20m -Xmx20m -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails

堆分为:par new generation(新生代)、concurrent mark-sweep generation(老年代)、Metaspace(元空间)

Heap
 par new generation   total 6144K, used 55K [0x00000000fec00000, 0x00000000ff2a0000, 0x00000000ff2a0000)
  eden space 5504K,   1% used [0x00000000fec00000, 0x00000000fec0dda0, 0x00000000ff160000)
  from space 640K,   0% used [0x00000000ff160000, 0x00000000ff160000, 0x00000000ff200000)
  to   space 640K,   0% used [0x00000000ff200000, 0x00000000ff200000, 0x00000000ff2a0000)
 concurrent mark-sweep generation total 13696K, used 10815K [0x00000000ff2a0000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 2843K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

备用担保机制:由于是并发执行,GC线程和用户线程会同时增加对堆内存的占用,所以CMS必须在老年代堆内存用尽之前完成垃圾回收,否抓CMS回收失败,会触发担保机制,Serial Old(老生代)垃圾收集器会暂停所有用户线程,进行GC,会造成用户线程较长的停顿时间。

拓展:-XX:CMSFullGCsBeforeCompaction=次数(默认为0),多少次CMS之后,进行一次压缩的Full GC(由于使用的是标记清除,会又内存碎片产生)

-XX:+UseSerialOldGC:Serial(新生代)+Serial Old(老生代)

新生代和老年代都使用单线程串行执行GC。

JVM参数配置:-Xms20m -Xmx20m -XX:+UseSerialOldGC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails

已被移除,可以通过-XX:+UseSerialGC实现

总结
在这里插入图片描述

GC应用配置场景

在这里插入图片描述

G1垃圾收集器

JVM参数配置:-Xms20m -Xmx20m -XX:+UseG1GC -XX:+PrintCommandLineFlags -XX:+PrintGCDetails

堆分为:garbage-first heap、Metaspace(元空间)

Heap
 garbage-first heap   total 20480K, used 11020K [0x00000000fec00000, 0x00000000fed000a0, 0x0000000100000000)
  region size 1024K, 2 young (2048K), 1 survivors (1024K)
 Metaspace       used 2843K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

总结:在第一段代码中,是否会内存泄漏,答案是会,当test()方法执行完成后,obj对象所分配的内存不会马上被认为是可以被释放的对象,只有在Test类创建的对象被释放后才会被释放,但是对于我们业务代码逻辑来说,这个obj对象在test()方法执行完成后,就已经不再使用了,可以被作为垃圾对象回收。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值