JVM--堆内存结构

接着上篇文末,来详细了解堆,这也是我们做性能优化时针对的地方
上次提到堆中存放着实例化的对象,我们知道c语言中没有类的概念,只有结构体,Java中的类最底层实际上也是一个结构体,实例化的类,我们又称为引用型对象,实际就是一个指针指向结构体的内存,结构体内存是连续的空间

下面的c代码计算了结构体的内存大小:

#include<stdio.h>
#include<string.h>

//结构体 定义方法:struct 名字 {};
struct lamei{
  int age;
  int height;
  int xiongwei;
  int weight;
  char name[30];
};


int main(){
  struct lamei la;

  printf("sizeof(lamei) = %d \n",sizeof(la));
  memset(&la,0,sizeof(la));

  strcpy(la.name,"凤姐");
  la.age    = 18;
  la.height = 165;
  la.xiongwei = 90;
  la.weight = 45;

  printf("%s %d %d %d %d\n",la.name,la.age,la.height,la.xiongwei,la.weight);
}

结果:
sizeof(lamei) = 48
凤姐 18 165 90 45

该结构体为4个int型变量,一个30大小的字符型数组,当前为CentOS7系统,c语言的int型变量占4个字节,char型变量占1个字节
4 * 4 + 30 = 46
当然了结构体是连续的内存,但是不一定是占按照内部定义的数据量总和,一般都会超出一点,因为考虑到内存的整体性,会做内存对齐操作

通过c语言我们知道,一个对象实例化时,申请的内存一定是连续的(这边不考虑内部有指针变量或链表等数据结构,链表中是一个个节点,当然了一个节点的内存也是连续的,只不过节点中有一个指针指向下一个节点的内存首地址),而Java中,一般情况下,实例化的对象都会存在堆中,有时也可以存放在栈中。上次用一张gif图展示了栈帧中的代码的执行,如果变量经过逃逸分析,逃逸不出方法的话,如仅作为方法的临时变量,那么它会存放在栈中,反之,如变量作为返回值,那么它会存放在堆中,这块知识只作为一个补充,了解一下即可
接下来开始正片内容
一、堆中的内存结构
堆内存结构

堆中内存分为两部分新生代(young gen)和老年代(old gen),而新生代中又分为三部分eden区、from区、to区,其中form区和to区又合称为生存区(survivor)
不特别指定的情况下,eden:from:to的内存比例为8:1:1

1.新生代

当我们实例化对象时,eden区内存足够,那么该对象会存储在eden区内存不够的情况从左往右以此类推,寻找可以存放的区进行存储,找不到则会报OOM异常

我们可以使用JDK自带的工具:HSDB,来查看常量池的对象分布,我的JDK目录为:C:\Program Files\Java\jdk1.7.0_80,在lib目录下有个sa-jdi.jar,HSDB工具就在这个jar中,使用它之前,先要把jre\bin目录下的sawindbg.dll文件复制到C:\Program Files\Java\jre7\bin下

接下来看下我们的代码:

/**
 * Created by aruba on 2021/9/29.
 */
class DumpTest {

