一场由deepClone引发的血案——log4j日志停写问题的排查和原理

本文详细记录了一次因第三方Jar包导致的日志系统停写问题排查过程。问题出现在使用Cloner进行深克隆时,错误地复制了Log对象中的FileOutputStream,导致日志句柄在对象被回收时失效。

一个月前系统开始发生一个奇怪的现象,log4j日志会突然停写,开始并没有很关注,总以为是系统的原因,后来发现在多台机器上依然发生,这个问题开始被重视。

 

说一下问题排查的路径:

 

 

  1. 最开始怀疑是org.apache.log4j.DailyRollingFileAppender的问题,换成org.apache.log4j.FileAppender后,问题依然
  2. 工程里日志包冲突的问题,因为工程里有log4j,sel4j,logback等多个包,将self4j和logback的依赖去掉后,问题依然。
  3. 也换过commons-logging包,log4j包的版本,还是无效
系统的内存,磁盘,CPU,句柄数全部正常...
 
经过多天的观察,终于发现日志停写的规律,每天早上8点开始停写(由于是org.apache.log4j.DailyRollingFileAppender,每天都会生成一个新的文件)。8点时,有一个任务,调用了一些接口,经过排查,终于定位到一个修改的方法,每次这个修改方法执行的时候,日志就会被停写。为什么普通的业务代码会造成日志的停写呢?
 
修改方法里有这样一段
 
		Cloner cloner = new Cloner();
		PricingCpmTrans pricingCpmTrans = cloner.deepClone(oldPricingCpmTrans);
		
 这里使用了一个第三方jar,Cloner(罪魁祸首啊),而在PricingCpmTrans里有一个Log对象
 
 我们看下Cloner是怎么做这个deepClone的
  1.  先将对象newInstance出来
  2.  然后递归的查找对象里的所有属性,并赋值
既然这样,那PricingCpmTrans里的Log对象也会被复制成一个新的,那如果有问题,也最多是这个对象的日志有问题,为什么所有的日志都不打了呢?
 
让我们接着看Log类里有什么?
 
Log里最后写日志是使用 FileOutputStream 进行,里面有一段:
 
    private static native void initIDs();
    
    static {
	initIDs();
    }
 
 
而initIDs做了什么?
 
JNIEXPORT void JNICALL
        Java_sun_nio_ch_IOUtil_initIDs(JNIEnv *env, jclass clazz)
        {
            OSVERSIONINFO ver;
            ver.dwOSVersionInfoSize = sizeof(ver);
            GetVersionEx(&ver);
            if (ver.dwPlatformId == VER_PLATFORM_WIN32_NT) {
               onNT = JNI_TRUE;
            }
        
            clazz = (*env)->FindClass(env, "java/io/FileDescriptor");
            fd_fdID = (*env)->GetFieldID(env, clazz, "fd", "I");
            handle_fdID = (*env)->GetFieldID(env, clazz, "handle", "J");
        }
 
 
系统生成一个指针指向 FileOutputStream 的 FileDescriptor(文件描述符)里的fd,也就是说文件对象里会持有一个系统的文件描述符的数字id
 
但在deepClone时,虽然FileOutputStream 对象被复制了,但这个数字ID是int的,所以还是这个数字ID当 PricingCpmTrans 被系统垃圾回收时 FileOutputStream 也被系统回收了
 
    protected void finalize() throws IOException {
        if (fd != null) {
            if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
                flush();
            } else {

                /*
                 * Finalizer should not release the FileDescriptor if another
                 * stream is still using it. If the user directly invokes
                 * close() then the FileDescriptor is also released.
                 */
                runningFinalize.set(Boolean.TRUE);
                try {
                    close();
                } finally {
                    runningFinalize.set(Boolean.FALSE);
                }
            }
        }
    }

    private native void close0() throws IOException;
 系统回收调用了close方法
 
所以当对象被回收时,指向文件的指针被删除了,导致虽然原来的log对象持有着这个句柄,但系统中这个句柄已经被删除了,导致日志再也无法输出,如果用控制台输入可以捕获到这个异常
 
java.io.IOException: 句柄无效。
	at java.io.FileOutputStream.writeBytes(Native Method)
	at java.io.FileOutputStream.write(FileOutputStream.java:282)
	at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:202)
	at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:272)
	at sun.nio.cs.StreamEncoder.implFlush(StreamEncoder.java:276)
	at sun.nio.cs.StreamEncoder.flush(StreamEncoder.java:122)
	at java.io.OutputStreamWriter.flush(OutputStreamWriter.java:212)
	at org.apache.log4j.helpers.QuietWriter.flush(QuietWriter.java:57)
 

再看下文件描述符的结构图,可能更好理解了

 

 

