Java内存管理

jvm虚拟机拿到分配的内存空间后分为五个部分,分别为栈(jvm栈)、堆、本地方法栈、程序计数器、方法区(元空间)

各个区各自的作用:

a.本地方法栈:用于管理本地方法的调用,里面并没有我们写的代码逻辑,其由native修饰,由 C 语言实现。

b.程序计数器:它是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、线程恢复等都依赖于计数器。

c.方法区(Java8叫元空间):用于存放已被虚拟机加载的类信息,常量,静态变量等数据。(jdk8之前叫永久区)

d.Java 虚拟机栈:用于存储局部变量表、操作数栈、动态链接、方法出口等信息。(栈里面存的是地址,实际指向的是堆里面的对象)

e.堆:Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;

线程私有、公有

a.线程私有:每个线程在开辟、运行的过程中会单独创建这样的一份内存,有多少个线程可能有多少个内存 Java虚拟机栈、本地方法栈、程序计数器是线程私有的

b.线程全局共享的 堆和方法栈 函数会有单独的函数栈,相应的函数栈在方法运行完毕了之后被清空了,这也是为什么在函数内部改变值不会改变实参的值(值传递)。但是堆上面的还没有被清空,所以引出了GC(垃圾回收),不能立马删除,因为不知道是否还有其它的也是引用了当前的地址来访问的。

方法区

static Integer i = 10

 (图源b站up:free_coder,下同)

回调函数

作为参数传递的函数

- 可用于通知函数做某些工作,如定时播放音乐;

- 使程序更加灵活,相同的函数流程处理不同类型的数据;
- 提高效率,将函数传入系统函数执行,无需在函数中调用系统接口

 GC/垃圾回收

GC算法:

  1. 三色标记清理

    • 强三色不变式:保证不会出现黑色对象到白色对象的引用

    • 弱三色不变式:黑色对象可指向白色对象,但保证有灰色对象指向这个白色对象

    实现强弱三色不变式的方法是读写屏障,写屏障会在写操作中插入指令,目的是将对象的修改通知到垃圾回收器,因此写屏障需要一个记录集,根据记录集可实现强三色不变式,如将被黑色对象引用的白色对象置为灰色,或将黑色对象退回为灰色等,这些都属于插入写屏障;弱三色不变式中用于破坏白色对象的多余引用,可将白色对象置为灰色对象,称为删除写屏障,

    读屏障:确保用户数据不会读取到旧副本的旧对象中,若检测到对象已存在新副本时,可将其原引用指向新副本

  2. 标记清理

  3. 标记整理

  4. 复制

  5. 分代回收

  6. 引用计数

堆区声明的对象若不及时得到回收处理会挤爆整个堆空间,引发程序崩溃,此时就需要将无用的空间回收。

GCRoot:栈区;本地方法栈;方法区直接或间接引用的对象,都不能被删除

- 标记清除法:第一遍扫描标记,第二遍扫描清除;不用STW,因为只对标记的对象进行处理,不影响其他对象,但缺点在于清除后会有内存碎片
- 标记整理法:清除后紧凑,但代价大,每次都要集体迁移,会产生STW
- 复制算法:将内存分为1区2区,标记后将要留下的对象复制进空的区域

 

实际运用的GC(如ParNew,CMS,jdk1.9后使用G1收集器)

分老年代和新生代,新生代中又有eden区和survive区;新生代中存在yongGC,新建的对象位于eden区中,当eden区满了之后,会产生yongGC,yongGC采用的是复制算法,经过标记复制算法将幸存对象放入survive区中,survive区有两个,两个区交替工作,大小大概是1:1:8,因为大部分的对象存活时间都较短。

 

在每次youngGC之后age都会+1,在7-15岁(6次GC以上,4位保存年龄,不同jdk阈值不同)之后被转移到Old区。大对象也会直接存到Old区,Old的GC一般会伴随着YoungGC,这时会引起STW(stop the world,整个程序暂停工作),利用标记整理或标记清理整理整个堆区(FullGC)

CMS:相比于其他几种垃圾收集器,专注于单次的垃圾收集,最大限度地降低了垃圾收集的时间,服务端垃圾收集器,位于老年代中,步骤如下

- 初次标记,标出GCRoot对象直接引用的对象,STW
- 并发标记,与用户线程同时进行,不会STW,扫描所有old区的对象
- 修正,由于CMS过程较长,可能出现新的垃圾,因此需要重新标记,修正第二步过程
- 并发清理(标记清理算法)

 

G1:将整个堆区内存分为非连续的region,每个region大小在1M-32M之间,总共大概有2000个region,并将region标记为相应的E\O\S区,空白表示尚未分配,当出现以下情况时

- 大于半个region小于1个region时,取H区
- 大于1个region时,取连续的H区

 

G1垃圾收集器之YoungGC:复制算法,从E区和S(from)区复制到S(to)区

