深入理解JVM(三)——垃圾收集策略具体解释

Java虚拟机的内存模型分为五个部分。各自是:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。

这五个区域既然是存储空间,那么为了避免Java虚拟机在执行期间内存存满的情况,就必须得有一个垃圾收集者的角色。不定期地回收一些无效内存,以保障Java虚拟机可以健康地持续执行。

这个垃圾收集者就是寻常我们所说的“垃圾收集器”。那么垃圾收集器在何时清扫内存?清扫哪些数据?这就是接下来我们要解决的问题。

程序计数器、Java虚拟机栈、本地方法栈都是线程私有的,也就是每条线程都拥有这三块区域,并且会随着线程的创建而创建。线程的结束而销毁。那么。垃圾收集器在何时清扫这三块区域的问题就攻克了。

此外,Java虚拟机栈、本地方法栈中的栈帧会随着方法的開始而入栈,方法的结束而出栈。并且每一个栈帧中的本地变量表都是在类被载入的时候就确定的。因此以上三个区域的垃圾收集工作具有确定性,垃圾收集器可以清楚地知道何时清扫这三块区域中的哪些数据。

然而,堆和方法区中的内存清理工作就没那么easy了。

堆和方法区全部线程共享,并且都在JVM启动时创建,一直得执行到JVM停止时。因此它们没办法依据线程的创建而创建、线程的结束而释放。

堆中存放JVM执行期间的全部对象,尽管每一个对象的内存大小在载入该对象所属类的时候就确定了。但到底创建多少个对象仅仅有在程序执行期间才干确定。

方法区中存放类信息、静态成员变量、常量。类的载入是在程序执行过程中,当须要创建这个类的对象时才会载入这个类。因此,JVM到底要载入多少个类也须要在程序执行期间确定。

因此,堆和方法区的内存回收具有不确定性,因此垃圾收集器在回收堆和方法区内存的时候花了一些心思。

堆内存的回收

1. 怎样判定哪些对象须要回收?

在对堆进行对象回收之前,首先要推断哪些是无效对象。我们知道。一个对象不被不论什么对象或变量引用。那么就是无效对象。须要被回收。

一般有两种判别方式:

  • 引用计数法

    每一个对象都有一个计数器,当这个对象被一个变量或还有一个对象引用一次,该计数器加一;若该引用失效则计数器减一。当计数器为0时,就觉得该对象是无效对象。

  • 可达性分析法

    全部和GC Roots直接或间接关联的对象都是有效对象。和GC Roots没有关联的对象就是无效对象。

    GC Roots是指:

    1. Java虚拟机栈所引用的对象(栈帧中局部变量表中引用类型的变量所引用的对象)
    2. 方法区中静态属性引用的对象
    3. 方法区中常量所引用的对象
    4. 本地方法栈所引用的对象

      PS:注意!GC Roots并不包含堆中对象所引用的对象!这样就不会出现循环引用。

两者对照:

引用计数法尽管简单,但存在一个严重的问题,它无法解决循环引用的问题。

因此。眼下主流语言均使用可达性分析方法来推断对象是否有效。

2. 回收无效对象的过程

当JVM筛选出失效的对象之后,并非马上清除,而是再给对象一次重生的机会。详细步骤例如以下:

  1. 推断该对象是否覆盖了finalize()方法

    • 若已覆盖该方法,并该对象的finalize()方法还没有被执行过。那么就会将finalize()扔到F-Queue队列中;
    • 若未覆盖该方法。则直接释放对象内存。
  2. 执行F-Queue队列中的finalize()方法

    虚拟机会以较低的优先级执行这些finalize()方法们。也不会确保全部的finalize()方法都会执行结束。假设finalize()方法中出现耗时操作,虚拟机就直接停止执行,将该对象清除。

  3. 对象重生或死亡

    假设在执行finalize()方法时,将this赋给了某一个引用。那么该对象就重生了。

    假设没有,那么就会被垃圾收集器清除。

注意:

强烈不建议使用finalize()函数进行不论什么操作!

假设须要释放资源,请使用try-finally。

因为finalize()不确定性大,开销大,无法保证顺利执行。

方法区的内存回收

我们知道,假设使用复制算法实现堆的内存回收,堆就会被分为新生代和老年代。新生代中的对象“朝生夕死”,每次垃圾回收都会清除掉大量的对象;而老年代中的对象生命较长。每次垃圾回收仅仅有少量的对象被清除掉。

因为方法区中存放生命周期较长的类信息、常量、静态变量,因此方法区就像是堆的老年代,每次垃圾收集的仅仅有少量的垃圾被清除掉。

方法区中主要清除两种垃圾:

1. 废弃常量

2. 废弃的类

1. 怎样判定废弃常量?

清除废弃的常量和清除对象相似。仅仅要常量池中的常量不被不论什么变量或对象引用,那么这些常量就会被清除掉。

2. 怎样废弃废弃的类?

清除废弃类的条件较为苛刻:

