diff --git a/core/core.services.yml b/core/core.services.yml
index e40ef9e3538cc7fd25749573cb0d7c5645f7147e..a98b28b5d464e5376de5c6e37edfb977d73a8179 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -124,6 +124,7 @@ services:
     class: Drupal\Core\PageCache\ChainResponsePolicy
     tags:
       - { name: service_collector, tag: page_cache_response_policy, call: addPolicy}
+    lazy: true
   page_cache_kill_switch:
     class: Drupal\Core\PageCache\ResponsePolicy\KillSwitch
     tags:
@@ -455,7 +456,7 @@ services:
       - { name: http_middleware, priority: 300 }
   http_middleware.page_cache:
     class: Drupal\Core\StackMiddleware\PageCache
-    arguments: ['@kernel']
+    arguments: ['@cache.render', '@page_cache_request_policy', '@page_cache_response_policy', '@content_negotiation']
     tags:
       - { name: http_middleware, priority: 200 }
   http_middleware.kernel_pre_handle:
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index ff20f28fba076425b37ed23a57bad7fc7b779aac..6fae299bf1d0ea4d4cabb63a6d4fe4cfbe818b7e 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -68,25 +68,18 @@
 const DRUPAL_BOOTSTRAP_KERNEL = 1;
 
 /**
- * Third bootstrap phase: try to serve a cached page.
+ * Third bootstrap phase: load code for subsystems and modules.
  *
  * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
  */
-const DRUPAL_BOOTSTRAP_PAGE_CACHE = 2;
-
-/**
- * Fourth bootstrap phase: load code for subsystems and modules.
- *
- * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
- */
-const DRUPAL_BOOTSTRAP_CODE = 3;
+const DRUPAL_BOOTSTRAP_CODE = 2;
 
 /**
  * Final bootstrap phase: initialize language, path, theme, and modules.
  *
  * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
  */
