Python3.5源码分析-垃圾回收机制
发布日期:2021-07-25 13:04:44 浏览次数:6 分类:技术文章

本文共 20244 字,大约阅读时间需要 67 分钟。

Python3源码分析

本文环境python3.5.2。参考书籍<
>python官网

Python3的垃圾回收概述

随着软硬件的发展,大多数语言都已经支持了垃圾回收机制,让使用者从内存管理的工作中解放出来。Python的垃圾回收机制,采用了引用计数来管理的,应用计数也算是一种垃圾回收机制,也是一种直观简单的垃圾回收计数,引用计数方法的优点即实时性,当引用计数为0时,就可以立即回收。引用计数有一个致命的弱点那就是循环引用。

循环引用

引用计数的机制相对简单,当一个对象被创建或复制时,对象的引用计数加1,当一个对象的引用被销毁时,对象的引用计数减1,如果对象的引用计数减少到0,则会启动垃圾回收机制将其回收。但是,循环引用可以使一组对象的引用计数都不为0,然而这些对象实际上并没有任何外部变量引用,它们之间只是相互引用,这时候意味着如果不使用这组对象,应该回收这些对象所占用的内存,然而由于此时的引用计数都不为0,这些对象所占用的内存都不会被回收。

为了解决这个问题,Python引入了主力垃圾收集技术中的标记-清除分代收集两种技术来填充其内存管理机制的弱点。

标记-清除的简要工作流程如下:1.寻找根对象的集合,根对象就是一些全局引用和函数栈中的引用;2.从根对象集合出发,沿着根对象集合中的每一个引用,如果能达到某个对象A,则A称为可到达的,可到达的对象不可被删除,这就是垃圾检测阶段;3.当垃圾检测结束后,所有的对象分为了可到达的和不可到达的两部分,可到达的要予以保留,而不可到达的对象所占用的内存将被回收,这就是垃圾回收阶段。这就是标记-清除的大致工作流程。

在Python中,由于循环引用是当可被引用的对象之间才会出现,所以Python中需要检查的对象就是可被其他对象所引用的对象,例如list、dict、class等可被引用的对象,像int、str等不引用其他对象的类型则不需要被检查,因此Python中的垃圾回收主要就是回收这些能被引用的对象,为了达到这一点Python在创建这些对象的时候就会把这些对象加入到一个可收集的对象链表中。

分代的垃圾回收机制,主要是通过研究表明,对于不同的语言,不同的应用程序,一些内存块的生存周期相对持续比较长,在这种情况下,当需回收的内存块越多时,垃圾检测带来的额外操作就越多,而垃圾回收带来的额外操作就越少,反之,当需要回收的内存块越少时,垃圾检测就将比垃圾回收带来更少的额外操作。为了使垃圾回收的效率提高,基于研究人员的统计规律,就出现了一种以空间换时间的策略,将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就称为一代,垃圾收集的频率随着代的存活时间的增大而减少,也就是说,活的越长的对象,就越不可能是垃圾,就应该少去收集,通常利用经过了几次垃圾收集动作来衡量,如果一个对象经过的垃圾收集次数越多,其存货时间就越长。例如,当某些内存块A经历了多次的垃圾收集之后仍然还不被释放,就将A划分到集合L1中,而新分配的内存都划分到集合L2中,当垃圾收集开始工作时,大多数情况都只对集合L2进行垃圾回收,对L1则等一段时间后再进行回收,这就使垃圾收集需要处理的内存变少了,效率则得到提高,在集合L2中的内存块再经历了多次收集之后仍然存活,则将其划分到L1中去,诚然在L1中确实是有一些需要回收的垃圾,但是这需要等过一段时间才会被收集,所以这就是一种空间换时间的策略。

在Python中使用的分代垃圾回收机制,总共分为三个代,一个代就是代表一代可收集对象的对象链表。

Python垃圾回收的执行过程

才疏学浅,如有问题依据官方文档为准,仅限个人理解,如有疏漏请批评指正

在Python中,由于使用了标记-清除和分代的两种作为引用计数的补充,我们就从list的过程来分析这一过程的执行。

首先,在list的初始化生成过程中,会调用list的tp_alloc方法去申请内存,此时list对应的tp_alloc方法为PyType_GenericAlloc,

