netty 资源泄漏探测器

netty 通道接口定义:[url]http://donald-draper.iteye.com/blog/2392740[/url]
netty 抽象通道初始化:[url]http://donald-draper.iteye.com/blog/2392801[/url]
netty 抽象Unsafe定义:[url]http://donald-draper.iteye.com/blog/2393053[/url]
netty 通道Outbound缓冲区:[url]http://donald-draper.iteye.com/blog/2393098[/url]
netty 抽象通道后续:[url]http://donald-draper.iteye.com/blog/2393166[/url]
netty 抽象nio通道:[url]http://donald-draper.iteye.com/blog/2393269[/url]
netty 抽象nio字节通道:[url]http://donald-draper.iteye.com/blog/2393323[/url]
netty 抽象nio消息通道:[url]http://donald-draper.iteye.com/blog/2393364[/url]
netty NioServerSocketChannel解析:[url]http://donald-draper.iteye.com/blog/2393443[/url]
netty 通道配置接口定义:[url]http://donald-draper.iteye.com/blog/2393484[/url]
netty 默认通道配置初始化:[url]http://donald-draper.iteye.com/blog/2393504[/url]
netty 默认通道配置后续:[url]http://donald-draper.iteye.com/blog/2393510[/url]
netty 字节buf定义:[url]http://donald-draper.iteye.com/blog/2393813[/url]
[size=medium][b]引言[/b][/size]
上一篇文章我们看了字节buf接口的定义,先来回顾一下:
对象引用计数器ReferenceCounted,主要记录对象的引用数量,当引用数量为0时,表示可以回收对象,在调试模式下,如果发现对象出现内存泄漏,可以用touch方法记录操作的相关信息,通过ResourceLeakDetector获取操作的相关信息,以便分析内存泄漏的原因。

字节缓存ByteBuf继承了对象引用计数器ReferenceCounted,拥有一个最大容量限制,如果用户尝试用 #capacity(int)和 #ensureWritable(int)方法,增加buf容量超过最大容量,将会抛出非法参数异常;字节buf有两个索引,一个为读索引readerIndex,一个为写索引writerIndex,读索引不能大于写索引,写索引不能小于读索引,buf可读字节数为writerIndex - readerIndex,buf可写字节数为capacity - writerIndex,buf可写的最大字节数为maxCapacity - writerIndex;

可以使用markReader/WriterIndex标记当前buf读写索引位置,resetReader/WriterIndex方法可以重回先前标记的索引位置;

当内存空间负载过度时,我们可以使用discardReadBytes丢弃一些数据,以节省空间;

我们可以使用ensureWritable检测当buf是否有足够的空间写数据;

提供了getBytes方法,可以将buf中的数据转移到目的ByteBuf,Byte数组,Nio字节buf ByteBuffer,OutputStream,聚集字节通道
GatheringByteChannel和文件通道FileChannel中,这些方法不会修改当前buf读写索引,具体是否修改目的对象索引或位置,见java doc 描述。

提供了setBytes方法,可以将源ByteBuf,Byte数组,Nio字节buf ByteBuffer,InputputStream,分散字节通道ScatteringByteChannel和文件通道FileChannel中的数据转移到当前buf中,这些方法不会修改当前buf的读写索引,至于源对象索引或位置,见java doc 描述。

提供了readBytes方法,可以将buf中的数据转移到目的ByteBuf,Byte数组,Nio字节buf ByteBuffer,OutputStream,聚集字节通道GatheringByteChannel和文件通道FileChannel中,这些方法具体会会修改当前buf读索引,至于会不会修改源对象索引或位置,见java doc 描述。

提供了writeBytes方法,可以将源ByteBuf,Byte数组,Nio字节buf ByteBuffer,
InputputStream,分散字节通道ScatteringByteChannel和文件通道FileChannel中的数据写到当前buf中,这些方法会修改当前buf的写索引,至于会不会修改源对象索引或位置,见java
doc 描述。


set*原始类型方法不会修改读写索引;
get*原始类型方法不会修改读写索引;

write*原始类型方法会修改写索引;
read*原始类型方法,会修改读索引;

字节buf中的set/get*方法不会修改当前buf的读写索引,而write*修改写索引,read*会修改读索引;

提供了copy,slice和retainSlice,duplicate和retainedDuplicate方法,用于拷贝,切割,复制当前buf数据,retained*方法会增加buf的
引用计数器;

提供nioBuffer和nioBuffers方法,用于包装当前buf可读数据为java nio ByteBuffer和ByteBuffer数组。

今天我们来看一下抽象字节buf的定义

/**
* A skeletal implementation of a buffer.
*/
public abstract class AbstractByteBuf extends ByteBuf {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractByteBuf.class);
private static final String PROP_MODE = "io.netty.buffer.bytebuf.checkAccessible";
private static final boolean checkAccessible;

static {
checkAccessible = SystemPropertyUtil.getBoolean(PROP_MODE, true);
if (logger.isDebugEnabled()) {
logger.debug("-D{}: {}", PROP_MODE, checkAccessible);
}
}
//内存泄漏探测器
static final ResourceLeakDetector<ByteBuf> leakDetector =
ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ByteBuf.class);

int readerIndex;//读索引
int writerIndex;//写索引
private int markedReaderIndex;//读索引标记
private int markedWriterIndex;//写索引标记
private int maxCapacity;//最大容量

protected AbstractByteBuf(int maxCapacity) {
if (maxCapacity < 0) {
throw new IllegalArgumentException("maxCapacity: " + maxCapacity + " (expected: >= 0)");
}
this.maxCapacity = maxCapacity;
}:
}

从上面来看,字节buf内部有两个索引,一个读索引,一个写索引,两个索引标记,即读写索引对应的标记,buf的最大容量为maxCapacity;buf的构造,主要是初始化最大容量。

在抽象字节buf的内部变量声明中有一个资源泄漏探测器,定义如下:
public abstract class AbstractByteBuf extends ByteBuf {

//内存泄漏探测器
static final ResourceLeakDetector<ByteBuf> leakDetector =
ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ByteBuf.class);
...
}

