Gerrad Zhang

垃圾回收算法与GC调优实战深度解析

深入解析JVM垃圾回收算法的设计原理、各种GC收集器的特点和适用场景。结合实际项目经验分享GC调优策略、性能监控方法,掌握生产环境GC问题诊断和解决方案。

Gerrad Zhang
武汉,中国
2 min read

🤔 问题背景与技术演进

我们要解决什么问题?

在Java应用开发中,**垃圾回收(Garbage Collection, GC)**是自动内存管理的核心机制,但也是性能优化的关键瓶颈。不合适的GC策略会导致严重的性能问题:

  • Stop-The-World停顿过长:影响应用响应时间和用户体验
  • GC频率过高:消耗大量CPU资源,降低应用吞吐量
  • 内存回收不及时:导致内存泄漏和OOM异常
  • GC调优复杂:参数众多,缺乏系统性的调优方法
// 常见的GC性能问题示例
public class GCPerformanceProblems {
    
    /**
     * 问题1:频繁的Young GC
     */
    public void demonstrateFrequentYoungGC() {
        // ❌ 大量短生命周期对象创建
        for (int i = 0; i < 1000000; i++) {
            String temp = new String("临时对象" + i); // 频繁创建对象
            List<String> tempList = new ArrayList<>(); // 临时集合
            tempList.add(temp);
            // 对象很快变成垃圾,导致频繁Young GC
        }
        
        // 结果:Young GC频率过高,影响应用性能
        // 解决:对象池、StringBuilder优化、调整年轻代大小
    }
    
    /**
     * 问题2:Full GC频繁触发
     */
    public void demonstrateFrequentFullGC() {
        // ❌ 大对象直接进入老年代
        List<byte[]> largeObjects = new ArrayList<>();
        
        for (int i = 0; i < 100; i++) {
            byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB大对象
            largeObjects.add(largeArray);
            // 大对象直接分配到老年代,快速填满
        }
        
        // 结果:老年代快速填满,频繁触发Full GC
        // 解决:调整大对象阈值、增大老年代、优化对象大小
    }
}

没有这个技术时是怎么做的?

在自动垃圾回收出现之前,内存管理完全依赖程序员手动操作:

手动内存管理(C/C++)

  • 使用malloc/free、new/delete手动分配释放内存
  • 问题:内存泄漏、野指针、双重释放等问题频发

2. 引用计数法

  • 为每个对象维护引用计数器
  • 问题:无法处理循环引用,性能开销大

3. 简单标记清除

  • 标记所有可达对象,清除不可达对象
  • 问题:停顿时间长,内存碎片严重

技术演进的历史脉络

2000年代早期: 并发和增量回收

  • CMS垃圾回收器引入
  • 并发标记和清除
  • 减少停顿时间

2010年代: 低延迟和大堆支持

  • G1垃圾回收器成熟
  • 分区回收策略
  • 可预测的停顿时间

2020年代: 超低延迟回收器

  • ZGC和Shenandoah引入
  • 停顿时间小于10ms
  • 支持TB级别堆内存

🎯 核心概念与原理

基础概念定义

**垃圾回收(Garbage Collection)**是JVM自动管理内存的机制,通过识别和回收不再被程序引用的对象来释放内存空间,避免内存泄漏和手动内存管理的复杂性。

核心特性

  • 自动化:无需程序员手动释放内存
  • 安全性:避免内存泄漏和野指针问题
  • 透明性:对应用程序基本透明
  • 可调优:提供丰富的参数进行性能调优

垃圾回收算法原理

标记-清除算法(Mark-Sweep)

/**
 * 标记-清除算法实现原理
 */
public class MarkSweepAlgorithm {
    