PyObject *PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems){    PyObject *obj;    const size_t size = _PyObject_VAR_SIZE(type, nitems+1);             // 获取生成的字节大小    /* note that we need to add one, for the sentinel */    if (PyType_IS_GC(type))                                             // 判断type是否是需要GC的类型        obj = _PyObject_GC_Malloc(size);                                // 加入收集列表并申请内存    else        obj = (PyObject *)PyObject_MALLOC(size);                        // 直接申请内存    if (obj == NULL)                                                    // 如果返回为空则表示申请内存失败        return PyErr_NoMemory();    memset(obj, '\0', size);                                            // 将申请到的内存重写'\0'    if (type->tp_flags & Py_TPFLAGS_HEAPTYPE)        Py_INCREF(type);                                                // 引用计数加1    if (type->tp_itemsize == 0)        (void)PyObject_INIT(obj, type);                                 // 初始化头部    else        (void) PyObject_INIT_VAR((PyVarObject *)obj, type, nitems);     // 初始化设置相关存在的值    if (PyType_IS_GC(type))                 _PyObject_GC_TRACK(obj);                                        // 如果是可GC的对象,则加入跟踪链表中    return obj;}

其中,PyType_IS_GC的定义如下,

/* Test if a type has a GC head */#define PyType_IS_GC(t) PyType_HasFeature((t), Py_TPFLAGS_HAVE_GC)...#define PyType_HasFeature(t,f)  (((t)->tp_flags & (f)) != 0)

就是判断这个类型的tp_flags是否拥有传入的type类型。

此时如果是GC对象,则调用_PyObject_GC_Malloc函数去申请内存,带哦用了位于gcmodule.c中的该函数,

PyObject *_PyObject_GC_Malloc(size_t basicsize){    return _PyObject_GC_Alloc(0, basicsize);}

调用了位于gcmodule.c的_PyObject_GC_Alloc函数,

static PyObject *_PyObject_GC_Alloc(int use_calloc, size_t basicsize){    PyObject *op;    PyGC_Head *g;    size_t size;    if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))                 return PyErr_NoMemory();    size = sizeof(PyGC_Head) + basicsize;                     // 申请内存的大小要包括PyGC_Head和申请的basicsize大小    if (use_calloc)        g = (PyGC_Head *)PyObject_Calloc(1, size);            // 申请内存    else        g = (PyGC_Head *)PyObject_Malloc(size);    if (g == NULL)        return PyErr_NoMemory();    g->gc.gc_refs = 0;                                        // 设置引用计数的副本    _PyGCHead_SET_REFS(g, GC_UNTRACKED);                      // 设置g为GC_UNTRACKED    generations[0].count++; /* number of allocated GC objects */   // 第一代总数值加1    if (generations[0].count > generations[0].threshold &&        enabled &&        generations[0].threshold &&        !collecting &&        !PyErr_Occurred()) {                                  // 如果第一代的计数值大于第一代的阈值        collecting = 1;        collect_generations();                                // 调用分代处理函数        collecting = 0;    }    op = FROM_GC(g);                                          // 跳过PyGC_Head头部的位置,指向剩余空闲的地址    return op;}

该函数中,PyGC_Head结构就是可收集对象链表的头部位置,

/* GC information is stored BEFORE the object structure. */#ifndef Py_LIMITED_APItypedef union _gc_head {    struct {        union _gc_head *gc_next;        // 上一个gc        union _gc_head *gc_prev;        // 下一个gc        Py_ssize_t gc_refs;             // gc_refs引用计数值的副本    } gc;    double dummy;  /* force worst-case alignment */} PyGC_Head;

在每一个可GC的对象的头部都会申请出该PyGC_Head,用来记录该链表。

此时作为一个可GC的对象,对调用_PyObject_GC_TRACK来添加到链表中,

/* Tell the GC to track this object.  NB: While the object is tracked the * collector it must be safe to call the ob_traverse method. */#define _PyObject_GC_TRACK(o) do { \    PyGC_Head *g = _Py_AS_GC(o); \                      // 调到GC_HEAD的头部信息    if (_PyGCHead_REFS(g) != _PyGC_REFS_UNTRACKED) \    // 判断是否已经跟踪了        Py_FatalError("GC object already tracked"); \    _PyGCHead_SET_REFS(g, _PyGC_REFS_REACHABLE); \      // 设置g为可到达的    g->gc.gc_next = _PyGC_generation0; \                // 将_PyGC_generation0的第一个位置设置到当前gc的next    g->gc.gc_prev = _PyGC_generation0->gc.gc_prev; \    // 设置当前gc的上一个是_PyGC_generation0的上一个    g->gc.gc_prev->gc.gc_next = g; \                    // 设置上一个的下一个为g    _PyGC_generation0->gc.gc_prev = g; \                // 设置_PyGC_generation0的上一个为g    } while (0);