来看内存探测器工厂
//ResourceLeakDetectorFactory
/**
* This static factory should be used to load {@link ResourceLeakDetector}s as needed
*/
public abstract class ResourceLeakDetectorFactory {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(ResourceLeakDetectorFactory.class);
//默认资源泄漏探测器工厂
private static volatile ResourceLeakDetectorFactory factoryInstance = new DefaultResourceLeakDetectorFactory();

/**
* Get the singleton instance of this factory class.
*获取资源泄漏探测器工厂
* @return the current {@link ResourceLeakDetectorFactory}
*/
public static ResourceLeakDetectorFactory instance() {
return factoryInstance;
}

/**
* Set the factory's singleton instance. This has to be called before the static initializer of the
* {@link ResourceLeakDetector} is called by all the callers of this factory. That is, before initializing a
* Netty Bootstrap.
*设置资源泄漏探测器工厂单例。此方法必须在素有的工作调用者,初始化ResourceLeakDetector前调用,
即在netty的引导启动配置前初始化
* @param factory the instance that will become the current {@link ResourceLeakDetectorFactory}'s singleton
*/
public static void setResourceLeakDetectorFactory(ResourceLeakDetectorFactory factory) {
factoryInstance = ObjectUtil.checkNotNull(factory, "factory");
}

/**
* Returns a new instance of a {@link ResourceLeakDetector} with the given resource class.
*使用给定的资源类,创建要给资源泄漏探测器
* @param resource the resource class used to initialize the {@link ResourceLeakDetector}
* @param <T> the type of the resource class
* @return a new instance of {@link ResourceLeakDetector}
*/
public final <T> ResourceLeakDetector<T> newResourceLeakDetector(Class<T> resource) {
return newResourceLeakDetector(resource, ResourceLeakDetector.DEFAULT_SAMPLING_INTERVAL);
}
/**
* Returns a new instance of a {@link ResourceLeakDetector} with the given resource class.
*使用给定的资源类和采样间隔,创建要给资源泄漏探测器
* @param resource the resource class used to initialize the {@link ResourceLeakDetector}
* @param samplingInterval the interval on which sampling takes place
* @param <T> the type of the resource class
* @return a new instance of {@link ResourceLeakDetector}
*/
@SuppressWarnings("deprecation")
public <T> ResourceLeakDetector<T> newResourceLeakDetector(Class<T> resource, int samplingInterval) {
return newResourceLeakDetector(resource, ResourceLeakDetector.DEFAULT_SAMPLING_INTERVAL, Long.MAX_VALUE);
}
/**
* @deprecated Use {@link #newResourceLeakDetector(Class, int)} instead.
* <p>
* Returns a new instance of a {@link ResourceLeakDetector} with the given resource class.
*待子类扩展
* @param resource the resource class used to initialize the {@link ResourceLeakDetector}
* @param samplingInterval the interval on which sampling takes place,采样间隔
* @param maxActive This is deprecated and will be ignored. 此参数已经丢弃,将会被忽略
* @param <T> the type of the resource class 资源类,类型
* @return a new instance of {@link ResourceLeakDetector}
*/
@Deprecated
public abstract <T> ResourceLeakDetector<T> newResourceLeakDetector(
Class<T> resource, int samplingInterval, long maxActive);

/**
* Default implementation that loads custom leak detector via system property
默认资源泄漏探测器工厂
*/
private static final class DefaultResourceLeakDetectorFactory extends ResourceLeakDetectorFactory {
private final Constructor<?> obsoleteCustomClassConstructor;
private final Constructor<?> customClassConstructor;

DefaultResourceLeakDetectorFactory() {
String customLeakDetector;
try {
//在当前线程相同访问控制权限下,获取系统io.netty.customResourceLeakDetector属性配置
customLeakDetector = AccessController.doPrivileged(new PrivilegedAction<String>() {
@Override
public String run() {
return SystemPropertyUtil.get("io.netty.customResourceLeakDetector");
}
});
} catch (Throwable cause) {
logger.error("Could not access System property: io.netty.customResourceLeakDetector", cause);
customLeakDetector = null;
}
if (customLeakDetector == null) {
//如果系统泄漏测器配置为空,则obsoleteCustom和custom构造为空
obsoleteCustomClassConstructor = customClassConstructor = null;
} else {
//否则初始泄漏探测器构造
obsoleteCustomClassConstructor = obsoleteCustomClassConstructor(customLeakDetector);
customClassConstructor = customClassConstructor(customLeakDetector);
}
}

private static Constructor<?> obsoleteCustomClassConstructor(String customLeakDetector) {
try {
//加载泄漏探测器类
final Class<?> detectorClass = Class.forName(customLeakDetector, true,
PlatformDependent.getSystemClassLoader());

if (ResourceLeakDetector.class.isAssignableFrom(detectorClass)) {
//构造资源泄漏探测器
return detectorClass.getConstructor(Class.class, int.class, long.class);
} else {
logger.error("Class {} does not inherit from ResourceLeakDetector.", customLeakDetector);
}
} catch (Throwable t) {
logger.error("Could not load custom resource leak detector class provided: {}",
customLeakDetector, t);
}
return null;
}

private static Constructor<?> customClassConstructor(String customLeakDetector) {
try {
//加载泄漏探测器类
final Class<?> detectorClass = Class.forName(customLeakDetector, true,
PlatformDependent.getSystemClassLoader());
//构造资源泄漏探测器
if (ResourceLeakDetector.class.isAssignableFrom(detectorClass)) {
return detectorClass.getConstructor(Class.class, int.class);
} else {
logger.error("Class {} does not inherit from ResourceLeakDetector.", customLeakDetector);
}
} catch (Throwable t) {
logger.error("Could not load custom resource leak detector class provided: {}",
customLeakDetector, t);
}
return null;
}
//根据资源类和采样间隔,创建资源泄漏探测器
@Override
public <T> ResourceLeakDetector<T> newResourceLeakDetector(Class<T> resource, int samplingInterval) {
if (customClassConstructor != null) {
try {
@SuppressWarnings("unchecked")
//使用customClassConstructor创建资源泄漏探测器
ResourceLeakDetector<T> leakDetector =
(ResourceLeakDetector<T>) customClassConstructor.newInstance(resource, samplingInterval);
logger.debug("Loaded custom ResourceLeakDetector: {}",
customClassConstructor.getDeclaringClass().getName());
return leakDetector;
} catch (Throwable t) {
logger.error(
"Could not load custom resource leak detector provided: {} with the given resource: {}",
customClassConstructor.getDeclaringClass().getName(), resource, t);
}
}
//直接创建资源泄漏探测器ResourceLeakDetector
ResourceLeakDetector<T> resourceLeakDetector = new ResourceLeakDetector<T>(resource, samplingInterval);
logger.debug("Loaded default ResourceLeakDetector: {}", resourceLeakDetector);
return resourceLeakDetector;
}
@SuppressWarnings("deprecation")
@Override
public <T> ResourceLeakDetector<T> newResourceLeakDetector(Class<T> resource, int samplingInterval,
long maxActive) {
if (obsoleteCustomClassConstructor != null) {
try {
//使用obsoleteCustomClassConstructor创建资源泄漏探测器
@SuppressWarnings("unchecked")
ResourceLeakDetector<T> leakDetector =
(ResourceLeakDetector<T>) obsoleteCustomClassConstructor.newInstance(
resource, samplingInterval, maxActive);
logger.debug("Loaded custom ResourceLeakDetector: {}",
obsoleteCustomClassConstructor.getDeclaringClass().getName());
return leakDetector;
} catch (Throwable t) {
logger.error(
"Could not load custom resource leak detector provided: {} with the given resource: {}",
obsoleteCustomClassConstructor.getDeclaringClass().getName(), resource, t);
}
}
//直接创建资源泄漏探测器ResourceLeakDetector
ResourceLeakDetector<T> resourceLeakDetector = new ResourceLeakDetector<T>(resource, samplingInterval,
logger.debug("Loaded default ResourceLeakDetector: {}", resourceLeakDetector);
return resourceLeakDetector;
}


}
}

从上面可以看出,默认的资源泄漏探测器工厂创建的资源泄漏探测器类型ResourceLeakDetector。


public class ResourceLeakDetector<T> {

private static final String PROP_LEVEL_OLD = "io.netty.leakDetectionLevel";
private static final String PROP_LEVEL = "io.netty.leakDetection.level";
private static final Level DEFAULT_LEVEL = Level.SIMPLE;//默认探测登记

private static final String PROP_MAX_RECORDS = "io.netty.leakDetection.maxRecords";
private static final int DEFAULT_MAX_RECORDS = 4;//默认最大的资源泄漏记录线索数
private static final int MAX_RECORDS;//最大的资源泄漏记录线索数

/**
* Represents the level of resource leak detection.
资源泄漏等级
*/
public enum Level {
/**
* Disables resource leak detection.
关闭资源泄漏探测
*/
DISABLED,
/**
* Enables simplistic sampling resource leak detection which reports there is a leak or not,
* at the cost of small overhead (default).
开启简单的采样资源泄漏探测,仅仅已少量的负载为代价报告是否为泄漏对象
*/
SIMPLE,
/**
* Enables advanced sampling resource leak detection which reports where the leaked object was accessed
* recently at the cost of high overhead.
开启高级采样泄漏探测,将会以高负载为代价,报告最近泄漏对象访问的地方
*/
ADVANCED,
/**
* Enables paranoid resource leak detection which reports where the leaked object was accessed recently,
* at the cost of the highest possible overhead (for testing purposes only).
开启终极采样泄漏探测,将会以高级负载为代价,报告最近泄漏对象访问的地方
*/
PARANOID;

/**
* Returns level based on string value. Accepts also string that represents ordinal number of enum.
*返回泄漏探测等级字符串。同时接受表示enum原始序号的字符串。
即根据等级字符串,返回对应的泄漏探测等级
* @param levelStr - level string : DISABLED, SIMPLE, ADVANCED, PARANOID. Ignores case.
* @return corresponding level or SIMPLE level in case of no match.
*/
static Level parseLevel(String levelStr) {
String trimmedLevelStr = levelStr.trim();
for (Level l : values()) {
if (trimmedLevelStr.equalsIgnoreCase(l.name()) || trimmedLevelStr.equals(String.valueOf(l.ordinal()))) {
return l;
}
}
return DEFAULT_LEVEL;
}
}

private static Level level; //当前static资源泄漏等级

private static final InternalLogger logger = InternalLoggerFactory.getInstance(ResourceLeakDetector.class);
//初始化资源泄漏探测状态及探测等级
static {
final boolean disabled;
//获取系统资源泄漏探测配置,判断是否开启资源探测
if (SystemPropertyUtil.get("io.netty.noResourceLeakDetection") != null) {
disabled = SystemPropertyUtil.getBoolean("io.netty.noResourceLeakDetection", false);
logger.debug("-Dio.netty.noResourceLeakDetection: {}", disabled);
logger.warn(
"-Dio.netty.noResourceLeakDetection is deprecated. Use '-D{}={}' instead.",
PROP_LEVEL, DEFAULT_LEVEL.name().toLowerCase());
} else {
disabled = false;
}
//关闭,则等级为DISABLED,否则为默认等级
Level defaultLevel = disabled? Level.DISABLED : DEFAULT_LEVEL;

// First read old property name
String levelStr = SystemPropertyUtil.get(PROP_LEVEL_OLD, defaultLevel.name());

// If new property name is present, use it
levelStr = SystemPropertyUtil.get(PROP_LEVEL, levelStr);
Level level = Level.parseLevel(levelStr);

MAX_RECORDS = SystemPropertyUtil.getInt(PROP_MAX_RECORDS, DEFAULT_MAX_RECORDS);

ResourceLeakDetector.level = level;
if (logger.isDebugEnabled()) {
logger.debug("-D{}: {}", PROP_LEVEL, level.name().toLowerCase());
logger.debug("-D{}: {}", PROP_MAX_RECORDS, MAX_RECORDS);
}
}

// Should be power of two. 默认采样间隔
static final int DEFAULT_SAMPLING_INTERVAL = 128;



/** the collection of active resources 激活资源集合*/
private final ConcurrentMap<DefaultResourceLeak, LeakEntry> allLeaks = PlatformDependent.newConcurrentHashMap();
//资源写对象引用队列
private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
//资源泄漏信息报告
private final ConcurrentMap<String, Boolean> reportedLeaks = PlatformDependent.newConcurrentHashMap();
private final String resourceType;//资源类型
private final int samplingInterval;//采样间隔
}

从上面可以看出,资源泄漏探测器,探测等级有四种,关闭DISABLED,SIMPLE简单,高级ADVANCED,终极PARANOID,SIMPLE开启简单的采样资源泄漏探测,仅仅已少量的负载为代价报告是否为泄漏对象;ADVANCED开启高级采样泄漏探测,将会以高负载为代价,报告最近泄漏对象访问的地方;PARANOID开启终极采样泄漏探测,将会以高级负载为代价,报告最近泄漏对象访问的地方(仅仅测试);开启资源探测的情况下,默认等级为SIMPLE。资源探测器内部有一个探测等级和采样间隔,资源类型,泄漏报告Map( ConcurrentMap<String, Boolean>),
激活资源集合ConcurrentMap<DefaultResourceLeak, LeakEntry>,

再来看构造
/**
* @deprecated use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}.
根据资源类型创建资源泄漏探测器
*/
@Deprecated
public ResourceLeakDetector(Class<?> resourceType) {
this(simpleClassName(resourceType));
}

/**
* @deprecated use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}.
根据资源类型名创建资源泄漏探测器
*/
@Deprecated
public ResourceLeakDetector(String resourceType) {
this(resourceType, DEFAULT_SAMPLING_INTERVAL, Long.MAX_VALUE);
}

/**
* @deprecated Use {@link ResourceLeakDetector#ResourceLeakDetector(Class, int)}.
此方法已丢弃,请使用#ResourceLeakDetector(Class, int)方法
* <p>
* This should not be used directly by users of {@link ResourceLeakDetector}.
* Please use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class)}
* or {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}
*用于不可直接调用此方法,请使用ResourceLeakDetectorFactory#newResourceLeakDetector(Class)
或ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)方法
* @param maxActive This is deprecated and will be ignored.
*/
@Deprecated
public ResourceLeakDetector(Class<?> resourceType, int samplingInterval, long maxActive) {
this(resourceType, samplingInterval);
}

/**
* This should not be used directly by users of {@link ResourceLeakDetector}.
* Please use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class)}
* or {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}
*/
@SuppressWarnings("deprecation")
public ResourceLeakDetector(Class<?> resourceType, int samplingInterval) {
this(simpleClassName(resourceType), samplingInterval, Long.MAX_VALUE);
}
/**
* @deprecated use {@link ResourceLeakDetectorFactory#newResourceLeakDetector(Class, int, long)}.
* <p>
* @param maxActive This is deprecated and will be ignored.
*/
@Deprecated
public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) {
if (resourceType == null) {
throw new NullPointerException("resourceType");
}

this.resourceType = resourceType;
this.samplingInterval = samplingInterval;
}


