Java基础学习---3、堆、GC

1、堆

1.1 概述
1.1.1 堆空间结构

在这里插入图片描述

1.1.2 堆空间工作机制
  • 新创建的对象会放在Eden区
  • 当Eden区中已使用的空间达到一定比例,会触发Minor GC
  • 每一次在Minor GC中没有被清理掉的对象就成了幸存者。
  • 幸存者对象会被转移到幸存者区
  • 幸存者区分成from区和to区
  • from区快满的时候,会将仍然在使用的对象转移到to区
  • 然后from和to这两个指针彼此交换位置
    口诀:复制必交换,谁空谁为to
  • 如果一个对象,经历15次GC仍然幸存,那么它将会被转移到老年代
  • 如果幸存者区已经满了,即使某个对象尚不到15次,仍然会被移动到老年代
  • 最终效果:
    • Eden区主要是生命周期很短的对象来来往往
    • 老年代主要是生命周期很长的对象。例如:IOC容器对象、线程池对象、数据库连接池对象等等。
    • 幸存者区作为两者之间的过度地带
  • 关于永久代
    • 从理论上来说属于堆
    • 从具体实现上来说不属于堆
1.1.3堆、栈、方法区之间关系

在这里插入图片描述

1.1.4 常驻Web对象存活时间

生产环境下:
ServletContext存活时间:
时间单位:月、年
HttpSession存活时间:
时间单位:分钟、小时
HttpServletRequest存活时间:服务器端接收到请求~服务器提交响应
时间单位:秒或毫秒
HttpServletResponse存活时间:服务器端接收到请求~服务器给客户端返回了响应数据
时间单位:秒或毫秒

2.GC

为什么要有垃圾回收?

  • 线程私有空间:无需由系统来执行GC。因为线程结束,释放自己刚才使用的空间即可,不影响其它线程。

  • 线程共享空间:任何一个线程结束时,都无法确定刚才使用的空间是不是还有别的线程在使用。所以不能因为线程结束而释放空间,必须在系统层面统一垃圾回收。
    GC的基本原则:

    • 频繁收集新生代
    • 较少收集老年代
    • 基本不动元空间
2.1 标记垃圾对象

垃圾对象的标准:不再被引用的对象。下面这两种方法都是要把这样的对象找出来。

1、引用计数法(不采用)
(1)本意

  • 在对象内部记录被引用次数
  • 被引用一次,计数器+1
  • 引用解除一个,计数器-1
  • 计数器归零则表示该对象变成垃圾

(2)问题
循环引用问题,会导致计数器无法归零。

2、GC Roots可达性分析
核心原理:判断一个对象,是否存在从堆外到堆内的引用

因为我们写Java代码时,是不可能直接访问到堆内对象的,要么是通过方法里面局部变量,要么通过static修饰的常量、类变量。局部变量、类变量、常量这些都在堆外。

3、GC Root对象
GC Root对象:就是作为根节点出发,顺着引用路径一直查找到堆空间内,找到堆空间中的对象。

  • 虚拟机栈(Java Stack栈帧中的局部变量区,也叫局部变量表)中引用的对象
  • 本地方法栈(Native Method Stack)中的局部变量
  • 方法区中的类变量、常量引用的对象(说白了就是用static修饰的成员变量指向的对象)
2.2 垃圾回收算法

1、回收范围
在这里插入图片描述
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。

  • minor GC:只针对新生代区域的GC,指发生在新生代的垃圾收集动作。
    因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。

  • major

    • 对老年代的垃圾回收
    • Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
    • 如果Major GC后,内存还不足,就报OOM了。
  • full GC:清理范围包括新生代、老年代和方法区,非常慢。

2、GC年龄
新生代的对象每经历一次GC,只要它还活着,GC年龄就会+1.当GC年龄达到15的时候,该对象就会转移到老年代。
对象在新生代的最大GC年龄可以设置:

-XX:MaxTenuringThreshold

从JDK8开始,64位虚拟机的最大GC年龄不能超过15。

2.2.1 基本算法

1、引用计数法(不采用)
优点:

  • 实时性较高,不需要等到内存不够时才回收
  • 垃圾回收时不用挂起整个程序,不影响程序正常运行

缺点:

  • 回收时不移动对象,所以会造成内存碎片问题
  • 不能解决对象间的循环引用问题(致命问题,一票否决)

小结:
正是由于引用计数法不能解决对象间的循环引用问题,所以事实上并没有哪一款JVM产品采用这个机制。

2、标记清除法
它的做法是当堆中的有效内存空间被耗尽时,就会暂停、挂起整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。

  • 标记:标记的过程其实就是从根对象开始变量所以得对象,然后将所有存活的对象标记为可达对象。
  • 清除:清除的过程中将遍历堆中的所有对象,将没有标记的对象全部清除掉。
    小结:
  • 优点:实现简单
  • 缺点:
    • 效率低,因为标记和清除两个动作都有遍历所有的对象
    • 垃圾收集后可能会造成大量·的内存碎片
    • 垃圾回收时会造成应用程序暂停

