JVM内存模型及GC回收机制的相关理解

在面试中我们经常会被问道关于JVM的面试问题。我们来整理下
这篇不错
这个可以让你恍然大悟

1 JAVA内存模型初体验

JVM内存模型:
1 堆 :对象
2 栈(本地方法栈,虚拟机栈):参数列表、基本数据类型
3 方法区(包括常量池):类变量、常量、代码段(code segement)
4 程序计数区
在这里插入图片描述
有时候我们又会讲JVM内存分为主内存和工作内存。
主内存: 堆内存、方法区 (共享)
工作内存:程序计数区、栈内存

请记住上面这些。我们再来介绍下这些区域是做什么用的:
1 堆内存:所有的对象实例以及数组都要在堆上分配。Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(Garbage Collected Heap,幸好国内没翻译成“垃圾堆”)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
简单讲:存放对象,GC回收。
2 方法区类信息、常量、静态变量、即时编译器编译后的代码等数据
3 程序计数器: 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。是线程私有的。
4 本地方法栈:为虚拟机使用到的Native 方法服务。
5 虚拟机栈:与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。保存局部变量的值,包括:1.用来保存基本数据类型的值;2.保存类的实例,即堆区对象的引用(指针)。也可以用来保存加载方法时的帧。

看了上面的内容可能有了一个大概的理解,我们再来针对具体的代码做一个解析。
看这里

2 JAVA中变量的内存分配

这个很详细
首先我们来分类一下JAVA中的变量:
全局变量(类变量、实例变量)
局部变量

public class Variable{
    static int allClicks=0;    // 类变量
 
    String str="hello world";  // 实例变量
 
    public void method(){
 
        int i =0;  // 局部变量 
    }
}

我们来看这些变量类型的存储位置
局部变量是方法私有的变量,是存储在栈内存中,随着方法的结束而消失
类变量及static修饰的变量,存储在方法区
实例变量存储在堆内存中
对下面进行内存分析:

class BirthDate { 
  private int day; 
  private int month; 
  private int year;   
  public BirthDate(int d, int m, int y) { 
    day = d;  
    month = m;  
    year = y; 
  } 
   
  省略get,set方法……… 
} 
  
public class Test{ 
  public static void main(String args[]){ 
    int date = 9; 
    Test test = new Test();    
      test.change(date);  
    BirthDate d1= new BirthDate(7,7,1970);     
  }  
  
  public void change(int i){ 
    i = 1234; 
  } 

1 int date = 9;
date在main方法中定义属于局部变量,且是int属于基本数据类型,所以其值、引用都在栈内存中

2 Test test = new Test();
test引用存在栈内存中,new Test() 对象存在堆内存中。

3 test.change(date);
i为局部变量,引用和值存在栈中。当方法change执行完成后,i就会从栈中消失。

4 BirthDate d1= new BirthDate(7,7,1970);
d1引用存在栈中,new BirthDate 对象存在堆内存中,且int d, int m, int y为构造函数中的变量存在栈内存中,而day,month,year是实例变量存在堆内存中

3 GC简单了解

这个不错

哪些区域需要回收
在JVM五种内存模型中,有三个是不需要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。因为它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放,所以只有方法区和堆需要进行GC。

如何判断对象已死
判断对象在程序中没有被引用即对象已死。

判断对象回收

引用计数算法
引用计数算法在每个对象中加入一个引用计数器,如果此对象的引用计数变为0,那么此对象就可以作为垃圾收集器的目标对象来收集。

优点:简单,直接,不需要暂停整个应用。
缺点:1.需要编译器的配合,编译器要生成特殊的指令来进行引用计数的操作;2.不能处理循环引用的问题。

可达性分析算法(根搜索算法)
通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。

finalize方法
如果要回收一个不可达的对象,要经历两次标记过程。首先是第一次标记,并判断对象是否覆写了 finalize 方法,如果没有覆写,则直接进行第二次标记并被回收。
不建议使用finalize 方法,它的运行代价高,不确定性大,GC 也不会等待它执行完成,它的功能完全可以被 try-finally 代替。

各种垃圾收集算法

  • 标记-清除算法
    步骤:
    1、标记:从根集合开始扫描,标记存活对象;
    2、清除:再次扫描真个内存空间,回收未被标记的对象。
    缺点:
    1:每个活跃的对象都要进行扫描,而且要扫描两次,效率较低,收集暂停的时间比较长。
    2:产生不连续的内存碎片
    标记-清除算法

  • 标记-整理算法
    步骤:
    1、标记:从根集合开始扫描,标记存活对象;
    2、整理:再次扫描真个内存空间,并往内存一段移动存活对象,再清理掉边界的对象。
    不会产生内存碎片,但是依旧移动对象的成本。
    在这里插入图片描述

  • 复制算法
    将内存分成两块容量大小相等的区域,每次只使用其中一块,当这一块内存用完了,就将所有存活对象复制到另一块内存空间,然后清除前一块内存空间。
    缺点:
    1、复制的代价较高,所有适合新生代,因为新生代的对象存活率较低,需要复制的对象较少;
    2、需要双倍的内存空间,而且总是有一块内存空闲,浪费空间。
    在这里插入图片描述

GC 类型
1.Minor GC 针对新生代的 GC Minor GC
2.Major GC 针对老年代的 GC
3.Full GC 针对新生代、老年代、永久带的 GC

为什么要分不同的 GC 类型,主要是 1、对象有不同的生命周期,经研究,98%的对象都是临时对象;2、根据各代的特点应用不同的 GC算法,提高 GC 效率。

现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法没什么特别的,无非是上面内容的结合罢了,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法。

在这里插入图片描述

不同区域采用不同的垃圾收集器。

  • 对象的一生 转载
    我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

  • 请问GC什么时候触发
    “什么时候”即就是GC触发的条件。
    Minor GC触发条件
    当Eden区满时,触发Minor GC。
    Full GC触发条件
    (1)调用System.gc时,系统建议执行Full GC,但是不必然执行
    (2)老年代空间不足
    (3)方法区空间不足
    (4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
    (5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小


上述内容是本人通过对其他优秀博客的整理,为自己对此部分的知识进行梳理,如有错误请指出,本人其他博客也会以此形式整理(自己理解+原文博客链接)。

猜你喜欢

转载自blog.csdn.net/qq_31941773/article/details/83277973
今日推荐