    /**
     * 标记-清除算法的工作流程
     */
    public void analyzeMarkSweepProcess() {
        /*
         * 标记-清除算法执行过程:
         * 
         * 第一阶段:标记阶段
         * ┌─────────────────────────────────────────────┐
         * │ 1. 从GC Roots开始遍历对象图                  │
         * │    GC Roots包括:                           │
         * │    - 虚拟机栈中的引用                       │
         * │    - 方法区中的静态引用                     │
         * │    - 方法区中的常量引用                     │
         * │    - 本地方法栈中的引用                     │
         * │                                             │
         * │ 2. 标记所有可达的对象                       │
         * │    使用深度优先搜索或广度优先搜索           │
         * │    将可达对象的对象头标记位设置为1          │
         * │                                             │
         * │ 3. 遍历完成后,未标记的对象即为垃圾         │
         * └─────────────────────────────────────────────┘
         * 
         * 第二阶段:清除阶段
         * ┌─────────────────────────────────────────────┐
         * │ 1. 遍历整个堆内存                           │
         * │    按地址顺序扫描所有内存区域               │
         * │                                             │
         * │ 2. 回收未标记的对象                         │
         * │    将垃圾对象的内存空间加入空闲列表         │
         * │                                             │
         * │ 3. 重置标记位                               │
         * │    将存活对象的标记位重置为0                │
         * └─────────────────────────────────────────────┘
         * 
         * 算法特点:
         * ✅ 优点:
         *   - 实现简单,逻辑清晰
         *   - 不需要额外的内存空间
         *   - 可以回收循环引用的对象
         * 
         * ❌ 缺点:
         *   - 效率较低,需要扫描整个堆
         *   - 产生内存碎片
         *   - 停顿时间与堆大小成正比
         */
    }
}

复制算法(Copying)

/**
 * 复制算法实现原理
 */
public class CopyingAlgorithm {
    
    /**
     * 复制算法的工作流程
     */
    public void analyzeCopyingProcess() {
        /*
         * 复制算法执行过程:
         * 
         * 初始状态:内存分为两个相等的区域
         * ┌─────────────────┐ ┌─────────────────┐
         * │   From Space    │ │    To Space     │
         * │ ┌─────┬─────┐   │ │                 │
         * │ │Obj1 │Obj2 │   │ │     (空闲)      │
         * │ │Obj3 │Obj4 │   │ │                 │
         * │ └─────┴─────┘   │ │                 │
         * └─────────────────┘ └─────────────────┘
         * 
         * 第一步:标记存活对象
         * ┌─────────────────┐ ┌─────────────────┐
         * │   From Space    │ │    To Space     │
         * │ ┌─────┬─────┐   │ │                 │
         * │ │Obj1✓│Obj2✗│   │ │     (空闲)      │
         * │ │Obj3✓│Obj4✗│   │ │                 │
         * │ └─────┴─────┘   │ │                 │
         * └─────────────────┘ └─────────────────┘
         * 
         * 第二步:复制存活对象到To Space
         * ┌─────────────────┐ ┌─────────────────┐
         * │   From Space    │ │    To Space     │
         * │                 │ │ ┌─────┬─────┐   │
         * │     (待清理)     │ │ │Obj1 │Obj3 │   │
         * │                 │ │ └─────┴─────┘   │
         * │                 │ │                 │
         * └─────────────────┘ └─────────────────┘
         * 
         * 算法特点:
         * ✅ 优点:
         *   - 没有内存碎片
         *   - 分配简单,只需移动指针
         *   - 复制过程中自动完成内存整理
         * 
         * ❌ 缺点:
         *   - 内存利用率只有50%
         *   - 对象存活率高时效率低
         *   - 需要额外空间作为复制缓冲区
         */
    }
}

标记-整理算法(Mark-Compact)

/**
 * 标记-整理算法实现原理
 */
public class MarkCompactAlgorithm {
    
    /**
     * 标记-整理算法的工作流程
     */
    public void analyzeMarkCompactProcess() {
        /*
         * 标记-整理算法执行过程:
         * 
         * 第一阶段:标记阶段(与标记-清除相同)
         * ┌─────────────────────────────────────────────┐
         * │ 从GC Roots开始标记所有可达对象               │
         * └─────────────────────────────────────────────┘
         * 
         * 第二阶段:整理阶段
         * 
         * 整理前的内存布局:
         * ┌──────┬──────┬──────┬──────┬──────┬──────┐
         * │ Obj1 │ 垃圾 │ Obj3 │ 垃圾 │ Obj5 │ 垃圾 │
         * │(存活)│      │(存活)│      │(存活)│      │
         * └──────┴──────┴──────┴──────┴──────┴──────┘
         * 
         * 整理后的内存布局:
         * ┌──────┬──────┬──────┬──────────────────────┐
         * │ Obj1 │ Obj3 │ Obj5 │        空闲空间       │
         * │(存活)│(存活)│(存活)│                      │
         * └──────┴──────┴──────┴──────────────────────┘
         * 
         * 算法特点:
         * ✅ 优点:
         *   - 没有内存碎片
         *   - 内存利用率高(100%)
         *   - 适合存活率高的场景
         * 
         * ❌ 缺点:
         *   - 整理过程复杂,需要移动对象
         *   - 停顿时间较长
         *   - 需要更新所有引用
         */
    }
}