在这里插入图片描述
3、标记压缩法
既然教标记压缩算法,那么它也分为两个阶段,一个时标记(mark),一个时压缩(compact)。所谓压缩就是把存在碎片的空间连起来。
标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象移动到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。

  • 标记:标记的过程其实就是从根对象开始遍历所以对象,然后将所有存活的对象标记为可达的对象。
  • 压缩:移动所以的可达对象到堆内存的同一个区域中,使它们紧凑的排列在一起,从而将所有非可达对象释放出来的空闲内存都集中在一起,通过这样的方式来达到减少内存碎片的目的。

小结

  • 优点:标记压缩算法是对标记清除算法的优化,解决了碎片化的问题
  • 缺点:还是效率问题,在标记清除算法上又多加了一步,效率就更低了

在这里插入图片描述

4、复制算法
复制算法的核心就是将原有的内存空间一分为二,每次只用其中的一块,在进行垃圾回收时,将正在使用的对象复制到另一个内存空间中,并依次排列,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。

小结:

  • 优点:
    • 在垃圾多的情况下(新生代),效率比较高
    • 清理后,内存无碎片
  • 缺点:
    • 浪费了一般的内存空间,在存活对象比较多的情况下(老年代),效率较差。极端情况下,如果假设所有对象存活,那么需要复制全部对象且重置全部引用地址。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.2.2 综合算法

1、分代算法
前面介绍了多种回收算法,每一种算法都有自己的优点也有缺点,谁都不能替代谁,所以根据垃圾回收对象的特点进行选择,才是明智的。

分代算法其实就是这样的,根据回收对象的特点进行选择。

新生代适合使用复制算法
老年代适合使用标记清除或标记压缩算法

2、分区算法
上面介绍的分代收集算法是将对象的生命周期按长短划分为两个部分,而分区算法则将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收。这样做的好处是可以控制一次回收多少个小区间。在相同条件下,堆空间越大。一次GC耗时就越长,从而产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割为多个小块,根据目标停顿时间每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿。

2.3 垃圾回收器

垃圾回收器没有在规范中进行过多的规定,可以由不同厂商、不同版本的JVM来各自实现。由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的垃圾回收器产品。从不同角度分析垃圾回收器,可以将GC分为不同的类型。

在JVM中,垃圾回收器可以按照并行和串行两种方式工作。

串行垃圾回收器一次只能使用一个线程进行垃圾回收,也就是在垃圾回收期间,JVM将停止应用程序的执行,直到垃圾回收完成为止。这种回收器适用于单处理器环境下或者是在较小的堆内存中使用。

并行垃圾回收器会使用多个线程来加快垃圾回收的速度。这种回收器通常比串行垃圾回收器速度更快,但需要更多的CPU和内存资源。在垃圾回收期间,应用程序可能需要被暂停,但停顿时间比串行垃圾回收器要短。

总的来说,串行垃圾回收器适用于小型应用并且操作系统资源有限的情况下,而并行垃圾回收器适用于大型应用和高性能服务器,当然这也要看具体应用场景的需求。

2.3.1串行垃圾回收器

串行:在一个线程内执行垃圾回收操作

新生代串行回收器 SerialGC:采用复制算法实现,单线程垃圾回收,独占式垃圾回收器。

新生代串行回收器 SerialGC:采用复制算法实现,单线程垃圾回收,独占式垃圾回。

2.3.2并行垃圾回收器

并行:在多个线程中执行垃圾回收操作。
新生代 ParNew 回收器:采用复制算法实现,多线程回收器,独占式垃圾回收器。
新生代 ParallelScavengeGC 回收器:采用复制算法多线程独占式回收器。
新生代 ParNew 回收器:采用复制算法实现,多线程回收器,独占式垃圾回收器。

  • CMS回收器
    CMS全称 (Concurrent Mark Sweep),是一款并发的、使用标记-清除算法的垃圾回收器。对CPU资源非常敏感。
    启用CMS回收器参数 :-XX:+UseConcMarkSweepGC。
    使用场景:GC过程短暂停顿,适合对时延要求较高的服务,用户线程不允许长时间的停顿。
    优点:最短回收停顿时间为目标的收集器。并发收集,低停顿。
    缺点:服务长时间运行,造成严重的内存碎片化。算法实现比较复杂。
  • G1回收器
    G1(Garbage-First)是一款面向服务端应用的并发垃圾回收器, 主要目标用于配备多颗CPU的服务器,治理大内存。是JDK1.7提供的一个新收集器,是当今收集器技术发展的最前沿成果之一。
    G1计划是并发标记-清除收集器的长期替代品。
    启用G1收集器参数:-XX:+UseG1GC启用G1收集器。
    G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合。