-const DRUPAL_BOOTSTRAP_FULL = 4;
+const DRUPAL_BOOTSTRAP_FULL = 3;
 
 /**
  * Role ID for anonymous users; should match what's in the "role" table.
@@ -310,39 +303,6 @@ function drupal_get_path($type, $name) {
   return dirname(drupal_get_filename($type, $name));
 }
 
-/**
- * Gets the page cache cid for this request.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- *   The request for this page.
- *
- * @return string
- *   The cid for this request.
- */
-function drupal_page_cache_get_cid(Request $request) {
-  $cid_parts = array(
-    $request->getUri(),
-    \Drupal::service('content_negotiation')->getContentType($request),
-  );
-  return implode(':', $cid_parts);
-}
-
-/**
- * Retrieves the current page from the cache.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- *   The request for this page.
- *
- * @return \Symfony\Component\HttpFoundation\Response
- *   The response, if the page was found in the cache, NULL otherwise.
- */
-function drupal_page_get_cache(Request $request) {
-  $cache = \Drupal::cache('render')->get(drupal_page_cache_get_cid($request));
-  if ($cache) {
-    return $cache->data;
-  }
-}
-
 /**
  * Sets an HTTP response header for the current page.
  *
@@ -426,72 +386,6 @@ function _drupal_set_preferred_header_name($name = NULL) {
   $header_names[strtolower($name)] = $name;
 }
 
-/**
- * Sets HTTP headers in preparation for a cached page response.
- *
- * The headers allow as much as possible in proxies and browsers without any
- * particular knowledge about the pages. Modules can override these headers
- * using _drupal_add_http_header().
- *
- * If the request is conditional (using If-Modified-Since and If-None-Match),
- * and the conditions match those currently in the cache, a 304 Not Modified
- * response is sent.
- */
-function drupal_serve_page_from_cache(Response $response, Request $request) {
-  // Only allow caching in the browser and prevent that the response is stored
-  // by an external proxy server when the following conditions apply:
-  // 1. There is a session cookie on the request.
-  // 2. The Vary: Cookie header is on the response.
-  // 3. The Cache-Control header does not contain the no-cache directive.
-  if ($request->cookies->has(session_name()) &&
-    in_array('Cookie', $response->getVary()) &&
-    !$response->headers->hasCacheControlDirective('no-cache')) {
-
-    $response->setPrivate();
-  }
-
-  // Negotiate whether to use compression.
-  if ($response->headers->get('Content-Encoding') == 'gzip' && extension_loaded('zlib')) {
-    if (strpos($request->headers->get('Accept-Encoding'), 'gzip') !== FALSE) {
-      // The response content is already gzip'ed, so make sure
-      // zlib.output_compression does not compress it once more.
-      ini_set('zlib.output_compression', '0');
-    }
-    else {
-      // The client does not support compression. Decompress the content and
-      // remove the Content-Encoding header.
-      $content = $response->getContent();
-      $content = gzinflate(substr(substr($content, 10), 0, -8));
-      $response->setContent($content);
-      $response->headers->remove('Content-Encoding');
-    }
-  }
-
-  // Perform HTTP revalidation.
-  // @todo Use Response::isNotModified() as per https://drupal.org/node/2259489
-  $last_modified = $response->getLastModified();
-  if ($last_modified) {
-    // See if the client has provided the required HTTP headers.
-    $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
-    $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
-
-    if ($if_modified_since && $if_none_match
-      && $if_none_match == $response->getEtag() // etag must match
-      && $if_modified_since == $last_modified->getTimestamp()) {  // if-modified-since must match
-      $response->setStatusCode(304);
-      $response->setContent(NULL);
-
-      // In the case of a 304 response, certain headers must be sent, and the
-      // remaining may not (see RFC 2616, section 10.3.5).
-      foreach (array_keys($response->headers->all()) as $name) {
-        if (!in_array($name, array('content-location', 'expires', 'cache-control', 'vary'))) {
-          $response->headers->remove($name);
-        }
-      }
-    }
-  }
-}
-
 /**
  * Translates a string to the current language or to a given language.
  *
@@ -830,10 +724,6 @@ function drupal_bootstrap($phase = NULL) {
           $kernel->boot();
           break;
 
-        case DRUPAL_BOOTSTRAP_PAGE_CACHE:
-          $kernel->handlePageCache($request);
-          break;
-
         case DRUPAL_BOOTSTRAP_CODE:
         case DRUPAL_BOOTSTRAP_FULL:
           $kernel->prepareLegacyRequest($request);
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 27c9c540d0b5414c09681cc092d9e27fdc1822c1..bfcbce6361fa0be506b74e4bfa13a340ea525fce 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -1267,48 +1267,6 @@ function drupal_clear_js_cache() {
   \Drupal::service('asset.js.collection_optimizer')->deleteAll();
 }
 
-/**
- * Stores the current page in the cache.
- *
- * If page_compression is enabled, a gzipped version of the page is stored in
- * the cache to avoid compressing the output on each request. The cache entry
- * is unzipped in the relatively rare event that the page is requested by a
- * client without gzip support.
- *
- * Page compression requires the PHP zlib extension
- * (http://php.net/manual/ref.zlib.php).
- *
- * @param \Symfony\Component\HttpFoundation\Response $response
- *   The fully populated response.
- * @param \Symfony\Component\HttpFoundation\Request $request
- *   The request for this page.
- */
-function drupal_page_set_cache(Response $response, Request $request) {
-  // Check if the current page may be compressed.
-  if (extension_loaded('zlib') && !$response->headers->get('Content-Encoding') &&
-      \Drupal::config('system.performance')->get('response.gzip')) {
-
-    $content = $response->getContent();
-    if ($content) {
-      $response->setContent(gzencode($content, 9, FORCE_GZIP));
-      $response->headers->set('Content-Encoding', 'gzip');
-    }
-
-    // When page compression is enabled, ensure that proxy caches will record
-    // and deliver different versions of a page depending on whether the
-    // client supports gzip or not.
-    $response->setVary('Accept-Encoding', FALSE);
-  }
-
-  // Use the actual timestamp from an Expires header, if available.
-  $date = $response->getExpires();
-  $expire = ($date > (new DateTime())) ? $date->getTimestamp() : Cache::PERMANENT;
-
-  $cid = drupal_page_cache_get_cid($request);
-  $tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
-  \Drupal::cache('render')->set($cid, $response, $expire, $tags);
-}
-
 /**
  * Pre-render callback: Renders a link into #markup.
  *
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index d722d6b2db0b34f56ef382f086fc492b8d91a3be..8ddeb1759e66703d14f87074881711018e667dba 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -76,6 +76,13 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
    */
   protected $booted = FALSE;
 
