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
*
* 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 <emmanuel@hibernate.org>
* @author Gunnar Morling
* @author Sanne Grinovero <sanne@hibernate.org>
*/
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.
* <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.
@ -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,9 +312,9 @@ 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 );
}
/**
@ -279,7 +322,7 @@ public final class EntityEntry implements Serializable {
* 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 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
}