分代垃圾回收理论

分代假设(Generational Hypothesis)

/**
 * 分代垃圾回收理论分析
 */
public class GenerationalGCTheory {
    
    /**
     * 分代假设的理论基础
     */
    public void analyzeGenerationalHypothesis() {
        /*
         * 分代假设的两个核心观察:
         * 
         * 1. 弱分代假设(Weak Generational Hypothesis):
         *    "大多数对象在年轻时就会死亡"
         * 
         *    统计数据支持:
         *    ┌─────────────────────────────────┐
         *    │ 对象存活时间分布:               │
         *    │                                │
         *    │ 98% │████████████████████████  │ ← 第一次GC后死亡
         *    │  1% │█                         │ ← 存活1-2次GC
         *    │  1% │█                         │ ← 长期存活
         *    └─────────────────────────────────┘
         * 
         * 2. 强分代假设(Strong Generational Hypothesis):
         *    "老对象很少引用年轻对象"
         * 
         *    跨代引用统计:
         *    - 老年代→年轻代引用:小于 1%
         *    - 年轻代→老年代引用:约 15%
         *    - 同代内引用:> 84%
         * 
         * 基于假设的优化策略:
         * 
         * 1. 分代收集:
         *    - 年轻代:频繁GC,使用复制算法
         *    - 老年代:较少GC,使用标记-整理算法
         * 
         * 2. 记忆集(Remembered Set):
         *    - 记录跨代引用关系
         *    - 避免扫描整个老年代
         *    - 提高年轻代GC效率
         * 
         * 3. 卡表(Card Table):
         *    - 将内存划分为固定大小的卡片
         *    - 标记包含跨代引用的卡片
         *    - 只扫描标记的卡片
         */
    }
}

🔧 实现原理与源码分析

现代垃圾回收器实现

G1垃圾回收器

/**
 * G1垃圾回收器实现分析
 */
public class G1GarbageCollector {
    
    /**
     * G1的设计理念和架构
     */
    public void analyzeG1Architecture() {
        /*
         * G1 (Garbage First) 设计理念:
         * 
         * 1. 分区管理 (Region-based):
         * ┌─────────────────────────────────────────────┐
         * │              G1 Heap Layout                │
         * ├─────┬─────┬─────┬─────┬─────┬─────┬─────────┤
         * │  E  │  S  │  O  │  O  │  E  │  S  │    H    │
         * ├─────┼─────┼─────┼─────┼─────┼─────┼─────────┤
         * │  O  │  E  │  E  │  S  │  O  │  O  │    H    │
         * ├─────┼─────┼─────┼─────┼─────┼─────┼─────────┤
         * │  E  │  O  │  S  │  E  │  E  │  O  │   Free  │
         * └─────┴─────┴─────┴─────┴─────┴─────┴─────────┘
         * 
         * E = Eden Region      S = Survivor Region
         * O = Old Region       H = Humongous Region
         * 
         * 2. 分区特点:
         *    - 每个分区大小相等(1MB-32MB)
         *    - 分区角色可以动态变化
         *    - 不要求物理上连续的分代
         * 
         * 3. 优先级回收策略:
         *    - 维护每个分区的回收价值
         *    - 优先回收垃圾最多的分区
         *    - 在停顿时间限制内回收价值最大的分区
         */
    }
    
    /**
     * G1的关键技术
     */
    public void analyzeG1KeyTechnologies() {
        /*
         * 1. 记忆集 (Remembered Set):
         *    - 每个分区维护一个记忆集
         *    - 记录其他分区对本分区的引用
         *    - 避免扫描整个堆来寻找引用
         * 
         * 2. 卡表 (Card Table):
         *    - 将每个分区划分为多个卡片
         *    - 标记包含跨分区引用的卡片
         *    - 只扫描脏卡片,提高效率
         * 
         * 3. SATB (Snapshot At The Beginning):
         *    - 在并发标记开始时创建对象图快照
         *    - 通过写屏障记录引用变化
         *    - 确保并发标记的正确性
         * 
         * 4. 停顿预测模型:
         *    - 记录历史GC数据
         *    - 预测每个分区的回收时间
         *    - 在停顿时间限制内选择最优分区组合
         */
    }
}

