diff --git a/jvm-streaming.cabal b/jvm-streaming.cabal
--- a/jvm-streaming.cabal
+++ b/jvm-streaming.cabal
@@ -1,5 +1,5 @@
 name:                jvm-streaming
-version:             0.1
+version:             0.2
 synopsis:            Expose Java iterators as streams from the streaming package.
 description:         Please see README.md.
 homepage:            http://github.com/tweag/inline-java/tree/master/jvm-streaming#readme
@@ -26,8 +26,8 @@
   build-depends:
     base >= 4.7 && < 5,
     distributed-closure >= 0.3,
-    jni >= 0.2.1,
-    jvm >= 0.1,
+    jni >= 0.3.0,
+    jvm >= 0.2.0,
     inline-java >= 0.6,
     singletons >= 2.2,
     streaming >= 0.1.4
@@ -48,4 +48,3 @@
     jvm-streaming,
     streaming
   default-language: Haskell2010
-  extra-libraries: pthread
diff --git a/src/Language/Java/Streaming.hs b/src/Language/Java/Streaming.hs
--- a/src/Language/Java/Streaming.hs
+++ b/src/Language/Java/Streaming.hs
@@ -20,11 +20,13 @@
 module Language.Java.Streaming () where
 
 import Control.Distributed.Closure.TH
+import qualified Data.Coerce as Coerce
 import Data.IORef (IORef, newIORef, readIORef, writeIORef)
 import Data.Int (Int64)
 import Data.Singletons (SomeSing(..))
 import Data.Word (Word8)
-import Foreign.Ptr (FunPtr, freeHaskellFunPtr, intPtrToPtr, ptrToIntPtr)
+import Foreign.Ptr (FunPtr, Ptr, freeHaskellFunPtr, intPtrToPtr, ptrToIntPtr)
+import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
 import qualified Foreign.JNI as JNI
 import Foreign.JNI.Types (jnull)
 import GHC.Stable
@@ -41,10 +43,11 @@
 
 isPoppableStream :: IORef (Stream (Of a) IO ()) -> IO Word8
 isPoppableStream ref = do
-    stream <- readIORef ref
-    Streaming.uncons stream >>= \case
+    readIORef ref >>= Streaming.uncons >>= \case
       Nothing -> return $ fromIntegral $ fromEnum False
-      Just _ -> return $ fromIntegral $ fromEnum True
+      Just (x, stream) -> do
+        writeIORef ref (Streaming.cons x stream)
+        return $ fromIntegral $ fromEnum True
 
 popStream :: Reflect a ty => IORef (Stream (Of a) IO ()) -> IO (J ty)
 popStream ref = do
@@ -58,25 +61,25 @@
         writeIORef ref stream'
         reflect x
 
-type JNIFun a = JNIEnv -> JObject -> IO a
+type JNIFun a = JNIEnv -> Ptr JObject -> IO a
 
 foreign import ccall "wrapper" wrapObjectFun
-  :: JNIFun (J ty) -> IO (FunPtr (JNIFun (J ty)))
+  :: JNIFun (Ptr (J ty)) -> IO (FunPtr (JNIFun (Ptr (J ty))))
 foreign import ccall "wrapper" wrapBooleanFun
   :: JNIFun Word8 -> IO (FunPtr (JNIFun Word8))
 
 -- Export only to get a FunPtr.
 foreign export ccall "jvm_streaming_freeIterator" freeIterator
-  :: JNIEnv -> JObject -> Int64 -> IO ()
+  :: JNIEnv -> Ptr JObject -> Int64 -> IO ()
 foreign import ccall "&jvm_streaming_freeIterator" freeIteratorPtr
-  :: FunPtr (JNIEnv -> JObject -> Int64 -> IO ())
+  :: FunPtr (JNIEnv -> Ptr JObject -> Int64 -> IO ())
 
 data FunPtrTable = forall ty. FunPtrTable
   { hasNextPtr :: FunPtr (JNIFun Word8)
-  , nextPtr :: FunPtr (JNIFun (J ty))
+  , nextPtr :: FunPtr (JNIFun (Ptr (J ty)))
   }
 
-freeIterator :: JNIEnv -> JObject -> Int64 -> IO ()
+freeIterator :: JNIEnv -> Ptr JObject -> Int64 -> IO ()
 freeIterator _ _ ptr = do
     let sptr = castPtrToStablePtr $ intPtrToPtr $ fromIntegral ptr
     FunPtrTable{..} <- deRefStablePtr sptr
@@ -91,7 +94,9 @@
 newIterator stream = do
     ref <- newIORef stream
     hasNextPtr <- wrapBooleanFun $ \_ _ -> isPoppableStream ref
-    nextPtr <- wrapObjectFun $ \_ _ -> popStream ref
+    nextPtr <- wrapObjectFun $ \_ _ ->
+      -- Conversion is safe, because result is always a reflected object.
+      unsafeForeignPtrToPtr <$> Coerce.coerce <$> popStream ref
     -- Keep FunPtr's in a table that can be referenced from the Java side, so
     -- that they can be freed.
     tblPtr :: Int64 <- fromIntegral . ptrToIntPtr . castStablePtrToPtr <$> newStablePtr FunPtrTable{..}
@@ -136,7 +141,10 @@
   type instance Interp (Stream (Of a) m r) = 'Iface "java.util.Iterator"
 
   instance Reify a ty => Reify (Stream (Of a) IO ()) ('Iface "java.util.Iterator") where
-    reify it = return $ Streaming.untilRight $ do
+    reify itLocal = do
+      -- We make sure the iterator remains valid while we reference it.
+      it <- JNI.newGlobalRef itLocal
+      return $ Streaming.untilRight $ do
         call it "hasNext" [] >>= \case
           False -> return (Right ())
           True -> do
diff --git a/tests/Language/Java/StreamingSpec.hs b/tests/Language/Java/StreamingSpec.hs
--- a/tests/Language/Java/StreamingSpec.hs
+++ b/tests/Language/Java/StreamingSpec.hs
@@ -4,6 +4,7 @@
 module Language.Java.StreamingSpec where
 
 import Data.Int
+import Data.IORef
 import Language.Java
 import Language.Java.Inline
 import Language.Java.Streaming ()
@@ -34,3 +35,9 @@
         iterator <- reflect (Streaming.each [1..10000] :: Stream (Of Int32) IO ())
         stream <- reify iterator
         Streaming.toList_ stream `shouldReturn` [1..10000 :: Int32]
+      it "don't redo effects" $ do
+        ref <- newIORef (1 :: Int32)
+        iterator <- reflect $
+          Streaming.replicateM 10 $ atomicModifyIORef ref (\i -> (i + 1, i))
+        stream <- reify iterator
+        Streaming.toList_ stream `shouldReturn` [1..10 :: Int32]
