Java jvm垃圾回收详解

目录
  • 常见面试题
  • 1.JVM内存回收和分配
    • 1.1主要的区域?
      • gc测试
    • 1.2大对象进入老年代
    • 1.3长期存活的对象进入老年代
    • 1.4主要进行gc的区域
      • gc的类型
      • Young Gc
      • Full Gc
    • 1.5空间分配担保?
  • 2.对象已经死亡?
    • 2.1引用计数法
    • 2.2可达性分析
    • 2.3再谈引用
      • 虚引用、软引用、弱引用的区别?
    • 2.4不可达对象不一定回收
    • 2.5如何判断一个常量是废弃常量?
    • 2.6如果判断一个类没有用?
  • 3.垃圾回收算法
    • hotspot为什么要区分老年代和新生代?
    • 跨代收集假说?
  • 4.垃圾回收器
    • 4.1Serial收集器
    • 4.2ParNew收集器
    • 4.3Parallel Scavenge收集器
    • 4.4SerialOld
    • 4.5Parallel Old收集器
    • 4.6CMS收集器
      • 问题?
    • 4.7G1收集器
    • 补充字符串池的本质
      • 第一个问题是String a="a"的时候做了什么?
      • 第二个问题new String(“a”)发生了什么?
      • 第三个问题intern的原理?
      • 第四个问题
    • finalize的原理
  • 总结

常见面试题

  • 如何判断对象是否死亡
  • 简单介绍一下强引用、软引用、弱引用、虚引用
  • 如何判断常量是一个废弃常量
  • 如何判断类是一个无用类
  • 垃圾收集有哪些算法、各自的特点?
  • 常见的垃圾回收器有哪些?
  • 介绍一下CMS,G1收集器?
  • minor gc和full gc有什么不同呢?

1.JVM内存回收和分配

1.1主要的区域?

  • 在伊甸区先产生对象
  • 然后发生一次gc之后去到幸存区幸存区
  • 如果年龄大于阈值那么就会升级到老年代

阈值的计算

如果某个年龄段的大小大于幸存区的一半,那么就取阈值或者是这个年龄最小的那个作为新的阈值升级到老年代

  • gc的时候是幸存区的from和伊甸区的存活对象复制到to,然后再清理其它的对象,接着from和to就会交换指针

gc测试

场景就是先给eden分配足量的空间,然后再申请大量空间,问题就是幸存区的空间不够用

  • 那么这个时候就会触发分配担保机制,把多余的对象分配到老年代,而不会触发full gc。仍然还是monor gc
public class GCTest {
    public static void main(String[] args) {
        byte[] allocation1, allocation2;
        allocation1 = new byte[50900*1024];
        allocation2 = new byte[9500*1024];
    }
}

1.2大对象进入老年代

  • 防止在标记复制的时候占用大量的时间,降低gc的效率

1.3长期存活的对象进入老年代

  • 每次gc都会把eden和from的存活对象放到to,每次gc存活年龄就会+1,如果超过阈值那么就能够升级到老年代,设置的参数是-XX:MaxTenuringThreshold
  • 下面是计算的方式,每个年龄的人数累加,累加一个就+1,如果对象数量大于幸存区的一半的时候就需要更新阈值(新计算的age和MaxTenuringThreshold)
  • 通常晋升阈值是15,但是CMS是6
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
    //survivor_capacity是survivor空间的大小
    size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100);
    size_t total = 0;
    uint age = 1;
    while (age < table_size) {
        //sizes数组是每个年龄段对象大小
        total += sizes[age];
        if (total > desired_survivor_size) {
            break;
        }
        age++;
    }
    uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
    ...
}

1.4主要进行gc的区域

gc的类型

  • Partial Gc

Young Gc:收集新生代的

Old Gc:只收集老年代的

Mixed Gc:新生代和部分老年代

  • Full Gc:新生代,老年代都会收集

Young Gc

  • 每次都是收集新生代的,并且晋升那些存活久的

Full Gc

  • 如果发现幸存区要晋升的对象内存空间比老年代内存空间更大那么就进行full Gc。有的虚拟机会先进行young gc来清理掉一些,减少full gc的时间消耗