从上面来看资源泄漏探测器构造,主要初始化资源类型名及探测间隔。

再来看开启资源泄漏探测功能和设置探测等级

/**
* @deprecated Use {@link #setLevel(Level)} instead.
开启资源泄漏探测
*/
@Deprecated
public static void setEnabled(boolean enabled) {
setLevel(enabled? Level.SIMPLE : Level.DISABLED);
}


/**
* Returns {@code true} if resource leak detection is enabled.
判断是否开启资源泄漏探测
*/
public static boolean isEnabled() {
//如果探测等级大于DISABLED,则开启
return getLevel().ordinal() > Level.DISABLED.ordinal();
}

我们来看一下枚举的ordinal方法
//Enum
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
/**
* The name of this enum constant, as declared in the enum declaration.
* Most programmers should use the {@link #toString} method rather than
* accessing this field.
枚举名,一般使用#toString方法获取,而不是直接方法此field
*/
private final String name;
/**
* The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*枚举常量在定义中的位置
* Most programmers will have no use for this field. It is designed
* for use by sophisticated enum-based data structures, such as
* {@link java.util.EnumSet} and {@link java.util.EnumMap}.
*/
private final int ordinal;

/**
* Returns the name of this enum constant, exactly as declared in its
* enum declaration.
*
* <b>Most programmers should use the {@link #toString} method in
* preference to this one, as the toString method may return
* a more user-friendly name.</b> This method is designed primarily for
* use in specialized situations where correctness depends on getting the
* exact name, which will not vary from release to release.
*
* @return the name of this enum constant
*/
public final String name() {
return name;
}
/**
* Returns the ordinal of this enumeration constant (its position
* in its enum declaration, where the initial constant is assigned
* an ordinal of zero).
*获取枚举常量在定义中的位置
* Most programmers will have no use for this method. It is
* designed for use by sophisticated enum-based data structures, such
* as {@link java.util.EnumSet} and {@link java.util.EnumMap}.
*
* @return the ordinal of this enumeration constant
*/
public final int ordinal() {
return ordinal;
}
...
}