2.3.3垃圾回收器对比

1、新生代回收器

名称串行/并行/并发回收算法
SerialGC串行复制
ParNew并行复制
ParallelScavengeGC并行复制
(1)SerialGC
SerialGC是JVM中最基本的垃圾回收器,也是串行垃圾回收器的一种实现。SerialGC在进行垃圾回收时只会使用一个线程,因此它的回收效率不高,但可以保证垃圾回收过程不会影响应用程序的并发执行。

SerialGC主要用于小型应用程序或者只使用单核CPU的环境下,它的优点是实现简单、消耗内存较少,缺点是效率较低,在大型应用程序或需要高性能的环境中表现不佳。在JDK9之前,它是默认的垃圾回收器。但在JDK9以后,G1成为了默认垃圾回收器。

由于SerialGC无法满足大型应用程序的需求,因此在多核CPU环境下,通常会采用并发垃圾回收器或者并行垃圾回收器来提高垃圾回收效率。

(2)ParNewGC
ParNewGC是JVM中支持多线程并行垃圾回收的垃圾回收器之一,它可以与CMS(Concurrent Mark Sweep)垃圾回收器配合使用,提高垃圾回收效率。

与SerialGC不同,ParNewGC可以使用多个线程来并发执行垃圾回收,从而充分利用多核CPU的优势。同时,ParNewGC还具有一定的并发能力,可以允许应用程序在垃圾回收过程中继续执行。

ParNewGC主要用于需要高性能的大型Java应用程序中,它的优点是能够充分利用CPU资源,加速垃圾回收过程,缺点是可能会增加一定的内存消耗,因为需要使用多个线程来执行垃圾回收。

需要注意的是,ParNewGC只能与CMS垃圾回收器结合使用,由于CMS垃圾回收器会解决的问题,ParNewGC并不能完全解决,因此在使用时需要进行适当的配置和调优。

(3)ParallelScavengeGC
ParallelScavengeGC是Java Virtual Machine (JVM) 默认的垃圾回收器之一,它属于并行垃圾回收器的一种。它的主要特点是在垃圾回收处理期间,会同时通过多个线程来处理垃圾回收,从而提高回收效率。

ParallelScavengeGC是一个适用于多核处理器的垃圾回收器,在大量并发系统中得到了广泛的应用。ParallelScavengeGC的主要任务是尽可能地减少应用程序的停顿时间,并且尽可能地提高垃圾回收的效率,对于需要高吞吐量的应用程序而言是一个非常好的选择。

ParallelScavengeGC将整个堆内存分成多个大小相等的区域(即各个Eden区和Survivor区),各个区域之间是等大小的,它通过多个线程同时回收各个区域中的垃圾,从而保证了垃圾回收的高速执行。同时,它还使用了一种“分代”概念,将堆内存划分为年轻代和老年代,以便更好地处理这些区域内不同生命周期的对象。

总之,ParallelScavengeGC是一种高吞吐量、低延迟的垃圾回收器,它适用于需要快速处理大量垃圾的并发应用程序,同时还能够提高Java程序的性能和可扩展性。

2、老年代回收期

名称串行/并行/并发回收算法
SerialOldGC串行标记压缩算法
ParNewOld并行标记压缩算法
CMS并行,几乎不会暂停用户线程标记压缩算法
(1)SerialOldGC
Serial Old GC是Java虚拟机中的一个垃圾回收器,主要用于回收Java堆中的老年代空间。与其配对使用的新生代垃圾回收器是Serial GC,两者共同组成了Java虚拟机中的默认垃圾回收器组合。

Serial Old GC采用的是“标记-整理”(Mark-Compact)的算法,它首先标记需要回收的内存块,然后将所有存活的对象向一端移动,最终在堆的另一端清理出空闲空间。由于Serial Old GC是串行回收器,因此它只能使用单个线程进行垃圾回收操作。

Serial Old GC的优点是简单可靠,由于采用串行回收的方式,可以消除线程同步的开销和复杂度,因此在小型应用程序和较老的硬件上性能表现较好。然而,由于其无法并发执行,因此在大型应用程序和现代硬件上的性能表现较为糟糕,会导致较长的应用程序暂停时间。

总的来说,Serial Old GC是一种适用于小型Java应用程序和较老硬件的垃圾回收器,在性能表现方面有一定局限性。
(2)ParNewOldGC
ParNewOldGC是Java虚拟机中的一种垃圾回收器组合,由ParNew GC和CMS GC两部分组成。

ParNew GC是一个多线程的垃圾回收器,主要用于回收Java堆中的新生代空间。它采用的是“标记-复制”(Mark-Copy)算法,首先标记需要回收的内存块,然后将存活的对象复制到另外一个空间中,并清理原有内存空间。由于ParNew GC是多线程回收器,可以并行执行垃圾回收操作,因此它能够充分利用多处理器系统和多核心CPU的优势,提高垃圾回收的效率。