进程到文件表的关系还在,但文件表到具体的文件节点的关系已经被丢失了,导致日志无法输出。

 

 

总结下这次的经验教训:

 

  1. 使用第三方包一定要慎重
  2. 查问题还是要先让它重现
  3. 任何说系统问题或是网络问题来掩盖异常的都是耍流氓

 

<think>好的,用户想知道如何手动实现深拷贝以及背后的原理。首先,我得回忆一下深拷贝浅拷贝的区别。浅拷贝只复制对象的顶层属性,如果属性是引用类型,拷贝后的对象原对象会共享这些引用,修改其中一个会影响另一个。而深拷贝是递归复制所有层级的对象,完全独立,互影响。用户需要的是手深拷贝的方法,所以得考虑如何递归处理各种数据类型。 接下来,我需要确定哪些数据类型需要特殊处理。比如基本类型(number、string、boolean等)可以直接返回,而对象数组需要递归处理。此外,还要处理循环引用的情况,避免无限递归导致栈溢出。这时候可能需要一个WeakMap来存储已经拷贝过的对象,每次拷贝前先检查是否已经存在,如果存在就直接返回存储的副本,防止循环引用的问题。 然后,考虑如何判断数据类型。可以用typeof判断基本类型,但要注意null的情况,因为typeof null返回的是'object',所以需要单独处理。对于对象,需要区分普通对象、数组、日期、正则等内置对象,因为这些对象的拷贝方式可能同。比如,日期对象可以通过new Date(value)来拷贝,正则表达式可以通过new RegExp(source, flags)来复制。 接下来,编递归函数。函数的大致步骤是:先处理基本类型null,然后处理特殊对象(如Date、RegExp),接着处理数组普通对象。对于数组,遍历每个元素,递归拷贝;对于对象,遍历所有属性,同样递归拷贝每个属性值。同时,使用WeakMap记录已经拷贝过的对象,遇到循环引用时直接返回已拷贝的副本。 用户可能还关心如何验证深拷贝的正确性。可以在示例中创建包含嵌套对象循环引用的结构,然后修改原对象,检查拷贝后的对象是否受到影响。如果深拷贝成功,修改原对象会影响拷贝后的对象。 最后,要注意代码的健壮性,处理各种边界情况,比如函数、Symbol等。过用户可能主要关注对象数组的深拷贝,所以示例中可以暂时忽略这些复杂情况,或者简单处理,比如直接返回函数本身,或者抛出错误。 现在,把这些思路整理成代码示例,并解释每一步的原理,确保用户能够理解递归过程循环引用的处理方式。同时,引用相关的资料,比如提到JSON方法的局限性,为什么需要手动实现深拷贝,以及WeakMap的作用,这样回答会更全面。</think># 深拷贝实现原理与手示例 ## 一、实现原理 1. **递归遍历**:通过递归遍历对象所有层级的属性,直到遇到基本数据类型(数字、字符串、布尔值等)为止[^1] 2. **类型判别**:需处理数组、日期、正则等特殊对象类型,例如日期对象需通过`new Date(value)`重建 3. **循环引用处理**:使用`WeakMap`存储已拷贝对象,解决对象内部循环引用导致的无限递归问题 4. **原型链保留**:通过`Object.create(Object.getPrototypeOf(obj))`保持原型链特性 ## 二、手实现代码 ```javascript function deepClone(target, map = new WeakMap()) { // 处理基础类型null if (target === null || typeof target !== 'object') return target // 处理循环引用 if (map.has(target)) return map.get(target) // 处理特殊对象类型 if (target instanceof Date) return new Date(target) if (target instanceof RegExp) return new RegExp(target) // 创建新对象并记录映射关系 const cloneObj = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target)) map.set(target, cloneObj) // 递归拷贝属性 for (let key in target) { if (target.hasOwnProperty(key)) { cloneObj[key] = deepClone(target[key], map) } } return cloneObj } ``` ## 三、验证示例 ```javascript const obj = { a: 1, b: [2, { c: 3 }], d: new Date(), e: /regexp/gi } obj.self = obj // 创建循环引用 const cloned = deepClone(obj) console.log(cloned.b !== obj.b) // true console.log(cloned.d !== obj.d) // true console.log(cloned.e !== obj.e) // true console.log(cloned.self === cloned) // true ``` ## 四、实现要点 1. **WeakMap优势**:相比普通Map,WeakMap使用弱引用,会阻止垃圾回收,避免内存泄漏[^2] 2. **原型链处理**:`Object.create()`方法比直接`{}`创建更准确保留原型信息 3. **属性过滤**:通过`hasOwnProperty`过滤原型链上的属性 4. **特殊对象处理**:需要单独处理Date、RegExp等内置对象才能正确拷贝
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值