+  /**
+   * Whether essential services have been set up properly by preHandle().
+   *
+   * @var bool
+   */
+  protected $prepared = FALSE;
+
   /**
    * Holds the list of enabled modules.
    *
@@ -474,42 +481,8 @@ public function preHandle(Request $request) {
 
     // Override of Symfony's mime type guesser singleton.
     MimeTypeGuesser::registerWithSymfonyGuesser($this->container);
-  }
-
-  /**
-   * {@inheritdoc}
-   *
-   * @todo Invoke proper request/response/terminate events.
-   */
-  public function handlePageCache(Request $request) {
-    $this->boot();
-
-    // Check for a cache mode force from settings.php.
-    if (Settings::get('page_cache_without_database')) {
-      $cache_enabled = TRUE;
-    }
-    else {
-      $config = $this->getContainer()->get('config.factory')->get('system.performance');
-      $cache_enabled = $config->get('cache.page.use_internal');
-    }
-
-    $request_policy = \Drupal::service('page_cache_request_policy');
-    if ($cache_enabled && $request_policy->check($request) === RequestPolicyInterface::ALLOW) {
-      // Get the page from the cache.
-      $response = drupal_page_get_cache($request);
-      // If there is a cached page, display it.
-      if ($response) {
-        $response->headers->set('X-Drupal-Cache', 'HIT');
 
-        drupal_serve_page_from_cache($response, $request);
-
-        // We are done.
-        $response->prepare($request);
-        $response->send();
-        exit;
-      }
-    }
-    return $this;
+    $this->prepared = TRUE;
   }
 
   /**
@@ -577,7 +550,9 @@ public function getServiceProviders($origin) {
    * {@inheritdoc}
    */
   public function terminate(Request $request, Response $response) {
-    if (FALSE === $this->booted) {
+    // Only run terminate() when essential services have been set up properly
+    // by preHandle() before.
+    if (FALSE === $this->prepared) {
       return;
     }
 
diff --git a/core/lib/Drupal/Core/DrupalKernelInterface.php b/core/lib/Drupal/Core/DrupalKernelInterface.php
index 0ba61dee2dfcfa9694ac8952d056befd3f569e6e..ab17b77ff09d491d09252cdf2b3698e2fe98e219 100644
--- a/core/lib/Drupal/Core/DrupalKernelInterface.php
+++ b/core/lib/Drupal/Core/DrupalKernelInterface.php
@@ -93,16 +93,6 @@ public function getAppRoot();
    */
   public function updateModules(array $module_list, array $module_filenames = array());
 
-  /**
-   * Attempts to serve a page from the cache.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The current request.
-   *
-   * @return $this
-   */
-  public function handlePageCache(Request $request);
-
   /**
    * Prepare the kernel for handling a request without handling the request.
    *
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 9634161bcb1176c8547099038f433ea6252cda4b..3c8bdf2f3461081825ccb5dd97e4e63bedfae566 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -136,17 +136,6 @@ public function onRespond(FilterResponseEvent $event) {
       // header declaring the response as not cacheable.
       $this->setResponseNotCacheable($response, $request);
     }
-
-    // Currently it is not possible to cache some types of responses. Therefore
-    // exclude binary file responses (generated files, e.g. images with image
-    // styles) and streamed responses (files directly read from the disk).
-    // see: https://github.com/symfony/symfony/issues/9128#issuecomment-25088678
-    if ($is_cacheable && $this->config->get('cache.page.use_internal') && !($response instanceof BinaryFileResponse) && !($response instanceof StreamedResponse)) {
-      // Store the response in the internal page cache.
-      drupal_page_set_cache($response, $request);
-      $response->headers->set('X-Drupal-Cache', 'MISS');
-      drupal_serve_page_from_cache($response, $request);
-    }
   }
 
   /**
diff --git a/core/lib/Drupal/Core/StackMiddleware/PageCache.php b/core/lib/Drupal/Core/StackMiddleware/PageCache.php
index ba59e936f48e0e86966bd23ce33eabb028ef93f0..b381018f49d751d89d54020933a2a84488e588cd 100644
--- a/core/lib/Drupal/Core/StackMiddleware/PageCache.php
+++ b/core/lib/Drupal/Core/StackMiddleware/PageCache.php
@@ -7,8 +7,16 @@
 
 namespace Drupal\Core\StackMiddleware;
 
-use Drupal\Core\DrupalKernelInterface;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\ContentNegotiation;
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\Core\Site\Settings;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\StreamedResponse;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
 
 /**
@@ -24,32 +32,321 @@ class PageCache implements HttpKernelInterface {
   protected $httpKernel;
 
   /**
-   * The main Drupal kernel.
+   * The cache bin.
    *
-   * @var \Drupal\Core\DrupalKernelInterface
+   * @var \Drupal\Core\Cache\CacheBackendInterface.
    */
-  protected $drupalKernel;
+  protected $cache;
+
+  /**
+   * A policy rule determining the cacheability of a request.
+   *
+   * @var \Drupal\Core\PageCache\RequestPolicyInterface
+   */
+  protected $requestPolicy;
+
+  /**
+   * A policy rule determining the cacheability of the response.
+   *
+   * @var \Drupal\Core\PageCache\ResponsePolicyInterface
+   */
+  protected $responsePolicy;
+
+  /**
+   * The content negotiation library.
+   *
+   * @var \Drupal\Core\ContentNegotiation
+   */
+  protected $contentNegotiation;
 
   /**
    * Constructs a ReverseProxyMiddleware object.
    *
    * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
    *   The decorated kernel.
-   * @param \Drupal\Core\DrupalKernelInterface $drupal_kernel
-   *   The main Drupal kernel.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache bin.
+   * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy
+   *   A policy rule determining the cacheability of a request.
+   * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy
+   *   A policy rule determining the cacheability of the response.
+   * @param \Drupal\Core\ContentNegotiation $content_negotiation
+   *   The content negotiation library.
    */
-  public function __construct(HttpKernelInterface $http_kernel, DrupalKernelInterface $drupal_kernel) {
+  public function __construct(HttpKernelInterface $http_kernel, CacheBackendInterface $cache, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, ContentNegotiation $content_negotiation) {
     $this->httpKernel = $http_kernel;
-    $this->drupalKernel = $drupal_kernel;
+    $this->cache = $cache;
+    $this->requestPolicy = $request_policy;
+    $this->responsePolicy = $response_policy;
+    $this->contentNegotiation = $content_negotiation;
   }
 
   /**
    * {@inheritdoc}
    */
   public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
-    $this->drupalKernel->handlePageCache($request);
+    if ($type !== static::MASTER_REQUEST) {
+      // Only allow page caching on master request.
+      $cache_enabled = FALSE;
+    }
+    elseif (Settings::get('page_cache_without_database')) {
+      // Check for a cache mode force from settings.php.
+      $cache_enabled = TRUE;
+    }
+    else {
+      $config = $this->config('system.performance');
+      $cache_enabled = $config->get('cache.page.use_internal');
+    }
 
+    if ($cache_enabled && $this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) {
+      $response = $this->lookup($request, $type, $catch);
+    }
+    else {
+      $response = $this->pass($request, $type, $catch);
+    }
+
+    return $response;
+  }
+
+  /**
+   * Sidesteps the page cache and directly forwards a request to the backend.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   * @param int $type
+   *   The type of the request (one of HttpKernelInterface::MASTER_REQUEST or
+   *   HttpKernelInterface::SUB_REQUEST)
+   * @param bool $catch
+   *   Whether to catch exceptions or not
+   *
+   * @returns \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   */
+  protected function pass(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
     return $this->httpKernel->handle($request, $type, $catch);
   }
 
+  /**
+   * Retrieves a response from the cache or fetches it from the backend.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   * @param int $type
+   *   The type of the request (one of HttpKernelInterface::MASTER_REQUEST or
+   *   HttpKernelInterface::SUB_REQUEST)
+   * @param bool $catch
+   *   Whether to catch exceptions or not
+   *
+   * @returns \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   */
+  protected function lookup(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
+    if ($response = $this->get($request)) {
+      $response->headers->set('X-Drupal-Cache', 'HIT');
+    }
+    else {
+      $response = $this->fetch($request, $type, $catch);
+    }
+
+    // Only allow caching in the browser and prevent that the response is stored
+    // by an external proxy server when the following conditions apply:
+    // 1. There is a session cookie on the request.
+    // 2. The Vary: Cookie header is on the response.
+    // 3. The Cache-Control header does not contain the no-cache directive.
+    if ($request->cookies->has(session_name()) &&
+      in_array('Cookie', $response->getVary()) &&
+      !$response->headers->hasCacheControlDirective('no-cache')) {
+
+      $response->setPrivate();
+    }
+
+    // Negotiate whether to use compression.
+    if (extension_loaded('zlib') && $response->headers->get('Content-Encoding') === 'gzip') {
+      if (strpos($request->headers->get('Accept-Encoding'), 'gzip') !== FALSE) {
+        // The response content is already gzip'ed, so make sure
+        // zlib.output_compression does not compress it once more.
+        ini_set('zlib.output_compression', '0');
+      }
+      else {
+        // The client does not support compression. Decompress the content and
+        // remove the Content-Encoding header.
+        $content = $response->getContent();
+        $content = gzinflate(substr(substr($content, 10), 0, -8));
+        $response->setContent($content);
+        $response->headers->remove('Content-Encoding');
+      }
+    }
+
+    // Perform HTTP revalidation.
+    // @todo Use Response::isNotModified() as per https://drupal.org/node/2259489
+    $last_modified = $response->getLastModified();
+    if ($last_modified) {
+      // See if the client has provided the required HTTP headers.
+      $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
+      $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
+
+      if ($if_modified_since && $if_none_match
+        && $if_none_match == $response->getEtag() // etag must match
+        && $if_modified_since == $last_modified->getTimestamp()) {  // if-modified-since must match
+        $response->setStatusCode(304);
+        $response->setContent(NULL);
+
+        // In the case of a 304 response, certain headers must be sent, and the
+        // remaining may not (see RFC 2616, section 10.3.5).
+        foreach (array_keys($response->headers->all()) as $name) {
+          if (!in_array($name, array('content-location', 'expires', 'cache-control', 'vary'))) {
+            $response->headers->remove($name);
+          }
+        }
+      }
+    }
+
+    return $response;
+  }
+
+  /**
+   * Fetches a response from the backend and stores it in the cache.
+   *
+   * If page_compression is enabled, a gzipped version of the page is stored in
+   * the cache to avoid compressing the output on each request. The cache entry
+   * is unzipped in the relatively rare event that the page is requested by a
+   * client without gzip support.
+   *
+   * Page compression requires the PHP zlib extension
+   * (http://php.net/manual/ref.zlib.php).
+   *
+   * @see drupal_page_header()
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   * @param int $type
+   *   The type of the request (one of HttpKernelInterface::MASTER_REQUEST or
+   *   HttpKernelInterface::SUB_REQUEST)
+   * @param bool $catch
+   *   Whether to catch exceptions or not
+   *
+   * @returns \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   */
+  protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
+    $response = $this->httpKernel->handle($request, $type, $catch);
+
+    // Currently it is not possible to cache some types of responses. Therefore
+    // exclude binary file responses (generated files, e.g. images with image
+    // styles) and streamed responses (files directly read from the disk).
+    // see: https://github.com/symfony/symfony/issues/9128#issuecomment-25088678
+    if ($response instanceof BinaryFileResponse || $response instanceof StreamedResponse) {
+      return $response;
+    }
+
+    if ($this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) {
+      return $response;
+    }
+
+    // Check if the current page may be compressed.
+    if (extension_loaded('zlib') && !$response->headers->get('Content-Encoding') && $this->config('system.performance')->get('response.gzip')) {
+      $content = $response->getContent();
+      if ($content) {
+        $response->setContent(gzencode($content, 9, FORCE_GZIP));
+        $response->headers->set('Content-Encoding', 'gzip');
+      }
+
+      // When page compression is enabled, ensure that proxy caches will record
+      // and deliver different versions of a page depending on whether the
+      // client supports gzip or not.
+      $response->setVary('Accept-Encoding', FALSE);
+    }
+
+    // Use the actual timestamp from an Expires header, if available.
+    $date = $response->getExpires();
+    $expire = ($date > (new \DateTime())) ? $date->getTimestamp() : Cache::PERMANENT;
+
+    $tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
+    $this->set($request, $response, $expire, $tags);
+
+    // Mark response as a cache miss.
+    $response->headers->set('X-Drupal-Cache', 'MISS');
+
+    return $response;
+  }
+
+  /**
+   * Returns a response object from the page cache.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   * @param bool $allow_invalid
+   *   (optional) If TRUE, a cache item may be returned even if it is expired or
+   *   has been invalidated. Such items may sometimes be preferred, if the
+   *   alternative is recalculating the value stored in the cache, especially
+   *   if another concurrent request is already recalculating the same value.
+   *   The "valid" property of the returned object indicates whether the item is
+   *   valid or not. Defaults to FALSE.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response|false
+   *   The cached response or FALSE on failure.
+   */
+  protected function get(Request $request, $allow_invalid = FALSE) {
+    $cid = $this->getCacheId($request);
+    if ($cache = $this->cache->get($cid, $allow_invalid)) {
+      return $cache->data;
+    }
+  }
+
+  /**
+   * Stores a response object in the page cache.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   The response to store in the cache.
+   * @param int $expire
+   *   One of the following values:
+   *   - CacheBackendInterface::CACHE_PERMANENT: Indicates that the item should
+   *     not be removed unless it is deleted explicitly.
+   *   - A Unix timestamp: Indicates that the item will be considered invalid
+   *     after this time, i.e. it will not be returned by get() unless
+   *     $allow_invalid has been set to TRUE. When the item has expired, it may
+   *     be permanently deleted by the garbage collector at any time.
+   * @param array $tags
+   *   An array of tags to be stored with the cache item. These should normally
+   *   identify objects used to build the cache item, which should trigger
+   *   cache invalidation when updated. For example if a cached item represents
+   *   a node, both the node ID and the author's user ID might be passed in as
+   *   tags. For example array('node' => array(123), 'user' => array(92)).
+   */
+  protected function set(Request $request, Response $response, $expire, array $tags) {
+    $cid = $this->getCacheId($request);
+    $this->cache->set($cid, $response, $expire, $tags);
+  }
+
+  /**
+   * Gets the page cache ID for this request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   *
+   * @return string
+   *   The cache ID for this request.
+   */
+  protected function getCacheId(Request $request) {
+    $cid_parts = array(
+      $request->getUri(),
+      $this->contentNegotiation->getContentType($request),
+    );
+    return implode(':', $cid_parts);
+  }
+
+  /**
+   * Wraps Drupal::config().
+   *
+   * Config factory is not injected into this class in order to prevent
+   * premature initialization of config storage (database).
+   *
+   * @see \Drupal::config()
+   */
+  protected function config($name) {
+    return \Drupal::config($name);
+  }
+
 }