1.5空间分配担保?

  • jdk1.6之前需要判断老年代剩余的空间是不是完全大于新生代的空间,如果是那么才能进行minorgc保证不会出现问题。如果是不行就会去检查-XX:handlePromotionFailure也就是晋升的对象平均大小是不是小于老年代剩余空间,如果是那么就直接minor gc否则就full gc
  • jdk1.6之后直接检查新生代晋升平均大小如果小于老年代那么就会直接晋升

2.对象已经死亡?

2.1引用计数法

  • 其实就是每次被引用那么计数+1,如果计数不是0那么就不会被回收
  • 但是不使用的原因就是循环引用依赖,如果两个对象互相引用就会导致计数永远不会为0

2.2可达性分析

  • Gc roots作为起点一直往下面的一条引用链

gc Roots的对象

  • 虚拟机栈引用的对象(栈的本地局部变量表)
  • 本地方法栈引用的对象
  • 方法区常量引用的对象(常量池引用的对象)
  • 方法区静态属性引用的对象
  • 被同步锁持有的对象
  • java虚拟机内部引用,比如Integer这些基本类型的

2.3再谈引用

  • 强引用:垃圾回收器不会对他进行回收
  • 软引用:内存空间不足会回收
  • 弱引用:gc就回收
  • 虚引用:随时会被回收而且需要引用队列

虚引用、软引用、弱引用的区别?

  • 虚引用的对象在gc之前会被送到引用队列,并且程序在对象回收之前做相应的活动(临死之前的处理)
  • 软引用是用的最多的,可以提高gc的效率,维护系统安全,防止内存溢出

2.4不可达对象不一定回收

  • 在回收之前会对对象进行一次标记,看是否会执行finalize方法。如果没有那么这些对象将会先被回收
  • 如果有那么进行第二次标记,让对象执行finalize之后再进行回收

2.5如何判断一个常量是废弃常量?

  • 如果常量池对象没有被任何对象引用就会被回收
  • jdk1.7之前运行时常量池包含字符串常量池,需要进行复制来返回新的引用(堆有一个,常量池有一个)
  • jdk1.7的时候字符串池已经不在运行时常量池,如果调用intern就会把当前对象放入常量池并且返回引用(只有常量池有一个)。如果本来就存在就会返回对象实例的地址。
  • jdk1.8之后运行时常量池已经转移到了元空间

2.6如果判断一个类没有用?

  • 类的实例都回收了
  • 类的类加载器回收了
  • 类信息没有被引用
  • 大量的反射和动态代理生成类信息会对方法区产生很大的压力

3.垃圾回收算法

hotspot为什么要区分老年代和新生代?

原因就是不同的存活对象需要不同的垃圾回收算法

  • 如果新生代用的是标记整理,问题就是每次清除大量的对象,移动时间很长,整理消耗很大。但是标记复制就很快,因为存活对象少
  • 但是老年代如果使用标记整理就很好,因为存活多移动少,复制就相反
  • 不能够统一设计为弱分代假说和强分代假说

跨代收集假说?

如果老年代和新生代互相引用,新生代的年龄就会被拉长。但是为了知道新生代什么时候被gc,这个时候可以给新生代加上一个记忆集(把老年代划分为很多个格子,代表谁引用了我),避免扫描整个老年代

4.垃圾回收器

4.1Serial收集器

  • 单线程收集器,每次都要阻塞其它线程(STW),一个垃圾线程单独回收
  • 新生代是标记复制,老年代是标记整理
  • 它简单高效,没有和其它线程交换不会产生并发问题
  • 但是STW会导致响应很慢

4.2ParNew收集器

  • Serial的多线程版本,但是还是会STW
  • 新生代是标记复制,老年代是标记整理

4.3Parallel Scavenge收集器

  • 新生代是标记复制,老年代是标记整理
  • 和ParNew不同的地方就是它完全关注cpu的利用率,也就是处理任务的吞吐量,而不会管STW到底停多久

4.4SerialOld

  • Serial的老年代版本,1.5以前和Parallel Scavenge一起使用,还有别的用途就是CMS的后备方案

4.5Parallel Old收集器

  • Parallel Scavenge收集器的老年代也是注重吞吐量

4.6CMS收集器

  • 注重最小响应时间
  • 垃圾收集器和用户线程同时工作
  • 初始标记记录gc root直接相连的对象
  • 并发标记遍历整个链,但是可以和用户线程并发运行
  • 重新标记修正那些更新的对象的引用链,比并发标记短
  • 并发清除