1. 该类的全部对象都已被清除

2. 该类的java.lang.Class对象没有被不论什么对象或变量引用

仅仅要一个类被虚拟机载入进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。

这个对象在类被载入进方法区的时候创建,在方法区中该类被删除时清除。

3. 载入该类的ClassLoader已经被回收

垃圾收集算法

如今我们知道了判定一个对象是无效对象、判定一个类是废弃类、判定一个常量是废弃常量的方法,也就是知道了垃圾收集器会清除哪些数据,那么接下来介绍怎样清除这些数据。

1. 标记-清除算法

首先利用刚才介绍的方法推断须要清除哪些数据,并给它们做上标记。然后清除被标记的数据。

分析:

这样的算法标记和清除过程效率都非常低。并且清除完后存在大量碎片空间。导致无法存储大对象,减少了空间利用率。

2. 复制算法

将内存分成两份,仅仅将数据存储在当中一块上。当须要回收垃圾时,也是首先标记出废弃的数据,然后将实用的数据拷贝到还有一块内存上,最后将第一块内存全部清除。

分析:

这样的算法避免了碎片空间,但内存被缩小了一半。

并且每次都须要将实用的数据全部拷贝到还有一片内存上去,效率不高。

解决空间利用率问题:

在新生代中。因为大量的对象都是“朝生夕死”。也就是一次垃圾收集后仅仅有少量对象存活。因此我们可以将内存划分成三块:Eden、Survior1、Survior2,内存大小各自是8:1:1。

分配内存时,仅仅使用Eden和一块Survior1。

当发现Eden+Survior1的内存即将满时,JVM会发起一次MinorGC,清除掉废弃的对象。并将全部存活下来的对象拷贝到还有一块Survior2中。

那么,接下来就使用Survior2+Eden进行内存分配。

通过这样的方式,仅仅须要浪费10%的内存空间就可以实现带有压缩功能的垃圾收集方法,避免了内存碎片的问题。

可是,当一个对象要申请内存空间时,发现Eden+Survior中剩下的空间无法放置该对象,此时须要进行Minor GC,假设MinorGC过后空暇出来的内存空间仍然无法放置该对象,那么此时就须要将对象转移到老年代中。这样的方式叫做“分配担保”。

什么是分配担保?

当JVM准备为一个对象分配内存空间时,发现此时Eden+Survior中空暇的区域无法装下该对象,那么就会触发MinorGC,对该区域的废弃对象进行回收。

但假设MinorGC过后仅仅有少量对象被回收,仍然无法装下新对象,那么此时须要将Eden+Survior中的全部对象都转移到老年代中,然后再将新对象存入Eden区。

这个过程就是“分配担保”。

3. 标记-整理算法

在回收垃圾前。首先将全部废弃的对象做上标记,然后将全部未被标记的对象移到一边,最后清空还有一边区域就可以。

分析:

它是一种老年代的垃圾收集算法。老年代中的对象一般寿命比較长。因此每次垃圾回收会有大量对象存活,因此假设选用“复制”算法。每次须要复制大量存活的对象,会导致效率非常低。

并且,在新生代中使用“复制”算法,当Eden+Survior中都装不下某个对象时,可以使用老年代的内存进行“分配担保”,而假设在老年代使用该算法。那么在老年代中假设出现Eden+Survior装不下某个对象时。没有其它区域给他作分配担保。因此,老年代中一般使用“标记-整理”算法。

4. 分代收集算法

将内存划分为老年代和新生代。老年代中存放寿命较长的对象,新生代中存放“朝生夕死”的对象。然后在不同的区域使用不同的垃圾收集算法。

Java中引用的种类

Java中依据生命周期的长短,将引用分为4类。

1. 强引用

我们平时所使用的引用就是强引用。

A a = new A();

也就是通过keywordnew创建的对象所关联的引用就是强引用。

仅仅要强引用存在,该对象永远也不会被回收。

2. 软引用

仅仅有当堆即将发生OOM异常时,JVM才会回收软引用所指向的对象。

软引用通过SoftReference类实现。

软引用的生命周期比强引用短一些。

3. 弱引用

仅仅要垃圾收集器执行。软引用所指向的对象就会被回收。

弱引用通过WeakReference类实现。

弱引用的生命周期比软引用短。

4. 虚引用

虚引用也叫幽灵引用,它和没有引用没有差别。无法通过虚引用訪问对象的不论什么属性或函数。

一个对象关联虚引用唯一的作用就是在该对象被垃圾收集器回收之前会受到一条系统通知。

虚引用通过PhantomReference类来实现。

时间: 2024-10-31 22:25:24

深入理解JVM(三)——垃圾收集策略具体解释的相关文章

深入理解JVM内存分配策略

