NO.2 内存区域&回收算法

零蚀


java 内存区域

  • 前言

    对于C/C++来说,程序员像是一个公民,既有管理内存的权利,也有维护内存义务,相较于Java,在虚拟机的自动内存管理机制下,我们不需要过多的去接受free/delete的处理,每当我们new出一个堆内存时,虚拟机都看似完美的处理了内存问题,但是如果出现问题,我们的处理难度,就异常难搞了。

  • 运行时的数据区域

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-enSXOujq-1588734759904)(media/15885825715470/15885849056667.jpg)]

  • 区域简介

    • 程序计数器(Program Counter Register): 它是线程私有的,在java虚拟机中,是有多个线程来执行字节码指令的,多线程就是通过多个线程轮番切换,来进行计数。为了让线程切换后都能执行到正确的位置,所以每个线程都需要一个独立的程序计数器,这些计数器相互不影响,独立存储,我们称这类的内存是线程私有的,我们的跳转,循环,分支,异常处理 etc. 都是要依赖程序计数器。当然如果我们的执行的是本地(native)方法,程序计数器为NULL。

    • java 虚拟机栈 (java Virtual Machine Stack): 它是线程私有的,声明周期和线程相投,每个方法被执行的时候,虚拟机都会同步的创建一个栈帧,用来存储方法里局部变量。

    • 本地方法栈 (Native Method Stacks): 与java虚拟机栈类似,虚拟机栈为虚拟机执行java方法(字节码)服务,而本地方法栈是为了执行本地(native)方法服务。

    • Java 堆 (Heap): 首先堆事内存管理中最大的一块,它是所有线程所共有的,在虚拟机创建的时候创建;其次,几乎所有的对象都是在这里分配内存对象(排除逃逸分析etc.,可见NO.2 java gc回收机制提及,后续也会说明)。Java堆是垃圾回收机制所管理的区域,因此也叫GC堆(Garbage Collected Heap,“垃圾堆”),暂时不谈GC堆的分代设置。最后 Java 堆的大小是可以设置的,当大小超过限制时会抛出OutOfMemoryError

    • 方法区 (Method Area) : 也叫做“非堆”(Non-Heap),它是线程共享的,它用于存放虚拟机已经加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存。其中还包括 运行时常量池(Runtime Constant Pool) 类具有一个常量池表,用于存放类的各种字面量(右值)和符号引用。

    • 直接内存(Direct Memory):它不是运行时数据区的一部分,它是在JDK1.4加入的一种 NIO (New Input/Output)类,引入了一种基于Channel和Buffer的I/O方式,他可以使用native函数库直接使用堆外内存,他的作用是某些情况下提高性能,避免在Java堆和native堆之间来回复制数据。当然内存就会有OutOfMemoryError异常。

  • Java堆中对象的分配、布局、访问

    先弄明白栈、堆、常量池的关系,就图说明没什么好说的。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BQKR6Bor-1588734759907)(media/15885825715470/15886133168452.jpg)]

    • 类的创建:类创建首先在接收到new这个指令后,检查这个指令在常量池中是否能定位到这个类的符号引用。通过这个符号引用,可以得知这个类是否给加载、解析、初始化过,如果没有就会执行类加载的过程。如果有虚拟机就会堆新生对象进行内存分配,对象所需的大小在类加载完就可以确定,然后虚拟机会从堆内存上划出一块内存。

    • 类的创建,内存分配方式,一般有以下两种:

      • 指针碰撞(Bump The Pointer):在已经使用内存和空闲内存之间有一个指针作为分界线,这个时候内存块是规整的,指针一边是空闲,一边是使用过的,当内存分配的时候指针会向内存块空闲一侧移动对应大小的距离。这种方式简单高效,但是内存一定要是规整的。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8dNIRK6l-1588734759908)(media/15885825715470/15886143273103.jpg)]

      • 空闲列表(Free List): Java堆不是规整的,已用的内存和空闲的内存交错在一起,那么虚拟机就要有一个列表,记录有哪些是已用内存,哪些是空闲内存他们有多大,然后划分一块足够的内存给创建的对象,并刷新信息列表。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WB3ZtLPN-1588734759909)(media/15885825715470/15886149971735.jpg)]

  • 对象的内存布局

    对象在堆里的内存布局可以分为三个部分:对象头,对象数据,对其填充。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QoegPeic-1588734759911)(media/15885825715470/15886161245898.jpg)]

    • 对象头:对象头又叫“Mark Word”一部分是左侧的运行时数据(HashCode、GC分代年龄……),一部分是右侧的类型指针,用来制定这个类是哪个类的实例,如果是数组的情况下,还会记录数组的擦痕高长度。

    • 对象值:这是对象存储的真正的有效信息。

    • 对齐填充:因为对象要保持是8字节的整数倍而出现的占位符,没有特殊含义(对象头已经被设置为8字节的整数倍)。

  • 对象的访问

    当我们创建了对象,后续的使用方式是,通过栈上面的reference来定位对象在堆里的地址。具体的访问方式是由虚拟机来定的,目前访问方式有两种,句柄和指针:

    • 句柄访问:java堆中会划出一个句柄池,里面存着对象的句柄地址,里面记录了对象的实例/类型地址。它的优点是当实例对象发生地址移动时,只会改变句柄的指针。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1dLxk5HR-1588734759912)(media/15885825715470/15886724852394.jpg)]

    • 指针访问:节省了开辟句柄池的空间,直接访问对象地址。它的速度快。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Tt0asGC-1588734759913)(media/15885825715470/15886727784331.jpg)]

  • 堆OOM(OutOfMemoryError)
    • 堆内存溢出

    我们可以设置虚拟的分配堆内存为一个固定值,然后进行报错测试。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sPleE5ej-1588734759913)(media/15885825715470/15886747822218.jpg)]

    这里设置了虚拟机的堆内存分配的最大值和最小值都为5M(堆的内存下限受虚拟机控制),这样时为了固定堆的大小防止他自己扩容,然后添加OOM异常抛出(HeapDumpOnOutOfMemoryError这个会给VM内部的异常做一个异常抛出,让我们能分析到VM内部原因)。

    -Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError
    

    java代码

    public class JavaOOM
    {
        public static void main(String[] args) {
            List<Demo> list=new ArrayList<>();
            while(true){
                list.add(new Demo());
                System.out.println(list.size());
            }
        }
    }
    
    class Demo{
    
    }
    
    // -------------print-------------
    Dumping heap to java_pid2195.hprof ...
    Exception in VM (AttachListener::init) : 
    java.lang.OutOfMemoryError: Java heap space
    Heap dump file created [9775076 bytes in 0.098 secs]
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    
  • 栈OOM

    设置栈的大小为144K(栈的内存下限受虚拟机控制)

    -Xss144k
    

    然后通过递归不断压栈。

    public class JavaOOM
    {
        static int a=0;
    
        public static void stackOOM(){
            System.out.println(a);
            stackOOM();
    
        }
        public static void main(String[] args) {
            stackOOM();
        }
    }
    
    // -------------print-------------
    Exception in thread "main" java.lang.StackOverflowError
    

    不断创建线程对象,也是会导致栈内存溢出的。