问题?

内存碎片多对cpu资源敏感

4.7G1收集器

同时满足响应快处理多的问题

特点

  • 并行和并发,使用多个cpu执行gc线程来缩短stw,而且还能与java线程并发执行
  • 分代收集
  • 空间整合:大部分时候使用标记复制
  • 可预测停顿:响应时间快,可以设置stw时间
  • 分区之间的跨代引用,young这里使用了rset(非收集区指向收集区)记录,老年代那个区域指向了我,老年代使用了卡表划分了很多个区域,那么minor gc的时候就不需要遍历整个其它所有区域去看看当前的区域的对象到底有没有被引用。

补充字符串池的本质

第一个问题是String a="a"的时候做了什么?

  • 先去找常量池是否存在a如果存在那么就直接返回常量池的引用地址返回,如果不存在那么就创建一个在常量池然后再返回引用地址

第二个问题new String(“a”)发生了什么?

  • 先看看常量池是否存在a,如果不存在创建一个在常量池,而且在堆单独创建一个a对象返回引用(而不是返回常量池的),相当于就是创建了两次。
  • 如果第二次创建发现已经存在就直接在堆中创建对象。

第三个问题intern的原理?

  • 看看常量池有没有这个字符串,没有就创建并返回常量池对象的地址引用
  • 如果有那么直接返回常量池对象的地址引用

String s1=new String(“a”)

String s2=s1.intern();

很明显s1不等于s2如果上面的问题都清晰知道。s1引用的是堆,而s2引用的是常量池的

第四个问题

String s3=new String(“1”)+new String(“1”);

String s5=s3.intern();

String s4=“11”

那么地方他们相等吗?当然是相等的,s3会把1存入常量池,但是不会吧11存入常量池因为,还没编译出来。调用了intern之后才会把对象存入常量池,而这个时候存入的对象就是s3指向的那个。所以s4指向的也是s3的。如果是s0="11"的话那就不一样了,s3.intern只会返回常量池的对象引用地址,而不是s3的,因为s3是不能重复intern 11进去的。jdk1.6的话那么无论怎么样都是错的,intern是复制一份,而不是把对象存入常量池(因为字符串常量池在方法区,而jdk1.7它在堆所以可以很好的保存s3的引用)

下面的代码正确分析应该是三个true,但是在test里面就会先缓存了11导致false, true,false的问题。

@Test
public void test4(){
    String s3 = new String("1") + new String("1");
    String s5 = s3.intern();
    String s4 = "11";
    System.out.println(s5 == s3);
    System.out.println(s5 == s4);
    System.out.println(s3 == s4);
    System.out.println("======================");
    String s6 = new String("go") +new String("od");
    String s7 = s6.intern();
    String s8 = "good";
    System.out.println(s6 == s7);
    System.out.println(s7 == s8);
    System.out.println(s6 == s8);
}

finalize的原理

  • 其实就是对象重写了finalize,那么第一次gc的时候如果发现有finalize,就会把对象带到F-Queue上面等待,执行finalize方法进行自救,下面就是一个自救过程,new了一个GCTest对象,这个时候test不引用了,那么正常来说这个GCTest就会被回收,但是它触发了finalize的方法,最后再次在finalize中使用test引用它所以对象没有被消除
  • 但是finalize是一个守护线程,防止有的finalize是个循环等待方法阻塞整个队列,影响回收效率
  • 最后一次标记就是在F-queue里面标记这个对象(如果没有引用)然后释放
  • finalize实际上是放到了Finalizer线程上实现。然后然引用队列指向这个双向链表,一旦遇到gc,那么就会调用ReferenceHandler来处理这些节点的finalize调用,调用之后断开节点,节点就会被回收了
  • finalize上锁导致执行很慢
