本文基于openjdk11及hotspot
Объектная модель Java: модель ООП-класса
Прежде чем официально обсудить создание объектов JVM, давайте кратко представим объектную модель Java, реализованную в хотспоте. В JVM объекты Java не сопоставляются напрямую с объектами C++, а используется модель oop-klass, главным образом потому, что не ожидается, что каждый объект будет содержать таблицу виртуальных функций, среди которых:
- OOP (обычная точка объекта), которая представляет информацию об экземпляре объекта.
- Klass, представляющий классы Java в C++, используется для описания информации о классах Java.
Проще говоря, в JVM класс Java разделен на две части: данные и описание, соответствующие ООП и Классу соответственно.
В конкретном исходном коде JVM при загрузке класса создается объект InstanceKlass, а созданный объект соответствует InstanceOopDesc, где InstanceKlass хранится в метапространстве, а InstanceOopDesc хранится в куче.
процесс создания объекта
Сначала посмотрите на структуру данных InstanceOopDesc, InstanceOopDesc наследует OopDesc, структура данных следующая
// 此处为了方便阅读,改写了一下代码
class instanceOopDesc : public oopDesc {
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
};
Среди них _metadata указывает на InstanceKlass объекта, а _mark хранит данные состояния объекта во время его работы.Структура данных следующая (данные на рисунке 32 бита, а 64 бита аналогичны)
32 bits:
--------
hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
size:32 ------------------------------------------>| (CMS free block)
PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
Каждая строка представляет собой ситуацию, описывающую информацию о состоянии, такую как хэш-код, возраст создания GC, блокировку и т. д., следующим образом:
hash: 哈希码
age: 分代年龄
biased_lock: 偏向锁标识位
lock: 锁状态标识位
JavaThread*: 持有偏向锁的线程ID
epoch: 偏向时间戳
instanceOopDesc фактически сохраняет информацию заголовка объекта.Помимо информации заголовка, у объекта также есть данные, и данные объекта следуют за заголовком, как показано ниже:
1. Вход
На приведенном выше рисунке перехвачен фрагмент байт-кода программы.Красная линия соответствует байт-коду новой операции в Java.Новая операция в Java соответствует трем операциям байт-кода.В этой статье в основном описывается первая операция (новая). Новая операция в байт-коде соответствует InterpreterRuntime::_new в JVM Код выглядит следующим образом:
// hotspot/share/interpreter/interpreterRuntime.cpp
IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, ConstantPool* pool, int index))
Klass* k = pool->klass_at(index, CHECK);
InstanceKlass* klass = InstanceKlass::cast(k);
klass->check_valid_for_instantiation(true, CHECK); // 校验:接口/抽象类/Class不能实例化
klass->initialize(CHECK); // 初始化klass
oop obj = klass->allocate_instance(CHECK); // 分配实例
thread->set_vm_result(obj);
IRT_END
В основном он состоит из двух частей: инициализация класса и выделение экземпляров.
2. Инициализировать класс
// hotspot/share/oops/instanceKlass.cpp
void InstanceKlass::initialize(TRAPS) {
if (this->should_be_initialized()) {
initialize_impl(CHECK);
} else {
assert(is_initialized(), "sanity check");
}
}
Здесь мы продолжаем видетьinitialize_impl()
метод
// hotspot/share/oops/instanceKlass.cpp
void InstanceKlass::initialize_impl(TRAPS) {
HandleMark hm(THREAD);
link_class(CHECK); // 链接class
bool wait = false;
// Step 1
{
Handle h_init_lock(THREAD, init_lock());
ObjectLocker ol(h_init_lock, THREAD, h_init_lock() != NULL);
Thread *self = THREAD;
// Step 2
while(is_being_initialized() && !is_reentrant_initialization(self)) {
wait = true;
ol.waitUninterruptibly(CHECK);
}
// Step 3
if (is_being_initialized() && is_reentrant_initialization(self)) {
DTRACE_CLASSINIT_PROBE_WAIT(recursive, -1, wait);
return;
}
// Step 4
if (is_initialized()) {
DTRACE_CLASSINIT_PROBE_WAIT(concurrent, -1, wait);
return;
}
// Step 5
if (is_in_error_state()) {
DTRACE_CLASSINIT_PROBE_WAIT(erroneous, -1, wait);
ResourceMark rm(THREAD);
const char* desc = "Could not initialize class ";
const char* className = external_name();
size_t msglen = strlen(desc) + strlen(className) + 1;
char* message = NEW_RESOURCE_ARRAY(char, msglen);
if (NULL == message) {
// Out of memory: can't create detailed error message
THROW_MSG(vmSymbols::java_lang_NoClassDefFoundError(), className);
} else {
jio_snprintf(message, msglen, "%s%s", desc, className);
THROW_MSG(vmSymbols::java_lang_NoClassDefFoundError(), message);
}
}
// Step 6
set_init_state(being_initialized);
set_init_thread(self);
}
// Step 7
if (!is_interface()) {
Klass* super_klass = super();
if (super_klass != NULL && super_klass->should_be_initialized()) {
super_klass->initialize(THREAD);
}
if (!HAS_PENDING_EXCEPTION && has_nonstatic_concrete_methods()) {
initialize_super_interfaces(THREAD);
}
if (HAS_PENDING_EXCEPTION) {
Handle e(THREAD, PENDING_EXCEPTION);
CLEAR_PENDING_EXCEPTION;
{
EXCEPTION_MARK;
// Locks object, set state, and notify all waiting threads
set_initialization_state_and_notify(initialization_error, THREAD);
CLEAR_PENDING_EXCEPTION;
}
DTRACE_CLASSINIT_PROBE_WAIT(super__failed, -1, wait);
THROW_OOP(e());
}
}
AOTLoader::load_for_klass(this, THREAD);
// Step 8
{
assert(THREAD->is_Java_thread(), "non-JavaThread in initialize_impl");
JavaThread* jt = (JavaThread*)THREAD;
DTRACE_CLASSINIT_PROBE_WAIT(clinit, -1, wait);
PerfClassTraceTime timer(ClassLoader::perf_class_init_time(),
ClassLoader::perf_class_init_selftime(),
ClassLoader::perf_classes_inited(),
jt->get_thread_stat()->perf_recursion_counts_addr(),
jt->get_thread_stat()->perf_timers_addr(),
PerfClassTraceTime::CLASS_CLINIT);
call_class_initializer(THREAD);
}
// Step 9
if (!HAS_PENDING_EXCEPTION) {
set_initialization_state_and_notify(fully_initialized, CHECK);
{
debug_only(vtable().verify(tty, true);)
}
}
else {
// Step 10 and 11
Handle e(THREAD, PENDING_EXCEPTION);
CLEAR_PENDING_EXCEPTION;
JvmtiExport::clear_detected_exception((JavaThread*)THREAD);
{
EXCEPTION_MARK;
set_initialization_state_and_notify(initialization_error, THREAD);
CLEAR_PENDING_EXCEPTION;
JvmtiExport::clear_detected_exception((JavaThread*)THREAD);
}
DTRACE_CLASSINIT_PROBE_WAIT(error, -1, wait);
if (e->is_a(SystemDictionary::Error_klass())) {
THROW_OOP(e());
} else {
JavaCallArguments args(e);
THROW_ARG(vmSymbols::java_lang_ExceptionInInitializerError(),
vmSymbols::throwable_void_signature(),
&args);
}
}
DTRACE_CLASSINIT_PROBE_WAIT(end, -1, wait);
}
2.1 Ссылки
// hotspot/share/oops/instanceKlass.cpp
bool InstanceKlass::link_class_impl(bool throw_verifyerror, TRAPS) {
if (is_linked()) {
return true;
}
assert(THREAD->is_Java_thread(), "non-JavaThread in link_class_impl");
JavaThread* jt = (JavaThread*)THREAD;
// 先链接父类
Klass* super_klass = super();
if (super_klass != NULL) {
if (super_klass->is_interface()) {
ResourceMark rm(THREAD);
Exceptions::fthrow(
THREAD_AND_LOCATION,
vmSymbols::java_lang_IncompatibleClassChangeError(),
"class %s has interface %s as super class",
external_name(),
super_klass->external_name()
);
return false;
}
InstanceKlass* ik_super = InstanceKlass::cast(super_klass);
ik_super->link_class_impl(throw_verifyerror, CHECK_false);
}
// 链接该类的所有借口
Array<Klass*>* interfaces = local_interfaces();
int num_interfaces = interfaces->length();
for (int index = 0; index < num_interfaces; index++) {
InstanceKlass* interk = InstanceKlass::cast(interfaces->at(index));
interk->link_class_impl(throw_verifyerror, CHECK_false);
}
if (is_linked()) {
return true;
}
// 验证 & 重写
{
HandleMark hm(THREAD);
Handle h_init_lock(THREAD, init_lock());
ObjectLocker ol(h_init_lock, THREAD, h_init_lock() != NULL);
if (!is_linked()) {
if (!is_rewritten()) {
{
bool verify_ok = verify_code(throw_verifyerror, THREAD);
if (!verify_ok) {
return false;
}
}
if (is_linked()) {
return true;
}
// 重写类
rewrite_class(CHECK_false);
} else if (is_shared()) {
SystemDictionaryShared::check_verification_constraints(this, CHECK_false);
}
// 重写完成后链接方法
link_methods(CHECK_false);
// 初始化vtable和itable
ClassLoaderData * loader_data = class_loader_data();
if (!(is_shared() &&
loader_data->is_the_null_class_loader_data())) {
ResourceMark rm(THREAD);
vtable().initialize_vtable(true, CHECK_false);
itable().initialize_itable(true, CHECK_false);
}
// 将类的状态标记为已链接
set_init_state(linked);
if (JvmtiExport::should_post_class_prepare()) {
Thread *thread = THREAD;
assert(thread->is_Java_thread(), "thread->is_Java_thread()");
JvmtiExport::post_class_prepare((JavaThread *) thread, this);
}
}
}
return true;
}
Процесс связывания классов выглядит следующим образом.
- Связать родительский класс и реализованный интерфейс
- Переопределить класс
- Инициализировать vtable и itable
- Пометить состояние класса как связанное
Есть новая глава о переписывании классов и инициализации vtable и itable, и в этой статье не будут описываться конкретные детали.
2.2 Процесс инициализации
Этот шаг класса инициализации подробно описан в спецификации JVM.Предполагая, что текущий класс (интерфейс) — C, он содержит уникальную блокировку инициализации LC.
- Синхронизация блокирует LC, чтобы параллелизм не вызывал множественных инициализаций.
- Если другой поток инициализирует C, отпустите LC и заблокируйте текущий поток, пока этот поток не завершит инициализацию.
- Если текущий поток выполняет инициализацию, это указывает на рекурсивный запрос, освобождает LC и завершает инициализацию в обычном режиме.
- Если C уже инициализирован, отпустите LC и завершите инициализацию в обычном режиме.
- Если объект C находится в состоянии ошибки, отпустите LC и создайте исключение NoClassDefFoundError.
- Запись C инициализируется текущим потоком и выпускает LC, инициализирует все окончательные статические поля в классе
- Если C является классом, инициализируйте его родительский класс и интерфейс
- Определить, включены ли в C утверждения
- Выполнить метод инициализации класса (интерфейса)
- Marks C полностью инициализирован и пробуждает все ожидающие потоки
- Если инициализация не удалась, создайте исключение и пометьте C как ошибку и разбудите все ожидающие потоки.
Выше приведены шаги из спецификации JVM 11. На самом деле видно, что точка доступа немного отличается от спецификации, когда она реализована, но в основном это то же самое.
3. Выделите экземпляр
// hotspot/share/oops/instanceKlass.cpp
instanceOop InstanceKlass::allocate_instance(TRAPS) {
bool has_finalizer_flag = has_finalizer(); // 是否存在非空finalize()方法
int size = size_helper(); // 类的大小
instanceOop i;
i = (instanceOop)Universe::heap()->obj_allocate(this, size, CHECK_NULL); // 分配对象
if (has_finalizer_flag && !RegisterFinalizersAtInit) {
i = register_finalizer(i, CHECK_NULL);
}
return i;
}
Здесь нас больше беспокоит ссылка на объект выделения пространства кучи,
3.1 Объекты выделения пространства кучи
код показывает, как показано ниже:
// hotspot/share/gc/shared/memAllocator.cpp
oop MemAllocator::allocate() const {
oop obj = NULL;
{
Allocation allocation(*this, &obj);
HeapWord* mem = mem_allocate(allocation);
if (mem != NULL) {
obj = initialize(mem);
}
}
return obj;
}
Легко заметить, что основной поток здесь делится на две части, выделение памяти и инициализацию.
3.1.1 Распределение памяти
Откройте код напрямую, как показано ниже:
// hotspot/share/gc/shared/memAllocator.cpp
HeapWord* MemAllocator::mem_allocate(Allocation& allocation) const {
if (UseTLAB) {
HeapWord* result = allocate_inside_tlab(allocation);
if (result != NULL) {
return result;
}
}
return allocate_outside_tlab(allocation);
}
В этом коде мы видим очень знакомую вещь — TLAB (ThreadLocalAllocBuffer), TLAB включен по умолчанию, и его улучшение производительности для Java очень существенно. Прежде всего, давайте кратко представим понятие TLAB.
Поскольку пространство кучи JVM совместно используется всеми потоками, вся куча блокируется при выделении объекта, что неэффективно. Поэтому JVM выделяет пространство в области eden в качестве частного буфера потока, который называется TLAB. Разные потоки не совместно используют TLAB, поэтому нет необходимости в блокировке при размещении объектов в TLAB, чтобы выделение могло быть быстрым.
В этом коде выделение памяти разделено на две части — выделение внутри TLAB и выделение вне TLAB.
А. Назначение внутри TLAB
Давайте сначала посмотрим на процесс распределения в TLAB,
// hotspot/share/gc/shared/memAllocator.cpp
HeapWord* MemAllocator::allocate_inside_tlab(Allocation& allocation) const {
HeapWord* mem = _thread->tlab().allocate(_word_size);
if (mem != NULL) {
return mem;
}
return allocate_inside_tlab_slow(allocation);
}
Точно так же процесс распределения TLAB также делится на два случая. Первый заключается в непосредственном использовании существующего TLAB потока для выделения. Код выглядит следующим образом. В следующем коде мы можем увидеть TLAB. Распределение просто увеличивает верхний указатель вверх на размер и присваивает исходную верхнюю позицию объекту obj, поэтому можно сказать, что эффективность выделения чрезвычайно высока. (Фактически TLAB помечает информацию о хранении и распределении пространства TLAB с помощью начального, верхнего, конечного и других указателей)
// hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp
inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) {
invariants(); // 校验TLAB是否合法
HeapWord* obj = top();
if (pointer_delta(end(), obj) >= size) {
set_top(obj + size);
invariants();
return obj;
}
return NULL;
}
Далее давайте посмотрим на медленное выделение в TLAB,
// hotspot/share/gc/shared/memAllocator.cpp
HeapWord* MemAllocator::allocate_inside_tlab_slow(Allocation& allocation) const {
HeapWord* mem = NULL;
ThreadLocalAllocBuffer& tlab = _thread->tlab();
if (ThreadHeapSampler::enabled()) {
tlab.set_back_allocation_end();
mem = tlab.allocate(_word_size);
if (mem != NULL) {
allocation._tlab_end_reset_for_sample = true;
return mem;
}
}
// 如果TLAB的剩余空间大于阈值,则保留TLAB,这样就会进入TLAB外分配。在这里,每次TLAB分配失败,该TLAB都会调大该阈值,以防线程重复分配同样大小的对象
if (tlab.free() > tlab.refill_waste_limit()) {
tlab.record_slow_allocation(_word_size);
return NULL;
}
// 计算一个新的TLAB的大小,公式=min{可用空间,期待空间+对象占据空间,最大TLAB空间}
size_t new_tlab_size = tlab.compute_size(_word_size);
// 清理原先的TLAB。会将剩余的未使用空间填充进一个假数组,创造EDEN连续的假象,并且将start、end、top等指针全部置为空
tlab.clear_before_allocation();
if (new_tlab_size == 0) {
return NULL;
}
// 创建一个新的TLAB,空间可能在min_tlab_size到new_tlab_size之间
size_t min_tlab_size = ThreadLocalAllocBuffer::compute_min_size(_word_size);
mem = _heap->allocate_new_tlab(min_tlab_size, new_tlab_size, &allocation._allocated_tlab_size);
if (mem == NULL) {
return NULL;
}
// 将分配的空间数据全部清0
if (ZeroTLAB) {
Copy::zero_to_words(mem, allocation._allocated_tlab_size);
}
// 将mem位置分配word_size大小给obj
tlab.fill(mem, mem + _word_size, allocation._allocated_tlab_size);
return mem;
}
б. Распределение вне TLAB
// hotspot/share/gc/shared/memAllocator.cpp
HeapWord* MemAllocator::allocate_outside_tlab(Allocation& allocation) const {
allocation._allocated_outside_tlab = true;
HeapWord* mem = _heap->mem_allocate(_word_size, &allocation._overhead_limit_exceeded);
if (mem == NULL) {
return mem;
}
NOT_PRODUCT(_heap->check_for_non_bad_heap_word_value(mem, _word_size));
size_t size_in_bytes = _word_size * HeapWordSize;
_thread->incr_allocated_bytes(size_in_bytes);
return mem;
}
Здесь есть только один основной акцент — выделение памяти в куче.В качестве примера мы возьмем сборщик мусора openjdk11 — G1 по умолчанию, чтобы взглянуть на процесс выделения.
// hotspot/share/gc/g1/g1CollectedHeap.cpp
HeapWord* G1CollectedHeap::mem_allocate(size_t word_size,
bool* gc_overhead_limit_was_exceeded) {
assert_heap_not_locked_and_not_at_safepoint();
if (is_humongous(word_size)) {
return attempt_allocation_humongous(word_size);
}
size_t dummy = 0;
return attempt_allocation(word_size, word_size, &dummy);
}
В G1 выделение объектов делится на две формы: выделение больших объектов и обычное выделение. Поскольку код относительно длинный, краткое описание процесса выделения больших объектов выглядит следующим образом:
- Проверьте, нужен ли GC, и при необходимости запустите GC, потому что большие объекты очень быстро потребляют кучу
- Подсчитайте, сколько блоков должен занимать большой объект, и попытайтесь выделить последовательные свободные блоки.
- Если не хватает непрерывного пространства, найдите смежный блок, содержащий как свободные, так и используемые, попробуйте расширить
- Попробуйте сборщик мусора, если сбой достигает порога, выделение завершается ошибкой и выполняется следующее нормальное выделение.
Следующий общий процесс распределения более сложен, и в этой статье мы не будем углубляться в него.
3.1.2 Инициализация объектов
код показывает, как показано ниже
// hotspot/share/gc/shared/memAllocator.cpp
oop ObjAllocator::initialize(HeapWord* mem) const {
mem_clear(mem);
return finish(mem);
}
вmem_clear()
Метод относительно прост, то есть установить все данные объекта, кроме головы, в 0, код выглядит следующим образом:
// hotspot/share/gc/shared/memAllocator.cpp
void MemAllocator::mem_clear(HeapWord* mem) const {
const size_t hs = oopDesc::header_size();
oopDesc::set_klass_gap(mem, 0);
Copy::fill_to_aligned_words(mem + hs, _word_size - hs);
}
см. далееfinish()
функция,
// hotspot/share/gc/shared/memAllocator.cpp
oop MemAllocator::finish(HeapWord* mem) const {
assert(mem != NULL, "NULL object pointer");
if (UseBiasedLocking) {
oopDesc::set_mark_raw(mem, _klass->prototype_header());
} else {
oopDesc::set_mark_raw(mem, markOopDesc::prototype());
}
oopDesc::release_set_klass(mem, _klass);
return oop(mem);
}
Помните, что в заголовке объекта есть два атрибута: метка и метаданные?finish()
Метод заключается в установке данных заголовка объекта.
3.2 Регистрация метода finalize()
Поскольку finalize() используется редко, а внутренняя логика относительно сложна, в этой статье пока не рассматривается механизм регистрации finalize.
4. Общий процесс
Общий процесс выделения всего объекта JVM примерно выглядит следующим образом:
Ссылаться на
[1] The Java® Virtual Machine Specification Java SE 11 Edition