回到资源泄漏探测器
/**
* Sets the resource leak detection level.
设置资源泄漏探测等级
*/
public static void setLevel(Level level) {
if (level == null) {
throw new NullPointerException("level");
}
ResourceLeakDetector.level = level;
}

/**
* Returns the current resource leak detection level.
获取资源泄漏探测等级
*/
public static Level getLevel() {
return level;
}

再来看其他方法:
/**
* Creates a new {@link ResourceLeak} which is expected to be closed via {@link ResourceLeak#close()} when the
* related resource is deallocated.
*创建一个资源泄漏ResourceLeak,当相关资源释放时,希望通过ResourceLeak#close关闭,已丢弃
* @return the {@link ResourceLeak} or {@code null}
* @deprecated use {@link #track(Object)}
*/
@Deprecated
public final ResourceLeak open(T obj) {
return track0(obj);
}

/**
* Creates a new {@link ResourceLeakTracker} which is expected to be closed via
* {@link ResourceLeakTracker#close(Object)} when the related resource is deallocated.
*创建一个资源泄漏ResourceLeakTracker,当相关资源释放时,希望通过ResourceLeakTracker#close(Object)关闭对象
* @return the {@link ResourceLeakTracker} or {@code null}
*/
public final ResourceLeakTracker<T> track(T obj) {
return track0(obj);
}