OldGC:没有单独针对old区的GC,会同时对young区进行GC,因此成为MixGC,与cms收集过程相似

1. 初次标记--》GCRoot直接引用对象,还标记对象所在的region,成为RootRegion,STW
2. 扫描old区的所有region,根据rset(用于记录并跟踪其他Region指向自身Region对象的引用)来查看是否有RootRegion的引用,若有引用则表明有对象存活
3. 并发标记,扫描上一步引用的区,找出指向GCroot的对象
4. 重新标记,STW
5. 清理(复制清理)STW,只选择垃圾较少的region进行清理,虽然清理的不完全,但能够保证系统的正常运行

 Golang内存管理

堆内存

堆内存 = arena + span + page + 内存块

go将堆分为小块的arena,在arm64的Linux下每个arena是64MB,起始地址也对齐到64MB;每个arena分为8192个page,每个page8KB;

为降低碎片化内存给程序性能造成的影响(如浪费内存空间、找不到合适大小的内存空间等),go使用了与tcmalloc内存分配器类似的算法,即按照预制的规则将内存分为几种大小的内存块,然后将不同规格的内存块放入对应的空闲链表中,程序申请内存时,分配器会根据申请内存的大小来找到最匹配的规格,再从相应的空闲链表中分配对应的内存块,1.16给出了67种规格的内存块空闲链表,最小8B,最大32KB;因此在每个arena里又会按需划分出不同的span,每个span可能包含一组连续的page,也可能多个span组成一个page,视span的大小而定,并按照特定规格划分成等大的内存块。

(图源b站up:幼麟实验室,下同)

 管理内存的数据结构:

根据申请空间的不同大小分配不同大小的内存空间:

负责分配的主要是mallocgc函数,主要有四步骤;1)辅助gc,2)空间分配,3)位图标记,4)收尾工作。

GolangGC

STW\增量式\并行\并发式垃圾回收模式

GC在准备阶段会为每个P创建一个 p.gcBgMarkWorker 协程,将对应的 p.gcBgMarkWorker 指针存入P中,并将其置为睡眠状态,等到标记阶段调度执行;

在标记准备阶段和终止阶段有STW,准备阶段的STW主要处理写屏障是否开启等问题,接着开始标记工作,终止阶段确认标记阶段工作已经完成,没有标记任务,并关闭写屏障,开启清除工作;

进入 _GCoff 之前被分配的对象会被至为黑色,之后的对象会被置为白色;由于清扫工作也是增量式进行的,因此每轮进入GC时也要完成上一轮未完成的清扫工作。

 

标记工作要从扫描bss段、数据段、协程栈上的root节点开始,首先根据元数据中的信息gcbssmask、gcdatamask等判断段上数据是否为指针,是否指向堆中的数据段;并根据堆中数据的元数据类型将其对应的 gcmarkBits 标记为1,并将它加入工作队列中;

全局变量work中存储着全局工作队列缓存,同时每个P都有一个本地工作队列;虽然在准备阶段为每个P都初始化了一个 p.gcBgMarkWorker ,但真正GC对于CPU的使用率默认为25%,用环境变量中的gomaxprocs * 25% 获得,为解决得到的协程数非整数的问题,将GC协程分为两个状态,分别为 Dedicated 模式和 Fractional 模式,Dedicated 模式下GC协程能够正常运行标记任务直到被抢占,Fractional 模式下的GC协程不仅会被抢占还会在达到 Fractional 部分(由全体P共同负责)的目标时主动让出;全局变量 gcController 会记录总共启用了多少个不同状态下的GC协程及其状态;通过记录每个P的 Fractional 模式累计时间来判断当前协程是否需要执行 Fractional 状态的GC;通过这样的方式就能较好的控制GC时CPU的使用率 。

 GC执行时程序也是在执行的,也会有内存分配的产生,为缓解GC时内存分配的压力,限制在GC标记阶段申请内存分配的协程需要承担一部分标记工作(辅助标记),要申请的内存越大,分配到的标记任务越多,这是一种借贷偿还机制,这里的借贷用 gcAssistBytes 标记,大于0表示有结余,小于0表示负债;在GC清扫阶段申请内存分配的协程可能需要进行清扫(辅助清扫);辅助标记和辅助清扫可避免出现并发垃圾回收中因过大的内存分配压力导致GC来不及回收的情况。 

 GC有多种触发方式:1)手动触发,入口在 runtime.GC 函数中;2)分配内存时,有些情况下需要检查是否需要触发GC,触发后需设置下次触发GC的内存分配量,入口为 runtime.mallocgc ;3)由监控线程强制执行GC,在runtime包初始化时会以 runtime.forcegchelper 为执行入口开启一个协程,不过很快休眠并置于全局runq中,它被调度执行时就会开始新一轮的GC。