2. ZGC垃圾回收器

/**
 * ZGC垃圾回收器实现分析
 */
public class ZGarbageCollector {
    
    /**
     * ZGC的设计目标和特点
     */
    public void analyzeZGCDesign() {
        /*
         * ZGC设计目标:
         * 
         * 1. 超低延迟:
         *    - 停顿时间小于10ms
         *    - 与堆大小无关的停顿时间
         *    - 支持TB级别堆内存
         * 
         * 2. 着色指针技术:
         * ┌─────────────────────────────────────────────┐
         * │  64位指针布局 (Linux x86_64)                │
         * ├─────────────────────────────────────────────┤
         * │ 63  62  61  60  59 ... 18  17 ... 0        │
         * │ │   │   │   │   │        │                 │
         * │ │   │   │   │   │        └─ 对象地址       │
         * │ │   │   │   │   └────────── 未使用         │
         * │ │   │   │   └─────────────── Remapped位    │
         * │ │   │   └─────────────────── Marked1位     │
         * │ │   └─────────────────────── Marked0位     │
         * │ └─────────────────────────── Finalizable位 │
         * └─────────────────────────────────────────────┘
         * 
         * 3. 读屏障技术:
         *    - 在读取对象引用时检查指针状态
         *    - 如果对象已移动,自动更新引用
         *    - 确保应用始终访问正确的对象
         */
    }
}

💡 实战案例与代码示例

GC调优实战案例

场景1:高并发Web应用GC调优

/**
 * 高并发Web应用GC调优实战
 */
public class HighConcurrencyWebGCTuning {
    
    /**
     * 应用特征分析
     */
    public void analyzeApplicationCharacteristics() {
        /*
         * 应用场景:
         * - 高并发Web应用 (QPS > 10000)
         * - 大量短生命周期对象 (请求对象、响应对象)
         * - 对响应时间敏感 (P99 小于 100ms)
         * - 8GB堆内存,4核8线程服务器
         * 
         * 性能问题:
         * - Young GC频繁,每秒触发2-3次
         * - Full GC偶发,每次停顿500ms+
         * - P99响应时间偶尔超过200ms
         */
    }
    
    /**
     * GC调优方案
     */
    public void implementGCTuningSolution() {
        /*
         * 调优前配置:
         * -Xms8g -Xmx8g
         * -XX:+UseParallelGC (默认)
         * 
         * 调优后配置:
         * # 基础内存设置
         * -Xms8g -Xmx8g
         * 
         * # 使用G1垃圾回收器
         * -XX:+UseG1GC
         * -XX:MaxGCPauseMillis=50          # 目标停顿时间50ms
         * -XX:G1HeapRegionSize=16m         # 分区大小16MB
         * 
         * # 年轻代调优
         * -XX:G1NewSizePercent=30          # 年轻代最小比例30%
         * -XX:G1MaxNewSizePercent=50       # 年轻代最大比例50%
         * 
         * # 并发线程设置
         * -XX:ConcGCThreads=2              # 并发GC线程数
         * -XX:ParallelGCThreads=8          # 并行GC线程数
         * 
         * 调优效果:
         * - Young GC频率:从每秒3次降到每秒1次
         * - GC停顿时间:从平均80ms降到30ms
         * - P99响应时间:从200ms降到80ms
         * - Full GC:基本消除
         */
    }
    
