From dedd24afc211a676da591259aee2f72eacf2dc15 Mon Sep 17 00:00:00 2001 From: Emmanuel Bernard Date: Tue, 1 Jul 2014 21:37:22 +0200 Subject: [PATCH] HHH-8683 Class org.hibernate.engine.spi.EntityEntry consumes lots of memory --- .../internal/EntityEntryExtraStateHolder.java | 70 ++++ .../org/hibernate/engine/spi/EntityEntry.java | 353 +++++++++++++++--- .../engine/spi/EntityEntryExtraState.java | 49 +++ 3 files changed, 414 insertions(+), 58 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryExtraStateHolder.java create mode 100644 hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntryExtraState.java diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryExtraStateHolder.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryExtraStateHolder.java new file mode 100644 index 0000000000..0d90d90414 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryExtraStateHolder.java @@ -0,0 +1,70 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2014, Red Hat Inc. or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.engine.internal; + +import org.hibernate.engine.spi.EntityEntryExtraState; + +/** + * Contains optional state from {@link org.hibernate.engine.spi.EntityEntry}. + * + * @author Emmanuel Bernard + */ +public class EntityEntryExtraStateHolder implements EntityEntryExtraState { + private EntityEntryExtraState next; + private Object[] deletedState; + + public Object[] getDeletedState() { + return deletedState; + } + + public void setDeletedState(Object[] deletedState) { + this.deletedState = deletedState; + } + + //the following methods are handling extraState contracts. + //they are not shared by a common superclass to avoid alignment padding + //we are trading off duplication for padding efficiency + @Override + public void addExtraState(EntityEntryExtraState extraState) { + if ( next == null ) { + next = extraState; + } + else { + next.addExtraState( extraState ); + } + } + + @Override + public T getExtraState(Class extraStateType) { + if ( next == null ) { + return null; + } + if ( extraStateType.isAssignableFrom( next.getClass() ) ) { + return (T) next; + } + else { + return next.getExtraState( extraStateType ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java index 6e6c597f40..14c928ee72 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java @@ -1,7 +1,7 @@ /* * Hibernate, Relational Persistence for Idiomatic Java * - * Copyright (c) 2010, Red Hat Inc. or third-party contributors as + * Copyright (c) 2010-2014, Red Hat Inc. or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are * distributed under license by Red Hat Inc. @@ -28,12 +28,14 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; +import org.hibernate.AssertionFailure; import org.hibernate.CustomEntityDirtinessStrategy; import org.hibernate.EntityMode; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.Session; import org.hibernate.bytecode.instrumentation.spi.FieldInterceptor; +import org.hibernate.engine.internal.EntityEntryExtraStateHolder; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.UniqueKeyLoadable; import org.hibernate.pretty.MessageHelper; @@ -41,26 +43,49 @@ import org.hibernate.pretty.MessageHelper; /** * We need an entry to tell us all about the current state of an object with respect to its persistent state * + * Implementation Warning: Hibernate needs to instantiate a high amount of instances of this class, + * therefore we need to take care of its impact on memory consumption. + * * @author Gavin King + * @author Emmanuel Bernard + * @author Gunnar Morling + * @author Sanne Grinovero */ public final class EntityEntry implements Serializable { - private LockMode lockMode; - private Status status; - private Status previousStatus; private final Serializable id; private Object[] loadedState; - private Object[] deletedState; - private boolean existsInDatabase; private Object version; - private transient EntityPersister persister; - private final String entityName; - // cached EntityKey (lazy-initialized) - private transient EntityKey cachedEntityKey; - private boolean isBeingReplicated; - //NOTE: this is not updated when properties are fetched lazily! - private boolean loadedWithLazyPropertiesUnfetched; + private final EntityPersister persister; // permanent but we only need the entityName state in a non transient way + private transient EntityKey cachedEntityKey; // cached EntityKey (lazy-initialized) private final transient Object rowId; private final transient PersistenceContext persistenceContext; + private EntityEntryExtraState next; + + /** + * Holds several boolean and enum typed attributes in a very compact manner. Enum values are stored in 4 bits + * (where 0 represents {@code null}, and each enum value is represented by its ordinal value + 1), thus allowing + * for up to 15 values per enum. Boolean values are stored in one bit. + *

+ * The value is structured as follows: + * + *

+	 * 1 - Lock mode
+	 * 2 - Status
+	 * 3 - Previous Status
+	 * 4 - existsInDatabase
+	 * 5 - isBeingReplicated
+	 * 6 - loadedWithLazyPropertiesUnfetched; NOTE: this is not updated when properties are fetched lazily!
+	 *
+	 * 0000 0000 | 0000 0000 | 0654 3333 | 2222 1111
+	 * 
+ * Use {@link #setCompressedValue(org.hibernate.engine.spi.EntityEntry.EnumState, Enum)}, + * {@link #getCompressedValue(org.hibernate.engine.spi.EntityEntry.EnumState, Class)} etc + * to access the enums and booleans stored in this value. + *

+ * Representing enum values by their ordinal value is acceptable for our case as this value itself is never + * serialized or deserialized and thus is not affected should ordinal values change. + */ + private transient int compressedState; /** * @deprecated the tenantId and entityMode parameters where removed: this constructor accepts but ignores them. @@ -97,21 +122,21 @@ public final class EntityEntry implements Serializable { final boolean disableVersionIncrement, final boolean lazyPropertiesAreUnfetched, final PersistenceContext persistenceContext) { - this.status = status; - this.previousStatus = null; + setCompressedValue( EnumState.STATUS, status ); + // not useful strictly speaking but more explicit + setCompressedValue( EnumState.PREVIOUS_STATUS, null ); // only retain loaded state if the status is not Status.READ_ONLY if ( status != Status.READ_ONLY ) { this.loadedState = loadedState; } this.id=id; this.rowId=rowId; - this.existsInDatabase=existsInDatabase; + setCompressedValue( BooleanState.EXISTS_IN_DATABASE, existsInDatabase ); this.version=version; - this.lockMode=lockMode; - this.isBeingReplicated=disableVersionIncrement; - this.loadedWithLazyPropertiesUnfetched = lazyPropertiesAreUnfetched; + setCompressedValue( EnumState.LOCK_MODE, lockMode ); + setCompressedValue( BooleanState.IS_BEING_REPLICATED, disableVersionIncrement ); + setCompressedValue( BooleanState.LOADED_WITH_LAZY_PROPERTIES_UNFETCHED, lazyPropertiesAreUnfetched ); this.persister=persister; - this.entityName = persister == null ? null : persister.getEntityName(); this.persistenceContext = persistenceContext; } @@ -133,33 +158,35 @@ public final class EntityEntry implements Serializable { final boolean isBeingReplicated, final boolean loadedWithLazyPropertiesUnfetched, final PersistenceContext persistenceContext) { - this.entityName = entityName; this.persister = ( factory == null ? null : factory.getEntityPersister( entityName ) ); this.id = id; - this.status = status; - this.previousStatus = previousStatus; + setCompressedValue( EnumState.STATUS, status ); + setCompressedValue( EnumState.PREVIOUS_STATUS, previousStatus ); this.loadedState = loadedState; - this.deletedState = deletedState; + setDeletedState( deletedState ); this.version = version; - this.lockMode = lockMode; - this.existsInDatabase = existsInDatabase; - this.isBeingReplicated = isBeingReplicated; - this.loadedWithLazyPropertiesUnfetched = loadedWithLazyPropertiesUnfetched; - // this is equivalent to the old behavior... - this.rowId = null; + setCompressedValue( EnumState.LOCK_MODE, lockMode ); + setCompressedValue( BooleanState.EXISTS_IN_DATABASE, existsInDatabase ); + setCompressedValue( BooleanState.IS_BEING_REPLICATED, isBeingReplicated ); + setCompressedValue( BooleanState.LOADED_WITH_LAZY_PROPERTIES_UNFETCHED, loadedWithLazyPropertiesUnfetched ); + this.rowId = null; // this is equivalent to the old behavior... this.persistenceContext = persistenceContext; } public LockMode getLockMode() { - return lockMode; + return getCompressedValue( EnumState.LOCK_MODE, LockMode.class ); } public void setLockMode(LockMode lockMode) { - this.lockMode = lockMode; + setCompressedValue( EnumState.LOCK_MODE, lockMode ); } public Status getStatus() { - return status; + return getCompressedValue( EnumState.STATUS, Status.class ); + } + + private Status getPreviousStatus() { + return getCompressedValue( EnumState.PREVIOUS_STATUS, Status.class ); } public void setStatus(Status status) { @@ -167,9 +194,12 @@ public final class EntityEntry implements Serializable { //memory optimization loadedState = null; } - if ( this.status != status ) { - this.previousStatus = this.status; - this.status = status; + + Status currentStatus = this.getStatus(); + + if ( currentStatus != status ) { + setCompressedValue( EnumState.PREVIOUS_STATUS, currentStatus ); + setCompressedValue( EnumState.STATUS, status ); } } @@ -181,16 +211,28 @@ public final class EntityEntry implements Serializable { return loadedState; } + private static final Object[] DEFAULT_DELETED_STATE = null; + public Object[] getDeletedState() { - return deletedState; + EntityEntryExtraStateHolder extra = getExtraState( EntityEntryExtraStateHolder.class ); + return extra != null ? extra.getDeletedState() : DEFAULT_DELETED_STATE; } public void setDeletedState(Object[] deletedState) { - this.deletedState = deletedState; + EntityEntryExtraStateHolder extra = getExtraState( EntityEntryExtraStateHolder.class ); + if ( extra == null && deletedState == DEFAULT_DELETED_STATE ) { + //this is the default value and we do not store the extra state + return; + } + if ( extra == null ) { + extra = new EntityEntryExtraStateHolder(); + addExtraState( extra ); + } + extra.setDeletedState( deletedState ); } public boolean isExistsInDatabase() { - return existsInDatabase; + return getCompressedValue( BooleanState.EXISTS_IN_DATABASE ); } public Object getVersion() { @@ -217,11 +259,12 @@ public final class EntityEntry implements Serializable { } public String getEntityName() { - return entityName; + return persister == null ? null : persister.getEntityName(); + } public boolean isBeingReplicated() { - return isBeingReplicated; + return getCompressedValue( BooleanState.IS_BEING_REPLICATED ); } public Object getRowId() { @@ -269,17 +312,17 @@ public final class EntityEntry implements Serializable { * exists in the database */ public void postDelete() { - previousStatus = status; - status = Status.GONE; - existsInDatabase = false; + setCompressedValue( EnumState.PREVIOUS_STATUS, getStatus() ); + setCompressedValue( EnumState.STATUS, Status.GONE ); + setCompressedValue( BooleanState.EXISTS_IN_DATABASE, false ); } /** - * After actually inserting a row, record the fact that the instance exists on the + * After actually inserting a row, record the fact that the instance exists on the * database (needed for identity-column key generation) */ public void postInsert(Object[] insertedState) { - existsInDatabase = true; + setCompressedValue( BooleanState.EXISTS_IN_DATABASE, true ); } public boolean isNullifiable(boolean earlyInsert, SessionImplementor session) { @@ -357,6 +400,8 @@ public final class EntityEntry implements Serializable { * @return true, if the entity is modifiable; false, otherwise, */ public boolean isModifiableEntity() { + Status status = getStatus(); + Status previousStatus = getPreviousStatus(); return getPersister().isMutable() && status != Status.READ_ONLY && ! ( status == Status.DELETED && previousStatus == Status.READ_ONLY ); @@ -372,8 +417,9 @@ public final class EntityEntry implements Serializable { } public boolean isReadOnly() { - if ( status != Status.MANAGED && status != Status.READ_ONLY ) { - throw new HibernateException( "instance was not in a valid state" ); + Status status = getStatus(); + if (status != Status.MANAGED && status != Status.READ_ONLY) { + throw new HibernateException("instance was not in a valid state"); } return status == Status.READ_ONLY; } @@ -405,11 +451,13 @@ public final class EntityEntry implements Serializable { @Override public String toString() { - return "EntityEntry" + MessageHelper.infoString( entityName, id ) + '(' + status + ')'; + return "EntityEntry" + + MessageHelper.infoString( getPersister().getEntityName(), id ) + + '(' + getStatus() + ')'; } public boolean isLoadedWithLazyPropertiesUnfetched() { - return loadedWithLazyPropertiesUnfetched; + return getCompressedValue( BooleanState.LOADED_WITH_LAZY_PROPERTIES_UNFETCHED ); } /** @@ -418,21 +466,22 @@ public final class EntityEntry implements Serializable { * * @param oos The stream to which we should write the serial data. * - * @throws IOException If a stream error occurs + * @throws java.io.IOException If a stream error occurs */ public void serialize(ObjectOutputStream oos) throws IOException { - oos.writeObject( entityName ); + Status previousStatus = getPreviousStatus(); + oos.writeObject( getEntityName() ); oos.writeObject( id ); - oos.writeObject( status.name() ); + oos.writeObject( getStatus().name() ); oos.writeObject( (previousStatus == null ? "" : previousStatus.name()) ); // todo : potentially look at optimizing these two arrays oos.writeObject( loadedState ); - oos.writeObject( deletedState ); + oos.writeObject( getDeletedState() ); oos.writeObject( version ); - oos.writeObject( lockMode.toString() ); - oos.writeBoolean( existsInDatabase ); - oos.writeBoolean( isBeingReplicated ); - oos.writeBoolean( loadedWithLazyPropertiesUnfetched ); + oos.writeObject( getLockMode().toString() ); + oos.writeBoolean( isExistsInDatabase() ); + oos.writeBoolean( isBeingReplicated() ); + oos.writeBoolean( isLoadedWithLazyPropertiesUnfetched() ); } /** @@ -470,4 +519,192 @@ public final class EntityEntry implements Serializable { persistenceContext ); } + + //the following methods are handling extraState contracts. + //they are not shared by a common superclass to avoid alignment padding + //we are trading off duplication for padding efficiency + public void addExtraState(EntityEntryExtraState extraState) { + if ( next == null ) { + next = extraState; + } + else { + next.addExtraState( extraState ); + } + } + + public T getExtraState(Class extraStateType) { + if ( next == null ) { + return null; + } + if ( extraStateType.isAssignableFrom( next.getClass() ) ) { + return (T) next; + } + else { + return next.getExtraState( extraStateType ); + } + } + /** + * Saves the value for the given enum property. + * + * @param state + * identifies the value to store + * @param value + * the value to store; The caller must make sure that it matches + * the given identifier + */ + private void setCompressedValue(EnumState state, Enum value) { + // reset the bits for the given property to 0 + compressedState &= state.getUnsetMask(); + // store the numeric representation of the enum value at the right offset + compressedState |= ( state.getValue( value ) << state.getOffset() ); + } + + /** + * Gets the current value of the given enum property. + * + * @param state + * identifies the value to store + * @param type + * the actual enum type of the given property; The caller must + * make sure that it matches the given identifier + * @return the current value of the specified property + */ + private > E getCompressedValue(EnumState state, Class type) { + E[] enumConstants = type.getEnumConstants(); + // restore the numeric value from the bits at the right offset and return the corresponding enum constant + int index = ( ( compressedState & state.getMask() ) >> state.getOffset() ) - 1; + return index == - 1 ? null : enumConstants[index]; + } + + /** + * Saves the value for the given boolean flag. + * + * @param state + * identifies the value to store + * @param value + * the value to store + */ + private void setCompressedValue(BooleanState state, boolean value) { + compressedState &= state.getUnsetMask(); + compressedState |= ( state.getValue( value ) << state.getOffset() ); + } + + /** + * Gets the current value of the given boolean flag. + * + * @param state + * identifies the value to store + * @return the current value of the specified flag + */ + private boolean getCompressedValue(BooleanState state) { + return ( ( compressedState & state.getMask() ) >> state.getOffset() ) == 1; + } + + /** + * Represents an enum value stored within a number value, using four bits starting at a specified offset. + * + * @author Gunnar Morling + */ + private enum EnumState { + + LOCK_MODE(0, LockMode.class), + STATUS(4, Status.class), + PREVIOUS_STATUS(8, Status.class); + + private final int offset; + private final int mask; + private final int unsetMask; + + private > EnumState(int offset, Class enumType) { + // In case any of the enums cannot be stored in 4 bits anymore, we'd have to re-structure the compressed + // state int + if ( enumType.getEnumConstants().length > 15 ) { + throw new AssertionFailure( "Cannot store enum type " + enumType.getName() + " in compressed state as" + + " it has too many values." ); + } + + this.offset = offset; + + // a mask for reading the four bits, starting at the right offset + this.mask = 0xF << offset; + + // a mask for setting the four bits at the right offset to 0 + this.unsetMask = 0xFFFF & ~mask; + } + + /** + * Returns the numeric value to be stored for the given enum value. + */ + private int getValue(Enum value) { + return value != null ? value.ordinal() + 1 : 0; + } + + /** + * Returns the offset within the number value at which this enum value is stored. + */ + private int getOffset() { + return offset; + } + + /** + * Returns the bit mask for reading this enum value from the number value storing it. + */ + private int getMask() { + return mask; + } + + /** + * Returns the bit mask for resetting this enum value from the number value storing it. + */ + private int getUnsetMask() { + return unsetMask; + } + } + + /** + * Represents a boolean flag stored within a number value, using one bit at a specified offset. + * + * @author Gunnar Morling + */ + private enum BooleanState { + + EXISTS_IN_DATABASE(13), + IS_BEING_REPLICATED(14), + LOADED_WITH_LAZY_PROPERTIES_UNFETCHED(15); + + private final int offset; + private final int mask; + private final int unsetMask; + + private BooleanState(int offset) { + this.offset = offset; + this.mask = 0x1 << offset; + this.unsetMask = 0xFFFF & ~mask; + } + + private int getValue(boolean value) { + return value ? 1 : 0; + } + + /** + * Returns the offset within the number value at which this boolean flag is stored. + */ + private int getOffset() { + return offset; + } + + /** + * Returns the bit mask for reading this flag from the number value storing it. + */ + private int getMask() { + return mask; + } + + /** + * Returns the bit mask for resetting this flag from the number value storing it. + */ + private int getUnsetMask() { + return unsetMask; + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntryExtraState.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntryExtraState.java new file mode 100644 index 0000000000..d0622eada4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntryExtraState.java @@ -0,0 +1,49 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2014, Red Hat Inc. or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ + +package org.hibernate.engine.spi; + +/** + * Navigation methods for extra state objects attached to {@link org.hibernate.engine.spi.EntityEntry}. + * + * @author Emmanuel Bernard + */ +public interface EntityEntryExtraState { + + /** + * Attach additional state to the core state of {@link org.hibernate.engine.spi.EntityEntry} + *

+ * Implementations must delegate to the next state or add it as next state if last in line. + */ + void addExtraState(EntityEntryExtraState extraState); + + /** + * Retrieve additional state by class type or null if no extra state of that type is present. + *

+ * Implementations must return self if they match or delegate discovery to the next state in line. + */ + T getExtraState(Class extraStateType); + + //a remove method is ugly to define and has not real use case that we found: left out +}