GC(Garbage Collection)

  • 前言

    垃圾回收之前,gc堆要判断哪些对象是‘活的’,哪些对象已经‘死了’。主要的方法分为两种,分别为引用计数算法 & 可达性分析算法

    • 引用计数算法: 在对象中添加一个引用计数器,当引用它时,引用+1,当引用失效时候,引用-1,当引用数值为0的时候,说明这个对象不可被使用,这种方法在Python中被使用,但是Java中却没有使用它来管理内存。因为这不能解决相互引用的问题B.param=A;A.param=B

    • 可达性分析算法:根据GC Root 是否和对象存在引用链关系,来决定是否可用,如果GC Root和对象间引用链相连表示对象可用,没有引用链关系表示不可用对象,如下图所示:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K041w9Q1-1588734759914)(media/15885825715470/15886790369688.jpg)]

    jdk1.2以后又添加了四种引用,分别是‘强引用’,‘软引用’,‘弱引用’,‘虚引用’四种,详细可看 Android篇[🔗 NO.2 java gc回收机制] ,这里值得一谈的是,一个对象持有虚引用不会对其生存时间有任何的影响,它的唯一作用当对象被回收时会有通知。

    垃圾回收中,对象的自我救赎:其实垃圾回收的时候,对象并没有立刻被回收,这个时候即使gc堆检测到这个对象已经没有引用链接关系的时候,也不会立刻死刑,因为这个时候虚拟机还会检查这个对象的finalize(),如果没有被这个方法覆盖,或者已经调用过了,就直接死刑,如果没有,回调用finalize方法,这个对象的唯一逃生机会,当然我们可以复写这个方法,添加引用,但是不一定就可以帮他逃生,主要还是看VM的心情。

  • 分代

    Java堆里一般分为 新生代 & 老年代:

    • 新生代(Young Generation):新声代在垃圾回收中,会大批死去,每次回收中存活的对象逐步变为老年代。
    • 老年代(Old Generation):老年代很难被回收,同时目前的老年代也不会和新生代产生跨代引用,防止新生代也难以被回收。

    回收的方式也不是对所有的内容进行全盘的检查,是否回收,而是有以下内容构成:

    • 部分收集:

      • 新生代回收(Minor GC/Young GC):
      • 老年代回收(Old GC):(CMS会单独收集老年代)
      • 混合回收(Mixed GC):整个青年代和部分老年代回收
    • 整体收集:对整个java堆和方法区的垃圾回收

  • 回收的算法
    • 标记-清除算法(Mark-Sweep):算法分为标记和清除两个过程,标记-清除会标记 清理/存活对象,来确定是否被GC清理掉,他也是最基础的垃圾回收的手段。这种清除手段会有以下问题:清除效率不稳定,因为如果内存中有大量的对象要被标记&清除,会有大量的清除、标记动作。其次会产生大量的内存碎片,如下图所示。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ndAQ3MaD-1588734759914)(media/15885825715470/15886929981115.jpg)]

    • 标记-复制算法:又称半区复制的垃圾回收算法,目的是为了解决标记清理的效率问题,它将可用内存等大的两块内容,每次只是用一块,将这一块内容用完了,就讲还活着区块复制到另一个内存空间,并将这边的内存空间清空,从而解决清除的效率问题,但是问题是,空间的大小被缩减了一半,空间浪费过大的代价。(如果内存不足,会将外部内存)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SlqrNWpj-1588734759915)(media/15885825715470/15886946202023.jpg)]

    • 标记-整理算法: 主要解决标记复制时存活对象一直存活而导致的效率低,空间浪费的问题,标记-整理算法的标记过程和标记-清除的标记过程是一致的,只是后续的过程不是直接清理,而是先对存活对象进行移动,向空间的一端移动,然后清理掉边界以外的内存。而应对老年代不会用这方法,因为其中老年代100%的存活率。并且这种移动式回收方式,移动活着的对象本身就是一个极耗性能的操作。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZNtlcdq0-1588734759915)(media/15885825715470/15887340446604.jpg)]

    内容参考自《深入理解java虚拟机》,无商业目的


🔗 前言
🔗 Android 知识栈
🔗 JVM 快速排序篇
🔗 NO.1 OpenJDK 前言
🔗 NO.3 垃圾收集器&ClassLoader
🔗 NO.4 原子&线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

零蚀zero eclipse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值