此时就将生成的对象加入到了第一代的链表中,其中第一代、第二代和第三代的相关定义如下;

/*** Global GC state ***/struct gc_generation {    PyGC_Head head;     int threshold; /* collection threshold */    int count; /* count of allocations or collections of younger                  generations */};#define NUM_GENERATIONS 3#define GEN_HEAD(n) (&generations[n].head)/* linked lists of container objects */static struct gc_generation generations[NUM_GENERATIONS] = {    /* PyGC_Head,                               threshold,      count */     {
{
{GEN_HEAD(0), GEN_HEAD(0), 0}}, 700, 0}, // 第一代阈值是700 {
{
{GEN_HEAD(1), GEN_HEAD(1), 0}}, 10, 0}, // 第二代的阈值是10 {
{
{GEN_HEAD(2), GEN_HEAD(2), 0}}, 10, 0}, // 第三代的阈值是10};PyGC_Head *_PyGC_generation0 = GEN_HEAD(0); // _PyGC_generation0为第一代数组值

如果此时在申请内存时,第一代的对象大于默认700的阈值,此时就会执行如下,

if (generations[0].count > generations[0].threshold &&    enabled &&    generations[0].threshold &&    !collecting &&    !PyErr_Occurred()) {                                  // 如果第一代的计数值大于第一代的阈值    collecting = 1;    collect_generations();                                // 调用分代处理函数    collecting = 0;}

此时就会调用collect_generations函数进行处理,

static Py_ssize_tcollect_generations(void){    int i;    Py_ssize_t n = 0;    /* Find the oldest generation (highest numbered) where the count     * exceeds the threshold.  Objects in the that generation and     * generations younger than it will be collected. */    for (i = NUM_GENERATIONS-1; i >= 0; i--) {                        // 从第三代开始往下分代        if (generations[i].count > generations[i].threshold) {        // 如果当前这一代超过了阈值            /* Avoid quadratic performance degradation in number               of tracked objects. See comments at the beginning               of this file, and issue #4074.            */            if (i == NUM_GENERATIONS - 1                && long_lived_pending < long_lived_total / 4)                continue;            n = collect_with_callback(i);                             // 调用该函数进行分代            break;        }    }    return n;}

此时就会找到最老的那一代满足收集条件的进行collect_with_callback函数操作,

/* Perform garbage collection of a generation and invoke * progress callbacks. */static Py_ssize_tcollect_with_callback(int generation){    Py_ssize_t result, collected, uncollectable;    invoke_gc_callback("start", generation, 0, 0);    result = collect(generation, &collected, &uncollectable, 0);          // 调用collect进行操作    invoke_gc_callback("stop", generation, collected, uncollectable);    return result;}

此时就会调用collect函数进行操作,有关collect的函数就是垃圾回收的所有的操作的流程都在这个流程中实现,其代码如下;

/* This is the main function.  Read this to understand how the * collection process works. */static Py_ssize_tcollect(int generation, Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable,        int nofail){    int i;    Py_ssize_t m = 0; /* # objects collected */    Py_ssize_t n = 0; /* # unreachable objects that couldn't be collected */    PyGC_Head *young; /* the generation we are examining */    PyGC_Head *old; /* next older generation */    PyGC_Head unreachable; /* non-problematic unreachable trash */    PyGC_Head finalizers;  /* objects with, & reachable from, __del__ */    PyGC_Head *gc;    _PyTime_t t1 = 0;   /* initialize to prevent a compiler warning */    struct gc_generation_stats *stats = &generation_stats[generation];    if (debug & DEBUG_STATS) {                                          // 如果是调试状态        PySys_WriteStderr("gc: collecting generation %d...\n",                          generation);        PySys_WriteStderr("gc: objects in each generation:");        for (i = 0; i < NUM_GENERATIONS; i++)            PySys_FormatStderr(" %zd",                              gc_list_size(GEN_HEAD(i)));        t1 = _PyTime_GetMonotonicClock();        PySys_WriteStderr("\n");    }    /* update collection and allocation counters */    if (generation+1 < NUM_GENERATIONS)                                        generations[generation+1].count += 1;                         // 当前收集次数加1    for (i = 0; i <= generation; i++)        generations[i].count = 0;                                     // 将剩余代数的收集次数设置成0    /* merge younger generations with one we are currently collecting */    for (i = 0; i < generation; i++) {        gc_list_merge(GEN_HEAD(i), GEN_HEAD(generation));             // 合并年轻一代的链表    }    /* handy references */    young = GEN_HEAD(generation);                                     // 获取当前传入这一代的对象    if (generation < NUM_GENERATIONS-1)                               // 如果传入的一代小于2        old = GEN_HEAD(generation+1);                                 // old为generation+1    else        old = young;                                                  // 否则old就为自己这一代    /* Using ob_refcnt and gc_refs, calculate which objects in the     * container set are reachable from outside the set (i.e., have a     * refcount greater than 0 when all the references within the     * set are taken into account).     */    update_refs(young);                                              // 更新引用计数的副本    subtract_refs(young);                                            // 摘除循环引用    /* Leave everything reachable from outside young in young, and move     * everything else (in young) to unreachable.     * NOTE:  This used to move the reachable objects into a reachable     * set instead.  But most things usually turn out to be reachable,     * so it's more efficient to move the unreachable things.     */    gc_list_init(&unreachable);                                     // 初始化unreachable链表            move_unreachable(young, &unreachable);                          // 将young中不可到达的移动到unreachable链表上    /* Move reachable objects to next generation. */    if (young != old) {                                             // 如果young和old不相等则证明还有上一代        if (generation == NUM_GENERATIONS - 2) {                                long_lived_pending += gc_list_size(young);              // 获取存活长久的对象的大小        }        gc_list_merge(young, old);                                  // 将可到达的设置到老一代    }    else {        /* We only untrack dicts in full collections, to avoid quadratic           dict build-up. See issue #14775. */        untrack_dicts(young);        long_lived_pending = 0;        long_lived_total = gc_list_size(young);                     // 设置存活的总数    }    /* All objects in unreachable are trash, but objects reachable from     * legacy finalizers (e.g. tp_del) can't safely be deleted.     */    gc_list_init(&finalizers);    move_legacy_finalizers(&unreachable, &finalizers);    /* finalizers contains the unreachable objects with a legacy finalizer;     * unreachable objects reachable *from* those are also uncollectable,     * and we move those into the finalizers list too.     */    move_legacy_finalizer_reachable(&finalizers);    /* Collect statistics on collectable objects found and print     * debugging information.     */    for (gc = unreachable.gc.gc_next; gc != &unreachable;                    gc = gc->gc.gc_next) {        m++;        if (debug & DEBUG_COLLECTABLE) {            debug_cycle("collectable", FROM_GC(gc));        }    }    /* Clear weakrefs and invoke callbacks as necessary. */    m += handle_weakrefs(&unreachable, old);                    // 处理弱引用相关    /* Call tp_finalize on objects which have one. */    finalize_garbage(&unreachable);                             // 调用含有__del__方法的对象进行释放    if (check_garbage(&unreachable)) {                          // 检查所有的是否是不可达到的        revive_garbage(&unreachable);                           // 设置列表中的为可到达的        gc_list_merge(&unreachable, old);                       // 合并可到达的到old列表中    }    else {        /* Call tp_clear on objects in the unreachable set.  This will cause         * the reference cycles to be broken.  It may also cause some objects         * in finalizers to be freed.         */        delete_garbage(&unreachable, old);                      // 对unreachable进行垃圾回收    }    /* Collect statistics on uncollectable objects found and print     * debugging information. */    for (gc = finalizers.gc.gc_next;                            // 打印相关调试信息         gc != &finalizers;         gc = gc->gc.gc_next) {        n++;        if (debug & DEBUG_UNCOLLECTABLE)            debug_cycle("uncollectable", FROM_GC(gc));    }    if (debug & DEBUG_STATS) {        _PyTime_t t2 = _PyTime_GetMonotonicClock();        if (m == 0 && n == 0)            PySys_WriteStderr("gc: done");        else            PySys_FormatStderr(                "gc: done, %zd unreachable, %zd uncollectable",                n+m, n);        PySys_WriteStderr(", %.4fs elapsed\n",                          _PyTime_AsSecondsDouble(t2 - t1));    }    /* Append instances in the uncollectable set to a Python     * reachable list of garbage.  The programmer has to deal with     * this if they insist on creating this type of structure.     */    (void)handle_legacy_finalizers(&finalizers, old);                 // 将unreachable但拥有finalizers放入  garbage列表中,并将finalizers合并入old中    /* Clear free list only during the collection of the highest     * generation */    if (generation == NUM_GENERATIONS-1) {        clear_freelists();    }    if (PyErr_Occurred()) {        if (nofail) {            PyErr_Clear();        }        else {            if (gc_str == NULL)                gc_str = PyUnicode_FromString("garbage collection");            PyErr_WriteUnraisable(gc_str);            Py_FatalError("unexpected exception during garbage collection");        }    }    /* Update stats */    if (n_collected)        *n_collected = m;    if (n_uncollectable)        *n_uncollectable = n;    stats->collections++;    stats->collected += m;    stats->uncollectable += n;    return n+m;}

在该函数中,还有对弱引用的处理,在弱引用的配置中还涉及到一些callback的调用,当带有del的对象如果是不可达的,则存入一个garbage的列表中,在其中比较重要的就是将相关的分代对象进行归类合并,然后进过标记清除的过程后,查找出需要释放回收的内存对象,一般情况下当该对象的引用计数为0 的时候就已经通过引用计数机制,被释放掉了,而引用计数不为0的对象一般是被程序使用的对象,或者循环引用中的对象。

其中,有关释放资源的操作delete_garbage执行流程如下;

/* Break reference cycles by clearing the containers involved.  This is * tricky business as the lists can be changing and we don't know which * objects may be freed.  It is possible I screwed something up here. */static voiddelete_garbage(PyGC_Head *collectable, PyGC_Head *old){    inquiry clear;    while (!gc_list_is_empty(collectable)) {        PyGC_Head *gc = collectable->gc.gc_next;        PyObject *op = FROM_GC(gc);                           // 跳过gc的头部指针空间        if (debug & DEBUG_SAVEALL) {                          // 如果是调试状态            PyList_Append(garbage, op);        }        else {            if ((clear = Py_TYPE(op)->tp_clear) != NULL) {    // 调用op的tp_clear方法去释放资源                Py_INCREF(op);                clear(op);                                    // 执行tp_clear方法                Py_DECREF(op);            }        }        if (collectable->gc.gc_next == gc) {                  // 如果释放完成后还是在            /* object is still alive, move it, it may die later */            gc_list_move(gc, old);                            // 添加到old列表中            _PyGCHead_SET_REFS(gc, GC_REACHABLE);             // 设置gc为可到达的        }    }}

在处理最后对当前不可处理的garbage则调用了handle_legacy_finalizers来处理该列表,将带有del属性的添加到garbage列表,没有的则重新合并到old列表中,

/* Handle uncollectable garbage (cycles with tp_del slots, and stuff reachable * only from such cycles). * If DEBUG_SAVEALL, all objects in finalizers are appended to the module * garbage list (a Python list), else only the objects in finalizers with * __del__ methods are appended to garbage.  All objects in finalizers are * merged into the old list regardless. * Returns 0 if all OK, <0 on error (out of memory to grow the garbage list). * The finalizers list is made empty on a successful return. */static inthandle_legacy_finalizers(PyGC_Head *finalizers, PyGC_Head *old){    PyGC_Head *gc = finalizers->gc.gc_next;    if (garbage == NULL) {                                          // 如果garbage为NULL        garbage = PyList_New(0);                                    // 初始化garbage为一个列表        if (garbage == NULL)            Py_FatalError("gc couldn't create gc.garbage list");    }    for (; gc != finalizers; gc = gc->gc.gc_next) {        PyObject *op = FROM_GC(gc);        if ((debug & DEBUG_SAVEALL) || has_legacy_finalizer(op)) {  // 如果是调试模式或者op的__del__不为空            if (PyList_Append(garbage, op) < 0)                     // 添加到该列表中                return -1;        }    }    gc_list_merge(finalizers, old);                                 // 将剩余的合并到old中    return 0;}

打破循环引用通过清零containers,但是根据注释可知,这也可能引起其他问题。

在处理最后对当前不可处理的garbage则调用了handle_legacy_finalizers来处理该列表,将带有del属性的添加到garbage列表,没有的则重新合并到old列表中,

/* Handle uncollectable garbage (cycles with tp_del slots, and stuff reachable * only from such cycles). * If DEBUG_SAVEALL, all objects in finalizers are appended to the module * garbage list (a Python list), else only the objects in finalizers with * __del__ methods are appended to garbage.  All objects in finalizers are * merged into the old list regardless. * Returns 0 if all OK, <0 on error (out of memory to grow the garbage list). * The finalizers list is made empty on a successful return. */static inthandle_legacy_finalizers(PyGC_Head *finalizers, PyGC_Head *old){    PyGC_Head *gc = finalizers->gc.gc_next;    if (garbage == NULL) {                                          // 如果garbage为NULL        garbage = PyList_New(0);                                    // 初始化garbage为一个列表        if (garbage == NULL)            Py_FatalError("gc couldn't create gc.garbage list");    }    for (; gc != finalizers; gc = gc->gc.gc_next) {        PyObject *op = FROM_GC(gc);        if ((debug & DEBUG_SAVEALL) || has_legacy_finalizer(op)) {  // 如果是调试模式或者op的__del__不为空            if (PyList_Append(garbage, op) < 0)                     // 添加到该列表中                return -1;        }    }    gc_list_merge(finalizers, old);                                 // 将剩余的合并到old中    return 0;}

当引用计数为0时,则直接就释放了该对象,

#define Py_DECREF(op)                                   \    do {                                                \        PyObject *_py_decref_tmp = (PyObject *)(op);    \        if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \        --(_py_decref_tmp)->ob_refcnt != 0)             \       // 检查引用计数是否为0             _Py_CHECK_REFCNT(_py_decref_tmp)            \        else                                            \        _Py_Dealloc(_py_decref_tmp);                    \       // 引用计数为0 则直接释放    } while (0)

总结

有关Python中的垃圾收集机制,使用了分代和标记清除作为辅助来克服循环引用的缺点,相对而言,关于垃圾回收机制还有更多的细节内容没有继续分析,如有兴趣可参考Python源码剖析这本书,然后对照源码继续分析。本文如有疏漏请批评指正。

转载地址:https://blog.csdn.net/qq_33339479/article/details/81634990 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Python3.5源码分析-List概述
下一篇:Python3.5源码分析-内存管理

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2024年03月17日 18时03分14秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

linux中卸载ambri-servle,Kerberos 命令使用 2019-04-21
linux二进制反编译,Xori:一款来自BlackHat 2018的二进制反汇编和静态分析工具 2019-04-21
linux两台主机添加信任,Linux两台机器间添加信任,实现不用密码问,互传文件... 2019-04-21
linux 自动获取ssl证书,linux生成自验证ssl证书的具体命令和步骤 2019-04-21
linux基础命令20个,20-linux中基础命令 2019-04-21
重置网络配置 android,重置Android移动网络信号? 2019-04-21
java约瑟夫环pta上_cdoj525-猴子选大王 (约瑟夫环) 2019-04-21
java++记录+运行_记录java+testng运行selenium(三)---xml、ini、excel、日志等配置 2019-04-21
mysql居左查询abcd_MySql速查手册 2019-04-21
loadrunner 错误: 无法找到 java.exe_LoadRunner错误及解决方法总结 2019-04-21
Java小魔女芭芭拉_沉迷蘑菇不可自拔,黏土人《小魔女学园》苏西·曼芭芭拉 图赏... 2019-04-21
php+mysql记事本_一个简单记事本php操作mysql辅助类创建 2019-04-21
300小时成为java程序员_直击面试现场: Java程序员3轮6小时面试, 成功拿到阿里offer!... 2019-04-21
中国网建java发送短信_短信验证登陆-中国网建提供的SMS短信平台 2019-04-21
隔行变色java代码_jquery入门—选择器实现隔行变色实例代码 2019-04-21
角标越界 Java_【新人求助】利用占位符操作数据库是总是提示数组角标越界是怎么回事 - Java论坛 - 51CTO技术论坛_中国领先的IT技术社区... 2019-04-21
java类中声明log对象_用于Android环境,java环境的log打印,可打印任何类型数据 2019-04-21
db2与mysql编目_DB2编目、联邦数据库 - Goopand's OS Space - OSCHINA - 中文开源技术交流社区... 2019-04-21
atomikosdatasourcebean mysql_SpringBoot2整合JTA组件实现多数据源事务管理 2019-04-21
webpack 入口文件 php,如何实现webpack多入口文件打包配置 2019-04-21