private DefaultResourceLeak track0(T obj) {
...
}


为了便于理解我们先看ResourceLeak,ResourceLeakTracker,DefaultResourceLeak的定义

//ResourceLeak
package io.netty.util;

/**
* @deprecated please use {@link ResourceLeakTracker} as it may lead to false-positives.
已丢弃,请使用ResourceLeakTracker
*/
@Deprecated
public interface ResourceLeak {
/**
* Records the caller's current stack trace so that the {@link ResourceLeakDetector} can tell where the leaked
* resource was accessed lastly. This method is a shortcut to {@link #record(Object) record(null)}.
记录当前调用者栈追踪,以便资源泄漏探测器可以告诉,泄漏资源最近访问的地方。此方法为
#record(Object)的快捷方式record(null)
*/
void record();

/**
* Records the caller's current stack trace and the specified additional arbitrary information
* so that the {@link ResourceLeakDetector} can tell where the leaked resource was accessed lastly.
记录当前调用者栈追踪和额外的主观信息,以便资源泄漏探测器可以告诉,泄漏资源最近访问的地方。
*/
void record(Object hint);

/**
* Close the leak so that {@link ResourceLeakDetector} does not warn about leaked resources.
*关闭泄漏资源,以便资源泄漏探测器不在警告泄漏资源。
* @return {@code true} if called first time, {@code false} if called already
第一次调用返回true,否则为false
*/
boolean close();
}

//ResourceLeakTracker

package io.netty.util;

public interface ResourceLeakTracker<T> {

/**
* Records the caller's current stack trace so that the {@link ResourceLeakDetector} can tell where the leaked
* resource was accessed lastly. This method is a shortcut to {@link #record(Object) record(null)}.
记录当前调用者栈追踪,以便资源泄漏探测器可以告诉,泄漏资源最近访问的地方。此方法为
#record(Object)的快捷方式record(null)
*/
void record();

/**
* Records the caller's current stack trace and the specified additional arbitrary information
* so that the {@link ResourceLeakDetector} can tell where the leaked resource was accessed lastly.
记录当前调用者栈追踪和额外的主观信息,以便资源泄漏探测器可以告诉,泄漏资源最近访问的地方。
*/
void record(Object hint);

/**
* Close the leak so that {@link ResourceLeakTracker} does not warn about leaked resources.
* After this method is called a leak associated with this ResourceLeakTracker should not be reported.
*关闭泄漏资源,以便资源泄漏探测器不在警告泄漏资源。在方法调用后,泄漏资源关联的ResourceLeakTracker不应该
上报。
* @return {@code true} if called first time, {@code false} if called already
第一次调用返回true,否则为false
*/
boolean close(T trackedObject);
}

从上面可以看出,资源泄漏追踪ResourceLeakTracker,主要定义了记录当前调用者栈追踪和额外的主观信息方法,以便资源泄漏探测器可以告诉,泄漏资源最近访问的地方;关闭泄漏资源方法,以便资源泄漏探测器不在警告泄漏资源。

//DefaultResourceLeak,为资源泄漏探测器ResourceLeakDetector的内部类

@SuppressWarnings("deprecation")
private final class DefaultResourceLeak extends PhantomReference<Object> implements ResourceLeakTracker<T>,
ResourceLeak {
private final String creationRecord;//创建记录
private final Deque<String> lastRecords = new ArrayDeque<String>();//记录队列
private final int trackedHash;
private int removedRecords;移除记录计数器

DefaultResourceLeak(Object referent) {
super(referent, refQueue);

assert referent != null;

// Store the hash of the tracked object to later assert it in the close(...) method.
// It's important that we not store a reference to the referent as this would disallow it from
// be collected via the PhantomReference.
//获取对象引用hash值
trackedHash = System.identityHashCode(referent);
Level level = getLevel();
if (level.ordinal() >= Level.ADVANCED.ordinal()) {
//如果探测等级大于ADVANCED,则创建记录
creationRecord = newRecord(null, 3);
} else {
creationRecord = null;
}
//将当前资源泄漏对象方法激活对象Map中
allLeaks.put(this, LeakEntry.INSTANCE);
}
}

我来看这一句:
//如果探测登记大于ADVANCED,则创建记录
creationRecord = newRecord(null, 3);



static String newRecord(Object hint, int recordsToSkip) {
StringBuilder buf = new StringBuilder(4096);

// Append the hint first if available.
if (hint != null) {
buf.append("\tHint: ");
// Prefer a hint string to a simple string form.
//如果对象时资源追踪线索,添加线索信息
if (hint instanceof ResourceLeakHint) {
buf.append(((ResourceLeakHint) hint).toHintString());
} else {
buf.append(hint);
}
buf.append(NEWLINE);
}

// Append the stack trace.
StackTraceElement[] array = new Throwable().getStackTrace();
for (StackTraceElement e: array) {
//跳过前3个栈追踪
if (recordsToSkip > 0) {
recordsToSkip --;
} else {
String estr = e.toString();

// Strip the noisy stack trace elements.
boolean excluded = false;
//剔除栈追踪噪音
for (String exclusion: STACK_TRACE_ELEMENT_EXCLUSIONS) {
if (estr.startsWith(exclusion)) {
excluded = true;
break;
}
}

if (!excluded) {
buf.append('\t');
buf.append(estr);
buf.append(NEWLINE);
}
}
}

return buf.toString();
}

