JVM虚拟机内存模型和GC垃圾回收机制小结

JVM内存模型是什么?可能百度的时候有一堆答案,但是死记硬背过一阵就忘了,无法真正的渗透自己的大脑,特别是像我一样非科班出身的,对于计算机还比较笼统,没有一个特定的概念,那么我这篇博客,就是通过自主学习,然后通过自己的理解,然后去理解JVM虚拟机,和内存模型等一些列概念,虽然可能对于一些人来讲说的比较浅显,但是也算自身的一种进步吧,由浅入深,逐渐的去理解就好了,嘿嘿。。。。

首先我们看一张图:

这个图应该是都知道的图,有的时候面试问知道JVM内存模型吗,可能会说这五个模型,但是面完试,写几周代码,这几个概念又忘了,来回反复的看,看了忘,忘了看,下面我就用我理解比较深刻的方式来讲解这几个知识点。

首先我们要清楚class文件是怎么来的,class文件是通过javac将java文件编译出来的,内部是一堆字节指令码,这些指令做了什么?是对你java程序的翻译。你每行java代码都会由一行或多行字节指令码来翻译出来给计算机执行,java作为高级语言是给程序员看的,字节指令码是给JVM虚拟机执行的。

字节指令码大约长这样,写了一个compute方法,然后字节指令码翻译后就是上述两张图,字节指令码是可以查找的,具体的执行逻辑可以去百度,Code对应的就是字节指令码行号。

先说一个 除了五大内存模型之外还有一个直接内存,可以理解成除了JVM分配之外的内存,不占用JVM分配的内存大小。

方法区:

方法区会存放什么信息呢?有常量、静态变量、类元信息(类的组成部分,class文件通过类加载之后,类的组成部分会进入到方法区,比如类的名称,方法,属性等,具体有哪些属性,怎么来的可以研究类加载机制部分,这章就不讲了)。

堆内存:

绝大部分new的对象都放入堆内存,这块区域也是JVM所管理的内存最大的一块区域,GC也是发生在这里面。

那么我们想一下,方法区和堆内存的关系?

举个例子:我创建个静态变量user,我们知道静态变量是在方法区里的,也就是说user我是放在方法区的,但是我还new User(),new的对象都是放入堆内存的,所以也就是说,对象类型的静态变量,是指向方法区的,对象类型的静态变量指向的是方法区对象的引用地址的。

虚拟机栈:

  • 栈帧是什么?简单来说就是每个方法都可以对应一个栈帧,比如我执行个main方法,main方法里面执行compute方法,那么我们main方法和compute方法都有个栈帧,栈帧跟栈的数据结构是一样的,都执行先进后出的原则,执行完毕就销毁掉出栈。
  • 局部变量表,就是方法内的局部变量。
  • 操作数栈,操作数栈是虚拟机专用的工作区,也就是说虚拟机执行字节指令码的时候,对数据进行处理的时候,就用这个操作数栈。简单来讲就是为了虚拟机执行字节指令码存在的。
  • 动态链接,在运行期间将符号引用转换为直接引用,那么这种转换就成为动态连接。符号引用是什么?符号引用是类加载的时候由于虚拟机并不知道类的引用地址,用一个符号代替,当找到内存地址的时候,将内存地址替换掉符号引用,就是动态连接。
  • 方法出口,方法执行完毕,需要指针指向哪里。比如执行一个方法return到哪里,那个地方的内存地址就是方法出口。

虚拟机栈是存放线程的,每个线程对应一个虚拟机栈的。其实栈和堆也是有关系的

例如 User user=new User(),栈内的user对象指向了堆内存User对象的内存地址,也是引用的关系。

本地方法栈

本地方法栈服务的对象是JVM执行的native方法,这块问的也比较少。

程序计数器

程序计数器就是执行字节指令码的时候记录的行号,也就是我第三张图的code,是由执行引擎控制的。

 

GC垃圾回收机制

GC发生在堆内存当中,堆内存又分为新生代(MinorGC)、老年代(FullGC),新生代又分为Eden(伊甸园区)、From Survivor、To Survivor,其中新生代和老年代的内存占比为1:2,Eden:From Survivor:To Survivor为8:1:1;这些都是默认的。

那么我们考虑下,如何判断对象是否应该被回收呢?

  • 引用计数法。每引用一次,计数器+1,取消引用的时候,计数器-1,当为0的时候,GC回收。但是会产生一个问题,就是相互引用的时候,GC无法回收。
  • 可达性分析。把GCROOT作为起点,从节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则说明此对象不可达,即无用对象。也就是垃圾收集器需要回收的对象。当对象不可达的时候不一定会回收,第一次不可达的时候进行标记,然后筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过了,虚拟机将这两种情况都视为“没有必要执行”。基本在第二次标记的时候就会被回收掉了。如果这个对象被判定有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、优先级低的Finalizer线程去执行它。这里所谓的执行是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很有可能导致F-Queue队列中其他的对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么第二次标记时它将被移除回收集合,如果对象这时候还没有逃脱,那基本上就真的被回收了。
  •    可作为GC Roots的对象:

        1.虚拟机栈中栈帧里的本地变量表中引用的对象;

        2.方法区中静态属性引用的对象;

        3.方法区中常量引用的对象;

        4.本地方法栈中JNI(也就是native方法)引用的对象。

当判断对象可以回收了之后,GC是通过什么算法回收的呢?

  • 标记-清除算法。标记出可回收的对象,回收被标记的对象所占用的空间。但是会有两个缺点,一个是标记清楚效率不高,二是容易产生大量的不连续的内存碎片,内存碎片过多会导致无法分配连续内存给较大对象,如果申请大对象的话,会导致gc频繁执行,直到oom。
  • 复制算法。就是将内存分配两块区域,每次只使用其中一块区域。当垃圾收集的时候,遍历当前区域,将存活的对象复制到另外的一块内存区域,当前区域中可回收的对象回收,主要用在新生代中。
  • 标记-整理。也可以叫标记压缩。主要用在老年代中。和标记-清除比较类似,但是标记整理不仅将不存活的对象回收,同时还对存活的对象进行整理,重新整理,避免产生内存碎片。
  • 分代收集。MinorGC新生代收集和FullGC老年代收集。当执行垃圾回收的时候,Eden区会将存活的对象复制到From Survivor区域,将不存活的对象回收。当From Survivor区域满了的时候,将此区域的存活对象和Eden区存活的对象复制到To Survivor区域,将From Survivor和Eden区域不存活的对象回收。然后交换FromSurvivor和ToSerivivor区域。如此反复进行15次(默认15次),将存活的对象放入到老年代。特别的是如果当ToSurvivor区域满了的时候,直接放入老年代。 如果是大对象的话,也会直接 放入老年代。

 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值