diff --git a/core/core.services.yml b/core/core.services.yml
index 51ce4c4..0e3d4ec 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -718,6 +718,12 @@ services:
     arguments: ['@authentication']
     calls:
       - [setRequest, ['@?request=']]
+  session.storage:
+    class: Drupal\Core\Session\Storage
+    calls:
+      - [initialize]
+    tags:
+      - { name: persist }
   asset.css.collection_renderer:
     class: Drupal\Core\Asset\CssCollectionRenderer
     arguments: [ '@state' ]
diff --git a/core/includes/session.inc b/core/includes/session.inc
index 76aa83b..5564be6 100644
--- a/core/includes/session.inc
+++ b/core/includes/session.inc
@@ -14,43 +14,7 @@
  * Initializes the session handler, starting a session if needed.
  */
 function drupal_session_initialize() {
-  global $user;
-
-  // Register the default session handler.
-  // @todo: Extract session storage from session handler into a service.
-  $handler = new SessionHandler(\Drupal::request(), \Drupal::database());
-  session_set_save_handler($handler, TRUE);
-
-  $is_https = \Drupal::request()->isSecure();
-  $cookies = \Drupal::request()->cookies;
-  if (($cookies->has(session_name()) && ($session_name = $cookies->get(session_name()))) || ($is_https && Settings::get('mixed_mode_sessions', FALSE) && ($cookies->has(substr(session_name(), 1))) && ($session_name = $cookies->get(substr(session_name(), 1))))) {
-    // If a session cookie exists, initialize the session. Otherwise the
-    // session is only started on demand in drupal_session_commit(), making
-    // anonymous users not use a session cookie unless something is stored in
-    // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
-    drupal_session_start();
-    if ($user->isAuthenticated() || !empty($_SESSION)) {
-      drupal_page_is_cacheable(FALSE);
-    }
-  }
-  else {
-    // Set a session identifier for this request. This is necessary because
-    // we lazily start sessions at the end of this request, and some
-    // processes (like drupal_get_token()) needs to know the future
-    // session ID in advance.
-    $GLOBALS['lazy_session'] = TRUE;
-    $user = new AnonymousUserSession();
-    // Less random sessions (which are much faster to generate) are used for
-    // anonymous users than are generated in drupal_session_regenerate() when
-    // a user becomes authenticated.
-    session_id(Crypt::randomBytesBase64());
-    if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) {
-      $insecure_session_name = substr(session_name(), 1);
-      $session_id = Crypt::randomBytesBase64();
-      $cookies->set($insecure_session_name, $session_id);
-    }
-  }
-  date_default_timezone_set(drupal_get_user_timezone());
+  Drupal::service('session.storage')->initialize();
 }
 
 /**
@@ -59,19 +23,8 @@ function drupal_session_initialize() {
  * @ingroup php_wrappers
  */
 function drupal_session_start() {
-  // Command line clients do not support cookies nor sessions.
-  if (!drupal_session_started() && !drupal_is_cli()) {
-    // Save current session data before starting it, as PHP will destroy it.
-    $session_data = isset($_SESSION) ? $_SESSION : NULL;
-
-    session_start();
-    drupal_session_started(TRUE);
-
-    // Restore session data.
-    if (!empty($session_data)) {
-      $_SESSION += $session_data;
-    }
-  }
+  // Just make sure that the session storage service gets initialized.
+  Drupal::service('session.storage');
 }
 
 /**
@@ -80,47 +33,14 @@ function drupal_session_start() {
  * If an anonymous user already have an empty session, destroy it.
  */
 function drupal_session_commit() {
-  global $user;
-
-  if (!drupal_save_session()) {
-    // We don't have anything to do if we are not allowed to save the session.
-    return;
-  }
-
-  if ($user->isAnonymous() && empty($_SESSION)) {
-    // There is no session data to store, destroy the session if it was
-    // previously started.
-    if (drupal_session_started()) {
-      session_destroy();
-    }
-  }
-  else {
-    // There is session data to store. Start the session if it is not already
-    // started.
-    if (!drupal_session_started()) {
-      drupal_session_start();
-      if (\Drupal::request()->isSecure() && Settings::get('mixed_mode_sessions', FALSE)) {
-        $insecure_session_name = substr(session_name(), 1);
-        $params = session_get_cookie_params();
-        $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
-        $cookie_params = \Drupal::request()->cookies;
-        setcookie($insecure_session_name, $cookie_params->get($insecure_session_name), $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
-      }
-    }
-    // Write the session data.
-    session_write_close();
-  }
+  Drupal::service('session.storage')->save();
 }
 
 /**
  * Returns whether a session has been started.
  */
-function drupal_session_started($set = NULL) {
-  static $session_started = FALSE;
-  if (isset($set)) {
-    $session_started = $set;
-  }
-  return $session_started && session_status() === \PHP_SESSION_ACTIVE;
+function drupal_session_started() {
+  return Drupal::service('session.storage')->isStarted();
 }
 
 /**
@@ -129,72 +49,7 @@ function drupal_session_started($set = NULL) {
  * @ingroup php_wrappers
  */
 function drupal_session_regenerate() {
-  global $user;
-
-  // Nothing to do if we are not allowed to change the session.
-  if (!drupal_save_session()) {
-    return;
-  }
-
-  $is_https = \Drupal::request()->isSecure();
-  $cookies = \Drupal::request()->cookies;
-
-  if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) {
-    $insecure_session_name = substr(session_name(), 1);
-    if (!isset($GLOBALS['lazy_session']) && $cookies->has($insecure_session_name)) {
-      $old_insecure_session_id = $cookies->get($insecure_session_name);
-    }
-    $params = session_get_cookie_params();
-    $session_id = Crypt::randomBytesBase64();
-    // If a session cookie lifetime is set, the session will expire
-    // $params['lifetime'] seconds from the current request. If it is not set,
-    // it will expire when the browser is closed.
-    $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
-    setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
-    $cookies->set($insecure_session_name, $session_id);
-  }
-
-  if (drupal_session_started()) {
-    $old_session_id = session_id();
-  }
-  session_id(Crypt::randomBytesBase64());
-
-  if (isset($old_session_id)) {
-    $params = session_get_cookie_params();
-    $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
-    setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
-    $fields = array('sid' => Crypt::hashBase64(session_id()));
-    if ($is_https) {
-      $fields['ssid'] = Crypt::hashBase64(session_id());
-      // If the "secure pages" setting is enabled, use the newly-created
-      // insecure session identifier as the regenerated sid.
-      if (Settings::get('mixed_mode_sessions', FALSE)) {
-        $fields['sid'] = Crypt::hashBase64($session_id);
-      }
-    }
-    db_update('sessions')
-      ->fields($fields)
-      ->condition($is_https ? 'ssid' : 'sid', Crypt::hashBase64($old_session_id))
-      ->execute();
-  }
-  elseif (isset($old_insecure_session_id)) {
-    // If logging in to the secure site, and there was no active session on the
-    // secure site but a session was active on the insecure site, update the
-    // insecure session with the new session identifiers.
-    db_update('sessions')
-      ->fields(array('sid' => Crypt::hashBase64($session_id), 'ssid' => Crypt::hashBase64(session_id())))
-      ->condition('sid', Crypt::hashBase64($old_insecure_session_id))
-      ->execute();
-  }
-  else {
-    // Start the session when it doesn't exist yet.
-    // Preserve the logged in user, as it will be reset to anonymous
-    // by _drupal_session_read.
-    $account = $user;
-    drupal_session_start();
-    $user = $account;
-  }
-  date_default_timezone_set(drupal_get_user_timezone());
+  Drupal::service('session.storage')->regenerate();
 }
 
 /**
@@ -204,11 +59,6 @@ function drupal_session_regenerate() {
  *   User ID.
  */
 function drupal_session_destroy_uid($uid) {
-  // Nothing to do if we are not allowed to change the session.
-  if (!drupal_save_session()) {
-    return;
-  }
-
   db_delete('sessions')
     ->condition('uid', $uid)
     ->execute();
diff --git a/core/lib/Drupal/Core/Session/Storage.php b/core/lib/Drupal/Core/Session/Storage.php
new file mode 100644
index 0000000..179761f
--- /dev/null
+++ b/core/lib/Drupal/Core/Session/Storage.php
@@ -0,0 +1,247 @@
+<?php
+
+/**
+ * @file
+ * User session handling functions.
+ */
+
+namespace Drupal\Core\Session;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\Settings;
+use Drupal\Core\Session\AnonymousUserSession;
+use Drupal\Core\Session\SessionHandler;
+
+class Storage {
+
+  /**
+   * Whether a lazy session has been started.
+   *
+   * @var bool
+   */
+  protected $lazySession;
+
+  /**
+   * Whether session management is disabled due to environment constraints.
+   *
+   * @var bool
+   */
+  protected $disabled;
+
+  /**
+   * Whether the session has been started.
+   *
+   * @var bool
+   */
+  protected $started;
+
+  /**
+   * Construct the session storage.
+   */
+  public function __construct() {
+    // @todo: Find a way to reliably disable any session management when running
+    // inside the simpletest parent site (e.g. during test setUp and tearDown).
+    // As a workaround simply check for the simpletest namespace in the stack
+    // trace.
+    ob_start();
+    debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
+    $trace = ob_get_clean();
+    if (strpos($trace, 'Drupal\\simpletest') !== FALSE) {
+      $this->disabled = TRUE;
+    }
+
+    // No session management on command line.
+    if (drupal_is_cli()) {
+      $this->disabled = TRUE;
+    }
+  }
+
+  /**
+   * Initialize session handling.
+   *
+   * @todo: This method is here for legacy compatibility.
+   */
+  public function initialize() {
+    global $user;
+
+    // Register the default session handler.
+    // @todo: Extract session storage from session handler into a service.
+    $handler = new SessionHandler(\Drupal::request(), \Drupal::database());
+    session_set_save_handler($handler, TRUE);
+
+    $is_https = \Drupal::request()->isSecure();
+    $cookies = \Drupal::request()->cookies;
+    if (($cookies->has(session_name()) && ($session_name = $cookies->get(session_name()))) || ($is_https && Settings::get('mixed_mode_sessions', FALSE) && ($cookies->has(substr(session_name(), 1))) && ($session_name = $cookies->get(substr(session_name(), 1))))) {
+      // If a session cookie exists, initialize the session. Otherwise the
+      // session is only started on demand in drupal_session_commit(), making
+      // anonymous users not use a session cookie unless something is stored in
+      // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
+      $this->start();
+      if ($user->isAuthenticated() || !empty($_SESSION)) {
+        drupal_page_is_cacheable(FALSE);
+      }
+    }
+    else {
+      // Set a session identifier for this request. This is necessary because
+      // we lazily start sessions at the end of this request, and some
+      // processes (like drupal_get_token()) needs to know the future
+      // session ID in advance.
+      $this->lazySession = TRUE;
+      $user = new AnonymousUserSession();
+      // Less random sessions (which are much faster to generate) are used for
+      // anonymous users than are generated in drupal_session_regenerate() when
+      // a user becomes authenticated.
+      session_id(Crypt::randomBytesBase64());
+      if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) {
+        $insecure_session_name = substr(session_name(), 1);
+        $session_id = Crypt::randomBytesBase64();
+        $cookies->set($insecure_session_name, $session_id);
+      }
+    }
+    date_default_timezone_set(drupal_get_user_timezone());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function start() {
+    if (!$this->disabled && !$this->isStarted()) {
+      // Save current session data before starting it, as PHP will destroy it.
+      $session_data = isset($_SESSION) ? $_SESSION : NULL;
+
+      session_start();
+      $this->started = TRUE;
+
+      // Restore session data.
+      if (!empty($session_data)) {
+        $_SESSION += $session_data;
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isStarted() {
+    if (!$this->disabled) {
+      return $this->started && session_status() === \PHP_SESSION_ACTIVE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save() {
+    global $user;
+
+    if ($this->disabled) {
+      return;
+    }
+
+    if (!drupal_save_session()) {
+      // We don't have anything to do if we are not allowed to save the session.
+      return;
+    }
+
+    if ($user->isAnonymous() && empty($_SESSION)) {
+      // There is no session data to store, destroy the session if it was
+      // previously started.
+      if ($this->isStarted()) {
+        session_destroy();
+      }
+    }
+    else {
+      // There is session data to store. Start the session if it is not
+      // already started.
+      if (!$this->isStarted()) {
+        $this->start();
+        if (\Drupal::request()->isSecure() && Settings::get('mixed_mode_sessions', FALSE)) {
+          $insecure_session_name = substr(session_name(), 1);
+          $params = session_get_cookie_params();
+          $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
+          $cookie_params = \Drupal::request()->cookies;
+          setcookie($insecure_session_name, $cookie_params->get($insecure_session_name), $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
+        }
+      }
+      // Write the session data.
+      session_write_close();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function regenerate() {
+    global $user;
+
+    if ($this->disabled) {
+      return;
+    }
+
+    // Nothing to do if we are not allowed to change the session.
+    if (!drupal_save_session()) {
+      return;
+    }
+
+    $is_https = \Drupal::request()->isSecure();
+    $cookies = \Drupal::request()->cookies;
+
+    if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) {
+      $insecure_session_name = substr(session_name(), 1);
+      if (!isset($this->lazySession) && $cookies->has($insecure_session_name)) {
+        $old_insecure_session_id = $cookies->get($insecure_session_name);
+      }
+      $params = session_get_cookie_params();
+      $session_id = Crypt::randomBytesBase64();
+      // If a session cookie lifetime is set, the session will expire
+      // $params['lifetime'] seconds from the current request. If it is not set,
+      // it will expire when the browser is closed.
+      $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
+      setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
+      $cookies->set($insecure_session_name, $session_id);
+    }
+
+    if ($this->isStarted()) {
+      $old_session_id = session_id();
+    }
+    session_id(Crypt::randomBytesBase64());
+
+    if (isset($old_session_id)) {
+      $params = session_get_cookie_params();
+      $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
+      setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
+      $fields = array('sid' => Crypt::hashBase64(session_id()));
+      if ($is_https) {
+        $fields['ssid'] = Crypt::hashBase64(session_id());
+        // If the "secure pages" setting is enabled, use the newly-created
+        // insecure session identifier as the regenerated sid.
+        if (Settings::get('mixed_mode_sessions', FALSE)) {
+          $fields['sid'] = Crypt::hashBase64($session_id);
+        }
+      }
+      db_update('sessions')
+        ->fields($fields)
+        ->condition($is_https ? 'ssid' : 'sid', Crypt::hashBase64($old_session_id))
+        ->execute();
+    }
+    elseif (isset($old_insecure_session_id)) {
+      // If logging in to the secure site, and there was no active session on
+      // the secure site but a session was active on the insecure site, update
+      // the insecure session with the new session identifiers.
+      db_update('sessions')
+        ->fields(array('sid' => Crypt::hashBase64($session_id), 'ssid' => Crypt::hashBase64(session_id())))
+        ->condition('sid', Crypt::hashBase64($old_insecure_session_id))
+        ->execute();
+    }
+    else {
+      // Start the session when it doesn't exist yet.
+      // Preserve the logged in user, as it will be reset to anonymous
+      // by _drupal_session_read.
+      $account = $user;
+      $this->start();
+      $user = $account;
+    }
+    date_default_timezone_set(drupal_get_user_timezone());
+  }
+
+}