//ResourceLeakHint
package io.netty.util;

/**
* A hint object that provides human-readable message for easier resource leak tracking.
资源泄漏线索,提供可读的资源泄漏追踪信息
*/
public interface ResourceLeakHint {
/**
* Returns a human-readable message that potentially enables easier resource leak tracking.
*/
String toHintString();
}


private static final String[] STACK_TRACE_ELEMENT_EXCLUSIONS = {
"io.netty.util.ReferenceCountUtil.touch(",
"io.netty.buffer.AdvancedLeakAwareByteBuf.touch(",
"io.netty.buffer.AbstractByteBufAllocator.toLeakAwareBuffer(",
"io.netty.buffer.AdvancedLeakAwareByteBuf.recordLeakNonRefCountingOperation("
};


//StringUtil
package io.netty.util.internal;
/**
* String utility class.
*/
public final class StringUtil {

public static final String EMPTY_STRING = "";
public static final String NEWLINE = SystemPropertyUtil.get("line.separator", "\n");

public static final char DOUBLE_QUOTE = '\"';
public static final char COMMA = ',';
public static final char LINE_FEED = '\n';
public static final char CARRIAGE_RETURN = '\r';
public static final char TAB = '\t';
...
}

//泄漏资源LeakEntryEntry
 private static final class LeakEntry {
static final LeakEntry INSTANCE = new LeakEntry();//内部实例
private static final int HASH = System.identityHashCode(INSTANCE);//实例hash值

private LeakEntry() {
}

@Override
public int hashCode() {
return HASH;
}
@Override
public boolean equals(Object obj) {
return obj == this;
}
}

从上面可以看出,默认资源泄漏DefaultResourceLeak,构造过程为,首先将资源添加到引用队列,如果探测等级大于ADVANCED,则创建记录,然后添加资源泄漏对象到资源泄漏探测器的激活对象Map中。

再来看DefaultResourceLeak的记录和关闭资源操作:


@Override
public void record() {
record0(null, 3);
}

@Override
public void record(Object hint) {
record0(hint, 3);
}

private void record0(Object hint, int recordsToSkip) {
if (creationRecord != null) {
//根据资源泄漏线索,创建泄漏线索信息
String value = newRecord(hint, recordsToSkip);

synchronized (lastRecords) {
int size = lastRecords.size();
if (size == 0 || !lastRecords.getLast().equals(value)) {
//如果泄漏线索信息集为空,或者与上次记录不同,则添加泄漏信息到泄漏记录集
lastRecords.add(value);
}
if (size > MAX_RECORDS) {
//如果记录超过最大记录数,则从对头移除记录信息
lastRecords.removeFirst();
++removedRecords;//更新移除记录计数器
}
}
}
}

从上面可以看出,记录资源泄漏,首先根据资源泄漏线索,创建泄漏线索信息,
如果泄漏线索信息集为空,或者与上次记录不同,则添加泄漏信息到泄漏记录集,
如果记录超过最大记录数,则从对头移除记录信息。

再来看关闭资源

 @Override
public boolean close() {
// Use the ConcurrentMap remove method, which avoids allocating an iterator.
//从资源泄漏探测器移除资源泄漏对象
return allLeaks.remove(this, LeakEntry.INSTANCE);
}

@Override
public boolean close(T trackedObject) {
// Ensure that the object that was tracked is the same as the one that was passed to close(...).
assert trackedHash == System.identityHashCode(trackedObject);

// We need to actually do the null check of the trackedObject after we close the leak because otherwise
// we may get false-positives reported by the ResourceLeakDetector. This can happen as the JIT / GC may
// be able to figure out that we do not need the trackedObject anymore and so already enqueue it for
// collection before we actually get a chance to close the enclosing ResourceLeak.
return close() && trackedObject != null;
}

从上面来看,关闭资源探测对象,就是从资源泄漏探测器移除资源泄漏对象。

//资源泄漏对象信息
@Override
public String toString() {
if (creationRecord == null) {
return EMPTY_STRING;
}

final Object[] array;
final int removedRecords;
synchronized (lastRecords) {
array = lastRecords.toArray();
removedRecords = this.removedRecords;
}

StringBuilder buf = new StringBuilder(16384).append(NEWLINE);
if (removedRecords > 0) {
//报告,由于泄漏记录数限制,丢弃的记录数
buf.append("WARNING: ")
.append(removedRecords)
.append(" leak records were discarded because the leak record count is limited to ")
.append(MAX_RECORDS)
.append(". Use system property ")
.append(PROP_MAX_RECORDS)
.append(" to increase the limit.")
.append(NEWLINE);
}
buf.append("Recent access records: ")
.append(array.length)
.append(NEWLINE);
//报告,记录的资源泄漏信息
if (array.length > 0) {
for (int i = array.length - 1; i >= 0; i --) {
buf.append('#')
.append(i + 1)
.append(':')
.append(NEWLINE)
.append(array[i]);
}
}
//报告资源泄漏创建信息
buf.append("Created at:")
.append(NEWLINE)
.append(creationRecord);

buf.setLength(buf.length() - NEWLINE.length());
return buf.toString();
}

从上面来看,报告资源泄漏信息,主要是报告由于泄漏记录数限制,丢弃的记录数,记录的资源泄漏信息,
资源泄漏创建信息。

回到资源泄漏探测器,根据资源,创建资源泄漏追踪器:

/
**
* Creates a new {@link ResourceLeakTracker} which is expected to be closed via
* {@link ResourceLeakTracker#close(Object)} when the related resource is deallocated.
*创建一个资源泄漏ResourceLeakTracker,当相关资源释放时,希望通过ResourceLeakTracker#close(Object)关闭对象
* @return the {@link ResourceLeakTracker} or {@code null}
*/
public final ResourceLeakTracker<T> track(T obj) {
return track0(obj);
}

//追踪资源泄漏对象线索
private DefaultResourceLeak track0(T obj) {
Level level = ResourceLeakDetector.level;
if (level == Level.DISABLED) {
return null;
}
if (level.ordinal() < Level.PARANOID.ordinal()) {
if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) {
//根据探测等级,报告探测结果
reportLeak(level);
//创建资源泄漏对象
return new DefaultResourceLeak(obj);
} else {
return null;
}
} else {
reportLeak(level);
return new DefaultResourceLeak(obj);
}
}