public class GCTest {
    static GCTest test;
    public void isAlive(){
        System.out.println("我还活着");
    }
    @Override
    protected void finalize() throws Throwable {
        System.out.println("我要死了");
        test=this;
    }
    public static void main(String[] args) throws InterruptedException {
       test = new GCTest();
        test=null;
        System.gc();
        Thread.sleep(500);
        if(test!=null){
            test.isAlive();
        }else{
            System.out.println("死了");
        }
        test=null;
        System.gc();
        if(test!=null){
            test.isAlive();
        }else{
            System.out.println("死了");
        }
    }
}

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • 详解Java内存管理中的JVM垃圾回收

    一.概述 相比起C和C++的自己回收内存,JAVA要方便得多,因为JVM会为我们自动分配内存以及回收内存. 在之前的JVM 之内存管理 中,我们介绍了JVM内存管理的几个区域,其中程序计数器以及虚拟机栈是线程私有的,随线程而灭,故而它是不用考虑垃圾回收的,因为线程结束其内存空间即释放. 而JAVA堆和方法区则不一样,JAVA堆和方法区时存放的是对象的实例信息以及对象的其他信息,这部分是垃圾回收的主要地点. 二.JAVA堆垃圾回收 垃圾回收主要考虑的问题有两个:一个是效率问题,一个是空间碎片问题.

  • JVM垃圾回收原理解析

    概述 Java运行时区域中,程序计数器,虚拟机栈,本地方法栈三个区域随着线程的而生,随线程而死,这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收问题.而Java堆和方法区则不一样,一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存可能也不一眼,我们只有在运行期,才能知道会创建的对象,这部分的内存分配和回收,是垃圾回收器所关注的.垃圾回收器需要完成三个问题:那些内存需要回收:什么时候回收以及如何回收. 那些垃圾需要回收 垃圾回收的基本思想是考察一个对象的可达性,即从根节点

  • 快速理解Java垃圾回收和jvm中的stw

    Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外).Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互:这些现象多半是由于gc引起. GC时的Stop the World(STW)是大家最大的敌人.但可能很多人还不清楚,除了GC,JVM下还会发生停顿现象. JVM里有一条特殊的线程--VM Threads,专门用来执行一些特殊的VM Operation

  • JVM的垃圾回收算法工作原理详解

    怎么判断对象是否可以被回收? 共有2种方法,引用计数法和可达性分析 1.引用计数法 所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一.当一个对象的引用计数器为零时,说明此对象没有被引用,也就是"死对象",将会被垃圾回收. 引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法

  • 从JVM的内存管理角度分析Java的GC垃圾回收机制

    一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率 ,才能提高整个应用程序的性能.本篇文章首先简单介绍GC的工作原理之后,然后再对GC的几个关键问题进行深入探讨,最后提出一些Java程序设计建议,从GC角度提高Java程序的性能.     GC的基本原理     Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放.     对于程序员来说,分配对象使用

  • JVM垃圾回收算法的概念与分析

    前言 在JVM内存模型中会将堆内存划分新生代.老年代两个区域,两块区域的主要区别在于新生代存放存活时间较短的对象,老年代存放存活时间较久的对象,除了存活时间不同外,还有垃圾回收策略的不同,在JVM中中有以下回收算法: 标记清除 标记整理 复制算法 分代收集算法 有了垃圾回收算法,那JVM是如果确定对象是垃圾对象的呢?判断对象是否存活JVM也会有几套自己判断算法了: 引用记数 可达性分析 有了垃圾回收和判断对象存在这两个概念后,再来逐步分析它们. JVM是如何判断对象是否存活的? 要是让开发人员来

  • Java jvm垃圾回收详解

    目录 常见面试题 1.JVM内存回收和分配 1.1主要的区域? gc测试 1.2大对象进入老年代 1.3长期存活的对象进入老年代 1.4主要进行gc的区域 gc的类型 Young Gc Full Gc 1.5空间分配担保? 2.对象已经死亡? 2.1引用计数法 2.2可达性分析 2.3再谈引用 虚引用.软引用.弱引用的区别? 2.4不可达对象不一定回收 2.5如何判断一个常量是废弃常量? 2.6如果判断一个类没有用? 3.垃圾回收算法 hotspot为什么要区分老年代和新生代? 跨代收集假说?

  • GC参考手册jvm垃圾回收详解

    1,什么是垃圾回收? 顾名思义,垃圾收集(Garbage Collection)的意思就是 —— 找到垃圾并进行清理.但现有的垃圾收集实现却恰恰相反: 垃圾收集器跟踪所有正在使用的对象,并把其余部分当做垃圾 我们不抠细节, 先从基础开始, 介绍垃圾收集的一般特征.核心概念以及实现算法. 2,手动内存管理(Manual Memory Management) 当今的自动垃圾收集算法极为先进, 但我们先来看看什么是手动内存管理.在那个时候, 如果要存储共享数据, 必须显式地进行 内存分配(alloca

  • 基于Python对象引用、可变性和垃圾回收详解

    变量不是盒子 在示例所示的交互式控制台中,无法使用"变量是盒子"做解释.图说明了在 Python 中为什么不能使用盒子比喻,而便利贴则指出了变量的正确工作方式. 变量 a 和 b 引用同一个列表,而不是那个列表的副本 >>> a = [1, 2, 3] >>> b = a >>> a.append(4) >>> b [1, 2, 3, 4] 如果把变量想象为盒子,那么无法解释 Python 中的赋值:应该把变量视作

  • Java JVM内存区域详解

    目录 程序计数器 Java虚拟机栈 方法/函数如何调用? 堆 总结 原网页:JavaGuide JVM在执行Java程序过程中会把它管理的内存划分成若干个不同的数据区域.JDK1.8和之前的版本略有不同,下面会介绍到. JDK1.8之前: JDK1.8之后: 这其中线程私有的: 虚拟机栈: 程序计数器: 本地方法栈: 线程共享的: 堆: 方法区: 直接内存(非运行时数据区的一部分) 程序计数器 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器.字节码解释器工作时通过改

  • jvm垃圾回收之GC调优工具分析详解

    进行GC性能调优时, 需要明确了解, 当前的GC行为对系统和用户有多大的影响.有多种监控GC的工具和方法, 本章将逐一介绍常用的工具. JVM 在程序执行的过程中, 提供了GC行为的原生数据.那么, 我们就可以利用这些原生数据来生成各种报告.原生数据(raw data) 包括: 各个内存池的当前使用情况, 各个内存池的总容量, 每次GC暂停的持续时间, GC暂停在各个阶段的持续时间. 可以通过这些数据算出各种指标, 例如: 程序的内存分配率, 提升率等等.本章主要介绍如何获取原生数据. 后续的章

  • 最新JVM垃圾回收算法详解

    目录 1.垃圾回收需要做什么 2.如何判断对象可被回收 2.1 引用计数算法 2.1.2 优点 2.1.2 缺点 2.2 可达性分析算法 2.2.1 算法思路 2.2.2 GC Roots对象(两栈两方法) 2.2.3 优点 2.2.4 缺点 3.判断对象生存还是死亡 3.1 两次标记过程 3.2 finalize()方法 4.HotSpot虚拟机中对象可达性分析的实现 4.1 枚举根节点 4.2 安全点 4.2.1 安全点是什么,为什么需要安全点 4.2.2 安全点的选定 4.2.3 如何在安

  • java虚拟机之JVM调优详解

    JVM常用命令行参数 1. 查看参数列表 虚拟机参数分为基本和扩展两类,在命令行中输入 JAVA_HOME\bin\java就可得到基本参数列表. 在命令行输入 JAVA_HOME\bin\java –X就可得到扩展参数列表. 2. 基本参数说明: -client,-server: 两种Java虚拟机启动方式,client模式启动比较快,但是性能和内存管理相对较差,server模式启动比较慢,但是运行性能比较高,windos上采用的是client模式,Linux采用server模式 -class

  • Java中final关键字详解及实例

    final在Java中可以声明成员变量.方法.类以及本地变量.一旦你将引用声明作final,你将不能改变这个引用了,如果你试图将变量再次初始化的话,编译器会报编译错误.  final的含义在不同的场景下有细微的差别,但总体来说,它指的是"不可变". 1. final变量 凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为final的都叫作final变量.final变量经常和static关键字一起使用,作为常量.用final关键字修饰的变量,只能进行一次赋值操作

  • Java中的ThreadLocal详解

    目录 一.ThreadLocal简介 二.ThreadLocal简单使用 三.ThreadLocal的实现原理 1.set方法源码 2.get方法源码 3.remove方法的实现 四.ThreadLocal不支持继承性 五.InheritableThreadLocal类 六.从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题 1.基础概念 2.分析ThreadLocalMap内部实现 一.ThreadLocal简介 多线程访问同一个共享变量的时候容易出现并发问题,特别是

随机推荐