CMS GC(Concurrent Mark Sweep GC)是一种并发的垃圾回收器,主要用于回收Java堆中的老年代空间。它采用“标记-清除”(Mark-Sweep)的算法,因此它能够在应用程序运行的同时执行垃圾回收操作,减少应用程序暂停的时间。不过,CMS GC回收老年代内存需要牺牲部分应用程序运行时间,因为它需要暂停应用程序执行一段时间来完成标记和清理操作。

ParNewOldGC将ParNew GC和CMS GC组合起来,可以充分利用多线程和并发执行的优势,减少Java堆内存回收过程中的延迟时间和应用程序暂停时间,同时保证系统吞吐量和性能。它适用于多核心CPU和高并发场景下的Java应用程序。
(3)Concurrent Mark Sweep(CMS)

Concurrent Mark Sweep(CMS)是一种针对大型Java应用程序的垃圾回收器。它使用多个线程来在垃圾回收过程中并发执行标记和清除操作,从而最大程度地减少应用程序的停顿时间。

CMS垃圾回收器采用了一种“标记-清除”(Mark-Sweep)的算法,它通过标记需要回收的内存块,并在回收时进行清除操作。CMS垃圾回收器在标记和清除操作中采用了一些优化措施,例如:使用“写屏障”和“增量标记”等技术来避免在标记和清除过程中出现大量的停顿时间。

由于CMS垃圾回收器是一种并发垃圾回收器,因此它可以在应用程序运行过程中进行垃圾回收操作,大大减少了应用程序的停顿时间。但是,CMS垃圾回收器也存在一些缺点,例如:由于在并发垃圾回收过程中需要保证内存一致性,它可能会导致一些内存资源的浪费和系统的性能下降。

总的来说,CMS垃圾回收器主要适用于大型Java应用程序,它可以通过优化标记和清除过程、减少停顿时间等方式提高系统的性能和吞吐量。

2.3.4查看当前JVM的垃圾回收器

1、Java程序启动时添加以下JVM参数,可以在控制台输出JVM的GC信息:

-XX:+PrintGC

2、该参数只会输出简单的GC日志信息,如果需要输出详细的日志信息,可以使用以下JVM参数:

-XX:+PrintGCDetails

3、在输出的日志信息中查找以下关键字,即可确定当前JVM使用的具体垃圾回收器:

  • PSYoungGen 和 ParNew 表示使用的是Parallel Scavenge收集器
  • PSOldGen 和 ConcurrentMarkSweep 表示使用的是Parallel Old收集器
  • G1 Young Generation 和 G1 Old Generation 表示使用的是Garbage-First收集器
[Full GC (Ergonomics) [PSYoungGen: 128K->0K(8192K)] [ParOldGen: 0K->14041K(17408K)] 128K->14041K(25600K), [Metaspace: 2577K->2577K(1056768K)], 0.0129332 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2.4 finalize机制

1、总体介绍
java.lang.Object 类中有一个方法:

protected void finalize() throws Throwable { }

方法体是空的,说明如果子类不重写这个方法,那么不执行任何逻辑。
在这里插入图片描述

  • 在执行GC操作之前,调用finalize()方法的是Finalizer线程,这个线程的优先级很低。
  • 在对象的整个生命周期过程中,finalize()方法只会被调用一次。
    2、代码验证
public class FinalizeTest {

    // 静态变量
    public static FinalizeTest testObj;

    @Override
    protected void finalize() throws Throwable {
        // 重写 finalize() 方法
        System.out.println(Thread.currentThread().getName() + " is working");

        // 给待回收的对象(this)重新建立引用
        testObj = this;
    }

    public static void main(String[] args) {

        // 1、创建 FinalizeTest 对象
        FinalizeTest testObj = new FinalizeTest();

        // 2、取消引用
        testObj = null;

        // 3、执行 GC 操作
        System.gc();

        // ※ 让主线程等待一会儿,以便调用 finalize() 的线程能够执行
        try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {}

        // 4、判断待回收的对象是否存在
        if (FinalizeTest.testObj == null) {
            System.out.println("待回收的对象没有获救,还是要被 GC 清理");
        } else {
            System.out.println("待回收的对象被成功解救");
        }

        // 5、再次取消引用
        FinalizeTest.testObj = null;

        // 6、再次执行 GC 操作
        System.gc();

        // 7、判断待回收的对象是否存在
        if (FinalizeTest.testObj == null) {
            System.out.println("待回收的对象没有获救,还是要被 GC 清理");
        } else {
            System.out.println("待回收的对象被成功解救");
        }
    }

}

执行效果:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星光下的赶路人star

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值