理解JVM内存分配策略 三大原则+担保机制 JVM分配内存机制有三大原则和担保机制 具体如下所示: 优先分配到eden区 大对象,直接进入到老年代 长期存活的对象分配到老年代 空间分配担保 对象优先在Eden上分配 如何验证对象优先在Eden上分配呢,我们进行如下实验. 打印内存分配信息 首先代码如下所示: public class A { public static void main(String[] args) { byte[] b1 = new byte[4*1024*1024]; }

深入理解JVM(二)--垃圾收集算法

一. 概述 说起垃圾收集(Garbage Collection, GC), 大部分人都把这项技术当做Java语言的伴随生产物. 事实上, GC的历史远远比Java久远, 1960年 诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言. 当Lisp还在胚胎时期时,人们就在思考GC需要完成的三件事情: 哪些内存需要回收? 什么时候回收? 如何回收? 现在内存的动态分配与内存回收技术已经相当成熟, 那为什么我们还要去了解GC和内存分配呢? 答案很简单: 当需要排查各种内存溢出, 内

深入理解JVM实战(三)——垃圾收集策略详解

极客头条用户请点击"阅读原文",阅读排版后原文 Java虚拟机的内存模型分为五个部分,分别是:程序计数器.Java虚拟机栈.本地方法栈.堆.方法区. 这五个区域既然是存储空间,那么为了避免Java虚拟机在运行期间内存存满的情况,就必须得有一个垃圾收集者的角色,不定期地回收一些无效内存,以保障Java虚拟机能够健康地持续运行. 这个垃圾收集者就是平常我们所说的"垃圾收集器",那么垃圾收集器在何时清扫内存?清扫哪些数据?这就是接下来我们要解决的问题. 程序计数器.Jav

深入理解JVM:垃圾收集器与内存分配策略

堆里面存放着Java世界几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还存活,哪些已经死去.判断对象的生命周期是否结束有以下几种方法 引用计数法 具体操作是给对象添加一个引用计数器,每当有一个地方引用时,计数器的值就加1,:当引用失效时,计数器就减1:任何时刻计数器为0的对象就 是不可能再被使用的.客观的说引用计数器算法实现简单,判定效率也很高,在大部分情况下他都是一个不错的算法.但是引用计数器有缺陷 举个简单的例子,对象A和对象B都有字段instance,

jvm 05-JVM垃圾收集策略

GC策略 新生代GC策略: 串行GC:Serial Copying 并行回收GC:Parallel Scavenge 并行GC:ParNew 老年代GC策略: 串行GC:Serial MSC 并行GC:Parallel MSC 并发GC:CMC 新生代GC策略 新生代--串行GC(Serial Copying) 算法:复制清理算法 操作步骤: 扫描新生代中所有存活的对象 使用Minor GC进行垃圾回收,同时将存活对象保存到S0 or S1区 在上一次Minor GC的基础上进行S0和S1区的角

JVM内存模型及垃圾收集策略解析

AD: JVM内存模型是Java的核心技术之一,之前51CTO曾为大家介绍过JVM分代垃圾回收策略的基础概念,现在很多编程语言都引入了类似Java JVM的内存模型和垃圾收集器的机制,下面我们将主要针对Java中的JVM内存模型及垃圾收集的具体策略进行综合的分析. 一 JVM内存模型 1.1 Java栈 Java栈是与每一个线程关联的,JVM在创建每一个线程的时候,会分配一定的栈空间给线程.它主要用来存储线程执行过程中的局部变量,方法的返回值,以及方法调用上下文.栈空间随着线程的终止而释放.St

JVM之垃圾收集器 (GC) 与内存分配策略

1.为什么要学习GC? GC (Garbage Collection)早于java出现,60年代出现的Lisp中最早使用了GC. 当需要排查各种内存溢出.内存漏斗问题时,当垃圾回收成为系统达到更高并发量的瓶颈时,就需要用到gc了. 总之,写出高性能的Java程序需要懂GC. 2.GC在JVM的体系结构中的位置 HotSpot JVM体系结构. 和应用性能相关的部分用紫色标出,调优从它们着手! 3.什么是性能? 在对Java应用程序进行调优时,主要关注两点:响应速度和吞吐量. 3.1响应速度 响应

【JVM】垃圾收集器和收集器的选择策略

前言:新生代的收集器有:Serial,ParNew,Parallel Scavenge等.老年代有:CMS,SerialOld,Paraller Old等.接下来将深入理解各个垃圾收集器的原理,以及它们如何在不同场景下进行搭配使用. 同时,先解释几个名次: 并行(Parallel):多个垃圾收集线程并行工作,此时用户线程处于等待状态 并发(Concurrent):用户线程和垃圾收集线程同时执行 吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收时间) (一) 新生代的收集器们 (1) Par

【深入理解JVM】:类加载机制

概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 与那些在编译时需要进行链接工作的语言不同,在Java语言里,类型的加载.连接和初始化过程都是在程序运行期间完成的,例如import java.util.*下面包含很多类,但是,在程序运行的时候,虚拟机只会加载哪些我们程序需要的类.这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性. 类加载的时机 类从