diff --git a/core/modules/system/src/Tests/DrupalKernel/ServiceDestructionTest.php b/core/modules/system/src/Tests/DrupalKernel/ServiceDestructionTest.php
index 4fdc9b7b4d05c7791c8dedf57014e3bc77c360fe..68e28a2e8501bbc8436af8ab1b872b32bc52d30d 100644
--- a/core/modules/system/src/Tests/DrupalKernel/ServiceDestructionTest.php
+++ b/core/modules/system/src/Tests/DrupalKernel/ServiceDestructionTest.php
@@ -25,13 +25,18 @@ public function testDestructionUsed() {
     // Enable the test module to add it to the container.
     $this->enableModules(array('service_provider_test'));
 
+    $request = $this->container->get('request_stack')->getCurrentRequest();
+    $kernel = $this->container->get('kernel');
+    $kernel->preHandle($request);
+
     // The service has not been destructed yet.
     $this->assertNull(\Drupal::state()->get('service_provider_test.destructed'));
 
     // Call the class and then terminate the kernel
     $this->container->get('service_provider_test_class');
+
     $response = new Response();
-    $this->container->get('kernel')->terminate($this->container->get('request_stack')->getCurrentRequest(), $response);
+    $kernel->terminate($request, $response);
     $this->assertTrue(\Drupal::state()->get('service_provider_test.destructed'));
   }
 
