编程语言中的GC对比

PHP

PHP普通的web开发模式,由于起程序执行周期短,一个请求结束进程就会释放的特性,通常不会内存泄漏的问题,这种设计方式天然的成为PHP内存安全的保障,PHP开发很少会关心项目整体的内存方面问题

但对于一些 cli web项目或者大数据处理场景,就需要GC的介入执行了

八股文概念

php5.3前使用引用计数法,但是无法解决循环应用的内存释放问题,现在又引入了循环垃圾回收
也就是 引用计数法+循环垃圾回收 组合

其中循环垃圾回收是一种周期回收清理机制,里面分为三步 遍历、删除、释放

而引用计算是个 +1 -1的过程,性能影响效果小一些

通俗的来说,PHP采用引用计数机制实时回收,为了解决循环引用问题,又融入了周期性的循环垃圾回收机制用于辅助回收

这些概念类东西,自行google

触发场景

  1. 达到内存阈值时:当分配的内存达到zend.memory_limit配置的阈值(默认通常是128M)时,会触发回收。

  2. 手动调用时:通过gc_collect_cycles()函数可以手动强制触发垃圾回收。

  3. 请求结束时:每个PHP请求执行结束后,会自动回收该请求期间产生的所有未释放内存。

  4. 周期性检测:当疑似循环引用的变量数量达到一定阈值(默认10000个)时,会启动循环引用检测并回收。

由于php还采用了两种模式组合GC,引用计数为0时,会实时立马回收内存

总结下来就是,引用计数(实时)+ 循环垃圾收集器(周期性)的设计,既保证了内存的及时释放,又能处理复杂的循环引用问题

下面是代码编写过程中,栈堆分配的场景

代码层次优化

当PHP项目也要需要对GC调优时,应当关注下业务代码情况
例如,常用的PHP手动gc函数,gc_collect_cycles gc_mem_caches unset 有实际效果的就这两个函数

php 的优化上限非常低,不建议在此浪费过多时间

堆分配

1. 大对象分配

// 当对象或数组超过一定大小时,会分配到堆上
$largeArray = array_fill(0, 100000, str_repeat('x', 1000));
// 大数组会直接分配到堆内存

$bigString = str_repeat('A', 10 * 1024 * 1024); // 10MB字符串
// 大字符串也会分配到堆上

2. 长期存活的对象

class LongLivedCache {
    private static $cache = [];
    
    public static function store($key, $value) {
        // 静态变量中的数据通常分配到堆上
        // 因为需要在整个脚本生命周期中保持
        self::$cache[$key] = $value;
    }
}

// 全局变量也会分配到堆上
$GLOBALS['config'] = [
    'database' => [...], // 配置数据分配到堆
    'cache' => [...]
];

3. 动态分配的复杂数据结构

// 循环引用的对象会分配到堆上
class Node {
    public $children = [];
    public $parent = null;
    public $data;
    
    public function addChild($child) {
        $child->parent = $this;
        $this->children[] = $child;
        // 这种复杂的引用关系通常在堆上管理
    }
}

// 深层嵌套的数组
function createDeepArray($depth) {
    if ($depth <= 0) return "leaf";
    return [
        'level' => $depth,
        'child' => createDeepArray($depth - 1)
    ];
    // 深层嵌套结构会分配到堆上
}

4. 资源类型对象

// 文件资源
$file = fopen('large_file.txt', 'r');
// 文件句柄及其缓冲区在堆上分配

// 数据库连接
$pdo = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
// 连接对象和相关数据在堆上

// curl资源
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://example.com');
// curl句柄及其数据在堆上分配

5. 超出栈限制的递归

function recursiveFunction($depth) {
    if ($depth > 1000) { // 深递归
        return "deep";
    }
    
    $localArray = range(1, 1000); // 每层递归的大数组
    return recursiveFunction($depth + 1);
    // 当递归深度很大时,数据可能被移到堆上
}

栈分配

1. 小的局部变量

function simpleFunction() {
    $a = 10;           // 简单整数,可能在栈上
    $b = "hello";      // 短字符串,可能在栈上
    $c = [1, 2, 3];    // 小数组,可能在栈上
    
    return $a + count($c);
}

2. 函数参数和返回值

function calculate($x, $y) { // 参数在栈上
    $result = $x + $y;       // 局部变量在栈上
    return $result;          // 返回值通过栈传递
}

Golang

STW 可以理解为程序阻塞,停顿的意思,一个损耗性能的物质

三色标记清除算法

原理

  • 三色标记法:将对象分为白色(未访问)、灰色(已访问但子对象未访问)、黑色(已访问且子对象已访问)
  • 并发执行:GC与应用程序并发运行,减少Stop-the-World时间
  • 写屏障:使用写屏障技术确保并发标记的正确性

通俗的理解,你就当作垃圾分类,干湿垃圾 可回收垃圾吧

这个写屏障,你可以理解为,GC是在并发执行的,当扫描内存的状态时,加了一把写锁,来防止程序修改操作内存的状态,性能开销很小

GC的执行过程

  1. 标记准备(Mark Setup)- STW
  2. 并发标记(Concurrent Mark)
  3. 标记终止(Mark Termination)- STW
  4. 并发清扫(Concurrent Sweep)

触发场景

  1. 内存分配量达到阈值:当新分配的内存达到上次GC后内存的2倍时(GOGC=100,默认值)
  2. 定时触发:超过2分钟未进行GC时强制触发
  3. 手动触发:调用runtime.GC()

这里我们来看一段代码
一个web项目的主程序中

ballast := make([]byte, 1*1024*1024*1024) // 1G
runtime.KeepAlive(ballast)

首先,这个大切片肯定是分配在堆上的,申请了1G内存相当于该项目的内存下限抬高了1G,GC不会回收这段内存,能够相对调整GC的执行此次,来降低一些CPU的波动

当然,优化项目还是要看具体的业务

GC执行的影响

先说好的地方

  1. 自动内存管理:开发者无需手动管理内存,减少内存泄漏风险
  2. 并发执行:大部分GC工作与应用程序并行,减少延迟
  3. 低延迟:Go 1.5+版本STW时间通常在亚毫秒级别

影响性能的地方

  1. CPU开销:GC过程消耗CPU资源,影响应用程序性能
  2. 内存开销:需要额外内存存储GC元数据
  3. 暂停时间:尽管很短,但仍有STW阶段

程序的优化

1. 调整GOGC参数

// 设置GOGC=200,降低GC频率但增加内存使用
os.Setenv("GOGC", "200")

// 或在代码中调整
debug.SetGCPercent(200)

2. 减少内存分配

// 避免频繁的小对象分配
// 使用对象池减少分配压力
var bufferPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024)
    },
}

func processData() {
    buf := bufferPool.Get().([]byte)
    defer bufferPool.Put(buf)
    // 使用buf处理数据
}

3. 合理使用指针

// 减少指针使用可以降低GC扫描压力
type Data struct {
    ID   int    // 值类型,GC无需扫描
    Name string // string内部有指针,但结构简单
}

4. 批量处理

// 批量分配减少GC压力
items := make([]Item, 1000) // 一次性分配
for i := range items {
    // 初始化items[i]
}

rust

以上的PHP Golang的垃圾回收,非常的相似,包括Java python 都属于高级编程语言,开发者对呀GC的人工干预还不是特别的多,这类统称为编程语言的自动GC过程

未完待续

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容