对一个简单的热部署代码的思考

431 阅读3分钟

热部署实现

image.png

此段代码使用循环的方式,定时创建自己实现的类加载器,并用类加载器加载自己的class文件,并调用需要实现的方法,由此来实现热部署。

为什么能实现热部署功能

主要是因为每个新建的ClassLoader享有一份独有的元空间内存,被称为Metachunk。当一个类加载器加载类时,会在元空间分配一个chunk块,通常一个普通类加载器会分配一个4KB的chunck。因此每个类加载器都能加载一份属于自己的klass,这样就能保证每次读入的klass是最新的klass。

如果只用一个类加载器不停的加载同一个类是不行的。因为加载器有多次加载判断,会调用findclass0()方法判断是否已经被本类加载器加载了,如果是就不会再次加载。

这些创建的类加载器会不会被gc回收?

实际上,对于每个ClassLoader,都会调用方法SystemDictionary::register_loader将一个ClassLoader实例与一个ClassLoaderData关联,而ClassLoaderData创建在元空间中,因此会形成元空间的ClassLoaderData引用堆区中的ClassLoader实例,ygc是不会回收这些ClassLoader的。

ClassLoaderData* SystemDictionary::register_loader(Handle class_loader, TRAPS) {
  if (class_loader() == NULL) return ClassLoaderData::the_null_class_loader_data();
  return ClassLoaderDataGraph::find_or_create(class_loader, CHECK_NULL);
}

ClassLoaderData保存在ClassLoaderDataGraph结构中(可以认为是一个链表)。只有在执行fgc的时候才会对元空间进行gc,只有在这时候才会清理ClassLoaderData。此时会在调用的 SystemDictionary::do_unloading()函数中调用ClassLoaderDataGraph::do_unloading()函数找出失效的类加载器卸载。

什么情况下才会回收这些创建的类加载器

首先,在元空间无法分配内存的情况下,会触发metaspace的gc。

MetaWord* Metaspace::allocate(ClassLoaderData* loader_data, size_t word_size,
                              bool read_only, MetaspaceObj::Type type, TRAPS) {
    ......
    // Try to allocate metadata.
    MetaWord* result = loader_data->metaspace_non_null()->allocate(word_size, mdtype);

    if (result == NULL) {
        // 为元数据信息分配空间失败
        tracer()->report_metaspace_allocation_failure(loader_data, word_size, type, mdtype);

        // Allocation failed.
        if (is_init_completed()) {
          // 触发GC进行内存回收后再分配
          // Only start a GC if the bootstrapping has completed.

          // Try to clean out some memory and retry.
          result = Universe::heap()->collector_policy()->satisfy_failed_metadata_allocation(
              loader_data, word_size, mdtype);
        }
    }
    ......
}

触发gc逻辑很简单,往任务队列中丢一个gc任务,由gc线程负责执行其中的doit()函数

// 触发Full GC回收那些已经无效的类加载器所占用的内存块
MetaWord* CollectorPolicy::satisfy_failed_metadata_allocation(
                                                 ClassLoaderData* loader_data,
                                                 size_t word_size,
                                                 Metaspace::MetadataType mdtype) {
 do {
         ......
         // Generate a VM operation
        VM_CollectForMetadataAllocation op(loader_data,
                                           word_size,
                                           mdtype,
                                           gc_count,
                                           full_gc_count,
                                           GCCause::_metadata_GC_threshold);
        VMThread::execute(&op);
        if (op.gc_locked()) {
          continue;
        }

        if (op.prologue_succeeded()) {
          return op.result();
        }
        ......
    } while (true);//直到gc被完成才会结束循环
}

可以看到,针对metaspace的垃圾回收都是fullgc

void CollectedHeap::collect_as_vm_thread(GCCause::Cause cause) {
  assert(Thread::current()->is_VM_thread(), "Precondition#1");
  assert(Heap_lock->is_locked(), "Precondition#2");
  GCCauseSetter gcs(this, cause);
  switch (cause) {
    case GCCause::_heap_inspection:
    case GCCause::_heap_dump:
    case GCCause::_metadata_GC_threshold : {
      HandleMark hm;
      do_full_collection(false);        // don't clear all soft refs
      break;
    }
    case GCCause::_last_ditch_collection: {
      HandleMark hm;
      do_full_collection(true);         // do clear all soft refs
      break;
    }
    default:
      ShouldNotReachHere(); // Unexpected use of this function
  }
}

要卸载一个类加载器,要卸载很多相关的信息,例如:

  // 卸载所有被标记为不再使用的Klass对象
  bool purged_class = SystemDictionary::do_unloading(&is_alive);

  // 卸载类中的nmethod
  CodeCache::do_unloading(&is_alive, purged_class);

  // Prune dead klasses from subklass/sibling/implementor lists.
  Klass::clean_weak_klass_links(&is_alive);

  // 卸载用不到的string对象
  StringTable::unlink(&is_alive);

  // Clean up unreferenced symbols in symbol table.
  SymbolTable::unlink();

因此大概率可以认为,代码中循环创建的类加载器是很难被卸载的,因此最好考虑热部署关键类。