来看根据探测等级,报告探测结果
//根据探测等级,报告探测结果  
reportLeak(level);



private void reportLeak(Level level) {
if (!logger.isErrorEnabled()) {
//如果日志Error级别没有开启,则将资源探测对象的引用队列中,poll探测对象,并关闭
for (;;) {
@SuppressWarnings("unchecked")
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
ref.close();
}
return;
}

// Detect and report previous leaks. 报告内存泄漏请求
for (;;) {
@SuppressWarnings("unchecked")
//
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
//清除引用
ref.clear();

if (!ref.close()) {
//如果资源泄漏对象没关闭,则跳出当前循环
continue;
}
//获取资源泄漏探测信息
String records = ref.toString();
//记录资源泄漏信息
if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
if (records.isEmpty()) {
//资源泄漏记录为空,则报告不可追踪
reportUntracedLeak(resourceType);
} else {
//报告资源泄漏信息
reportTracedLeak(resourceType, records);
}
}
}
}

从上面可以看出,资源泄漏探测器,追踪资源泄漏探测信息,在采样点到达时,首先从资源泄漏引用队列,获取资源泄漏对象对象,关闭资源泄漏对象,并将探测结果添加到探测器的资源泄漏信息Map中,然后报告资源泄漏探测信息,最后创建资源探测对象。


来看需要关注的几点:
1.
//清除引用
//Reference
public abstract class Reference<T> {
private T referent; /* Treated specially by GC */

ReferenceQueue<? super T> queue;

Reference next;
transient private Reference<T> discovered; /* used by VM */


/* Object used to synchronize with the garbage collector. The collector
* must acquire this lock at the beginning of each collection cycle. It is
* therefore critical that any code holding this lock complete as quickly
* as possible, allocate no new objects, and avoid calling user code.
*/
static private class Lock { };
private static Lock lock = new Lock();


/* List of References waiting to be enqueued. The collector adds
* References to this list, while the Reference-handler thread removes
* them. This list is protected by the above lock object.
*/
private static Reference pending = null;

/**
* Clears this reference object. Invoking this method will not cause this
* object to be enqueued.
* 清除对象引用
* <p> This method is invoked only by Java code; when the garbage collector
* clears references it does so directly, without invoking this method.
*/
public void clear() {
this.referent = null;
}
...
}

2.
/**
* This method is called when a traced leak is detected. It can be overridden for tracking how many times leaks
* have been detected.
报告资源泄漏信息
*/
protected void reportTracedLeak(String resourceType, String records) {
logger.error(
"LEAK: {}.release() was not called before it's garbage-collected. " +
"See http://netty.io/wiki/reference-counted-objects.html for more information.{}",
resourceType, records);
}

3.
/**
* This method is called when an untraced leak is detected. It can be overridden for tracking how many times leaks
* have been detected.
报告不可追踪
*/
protected void reportUntracedLeak(String resourceType) {
logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
"Enable advanced leak reporting to find out where the leak occurred. " +
"To enable advanced leak reporting, " +
"specify the JVM option '-D{}={}' or call {}.setLevel() " +
"See http://netty.io/wiki/reference-counted-objects.html for more information.",
resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
}


[size=medium][b]总结:[/b][/size]
[color=blue]默认的资源泄漏探测器工厂创建的资源泄漏探测器为ResourceLeakDetector。

资源泄漏探测器,探测等级有四种,关闭DISABLED,SIMPLE简单,高级ADVANCED,终极PARANOID,SIMPLE开启简单的采样资源泄漏探测,仅仅已少量的负载为代价报告是否为泄漏对象;ADVANCED开启高级采样泄漏探测,将会以高负载为代价,报告最近泄漏对象访问的地方;
PARANOID开启终极采样泄漏探测,将会以高级负载为代价,报告最近泄漏对象访问的地方(仅仅测试);开启资源探测的情况下,默认等级为SIMPLE。资源探测器内部有一个探测等级和采样间隔,资源类型,泄漏报告Map( ConcurrentMap<String, Boolean>),激活资源集合ConcurrentMap<DefaultResourceLeak, LeakEntry>。

资源泄漏探测器构造,主要初始化资源类型名及探测间隔。

资源泄漏追踪ResourceLeakTracker,主要定义了记录当前调用者栈追踪和额外的主观信息方法,
以便资源泄漏探测器可以告诉,泄漏资源最近访问的地方;关闭泄漏资源方法,以便资源泄漏探测器不在警告泄漏资源。

默认资源泄漏DefaultResourceLeak,构造过程为,首先将资源添加到引用队列,如果探测等级大于ADVANCED,则创建记录,然后添加资源泄漏对象到资源泄漏探测器的激活对象Map中。

记录资源泄漏,首先根据资源泄漏线索,创建泄漏线索信息,如果泄漏线索信息集为空,或者与上次记录不同,则添加泄漏信息到泄漏记录集,如果记录超过最大记录数,则从对头移除记录信息。

关闭资源探测对象,就是从资源泄漏探测器移除资源泄漏对象。

报告资源泄漏信息,主要是报告由于泄漏记录数限制,丢弃的记录数,记录的资源泄漏信息,
资源泄漏创建信息。

资源泄漏探测器,追踪资源泄漏探测信息,在采样点到达时,首先从资源泄漏引用队列,获取资源泄漏对象对象,关闭资源泄漏对象,并将探测结果添加到探测器的资源泄漏信息Map中,然后报告资源泄漏探测信息,最后创建资源探测对象。[/color]

附:

//PhantomReference
package java.lang.ref;