    /**
     * 应用层面优化
     */
    public void implementApplicationOptimization() {
        // ✅ 优化1:对象池化
        private static final ObjectPool<StringBuilder> STRING_BUILDER_POOL = 
            new GenericObjectPool<>(new StringBuilderFactory());
        
        public String processRequest(RequestData request) {
            StringBuilder sb = null;
            try {
                sb = STRING_BUILDER_POOL.borrowObject();
                sb.setLength(0); // 重置
                
                // 使用StringBuilder构建响应
                sb.append("Response for ").append(request.getId());
                return sb.toString();
            } finally {
                if (sb != null) {
                    STRING_BUILDER_POOL.returnObject(sb);
                }
            }
        }
        
        // ✅ 优化2:缓存复用
                        private static final ConcurrentHashMap<String, ResponseData> RESPONSE_CACHE = 
            new ConcurrentHashMap<>();
        
        public ResponseData getCachedResponse(String key) {
            return RESPONSE_CACHE.computeIfAbsent(key, k -> {
                // 只有缓存未命中时才创建新对象
                return createNewResponse(k);
            });
        }
    }
}

场景2:大数据处理应用GC调优

/**
 * 大数据处理应用GC调优实战
 */
public class BigDataProcessingGCTuning {
    
    /**
     * GC调优方案
     */
    public void implementGCTuningSolution() {
        /*
         * 调优配置:
         * # 基础内存设置
         * -Xms32g -Xmx32g
         * 
         * # 使用ZGC (JDK 17+)
         * -XX:+UseZGC
         * -XX:+UnlockExperimentalVMOptions
         * 
         * # 大对象处理
         * -XX:PretenureSizeThreshold=1m    # 1MB以上对象直接进老年代
         * 
         * # 并发设置
         * -XX:ConcGCThreads=8              # 并发GC线程数
         * -XX:ParallelGCThreads=16         # 并行GC线程数
         * 
         * # 内存管理
         * -XX:+UseLargePages               # 使用大页内存
         * -XX:LargePageSizeInBytes=2m      # 大页大小2MB
         * 
         * 调优效果:
         * - GC停顿时间:从10秒降到<10ms (ZGC)
         * - 吞吐量提升:30%+
         * - 内存利用率:提升20%
         */
    }
    
    /**
     * 内存使用优化
     */
    public void implementMemoryOptimization() {
        // ✅ 优化1:流式处理
        public void processLargeDataset(String inputFile) {
            try (Stream<String> lines = Files.lines(Paths.get(inputFile))) {
                lines.parallel()
                     .map(this::parseData)
                     .filter(this::isValidData)
                     .forEach(this::processData);
            } catch (IOException e) {
                logger.error("Error processing file", e);
            }
            // 避免一次性加载所有数据到内存
        }
        
        // ✅ 优化2:堆外缓存
        private static final Cache<String, byte[]> OFF_HEAP_CACHE = 
            CacheBuilder.newBuilder()
                .maximumSize(10000)
                .expireAfterWrite(1, TimeUnit.HOURS)
                .build();
        
        public byte[] getCachedData(String key) {
            return OFF_HEAP_CACHE.get(key, this::loadDataFromDisk);
        }
    }
}

🎯 面试高频问题精讲

核心面试问题解析

问题1:请详细对比G1、CMS、ZGC三种垃圾回收器的特点和适用场景

对比维度CMSG1ZGC
停顿时间100-300ms10-100ms小于10ms
吞吐量90-95%85-90%85-90%
内存开销小于5%10-15%15-20%
堆大小适用范围小于32GB小于128GB无限制
内存碎片问题严重

适用场景选择

  • CMS:对延迟敏感但堆内存不大的应用
  • G1:需要平衡延迟和吞吐量的企业级应用
  • ZGC:对延迟极度敏感的实时系统

问题2:什么是三色标记算法?如何解决并发标记中的漏标问题?

/**
 * 三色标记算法详解
 */
public class TriColorMarkingAlgorithm {
    
    /**
     * 漏标问题产生条件:
     * 1. 黑色对象新增了对白色对象的引用
     * 2. 灰色对象删除了对该白色对象的引用
     * 
     * 解决方案:
     * 1. 增量更新 (Incremental Update) - CMS使用
     * 2. 原始快照 (SATB) - G1使用
     */
}

问题3:G1的Mixed GC是如何工作的?

Mixed GC执行过程:

  • 选择回收集合:所有年轻代分区 + 部分老年代分区
  • 并行回收:使用复制算法回收选中的分区
  • 优先级策略:优先回收垃圾最多的分区

问题4:ZGC的着色指针技术是如何实现的?

