diff --git a/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java b/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java
index 50f0cc17e2f1ee67d2c8ded378dcd7e24b97e831..db07ed5d4d6a8188fd687f8ff55f33dbbba5f76a 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java
@@ -21,6 +21,7 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.RandomAccessFile;
+import java.lang.ref.SoftReference;
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.text.MessageFormat;
@@ -118,8 +119,9 @@ public abstract class AbstractFile extends AbstractContent {
 	private volatile String uniquePath;
 	private volatile FileSystem parentFileSystem;
 	
-	private ContentStream contentStream;
 	private boolean tryContentStream;
+	private Object contentStreamLock = new Object();
+	private SoftReference<ContentStream> contentStreamRef = null;
 
 	/**
 	 * Initializes common fields used by AbstactFile implementations (objects in
@@ -1070,44 +1072,54 @@ short getMetaFlagsAsInt() {
 		return TSK_FS_META_FLAG_ENUM.toInt(metaFlags);
 	}
 	
-	/**
-	 * Attempts to load the content stream for this file.  If none exists, returns false.
-	 * @return  False if no content stream exists for this file.
-	 * @throws TskCoreException 
-	 */
-	private boolean loadContentStream() throws TskCoreException {
-		if (contentStream != null) {
-			return true;
-		} else if (tryContentStream) {
-			// only attempt to load if the flag indicates it should be tried	
-			contentStream = getSleuthkitCase().getContentProvider().getContentStream(this).orElse(null);
-
-			if (contentStream == null) {
-				// if no content stream could be loaded, mark tryContentStream as false so load 
-				// isn't attempted again
-				tryContentStream = false;
-				return false;
-			} else {
-				return true;
-			}
-		} else {
-			return false;
+
+	
+	private boolean hasContentStream() {
+		ContentStream contentStream = null;
+		try {
+			contentStream = getContentStream();	
+		} catch (TskCoreException ex) {
+			LOGGER.log(Level.WARNING, "An error occurred while loading content stream for file with id: " + getId(), ex);
 		}
+		return contentStream != null;
 	}
 	
 	/**
-	 * @return True if the custom content provider for the case should be used to fetch content bytes.
+	 * Attempts to load the content stream for this file.  If none exists, returns null.
+	 * @return  The content stream for this file or null if none exists.
+	 * @throws TskCoreException 
 	 */
-	private boolean useContentProvider() {
-		return this.getCollected() == CollectedStatus.YES_REPO && getSleuthkitCase().getContentProvider() != null;
+	private ContentStream getContentStream() throws TskCoreException {
+		ContentStream contentStream = null;
+		if (tryContentStream) {
+			try {
+				synchronized (contentStreamLock) {
+					// try to get soft reference content stream
+					contentStream = contentStreamRef == null ? null : contentStreamRef.get();
+					// load if not cached and then cache if present
+					if (contentStream == null) {
+						contentStream = getSleuthkitCase().getContentProvider().getContentStream(this).orElse(null);
+						if (contentStream != null) {
+							this.contentStreamRef = new SoftReference<>(contentStream);
+						}
+					}
+				}
+			} finally {
+				if (contentStream == null) {
+					// don't try to load the content stream again if it fails to load (either through exception or not existing)
+					tryContentStream = false;
+				}				
+			}
+		}
+		return contentStream;
 	}
 	
-	
 	@Override
 	public final int read(byte[] buf, long offset, long len) throws TskCoreException {
-		//template method
-		if (useContentProvider() && loadContentStream()) {
-			return this.contentStream.read(buf, offset, len);
+		// try to use content stream if present
+		ContentStream contentStream = getContentStream();
+		if (contentStream != null) {
+			return contentStream.read(buf, offset, len);
 		}
 		
 		//if localPath is set, use local, otherwise, use readCustom() supplied by derived class
@@ -1283,13 +1295,8 @@ final void setEncodingType(TskData.EncodingType encodingType) {
 	 * @return true if the file exists, false otherwise
 	 */
 	public boolean exists() {
-		if (useContentProvider()) {
-			try {
-				return loadContentStream();
-			} catch (TskCoreException ex) {
-				LOGGER.log(Level.SEVERE, ex.getMessage());
-				return false;
-			}
+		if (hasContentStream()) {
+			return true;
 		}
 		
 		if (!localPathSet) {
@@ -1313,13 +1320,8 @@ public boolean exists() {
 	 * @return true if the file is readable
 	 */
 	public boolean canRead() {
-		if (useContentProvider()) {
-			try {
-				return loadContentStream();
-			} catch (TskCoreException ex) {
-				LOGGER.log(Level.SEVERE, ex.getMessage());
-				return false;
-			}
+		if (hasContentStream()) {
+			return true;
 		}
 		
 		if (!localPathSet) {
diff --git a/bindings/java/src/org/sleuthkit/datamodel/ContentStream.java b/bindings/java/src/org/sleuthkit/datamodel/ContentStream.java
index 8037219c322d936593574874536f4f9f93ba2cac..00055d0a78188f1dada7745736c29747fdeecccb 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/ContentStream.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/ContentStream.java
@@ -23,7 +23,7 @@
 /**
  * Custom provider for content bytes.
  */
-public interface ContentStream {
+public interface ContentStream extends AutoCloseable {
 
 	/**
 	 * Reads data that this content object is associated with (file contents,