HHH-8683 Class org.hibernate.engine.spi.EntityEntry consumes lots of memory

This commit is contained in:
Emmanuel Bernard 2014-07-01 21:37:22 +02:00 committed by Sanne Grinovero
parent d84c6b35aa
commit dedd24afc2
3 changed files with 414 additions and 58 deletions

View File

@ -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 <emmanuel@hibernate.org>
*/
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 extends EntityEntryExtraState> T getExtraState(Class<T> extraStateType) {
if ( next == null ) {
return null;
}
if ( extraStateType.isAssignableFrom( next.getClass() ) ) {
return (T) next;
}
else {
return next.getExtraState( extraStateType );
}
}
}

View File

@ -1,7 +1,7 @@
/* /*
* Hibernate, Relational Persistence for Idiomatic Java * 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 * indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are * statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc. * distributed under license by Red Hat Inc.
@ -28,12 +28,14 @@
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.io.Serializable; import java.io.Serializable;
import org.hibernate.AssertionFailure;
import org.hibernate.CustomEntityDirtinessStrategy; import org.hibernate.CustomEntityDirtinessStrategy;
import org.hibernate.EntityMode; import org.hibernate.EntityMode;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
import org.hibernate.LockMode; import org.hibernate.LockMode;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.bytecode.instrumentation.spi.FieldInterceptor; import org.hibernate.bytecode.instrumentation.spi.FieldInterceptor;
import org.hibernate.engine.internal.EntityEntryExtraStateHolder;
import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.UniqueKeyLoadable; import org.hibernate.persister.entity.UniqueKeyLoadable;
import org.hibernate.pretty.MessageHelper; import org.hibernate.pretty.MessageHelper;
@ -41,26 +43,49 @@
/** /**
* We need an entry to tell us all about the current state of an object with respect to its persistent state * 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 Gavin King
* @author Emmanuel Bernard <emmanuel@hibernate.org>
* @author Gunnar Morling
* @author Sanne Grinovero <sanne@hibernate.org>
*/ */
public final class EntityEntry implements Serializable { public final class EntityEntry implements Serializable {
private LockMode lockMode;
private Status status;
private Status previousStatus;
private final Serializable id; private final Serializable id;
private Object[] loadedState; private Object[] loadedState;
private Object[] deletedState;
private boolean existsInDatabase;
private Object version; private Object version;
private transient EntityPersister persister; private final EntityPersister persister; // permanent but we only need the entityName state in a non transient way
private final String entityName; private transient EntityKey cachedEntityKey; // cached EntityKey (lazy-initialized)
// 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 transient Object rowId; private final transient Object rowId;
private final transient PersistenceContext persistenceContext; 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.
* <p>
* The value is structured as follows:
*
* <pre>
* 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
* </pre>
* 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.
* <p>
* 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. * @deprecated the tenantId and entityMode parameters where removed: this constructor accepts but ignores them.
@ -97,21 +122,21 @@ public EntityEntry(
final boolean disableVersionIncrement, final boolean disableVersionIncrement,
final boolean lazyPropertiesAreUnfetched, final boolean lazyPropertiesAreUnfetched,
final PersistenceContext persistenceContext) { final PersistenceContext persistenceContext) {
this.status = status; setCompressedValue( EnumState.STATUS, status );
this.previousStatus = null; // not useful strictly speaking but more explicit
setCompressedValue( EnumState.PREVIOUS_STATUS, null );
// only retain loaded state if the status is not Status.READ_ONLY // only retain loaded state if the status is not Status.READ_ONLY
if ( status != Status.READ_ONLY ) { if ( status != Status.READ_ONLY ) {
this.loadedState = loadedState; this.loadedState = loadedState;
} }
this.id=id; this.id=id;
this.rowId=rowId; this.rowId=rowId;
this.existsInDatabase=existsInDatabase; setCompressedValue( BooleanState.EXISTS_IN_DATABASE, existsInDatabase );
this.version=version; this.version=version;
this.lockMode=lockMode; setCompressedValue( EnumState.LOCK_MODE, lockMode );
this.isBeingReplicated=disableVersionIncrement; setCompressedValue( BooleanState.IS_BEING_REPLICATED, disableVersionIncrement );
this.loadedWithLazyPropertiesUnfetched = lazyPropertiesAreUnfetched; setCompressedValue( BooleanState.LOADED_WITH_LAZY_PROPERTIES_UNFETCHED, lazyPropertiesAreUnfetched );
this.persister=persister; this.persister=persister;
this.entityName = persister == null ? null : persister.getEntityName();
this.persistenceContext = persistenceContext; this.persistenceContext = persistenceContext;
} }
@ -133,33 +158,35 @@ private EntityEntry(
final boolean isBeingReplicated, final boolean isBeingReplicated,
final boolean loadedWithLazyPropertiesUnfetched, final boolean loadedWithLazyPropertiesUnfetched,
final PersistenceContext persistenceContext) { final PersistenceContext persistenceContext) {
this.entityName = entityName;
this.persister = ( factory == null ? null : factory.getEntityPersister( entityName ) ); this.persister = ( factory == null ? null : factory.getEntityPersister( entityName ) );
this.id = id; this.id = id;
this.status = status; setCompressedValue( EnumState.STATUS, status );
this.previousStatus = previousStatus; setCompressedValue( EnumState.PREVIOUS_STATUS, previousStatus );
this.loadedState = loadedState; this.loadedState = loadedState;
this.deletedState = deletedState; setDeletedState( deletedState );
this.version = version; this.version = version;
this.lockMode = lockMode; setCompressedValue( EnumState.LOCK_MODE, lockMode );
this.existsInDatabase = existsInDatabase; setCompressedValue( BooleanState.EXISTS_IN_DATABASE, existsInDatabase );
this.isBeingReplicated = isBeingReplicated; setCompressedValue( BooleanState.IS_BEING_REPLICATED, isBeingReplicated );
this.loadedWithLazyPropertiesUnfetched = loadedWithLazyPropertiesUnfetched; setCompressedValue( BooleanState.LOADED_WITH_LAZY_PROPERTIES_UNFETCHED, loadedWithLazyPropertiesUnfetched );
// this is equivalent to the old behavior... this.rowId = null; // this is equivalent to the old behavior...
this.rowId = null;
this.persistenceContext = persistenceContext; this.persistenceContext = persistenceContext;
} }
public LockMode getLockMode() { public LockMode getLockMode() {
return lockMode; return getCompressedValue( EnumState.LOCK_MODE, LockMode.class );
} }
public void setLockMode(LockMode lockMode) { public void setLockMode(LockMode lockMode) {
this.lockMode = lockMode; setCompressedValue( EnumState.LOCK_MODE, lockMode );
} }
public Status getStatus() { 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) { public void setStatus(Status status) {
@ -167,9 +194,12 @@ public void setStatus(Status status) {
//memory optimization //memory optimization
loadedState = null; loadedState = null;
} }
if ( this.status != status ) {
this.previousStatus = this.status; Status currentStatus = this.getStatus();
this.status = status;
if ( currentStatus != status ) {
setCompressedValue( EnumState.PREVIOUS_STATUS, currentStatus );
setCompressedValue( EnumState.STATUS, status );
} }
} }
@ -181,16 +211,28 @@ public Object[] getLoadedState() {
return loadedState; return loadedState;
} }
private static final Object[] DEFAULT_DELETED_STATE = null;
public Object[] getDeletedState() { public Object[] getDeletedState() {
return deletedState; EntityEntryExtraStateHolder extra = getExtraState( EntityEntryExtraStateHolder.class );
return extra != null ? extra.getDeletedState() : DEFAULT_DELETED_STATE;
} }
public void setDeletedState(Object[] deletedState) { 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() { public boolean isExistsInDatabase() {
return existsInDatabase; return getCompressedValue( BooleanState.EXISTS_IN_DATABASE );
} }
public Object getVersion() { public Object getVersion() {
@ -217,11 +259,12 @@ public EntityKey getEntityKey() {
} }
public String getEntityName() { public String getEntityName() {
return entityName; return persister == null ? null : persister.getEntityName();
} }
public boolean isBeingReplicated() { public boolean isBeingReplicated() {
return isBeingReplicated; return getCompressedValue( BooleanState.IS_BEING_REPLICATED );
} }
public Object getRowId() { public Object getRowId() {
@ -269,9 +312,9 @@ public void postUpdate(Object entity, Object[] updatedState, Object nextVersion)
* exists in the database * exists in the database
*/ */
public void postDelete() { public void postDelete() {
previousStatus = status; setCompressedValue( EnumState.PREVIOUS_STATUS, getStatus() );
status = Status.GONE; setCompressedValue( EnumState.STATUS, Status.GONE );
existsInDatabase = false; setCompressedValue( BooleanState.EXISTS_IN_DATABASE, false );
} }
/** /**
@ -279,7 +322,7 @@ public void postDelete() {
* database (needed for identity-column key generation) * database (needed for identity-column key generation)
*/ */
public void postInsert(Object[] insertedState) { public void postInsert(Object[] insertedState) {
existsInDatabase = true; setCompressedValue( BooleanState.EXISTS_IN_DATABASE, true );
} }
public boolean isNullifiable(boolean earlyInsert, SessionImplementor session) { public boolean isNullifiable(boolean earlyInsert, SessionImplementor session) {
@ -357,6 +400,8 @@ private boolean isUnequivocallyNonDirty(Object entity) {
* @return true, if the entity is modifiable; false, otherwise, * @return true, if the entity is modifiable; false, otherwise,
*/ */
public boolean isModifiableEntity() { public boolean isModifiableEntity() {
Status status = getStatus();
Status previousStatus = getPreviousStatus();
return getPersister().isMutable() return getPersister().isMutable()
&& status != Status.READ_ONLY && status != Status.READ_ONLY
&& ! ( status == Status.DELETED && previousStatus == Status.READ_ONLY ); && ! ( status == Status.DELETED && previousStatus == Status.READ_ONLY );
@ -372,8 +417,9 @@ public void forceLocked(Object entity, Object nextVersion) {
} }
public boolean isReadOnly() { public boolean isReadOnly() {
if ( status != Status.MANAGED && status != Status.READ_ONLY ) { Status status = getStatus();
throw new HibernateException( "instance was not in a valid state" ); if (status != Status.MANAGED && status != Status.READ_ONLY) {
throw new HibernateException("instance was not in a valid state");
} }
return status == Status.READ_ONLY; return status == Status.READ_ONLY;
} }
@ -405,11 +451,13 @@ public void setReadOnly(boolean readOnly, Object entity) {
@Override @Override
public String toString() { public String toString() {
return "EntityEntry" + MessageHelper.infoString( entityName, id ) + '(' + status + ')'; return "EntityEntry" +
MessageHelper.infoString( getPersister().getEntityName(), id ) +
'(' + getStatus() + ')';
} }
public boolean isLoadedWithLazyPropertiesUnfetched() { public boolean isLoadedWithLazyPropertiesUnfetched() {
return loadedWithLazyPropertiesUnfetched; return getCompressedValue( BooleanState.LOADED_WITH_LAZY_PROPERTIES_UNFETCHED );
} }
/** /**
@ -418,21 +466,22 @@ public boolean isLoadedWithLazyPropertiesUnfetched() {
* *
* @param oos The stream to which we should write the serial data. * @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 { public void serialize(ObjectOutputStream oos) throws IOException {
oos.writeObject( entityName ); Status previousStatus = getPreviousStatus();
oos.writeObject( getEntityName() );
oos.writeObject( id ); oos.writeObject( id );
oos.writeObject( status.name() ); oos.writeObject( getStatus().name() );
oos.writeObject( (previousStatus == null ? "" : previousStatus.name()) ); oos.writeObject( (previousStatus == null ? "" : previousStatus.name()) );
// todo : potentially look at optimizing these two arrays // todo : potentially look at optimizing these two arrays
oos.writeObject( loadedState ); oos.writeObject( loadedState );
oos.writeObject( deletedState ); oos.writeObject( getDeletedState() );
oos.writeObject( version ); oos.writeObject( version );
oos.writeObject( lockMode.toString() ); oos.writeObject( getLockMode().toString() );
oos.writeBoolean( existsInDatabase ); oos.writeBoolean( isExistsInDatabase() );
oos.writeBoolean( isBeingReplicated ); oos.writeBoolean( isBeingReplicated() );
oos.writeBoolean( loadedWithLazyPropertiesUnfetched ); oos.writeBoolean( isLoadedWithLazyPropertiesUnfetched() );
} }
/** /**
@ -470,4 +519,192 @@ public static EntityEntry deserialize(
persistenceContext 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 extends EntityEntryExtraState> T getExtraState(Class<T> 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 extends Enum<?>> E getCompressedValue(EnumState state, Class<E> 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 <E extends Enum<?>> EnumState(int offset, Class<E> 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;
}
}
} }

View File

@ -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 <emmanuel@hibernate.org>
*/
public interface EntityEntryExtraState {
/**
* Attach additional state to the core state of {@link org.hibernate.engine.spi.EntityEntry}
* <p>
* 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.
* <p>
* Implementations must return self if they match or delegate discovery to the next state in line.
*/
<T extends EntityEntryExtraState> T getExtraState(Class<T> extraStateType);
//a remove method is ugly to define and has not real use case that we found: left out
}