    public static void main(String[] args) {
        DumpTest eden = new DumpTest();

        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们在主线程实例化一个对象,并无限等待

1.1 使用jps命令,查看jvm上跑的进程
jps
image.png
1.2 启动HSDB图形化界面

在JDK的lib目录下,打开命令行工具,执行下面命令:

java -cp ./sa-jdi.jar sun.jvm.hotspot.HSDB

打开后,选择Attach to HotSpot process,并输入DumpTest的进程id:8040

在下图中选择main线程,然后选择第二个选项

就可以看到我们对象在内存的哪里了

图太小了哈哈,可以看到图中DumpTest在新生代(YoungGen)区

2.老年代

老年代中存放着gc(JVM垃圾回收)15次后,还未能回收内存的对象,还存放着大对象和新生代内存不足,无法存放下的对象,我们修改下刚刚的代码,手动gc15次,再来看看对象处于哪个区

/**
 * Created by aruba on 2021/9/29.
 */
class DumpTest {

    public static void main(String[] args) {
        DumpTest eden = new DumpTest();


        for (int i = 0; i < 15; i++) {
            System.gc();
        }

        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

再次执行jps命令,查看进程id,并使用HSDB工具查看,最后的内存为下图:

可以看到现在的DumpTest对象处于老年代(OldGen)区

二、minor GC与full GC

Java程序员不需要关注内存回收,是因为JVM中的垃圾收集器会自动收集垃圾对象,并释放它们的内存。对对象进行垃圾回收的操作,虽然后续出了更高性能的垃圾回收器,也无法避免性能的消耗,即所有GC都有stop the world。
GC分为两个部分:

  • minor GC:对堆中的新生代中所有对象进行一次垃圾回收,轻量级,该gc比较频繁,每回收一次,如果对象未被回收,那么标志位加1,当达到15时,对象进入老年代
  • full GC:对新生代和老年代进行一次gc,就是minor GC + Major GC,Major GC为老年代的gc,只有内存不够时,才会进行,比minor GC慢十倍以上

至此,JVM为什么把堆中分为新生代和老年代的原因可以得知,minor GC比较频繁,但是有些对象会长期存在内存中,不需要回收,所以对他进行gc检测是没有必要的,那么分为新生代和老年代,把内存分为临时创建的对象和较为常驻内存的对象可以优化性能

1.垃圾回收机制
1.1 不适用的引用计数算法

说到垃圾回收机制,很容易就能想到引用计数算法,当一个对象被其他对象引用时,他的引用计数就会加1,当一个对象的引用计数为0时,那么说明这个对象可以被回收了

面试中常问的一个问题:当A中有个对象引用B,当B中有个对象引用A,但没有别的对象引用它们,那么这两个对象能否被回收?
我们想要的答案很显然是能被回收,但是使用引用计数算法就无法回收它们了

1.2 可达性分析算法

该算法思想和 最小生成树kruskal算法类似, 每个对象都是一个顶点,有一些GC Roots顶点作为不可回收的顶点,当一个顶点的边的最远端无法到达GC Roots时,那么该顶点就可以被回收

使用该算法,即使发生1.1 中的情况,对象也可以被回收,因为两个对象并不是GC Roots树下的节点

三、垃圾回收算法

新生代和老年代也有一系列的垃圾回收算法,其中新生代分为三个部分:
以下不考虑内存不够的情况

  • eden:新创建的对象存放的位置
  • from(s0):复制算法相关
  • to(s1):复制算法相关
1.标记-清除算法

最基础的算法,分为两个步骤:1.对可以被回收的对象进行标记2.对标记的对象进行内存回收

优点:简单缺点:标记和回收效率不高,并且造成大量内存碎片,导致申请大内存对象时内存不够现象,导致触发full GC

2.复制算法

复制算法为了解决内存碎片问题,将内存分成1:1的两块,每次只使用一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。缺点:内存只有一半

上面提到eden:from:to的内存为8:1:1,因为使用的是复制算法
当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间

3.标记-整理算法

老年代不能接收内存浪费,所以不使用复制算法,标记-整理算法分为两个步骤:1:标记过程和标记-清楚算法相同,对可以被回收的对象进行标记2:整理是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存(双指针算法,一次遍历)

优点:内存连续 缺点:低效
4.分代收集算法

分代收集算法就是对内存进行划分,各个垃圾收集器不太一样,本文基础标准HotSpot虚拟机,一般是分为新生代和老年代,针对不同的内存区域,使用不同的算法,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

四、垃圾回收器

垃圾回收器分为很多种,并不断有新的垃圾回收器出现,很多公司拥有自己编写的垃圾回收器

stop the world:所有垃圾回收器都避免不了stop the world带来的延迟,试想下,如果我们要对内存进行一次扫描,挑出可以被回收的对象,那么此时的内存还能不能变化?如:实例化一个对象分配新的内存,很显然我们是不希望内存变动的,也就是说stop the world必须暂停所有线程

五、引用类型

Java中的引用对象分为四种:

  • 强引用:不能被gc回收
  • 软引用:当内存不够时,发生gc,优先被回收
  • 软引用:gc发生时,立刻被回收
  • 虚引用:创建一个虚引用对象后,返回的都是一个null,用来检测gc发生情况
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值