/*
 * ZGC着色指针 (64位):
 * ┌─────────────────────────────────────────────┐
 * │ 63  62  61  60  59 ... 18  17 ... 0        │
 * │ │   │   │   │   │        │                 │
 * │ │   │   │   │   │        └─ 对象地址(42位) │
 * │ │   │   │   │   └────────── 未使用         │
 * │ │   │   │   └─────────────── Remapped位    │
 * │ │   │   └─────────────────── Marked1位     │
 * │ │   └─────────────────────── Marked0位     │
 * │ └─────────────────────────── Finalizable位 │
 * └─────────────────────────────────────────────┘
 */

问题5:如何诊断和解决生产环境的GC问题?

诊断步骤:

  • 收集GC日志:使用-XX:+PrintGC -XX:+PrintGCDetails
  • 分析关键指标:GC频率、停顿时间、内存使用、吞吐量
  • 使用分析工具:GCViewer、Eclipse MAT、jstat
  • 识别问题模式:频繁Young GC、长时间Full GC、内存泄漏

⚡ 性能优化与注意事项

GC调优最佳实践

调优原则

  • 优先应用层优化:减少对象创建、复用对象、优化数据结构
  • 选择合适的GC算法:根据堆大小和延迟要求选择
  • 设置合理的堆大小:避免动态扩展开销
  • 渐进式调优:一次只调整一个参数

常见调优参数

# 内存设置
-Xms8g -Xmx8g                    # 堆大小
-XX:NewRatio=2                   # 老年代与年轻代比例

# G1参数
-XX:+UseG1GC                     # 启用G1
-XX:MaxGCPauseMillis=200         # 目标停顿时间
-XX:G1HeapRegionSize=16m         # 分区大小

# 监控设置
-XX:+PrintGC                     # 打印GC信息
-Xloggc:/var/log/gc.log          # GC日志文件

应用层面优化

// ✅ 优化1:减少对象创建
public String goodConcatenation(List<String> items) {
    StringBuilder sb = new StringBuilder();
    for (String item : items) {
        sb.append(item).append(",");
    }
    return sb.toString();
}

// ✅ 优化2:对象复用
private static final ThreadLocal<DateFormat> DATE_FORMAT = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

// ✅ 优化3:及时释放大对象引用
public void processLargeData() {
    byte[] largeArray = new byte[100 * 1024 * 1024];
    try {
        processArray(largeArray);
    } finally {
        largeArray = null;  // 及时释放引用
    }
}

📚 总结与技术对比

垃圾回收器选择指南

应用类型堆大小延迟要求推荐GC理由
小型Web应用小于4GB中等Parallel GC简单高效,吞吐量好
企业级应用4-32GB较高G1 GC平衡延迟和吞吐量
大数据处理32-128GB中等G1 GC支持大堆,可预测停顿
实时系统任意极高ZGC超低延迟,小于10ms停顿
高频交易任意极高ZGC + 应用优化亚毫秒级延迟

核心技术要点总结

算法演进路径

  • 标记-清除 → 复制算法 → 标记-整理
  • 串行 → 并行 → 并发 → 低延迟
  • 分代假设 → 分区管理 → 着色指针

2. 性能优化策略

  • 应用层优化:减少对象创建、复用对象、优化数据结构
  • GC参数调优:选择合适的收集器、设置合理的堆大小和停顿目标
  • 监控和诊断:收集GC日志、分析性能指标、定位问题根因

3. 未来发展趋势

  • 更低延迟:ZGC、Shenandoah等低延迟收集器持续优化
  • 更大堆支持:支持TB级别堆内存的管理
  • 智能化调优:自适应参数调整和性能优化
  • 云原生适配:适应容器化和微服务架构

最佳实践建议

开发阶段

  • 遵循良好的编程习惯,减少不必要的对象创建
  • 合理使用缓存和对象池
  • 及时释放大对象引用

测试阶段

  • 进行压力测试,模拟生产环境负载
  • 收集和分析GC日志
  • 验证性能指标是否满足要求

生产阶段

  • 持续监控GC性能指标
  • 建立GC问题的告警机制
  • 定期分析和优化GC配置

通过深入理解垃圾回收算法的原理和实现,结合实际的调优经验,我们可以为不同类型的Java应用选择最适合的GC策略,实现性能的最大化。记住,GC调优是一个持续的过程,需要根据应用的实际运行情况不断调整和优化。

Comments

Link copied to clipboard!