/**
* Phantom reference objects, which are enqueued after the collector
* determines that their referents may otherwise be reclaimed. Phantom
* references are most often used for scheduling pre-mortem cleanup actions in
* a more flexible way than is possible with the Java finalization mechanism.
*
* <p> If the garbage collector determines at a certain point in time that the
* referent of a phantom reference is <a
* href="package-summary.html#reachability">phantom reachable</a>, then at that
* time or at some later time it will enqueue the reference.
*
* <p> In order to ensure that a reclaimable object remains so, the referent of
* a phantom reference may not be retrieved: The <code>get</code> method of a
* phantom reference always returns <code>null</code>.
*
* <p> Unlike soft and weak references, phantom references are not
* automatically cleared by the garbage collector as they are enqueued. An
* object that is reachable via phantom references will remain so until all
* such references are cleared or themselves become unreachable.
*
* @author Mark Reinhold
* @since 1.2
*/

public class PhantomReference<T> extends Reference<T> {

/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get() {
return null;
}

/**
* Creates a new phantom reference that refers to the given object and
* is registered with the given queue.
*
* <p> It is possible to create a phantom reference with a <tt>null</tt>
* queue, but such a reference is completely useless: Its <tt>get</tt>
* method will always return null and, since it does not have a queue, it
* will never be enqueued.
*
* @param referent the object the new phantom reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*/
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}

}


//Reference
/**
* Abstract base class for reference objects. This class defines the
* operations common to all reference objects. Because reference objects are
* implemented in close cooperation with the garbage collector, this class may
* not be subclassed directly.
*
* @author Mark Reinhold
* @since 1.2
*/

public abstract class Reference<T> {

/* A Reference instance is in one of four possible internal states:
*
* Active: Subject to special treatment by the garbage collector. Some
* time after the collector detects that the reachability of the
* referent has changed to the appropriate state, it changes the
* instance's state to either Pending or Inactive, depending upon
* whether or not the instance was registered with a queue when it was
* created. In the former case it also adds the instance to the
* pending-Reference list. Newly-created instances are Active.
*
* Pending: An element of the pending-Reference list, waiting to be
* enqueued by the Reference-handler thread. Unregistered instances
* are never in this state.
*
* Enqueued: An element of the queue with which the instance was
* registered when it was created. When an instance is removed from
* its ReferenceQueue, it is made Inactive. Unregistered instances are
* never in this state.
*
* Inactive: Nothing more to do. Once an instance becomes Inactive its
* state will never change again.
*
* The state is encoded in the queue and next fields as follows:
*
* Active: queue = ReferenceQueue with which instance is registered, or
* ReferenceQueue.NULL if it was not registered with a queue; next =
* null.
*
* Pending: queue = ReferenceQueue with which instance is registered;
* next = Following instance in queue, or this if at end of list.
*
* Enqueued: queue = ReferenceQueue.ENQUEUED; next = Following instance
* in queue, or this if at end of list.
*
* Inactive: queue = ReferenceQueue.NULL; next = this.
*
* With this scheme the collector need only examine the next field in order
* to determine whether a Reference instance requires special treatment: If
* the next field is null then the instance is active; if it is non-null,
* then the collector should treat the instance normally.
*
* To ensure that concurrent collector can discover active Reference
* objects without interfering with application threads that may apply
* the enqueue() method to those objects, collectors should link
* discovered objects through the discovered field.
*/

private T referent; /* Treated specially by GC */

ReferenceQueue<? super T> queue;

Reference next;
transient private Reference<T> discovered; /* used by VM */


/* Object used to synchronize with the garbage collector. The collector
* must acquire this lock at the beginning of each collection cycle. It is
* therefore critical that any code holding this lock complete as quickly
* as possible, allocate no new objects, and avoid calling user code.
*/
static private class Lock { };
private static Lock lock = new Lock();


/* List of References waiting to be enqueued. The collector adds
* References to this list, while the Reference-handler thread removes
* them. This list is protected by the above lock object.
*/
private static Reference pending = null;

/* High-priority thread to enqueue pending References
*/
private static class ReferenceHandler extends Thread {

ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}

public void run() {
for (;;) {

Reference r;
synchronized (lock) {
if (pending != null) {
r = pending;
Reference rn = r.next;
pending = (rn == r) ? null : rn;
r.next = r;
} else {
try {
lock.wait();
} catch (InterruptedException x) { }
continue;
}
}

// Fast path for cleaners
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}

ReferenceQueue q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}

static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
}


/* -- Referent accessor and setters -- */

/**
* Returns this reference object's referent. If this reference object has
* been cleared, either by the program or by the garbage collector, then
* this method returns <code>null</code>.
*
* @return The object to which this reference refers, or
* <code>null</code> if this reference object has been cleared
*/
public T get() {
return this.referent;
}

/**
* Clears this reference object. Invoking this method will not cause this
* object to be enqueued.
*
* <p> This method is invoked only by Java code; when the garbage collector
* clears references it does so directly, without invoking this method.
*/
public void clear() {
this.referent = null;
}


/* -- Queue operations -- */

/**
* Tells whether or not this reference object has been enqueued, either by
* the program or by the garbage collector. If this reference object was
* not registered with a queue when it was created, then this method will
* always return <code>false</code>.
*
* @return <code>true</code> if and only if this reference object has
* been enqueued
*/
public boolean isEnqueued() {
/* In terms of the internal states, this predicate actually tests
whether the instance is either Pending or Enqueued */
synchronized (this) {
return (this.queue != ReferenceQueue.NULL) && (this.next != null);
}
}

/**
* Adds this reference object to the queue with which it is registered,
* if any.
*
* <p> This method is invoked only by Java code; when the garbage collector
* enqueues references it does so directly, without invoking this method.
*
* @return <code>true</code> if this reference object was successfully
* enqueued; <code>false</code> if it was already enqueued or if
* it was not registered with a queue when it was created
*/
public boolean enqueue() {
return this.queue.enqueue(this);
}


/* -- Constructors -- */

Reference(T referent) {
this(referent, null);
}

Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值