diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 0958c3a..77fd3a2 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -47,14 +47,7 @@ function _batch_page(Request $request) {
       return new RedirectResponse(\Drupal::url('<front>', [], ['absolute' => TRUE]));
     }
   }
-  // Restore safe strings from previous batches.
-  // This is safe because we are passing through the known safe values from
-  // SafeMarkup::getAll(). See _batch_shutdown().
-  // @todo Ensure we are not storing an excessively large string list in:
-  //   https://www.drupal.org/node/2295823
-  if (!empty($batch['safe_strings'])) {
-    SafeMarkup::setMultiple($batch['safe_strings']);
-  }
+
   // Register database update for the end of processing.
   drupal_register_shutdown_function('_batch_shutdown');
 
@@ -521,10 +514,6 @@ function _batch_finished() {
  */
 function _batch_shutdown() {
   if ($batch = batch_get()) {
-    // Update safe strings.
-    // @todo Ensure we are not storing an excessively large string list in:
-    //   https://www.drupal.org/node/2295823
-    $batch['safe_strings'] = SafeMarkup::getAll();
     \Drupal::service('batch.storage')->update($batch);
   }
 }
diff --git a/core/includes/form.inc b/core/includes/form.inc
index 16353d8..48fa93f 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -6,7 +6,6 @@
  */
 
 use Drupal\Component\Utility\NestedArray;
-use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Database\Database;
@@ -701,8 +700,6 @@ function template_preprocess_form_element_label(&$variables) {
  *   - css: Array of paths to CSS files to be used on the progress page.
  *   - url_options: options passed to url() when constructing redirect URLs for
  *     the batch.
- *   - safe_strings: Internal use only. Used to store and retrieve strings
- *     marked as safe between requests.
  *   - progressive: A Boolean that indicates whether or not the batch needs to
  *     run progressively. TRUE indicates that the batch will run in more than
  *     one run. FALSE (default) indicates that the batch will finish in a single
@@ -854,11 +851,6 @@ function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = N
         $request->query->remove('destination');
       }
 
-      // Store safe strings.
-      // @todo Ensure we are not storing an excessively large string list in:
-      //   https://www.drupal.org/node/2295823
-      $batch['safe_strings'] = SafeMarkup::getAll();
-
       // Store the batch.
       \Drupal::service('batch.storage')->create($batch);
 
diff --git a/core/lib/Drupal/Component/Utility/EscapedString.php b/core/lib/Drupal/Component/Utility/EscapedString.php
new file mode 100644
index 0000000..9a3dcd8
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/EscapedString.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Utility\EscapedString.
+ */
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Escapes a string for HTML display.
+ *
+ * @ingroup sanitization
+ */
+class EscapedString implements EscapedStringInterface {
+
+  /**
+   * The string to escape.
+   *
+   * @var string
+   */
+  protected $string;
+
+  /**
+   * Constructs an EscapedString object.
+   *
+   * @param $string
+   *   The string to escape. This value will be cast to a string.
+   */
+  public function __construct($string) {
+    $this->string = (string) $string;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __toString() {
+    return Html::escape($this->string);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function count() {
+    return Unicode::strlen($this->string);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function jsonSerialize() {
+    return $this->__toString();
+  }
+
+}
diff --git a/core/lib/Drupal/Component/Utility/EscapedStringInterface.php b/core/lib/Drupal/Component/Utility/EscapedStringInterface.php
new file mode 100644
index 0000000..68bcdad
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/EscapedStringInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Utility\EscapedStringInterface.
+ */
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Marks an object's __toString() method as returning escaped HTML.
+ *
+ * This interface should only be used on objects that emit escaped strings from
+ * their __toString() method. If the output of __toString() contains unescaped
+ * HTML, it must not be used.
+ *
+ * @ingroup sanitization
+ *
+ */
+interface EscapedStringInterface extends SafeStringInterface, \Countable {
+
+  /**
+   * Returns an escaped string..
+   */
+  public function __toString();
+
+}
diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php
index 6f42d13..2e3b115 100644
--- a/core/lib/Drupal/Component/Utility/SafeMarkup.php
+++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
@@ -25,25 +25,14 @@
  * @link theme_render theme and render systems @endlink so that the output can
  * can be themed, escaped, and altered properly.
  *
+ * @deprecated Will be removed before Drupal 9.0.0.
+ *
  * @see TwigExtension::escapeFilter()
  * @see twig_render_template()
  * @see sanitization
  * @see theme_render
  */
 class SafeMarkup {
-  use PlaceholderTrait;
-
-  /**
-   * The list of safe strings.
-   *
-   * Strings in this list are marked as secure for the entire page render, not
-   * just the code or element that set it. Therefore, only valid HTML should be
-   * marked as safe (never partial markup). For example, you should never mark
-   * string such as '<' or '<script>' safe.
-   *
-   * @var array
-   */
-  protected static $safeStrings = array();
 
   /**
    * Checks if a string is safe to output.
@@ -51,82 +40,19 @@ class SafeMarkup {
    * @param string|\Drupal\Component\Utility\SafeStringInterface $string
    *   The content to be checked.
    * @param string $strategy
-   *   The escaping strategy. Defaults to 'html'. Two escaping strategies are
-   *   supported by default:
-   *   - 'html': (default) The string is safe for use in HTML code.
-   *   - 'all': The string is safe for all use cases.
-   *   See the
-   *   @link http://twig.sensiolabs.org/doc/filters/escape.html Twig escape documentation @endlink
-   *   for more information on escaping strategies in Twig.
+   *   (optional) This value is ignored.
    *
    * @return bool
    *   TRUE if the string has been marked secure, FALSE otherwise.
+   *
+   * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
+   *   Instead, you should just check if a variable is an instance of
+   *   \Drupal\Component\Utility\SafeStringInterface.
    */
   public static function isSafe($string, $strategy = 'html') {
     // Do the instanceof checks first to save unnecessarily casting the object
     // to a string.
-    return $string instanceOf SafeStringInterface || isset(static::$safeStrings[(string) $string][$strategy]) ||
-      isset(static::$safeStrings[(string) $string]['all']);
-  }
-
-  /**
-   * Adds previously retrieved known safe strings to the safe string list.
-   *
-   * This method is for internal use. Do not use it to prevent escaping of
-   * markup; instead, use the appropriate
-   * @link sanitization sanitization functions @endlink or the
-   * @link theme_render theme and render systems @endlink so that the output
-   * can be themed, escaped, and altered properly.
-   *
-   * This marks strings as secure for the entire page render, not just the code
-   * or element that set it. Therefore, only valid HTML should be
-   * marked as safe (never partial markup). For example, you should never do:
-   * @code
-   *   SafeMarkup::setMultiple(['<' => ['html' => TRUE]]);
-   * @endcode
-   * or:
-   * @code
-   *   SafeMarkup::setMultiple(['<script>' => ['all' => TRUE]]);
-   * @endcode
-
-   * @param array $safe_strings
-   *   A list of safe strings as previously retrieved by self::getAll().
-   *   Every string in this list will be represented by a multidimensional
-   *   array in which the keys are the string and the escaping strategy used for
-   *   this string, and in which the value is the boolean TRUE.
-   *   See self::isSafe() for the list of supported escaping strategies.
-   *
-   * @throws \UnexpectedValueException
-   *
-   * @internal This is called by FormCache, StringTranslation and the Batch API.
-   *   It should not be used anywhere else.
-   */
-  public static function setMultiple(array $safe_strings) {
-    foreach ($safe_strings as $string => $strategies) {
-      foreach ($strategies as $strategy => $value) {
-        $string = (string) $string;
-        if ($value === TRUE) {
-          static::$safeStrings[$string][$strategy] = TRUE;
-        }
-        else {
-          // Danger - something is very wrong.
-          throw new \UnexpectedValueException('Only the value TRUE is accepted for safe strings');
-        }
-      }
-    }
-  }
-
-  /**
-  * Gets all strings currently marked as safe.
-  *
-  * This is useful for the batch and form APIs, where it is important to
-  * preserve the safe markup state across page requests.
-  *
-  * @return array
-  *   An array of strings currently marked safe.
-  */
-  public static function getAll() {
-    return static::$safeStrings;
+    return $string instanceOf SafeStringInterface;
   }
 
   /**
@@ -138,13 +64,12 @@ public static function getAll() {
    * @param string $text
    *   The text to be checked or processed.
    *
-   * @return string
-   *   An HTML safe version of $text, or an empty string if $text is not valid
-   *   UTF-8.
+   * @return \Drupal\Component\Utility\EscapedStringInterface
+   *   An EscapedString object that escapes when rendered to string.
    *
    * @ingroup sanitization
    *
-   * @deprecated Will be removed before Drupal 8.0.0. Rely on Twig's
+   * @deprecated Will be removed before Drupal 9.0.0. Rely on Twig's
    *   auto-escaping feature, or use the @link theme_render #plain_text @endlink
    *   key when constructing a render array that contains plain text in order to
    *   use the renderer's auto-escaping feature. If neither of these are
@@ -154,9 +79,7 @@ public static function getAll() {
    * @see drupal_validate_utf8()
    */
   public static function checkPlain($text) {
-    $string = Html::escape($text);
-    static::$safeStrings[$string]['html'] = TRUE;
-    return $string;
+    return new EscapedString($text);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Form/FormCache.php b/core/lib/Drupal/Core/Form/FormCache.php
index 5c8c14f..b091ed8 100644
--- a/core/lib/Drupal/Core/Form/FormCache.php
+++ b/core/lib/Drupal/Core/Form/FormCache.php
@@ -169,14 +169,6 @@ protected function loadCachedFormState($form_build_id, FormStateInterface $form_
           require_once $this->root . '/' . $file;
         }
       }
-      // Retrieve the list of safe strings and store it for this request. The
-      // safety of these strings has already been determined in ::setCache().
-      // @todo Ensure we are not storing an excessively large string list
-      //   in: https://www.drupal.org/node/2295823
-      $build_info += ['safe_strings' => []];
-      SafeMarkup::setMultiple($build_info['safe_strings']);
-      unset($build_info['safe_strings']);
-      $form_state->setBuildInfo($build_info);
     }
   }
 
@@ -206,11 +198,6 @@ public function setCache($form_build_id, $form, FormStateInterface $form_state)
       $this->keyValueExpirableFactory->get('form')->setWithExpire($form_build_id, $form, $expire);
     }
 
-    // Store the known list of safe strings for form re-use.
-    // @todo Ensure we are not storing an excessively large string list in:
-    //   https://www.drupal.org/node/2295823
-    $form_state->addBuildInfo('safe_strings', SafeMarkup::getAll());
-
     if ($data = $form_state->getCacheableArray()) {
       $this->keyValueExpirableFactory->get('form_state')->setWithExpire($form_build_id, $data, $expire);
     }
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index c2e03c9..251a130 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Render;
 
+use Drupal\Component\Utility\EscapedString;
+use Drupal\Component\Utility\EscapedStringInterface;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\Xss;
@@ -732,7 +734,9 @@ protected function ensureMarkupIsSafe(array $elements) {
     }
 
     if (!empty($elements['#plain_text'])) {
-      $elements['#markup'] = SafeString::create(Html::escape($elements['#plain_text']));
+      // Prevent double escaping by calling htmlspecialchars() directly with
+      // $double_encode set to FALSE.
+      $elements['#markup'] = $elements['#plain_text'] instanceof EscapedStringInterface ? $elements['#plain_text'] : new EscapedString($elements['#plain_text']);
     }
     elseif (!SafeMarkup::isSafe($elements['#markup'])) {
       // The default behaviour is to XSS filter using the admin tag list.
diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist
index 2e496b8..31169cd 100644
--- a/core/phpunit.xml.dist
+++ b/core/phpunit.xml.dist
@@ -46,8 +46,6 @@
   <listeners>
     <listener class="\Drupal\Tests\Listeners\DrupalStandardsListener">
     </listener>
-    <listener class="\Drupal\Tests\Listeners\SafeMarkupSideEffects">
-    </listener>
   </listeners>
   <!-- Filter for coverage reports. -->
   <filter>
diff --git a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
index 279bdd4..93c32c6 100644
--- a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Component\Utility;
 
+use Drupal\Component\Utility\EscapedString;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Component\Utility\SafeStringTrait;
@@ -30,85 +31,6 @@ protected function tearDown() {
     UrlHelper::setAllowedProtocols(['http', 'https']);
   }
 
-
-  /**
-   * Helper function to add a string to the safe list for testing.
-   *
-   * @param string $string
-   *   The content to be marked as secure.
-   * @param string $strategy
-   *   The escaping strategy used for this string. Two values are supported
-   *   by default:
-   *   - 'html': (default) The string is safe for use in HTML code.
-   *   - 'all': The string is safe for all use cases.
-   *   See the
-   *   @link http://twig.sensiolabs.org/doc/filters/escape.html Twig escape documentation @endlink
-   *   for more information on escaping strategies in Twig.
-   *
-   * @return string
-   *   The input string that was marked as safe.
-   */
-  protected function safeMarkupSet($string, $strategy = 'html') {
-    $reflected_class = new \ReflectionClass('\Drupal\Component\Utility\SafeMarkup');
-    $reflected_property = $reflected_class->getProperty('safeStrings');
-    $reflected_property->setAccessible(true);
-    $current_value = $reflected_property->getValue();
-    $current_value[$string][$strategy] = TRUE;
-    $reflected_property->setValue($current_value);
-    return $string;
-  }
-
-  /**
-   * Tests SafeMarkup::isSafe() with different providers.
-   *
-   * @covers ::isSafe
-   */
-  public function testStrategy() {
-    $returned = $this->safeMarkupSet('string0', 'html');
-    $this->assertTrue(SafeMarkup::isSafe($returned), 'String set with "html" provider is safe for default (html)');
-    $returned = $this->safeMarkupSet('string1', 'all');
-    $this->assertTrue(SafeMarkup::isSafe($returned), 'String set with "all" provider is safe for default (html)');
-    $returned = $this->safeMarkupSet('string2', 'css');
-    $this->assertFalse(SafeMarkup::isSafe($returned), 'String set with "css" provider is not safe for default (html)');
-    $returned = $this->safeMarkupSet('string3');
-    $this->assertFalse(SafeMarkup::isSafe($returned, 'all'), 'String set with "html" provider is not safe for "all"');
-  }
-
-  /**
-   * Data provider for testSet().
-   */
-  public function providerSet() {
-    // Checks that invalid multi-byte sequences are escaped.
-    $tests[] = array(
-      'Foo�barbaz',
-      'SafeMarkup::setMarkup() functions with valid sequence "Foo�barbaz"',
-      TRUE
-    );
-    $tests[] = array(
-      "Fooÿñ",
-      'SafeMarkup::setMarkup() functions with valid sequence "Fooÿñ"'
-    );
-    $tests[] = array("<div>", 'SafeMarkup::setMultiple() does not escape HTML');
-
-    return $tests;
-  }
-
-  /**
-   * Tests SafeMarkup::setMultiple().
-   * @dataProvider providerSet
-   *
-   * @param string $text
-   *   The text or object to provide to SafeMarkup::setMultiple().
-   * @param string $message
-   *   The message to provide as output for the test.
-   *
-   * @covers ::setMultiple
-   */
-  public function testSet($text, $message) {
-    SafeMarkup::setMultiple([$text => ['html' => TRUE]]);
-    $this->assertTrue(SafeMarkup::isSafe($text), $message);
-  }
-
   /**
    * Tests SafeMarkup::isSafe() with different objects.
    *
@@ -122,71 +44,60 @@ public function testIsSafe() {
   }
 
   /**
-   * Tests SafeMarkup::setMultiple().
-   *
-   * @covers ::setMultiple
-   */
-  public function testSetMultiple() {
-    $texts = array(
-      'multistring0' => array('html' => TRUE),
-      'multistring1' => array('all' => TRUE),
-    );
-    SafeMarkup::setMultiple($texts);
-    foreach ($texts as $string => $providers) {
-      $this->assertTrue(SafeMarkup::isSafe($string), 'The value has been marked as safe for html');
-    }
-  }
-
-  /**
-   * Tests SafeMarkup::setMultiple().
-   *
-   * Only TRUE may be passed in as the value.
+   * Tests SafeMarkup::checkPlain().
    *
-   * @covers ::setMultiple
+   * @dataProvider providerCheckPlain
+   * @covers ::checkPlain
    *
-   * @expectedException \UnexpectedValueException
+   * @param string $text
+   *   The text to provide to SafeMarkup::checkPlain().
+   * @param string $expected
+   *   The expected output from the function.
+   * @param string $message
+   *   The message to provide as output for the test.
    */
-  public function testInvalidSetMultiple() {
-    $texts = array(
-      'invalidstring0' => array('html' => 1),
-    );
-    SafeMarkup::setMultiple($texts);
+  function testCheckPlain($text, $expected, $message) {
+    $result = SafeMarkup::checkPlain($text);
+    $this->assertTrue($result instanceof EscapedString);
+    $this->assertEquals($expected, $result, $message);
   }
 
   /**
-   * Tests SafeMarkup::checkPlain().
+   * Tests Drupal\Component\Utility\EscapedString.
+   *
+   * Verifies that the result of SafeMarkup::checkPlain() is the same as
+   * using EscapedString directly.
    *
    * @dataProvider providerCheckPlain
-   * @covers ::checkPlain
    *
    * @param string $text
-   *   The text to provide to SafeMarkup::checkPlain().
+   *   The text to provide to the EscapedString constructor.
    * @param string $expected
    *   The expected output from the function.
    * @param string $message
    *   The message to provide as output for the test.
-   * @param bool $ignorewarnings
-   *   Whether or not to ignore PHP 5.3+ invalid multibyte sequence warnings.
    */
-  function testCheckPlain($text, $expected, $message, $ignorewarnings = FALSE) {
-    $result = $ignorewarnings ? @SafeMarkup::checkPlain($text) : SafeMarkup::checkPlain($text);
+  function testEscapeString($text, $expected, $message) {
+    $result = new EscapedString($text);
     $this->assertEquals($expected, $result, $message);
   }
 
   /**
-   * Data provider for testCheckPlain().
+   * Data provider for testCheckPlain() and testEscapeString().
    *
    * @see testCheckPlain()
    */
   function providerCheckPlain() {
     // Checks that invalid multi-byte sequences are escaped.
-    $tests[] = array("Foo\xC0barbaz", 'Foo�barbaz', 'SafeMarkup::checkPlain() escapes invalid sequence "Foo\xC0barbaz"', TRUE);
-    $tests[] = array("\xc2\"", '�&quot;', 'SafeMarkup::checkPlain() escapes invalid sequence "\xc2\""', TRUE);
-    $tests[] = array("Fooÿñ", "Fooÿñ", 'SafeMarkup::checkPlain() does not escape valid sequence "Fooÿñ"');
+    $tests[] = array("Foo\xC0barbaz", 'Foo�barbaz', 'Escapes invalid sequence "Foo\xC0barbaz"');
+    $tests[] = array("\xc2\"", '�&quot;', 'Escapes invalid sequence "\xc2\""');
+    $tests[] = array("Fooÿñ", "Fooÿñ", 'Does not escape valid sequence "Fooÿñ"');
 
     // Checks that special characters are escaped.
-    $tests[] = array("<script>", '&lt;script&gt;', 'SafeMarkup::checkPlain() escapes &lt;script&gt;');
-    $tests[] = array('<>&"\'', '&lt;&gt;&amp;&quot;&#039;', 'SafeMarkup::checkPlain() escapes reserved HTML characters.');
+    $tests[] = array(SafeMarkupTestSafeString::create("<script>"), '&lt;script&gt;', 'Escapes &lt;script&gt; even inside a SafeString.');
+    $tests[] = array("<script>", '&lt;script&gt;', 'Escapes &lt;script&gt;');
+    $tests[] = array('<>&"\'', '&lt;&gt;&amp;&quot;&#039;', 'Escapes reserved HTML characters.');
+    $tests[] = array(SafeMarkupTestSafeString::create('<>&"\''), '&lt;&gt;&amp;&quot;&#039;', 'Escapes reserved HTML characters even inside a SafeString.');
 
     return $tests;
   }
@@ -251,6 +162,9 @@ function providerFormat() {
 
 }
 
+/**
+ * Does not implement SafeStringInterface so casting to string will escape it.
+ */
 class SafeMarkupTestString {
 
   protected $string;
@@ -266,10 +180,7 @@ public function __toString() {
 }
 
 /**
- * Marks text as safe.
- *
- * SafeMarkupTestSafeString is used to mark text as safe because
- * SafeMarkup::$safeStrings is a global static that affects all tests.
+ * Implements SafeStringInterface to wrap text that is safe to render as HTML.
  */
 class SafeMarkupTestSafeString implements SafeStringInterface {
   use SafeStringTrait;
diff --git a/core/tests/Drupal/Tests/Core/Form/FormCacheTest.php b/core/tests/Drupal/Tests/Core/Form/FormCacheTest.php
index 156a125..c6032f0 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormCacheTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormCacheTest.php
@@ -322,34 +322,6 @@ public function testLoadCachedFormStateWithFiles() {
   }
 
   /**
-   * @covers ::loadCachedFormState
-   */
-  public function testLoadCachedFormStateWithSafeStrings() {
-    $this->assertEmpty(SafeMarkup::getAll());
-    $form_build_id = 'the_form_build_id';
-    $form_state = new FormState();
-    $cached_form = ['#cache_token' => NULL];
-
-    $this->formCacheStore->expects($this->once())
-      ->method('get')
-      ->with($form_build_id)
-      ->willReturn($cached_form);
-    $this->account->expects($this->once())
-      ->method('isAnonymous')
-      ->willReturn(TRUE);
-
-    $cached_form_state = ['build_info' => ['safe_strings' => [
-      'a_safe_string' => ['html' => TRUE],
-    ]]];
-    $this->formStateCacheStore->expects($this->once())
-      ->method('get')
-      ->with($form_build_id)
-      ->willReturn($cached_form_state);
-
-    $this->formCache->getCache($form_build_id, $form_state);
-  }
-
-  /**
    * @covers ::setCache
    */
   public function testSetCacheWithForm() {
@@ -364,7 +336,6 @@ public function testSetCacheWithForm() {
       ->with($form_build_id, $form, $this->isType('int'));
 
     $form_state_data = $form_state->getCacheableArray();
-    $form_state_data['build_info']['safe_strings'] = [];
     $this->formStateCacheStore->expects($this->once())
       ->method('setWithExpire')
       ->with($form_build_id, $form_state_data, $this->isType('int'));
@@ -384,7 +355,6 @@ public function testSetCacheWithoutForm() {
       ->method('setWithExpire');
 
     $form_state_data = $form_state->getCacheableArray();
-    $form_state_data['build_info']['safe_strings'] = [];
     $this->formStateCacheStore->expects($this->once())
       ->method('setWithExpire')
       ->with($form_build_id, $form_state_data, $this->isType('int'));
@@ -408,7 +378,6 @@ public function testSetCacheAuthUser() {
       ->with($form_build_id, $form_data, $this->isType('int'));
 
     $form_state_data = $form_state->getCacheableArray();
-    $form_state_data['build_info']['safe_strings'] = [];
     $this->formStateCacheStore->expects($this->once())
       ->method('setWithExpire')
       ->with($form_build_id, $form_state_data, $this->isType('int'));
@@ -426,34 +395,6 @@ public function testSetCacheAuthUser() {
   /**
    * @covers ::setCache
    */
-  public function testSetCacheWithSafeStrings() {
-    SafeMarkup::setMultiple([
-      'a_safe_string' => ['html' => TRUE],
-    ]);
-    $form_build_id = 'the_form_build_id';
-    $form = [
-      '#form_id' => 'the_form_id'
-    ];
-    $form_state = new FormState();
-
-    $this->formCacheStore->expects($this->once())
-      ->method('setWithExpire')
-      ->with($form_build_id, $form, $this->isType('int'));
-
-    $form_state_data = $form_state->getCacheableArray();
-    $form_state_data['build_info']['safe_strings'] = [
-      'a_safe_string' => ['html' => TRUE],
-    ];
-    $this->formStateCacheStore->expects($this->once())
-      ->method('setWithExpire')
-      ->with($form_build_id, $form_state_data, $this->isType('int'));
-
-    $this->formCache->setCache($form_build_id, $form, $form_state);
-  }
-
-  /**
-   * @covers ::setCache
-   */
   public function testSetCacheBuildIdMismatch() {
     $form_build_id = 'the_form_build_id';
     $form = [
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index 4d0dec5..60c4958 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Render;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Access\AccessResultInterface;
@@ -36,18 +37,6 @@ class RendererTest extends RendererTestBase {
   ];
 
   /**
-   * {@inheritdoc}
-   */
-  protected function setUp() {
-    parent::setUp();
-    // Reset the static list of SafeStrings to prevent bleeding between tests.
-    $reflected_class = new \ReflectionClass('\Drupal\Component\Utility\SafeMarkup');
-    $reflected_property = $reflected_class->getProperty('safeStrings');
-    $reflected_property->setAccessible(true);
-    $reflected_property->setValue([]);
-  }
-
-  /**
    * @covers ::render
    * @covers ::doRender
    *
@@ -113,6 +102,10 @@ public function providerTestRenderBasic() {
     $data[] = [[
       '#plain_text' => SafeString::create('<em>foo</em>'),
     ], '&lt;em&gt;foo&lt;/em&gt;'];
+    // Double escaping is prevented in #plain_text.
+    $data[] = [[
+      '#plain_text' => Html::escape("<em>\"'foo'\"</em>"),
+    ], Html::escape("<em>\"'foo'\"</em>")];
     // Renderable child element.
     $data[] = [[
       'child' => ['#markup' => 'bar'],
diff --git a/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php
index d01641b..fd8b405 100644
--- a/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php
+++ b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php
@@ -7,10 +7,10 @@
 
 namespace Drupal\Tests\Core\Template;
 
-use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Render\RenderableInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Template\Loader\StringLoader;
+use Drupal\Core\Render\SafeString;
 use Drupal\Core\Template\TwigEnvironment;
 use Drupal\Core\Template\TwigExtension;
 use Drupal\Tests\UnitTestCase;
@@ -161,14 +161,10 @@ public function testSafeJoin() {
     $twig_extension = new TwigExtension($renderer);
     $twig_environment = $this->prophesize(TwigEnvironment::class)->reveal();
 
-
-    // Simulate t().
-    $string = '<em>will be markup</em>';
-    SafeMarkup::setMultiple([$string => ['html' => TRUE]]);
-
     $items = [
       '<em>will be escaped</em>',
-      $string,
+      // Simulate t().
+      SafeString::create('<em>will be markup</em>'),
       ['#markup' => '<strong>will be rendered</strong>']
     ];
     $result = $twig_extension->safeJoin($twig_environment, $items, '<br/>');
diff --git a/core/tests/Drupal/Tests/Listeners/SafeMarkupSideEffects.php b/core/tests/Drupal/Tests/Listeners/SafeMarkupSideEffects.php
deleted file mode 100644
index 54939cb..0000000
--- a/core/tests/Drupal/Tests/Listeners/SafeMarkupSideEffects.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Tests\Listeners\SafeMarkupSideEffects.
- *
- * Listener for PHPUnit tests, to enforce that data providers don't add to the
- * SafeMarkup static safe string list.
- */
-
-namespace Drupal\Tests\Listeners;
-
-use Drupal\Component\Utility\SafeMarkup;
-
-/**
- * Listens for PHPUnit tests and fails those with SafeMarkup side effects.
- */
-class SafeMarkupSideEffects extends \PHPUnit_Framework_BaseTestListener {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function startTestSuite(\PHPUnit_Framework_TestSuite $suite) {
-    // Use a static so we only do this test once after all the data providers
-    // have run.
-    static $tested = FALSE;
-    if ($suite->getName() !== '' && !$tested) {
-      $tested = TRUE;
-      if (!empty(SafeMarkup::getAll())) {
-        throw new \RuntimeException('SafeMarkup string list polluted by data providers');
-      }
-    }
-  }
-
-}
diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php
index 5b950d4..3435e6b 100644
--- a/core/tests/Drupal/Tests/UnitTestCase.php
+++ b/core/tests/Drupal/Tests/UnitTestCase.php
@@ -54,12 +54,6 @@ protected function setUp() {
     FileCacheFactory::setConfiguration(['default' => ['class' => '\Drupal\Component\FileCache\NullFileCache']]);
 
     $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
-
-    // Reset the static list of SafeStrings to prevent bleeding between tests.
-    $reflected_class = new \ReflectionClass('\Drupal\Component\Utility\SafeMarkup');
-    $reflected_property = $reflected_class->getProperty('safeStrings');
-    $reflected_property->setAccessible(true);
-    $reflected_property->setValue([]);
   }
 
   /**
