PHP
PHP普通的web开发模式,由于起程序执行周期短,一个请求结束进程就会释放的特性,通常不会内存泄漏的问题,这种设计方式天然的成为PHP内存安全的保障,PHP开发很少会关心项目整体的内存方面问题
但对于一些 cli web项目或者大数据处理场景,就需要GC的介入执行了
八股文概念
php5.3前使用引用计数法,但是无法解决循环应用的内存释放问题,现在又引入了循环垃圾回收
也就是 引用计数法+循环垃圾回收 组合
其中循环垃圾回收是一种周期回收清理机制,里面分为三步 遍历、删除、释放
而引用计算是个 +1 -1的过程,性能影响效果小一些
通俗的来说,PHP采用引用计数机制实时回收,为了解决循环引用问题,又融入了周期性的循环垃圾回收机制用于辅助回收
这些概念类东西,自行google
触发场景
达到内存阈值时:当分配的内存达到
zend.memory_limit配置的阈值(默认通常是128M)时,会触发回收。手动调用时:通过
gc_collect_cycles()函数可以手动强制触发垃圾回收。请求结束时:每个PHP请求执行结束后,会自动回收该请求期间产生的所有未释放内存。
周期性检测:当疑似循环引用的变量数量达到一定阈值(默认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的执行过程
- 标记准备(Mark Setup)- STW
- 并发标记(Concurrent Mark)
- 标记终止(Mark Termination)- STW
- 并发清扫(Concurrent Sweep)
触发场景
- 内存分配量达到阈值:当新分配的内存达到上次GC后内存的2倍时(GOGC=100,默认值)
- 定时触发:超过2分钟未进行GC时强制触发
- 手动触发:调用runtime.GC()
这里我们来看一段代码
一个web项目的主程序中
ballast := make([]byte, 1*1024*1024*1024) // 1G
runtime.KeepAlive(ballast)
首先,这个大切片肯定是分配在堆上的,申请了1G内存相当于该项目的内存下限抬高了1G,GC不会回收这段内存,能够相对调整GC的执行此次,来降低一些CPU的波动
当然,优化项目还是要看具体的业务
GC执行的影响
先说好的地方
- 自动内存管理:开发者无需手动管理内存,减少内存泄漏风险
- 并发执行:大部分GC工作与应用程序并行,减少延迟
- 低延迟:Go 1.5+版本STW时间通常在亚毫秒级别
影响性能的地方
- CPU开销:GC过程消耗CPU资源,影响应用程序性能
- 内存开销:需要额外内存存储GC元数据
- 暂停时间:尽管很短,但仍有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过程
未完待续