@@ -42,13 +47,17 @@ public function testDestructionUnused() {
     // Enable the test module to add it to the container.
     $this->enableModules(array('service_provider_test'));
 
+    $request = $this->container->get('request_stack')->getCurrentRequest();
+    $kernel = $this->container->get('kernel');
+    $kernel->preHandle($request);
+
     // The service has not been destructed yet.
     $this->assertNull(\Drupal::state()->get('service_provider_test.destructed'));
 
     // Terminate the kernel. The test class has not been called, so it should not
     // be destructed.
     $response = new Response();
-    $this->container->get('kernel')->terminate($this->container->get('request_stack')->getCurrentRequest(), $response);
+    $kernel->terminate($request, $response);
     $this->assertNull(\Drupal::state()->get('service_provider_test.destructed'));
   }
 }
diff --git a/core/modules/system/tests/http.php b/core/modules/system/tests/http.php
index 33869799e316b1e398610774723187c985be3702..8c7d556be39f8d19091ca21d08643502bf680302 100644
--- a/core/modules/system/tests/http.php
+++ b/core/modules/system/tests/http.php
@@ -26,7 +26,6 @@
 $request = Request::createFromGlobals();
 $kernel = TestKernel::createFromRequest($request, $autoloader, 'testing', TRUE);
 $response = $kernel
-  ->handlePageCache($request)
   ->handle($request)
     // Handle the response object.
     ->prepare($request)->send();
diff --git a/core/modules/system/tests/https.php b/core/modules/system/tests/https.php
index 00a6b13d6155ee7bb55e0f4fe841913ded4329bd..702184df62702cfea5a354976b798916327d8d6e 100644
--- a/core/modules/system/tests/https.php
+++ b/core/modules/system/tests/https.php
@@ -28,7 +28,6 @@
 $request = Request::createFromGlobals();
 $kernel = TestKernel::createFromRequest($request, $autoloader, 'testing', TRUE);
 $response = $kernel
-  ->handlePageCache($request)
   ->handle($request)
     // Handle the response object.
     ->prepare($request)->send();