diff --git a/core/authorize.php b/core/authorize.php
index 7f80850..ae2c907 100644
--- a/core/authorize.php
+++ b/core/authorize.php
@@ -32,8 +32,8 @@
  * Global flag to identify update.php and authorize.php runs.
  *
  * Identifies update.php and authorize.php runs, avoiding unwanted operations
- * such as hook_init() and hook_exit() invokes, css/js preprocessing and
- * translation, and solves some theming issues. The flag is checked in other
+ * such as hook_init() and event close subscriber invokes, css/js preprocessing
+ * and translation, and solves some theming issues. The flag is checked in other
  * places in Drupal code (not just authorize.php).
  */
 const MAINTENANCE_MODE = 'update';
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 4ebe881..bfd8c0c 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -2342,10 +2342,9 @@ function _drupal_bootstrap_page_cache() {
         bootstrap_invoke_all('boot');
       }
       drupal_serve_page_from_cache($cache);
-      // If the skipping of the bootstrap hooks is not enforced, call
-      // hook_exit.
-      if (variable_get('page_cache_invoke_hooks', TRUE)) {
-        bootstrap_invoke_all('exit');
+      // Terminate kernel
+      if ($kernel = drupal_container()->get('kernel', ContainerInterface::NULL_ON_INVALID_REFERENCE)) {
+        $kernel->terminate;
       }
       // We are done.
       exit;
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 20d24a7..5275107 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -2265,19 +2265,15 @@ function l($text, $path, array $options = array()) {
  * Performs end-of-request tasks.
  *
  * There should rarely be a reason to call exit instead of drupal_exit();
- *
- * @param $destination
- *   If this function is called from drupal_goto(), then this argument
- *   will be a fully-qualified URL that is the destination of the redirect.
- *   This should be passed along to hook_exit() implementations.
  */
-function drupal_exit($destination = NULL) {
+function drupal_exit() {
   if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) {
-    if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
-      module_invoke_all('exit', $destination);
-    }
     drupal_session_commit();
   }
+  // Terminate kernel
+  if ($kernel = drupal_container()->get('kernel', ContainerInterface::NULL_ON_INVALID_REFERENCE)) {
+    $kernel->terminate;
+  }
   exit;
 }
 
diff --git a/core/modules/overlay/lib/Drupal/overlay/EventSubscriber/OverlayCloseSubscriber.php b/core/modules/overlay/lib/Drupal/overlay/EventSubscriber/OverlayCloseSubscriber.php
new file mode 100644
index 0000000..4572a10
--- /dev/null
+++ b/core/modules/overlay/lib/Drupal/overlay/EventSubscriber/OverlayCloseSubscriber.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\overlay\EventSubscriber\OverlayCloseSubscriber.
+ */
+
+namespace Drupal\overlay\EventSubscriber;
+
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\HttpKernel\Event\PostResponseEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Subscriber for all responses.
+ */
+class OverlayCloseSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Performs end of request tasks.
+   *
+   * When viewing an overlay child page, check if we need to trigger a refresh of
+   * the supplemental regions of the overlay on the next page request.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\PostResponseEvent $event
+   *   The Event to process.
+   */
+  public function onTerminate(PostResponseEvent $event) {
+    // Check that we are in an overlay child page. Note that this should never
+    // return TRUE on a cached page view, since the child mode is not set until
+    // overlay_init() is called.
+    if (overlay_get_mode() == 'child') {
+      // Load any markup that was stored earlier in the page request, via calls
+      // to overlay_store_rendered_content(). If none was stored, this is not a
+      // page request where we expect any changes to the overlay supplemental
+      // regions to have occurred, so we do not need to proceed any further.
+      $original_markup = overlay_get_rendered_content();
+      if (!empty($original_markup)) {
+        // Compare the original markup to the current markup that we get from
+        // rendering each overlay supplemental region now. If they don't match,
+        // something must have changed, so we request a refresh of that region
+        // within the parent window on the next page request.
+        foreach (overlay_supplemental_regions() as $region) {
+          if (!isset($original_markup[$region]) || $original_markup[$region] != overlay_render_region($region)) {
+            overlay_request_refresh($region);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  static function getSubscribedEvents() {
+    $events[KernelEvents::TERMINATE][] = array('onTerminate', 50);
+
+    return $events;
+  }
+}
diff --git a/core/modules/overlay/lib/Drupal/overlay/OverlayBundle.php b/core/modules/overlay/lib/Drupal/overlay/OverlayBundle.php
new file mode 100644
index 0000000..b8637ad
--- /dev/null
+++ b/core/modules/overlay/lib/Drupal/overlay/OverlayBundle.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\overlay\OverlayBundle.
+ */
+
+namespace Drupal\overlay;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+/**
+ * Bundle class for the overlay module.
+ */
+class OverlayBundle extends Bundle {
+
+  /**
+   * Implements \Symfony\Component\HttpKernel\Bundle\BundleInterface::build().
+   */
+  public function build(ContainerBuilder $container) {
+    $container->register('overlay_close_subscriber', 'Drupal\overlay\EventSubscriber\OverlayCloseSubscriber')
+      ->addTag('event_subscriber');
+  }
+
+}
diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module
index 9863915..5bbfd1c 100644
--- a/core/modules/overlay/overlay.module
+++ b/core/modules/overlay/overlay.module
@@ -166,36 +166,6 @@ function overlay_init() {
 }
 
 /**
- * Implements hook_exit().
- *
- * When viewing an overlay child page, check if we need to trigger a refresh of
- * the supplemental regions of the overlay on the next page request.
- */
-function overlay_exit() {
-  // Check that we are in an overlay child page. Note that this should never
-  // return TRUE on a cached page view, since the child mode is not set until
-  // overlay_init() is called.
-  if (overlay_get_mode() == 'child') {
-    // Load any markup that was stored earlier in the page request, via calls
-    // to overlay_store_rendered_content(). If none was stored, this is not a
-    // page request where we expect any changes to the overlay supplemental
-    // regions to have occurred, so we do not need to proceed any further.
-    $original_markup = overlay_get_rendered_content();
-    if (!empty($original_markup)) {
-      // Compare the original markup to the current markup that we get from
-      // rendering each overlay supplemental region now. If they don't match,
-      // something must have changed, so we request a refresh of that region
-      // within the parent window on the next page request.
-      foreach (overlay_supplemental_regions() as $region) {
-        if (!isset($original_markup[$region]) || $original_markup[$region] != overlay_render_region($region)) {
-          overlay_request_refresh($region);
-        }
-      }
-    }
-  }
-}
-
-/**
  * Implements hook_library_info().
  */
 function overlay_library_info() {
diff --git a/core/modules/statistics/lib/Drupal/statistics/EventSubscriber/StatisticsCloseSubscriber.php b/core/modules/statistics/lib/Drupal/statistics/EventSubscriber/StatisticsCloseSubscriber.php
new file mode 100644
index 0000000..c8df351
--- /dev/null
+++ b/core/modules/statistics/lib/Drupal/statistics/EventSubscriber/StatisticsCloseSubscriber.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\statistics\EventSubscriber\StatisticsCloseSubscriber.
+ */
+
+namespace Drupal\statistics\EventSubscriber;
+
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\HttpKernel\Event\PostResponseEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Subscriber for all responses.
+ */
+class StatisticsCloseSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Performs end of request tasks.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\PostResponseEvent $event
+   *   The Event to process.
+   */
+  public function onTerminate(PostResponseEvent $event) {
+    global $user;
+
+    if (config('statistics.settings')->get('access_log.enabled')) {
+      drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION);
+
+      // For anonymous users unicode.inc will not have been loaded.
+      include_once DRUPAL_ROOT . '/core/includes/unicode.inc';
+      // Log this page access.
+      db_insert('accesslog')
+        ->fields(array(
+          'title' => truncate_utf8(strip_tags(drupal_get_title()), 255),
+          // @todo The public function current_path() is not available on a cache
+          //   hit.
+          'path' => truncate_utf8(_current_path(), 255),
+          'url' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
+          'hostname' => ip_address(),
+          'uid' => $user->uid,
+          'sid' => session_id(),
+          'timer' => (int) timer_read('page'),
+          'timestamp' => REQUEST_TIME,
+        ))
+        ->execute();
+    }
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  static function getSubscribedEvents() {
+    $events[KernelEvents::TERMINATE][] = array('onTerminate', 50);
+
+    return $events;
+  }
+}
diff --git a/core/modules/statistics/lib/Drupal/statistics/StatisticsBundle.php b/core/modules/statistics/lib/Drupal/statistics/StatisticsBundle.php
new file mode 100644
index 0000000..c2743c9
--- /dev/null
+++ b/core/modules/statistics/lib/Drupal/statistics/StatisticsBundle.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\statistics\StatisticsBundle.
+ */
+
+namespace Drupal\statistics;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+/**
+ * Bundle class for the statistics module.
+ */
+class StatisticsBundle extends Bundle {
+
+  /**
+   * Implements \Symfony\Component\HttpKernel\Bundle\BundleInterface::build().
+   */
+  public function build(ContainerBuilder $container) {
+    $container->register('statistics_close_subscriber', 'Drupal\statistics\EventSubscriber\StatisticsCloseSubscriber')
+      ->addTag('event_subscriber');
+  }
+
+}
diff --git a/core/modules/statistics/statistics.module b/core/modules/statistics/statistics.module
index a359981..cf0937e 100644
--- a/core/modules/statistics/statistics.module
+++ b/core/modules/statistics/statistics.module
@@ -44,44 +44,6 @@ function statistics_help($path, $arg) {
 }
 
 /**
- * Implements hook_exit().
- *
- * Gathers statistics for page accesses.
- */
-function statistics_exit() {
-  global $user;
-
-  // When serving cached pages with the 'page_cache_without_database'
-  // configuration, system variables need to be loaded. This is a major
-  // performance decrease for non-database page caches, but with Statistics
-  // module, it is likely to also have 'statistics.settings:access_log.enabled'
-  // enabled, in which case we need to bootstrap to the session phase anyway.
-  drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES);
-
-  if (config('statistics.settings')->get('access_log.enabled')) {
-    drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION);
-
-    // For anonymous users unicode.inc will not have been loaded.
-    include_once DRUPAL_ROOT . '/core/includes/unicode.inc';
-    // Log this page access.
-    db_insert('accesslog')
-      ->fields(array(
-        'title' => truncate_utf8(strip_tags(drupal_get_title()), 255),
-        // @todo The public function current_path() is not available on a cache
-        //   hit.
-        'path' => truncate_utf8(_current_path(), 255),
-        'url' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
-        'hostname' => ip_address(),
-        'uid' => $user->uid,
-        'sid' => session_id(),
-        'timer' => (int) timer_read('page'),
-        'timestamp' => REQUEST_TIME,
-      ))
-      ->execute();
-  }
-}
-
-/**
  * Implements hook_permission().
  */
 function statistics_permission() {
diff --git a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/HookBootExitTest.php b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/HookBootExitTest.php
index e9eff7a..2ba3a1b 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/HookBootExitTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/HookBootExitTest.php
@@ -30,7 +30,7 @@ public static function getInfo() {
   }
 
   /**
-   * Tests calling of hook_boot() and hook_exit().
+   * Tests calling of hook_boot() and the terminate event subscribers.
    */
   function testHookBootExit() {
     // Test with cache disabled. Boot and exit should always fire.
@@ -41,7 +41,7 @@ function testHookBootExit() {
     $this->drupalGet('');
     $calls = 1;
     $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, 'hook_boot called with disabled cache.');
-    $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_exit'))->fetchField(), $calls, 'hook_exit called with disabled cache.');
+    $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'SystemTestCloseSubscriber'))->fetchField(), $calls, 'SystemTestCloseSubscriber called with disabled cache.');
 
     // Test with normal cache. Boot and exit should be called.
     $config->set('cache.page.enabled', 1);
@@ -49,20 +49,20 @@ function testHookBootExit() {
     $this->drupalGet('');
     $calls++;
     $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, 'hook_boot called with normal cache.');
-    $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_exit'))->fetchField(), $calls, 'hook_exit called with normal cache.');
+    $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'SystemTestCloseSubscriber'))->fetchField(), $calls, 'SystemTestCloseSubscriber called with normal cache.');
 
     // Boot and exit should not fire since the page is cached.
     variable_set('page_cache_invoke_hooks', FALSE);
     $this->assertTrue(cache('page')->get(url('', array('absolute' => TRUE))), 'Page has been cached.');
     $this->drupalGet('');
     $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, 'hook_boot not called with aggressive cache and a cached page.');
-    $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_exit'))->fetchField(), $calls, 'hook_exit not called with aggressive cache and a cached page.');
+    $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'SystemTestCloseSubscriber'))->fetchField(), $calls, 'SystemTestCloseSubscriber not called with aggressive cache and a cached page.');
 
     // Test with page cache cleared, boot and exit should be called.
     $this->assertTrue(db_delete('cache_page')->execute(), 'Page cache cleared.');
     $this->drupalGet('');
     $calls++;
     $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, 'hook_boot called with aggressive cache and no cached page.');
-    $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_exit'))->fetchField(), $calls, 'hook_exit called with aggressive cache and no cached page.');
+    $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'SystemTestCloseSubscriber'))->fetchField(), $calls, 'SystemTestCloseSubscriber called with aggressive cache and no cached page.');
   }
 }
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 926fd2c..edab6e0 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -328,31 +328,6 @@ function hook_element_info_alter(&$type) {
 }
 
 /**
- * Perform cleanup tasks.
- *
- * This hook is run at the end of each page request. It is often used for
- * page logging and specialized cleanup. This hook MUST NOT print anything
- * because by the time it runs the response is already sent to the browser.
- *
- * Only use this hook if your code must run even for cached page views.
- * If you have code which must run once on all non-cached pages, use
- * hook_init() instead. That is the usual case. If you implement this hook
- * and see an error like 'Call to undefined function', it is likely that
- * you are depending on the presence of a module which has not been loaded yet.
- * It is not loaded because Drupal is still in bootstrap mode.
- *
- * @param $destination
- *   If this hook is invoked as part of a drupal_goto() call, then this argument
- *   will be a fully-qualified URL that is the destination of the redirect.
- */
-function hook_exit($destination = NULL) {
-  db_update('counter')
-    ->expression('hits', 'hits + 1')
-    ->condition('type', 1)
-    ->execute();
-}
-
-/**
  * Perform necessary alterations to the JavaScript before it is presented on
  * the page.
  *
diff --git a/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/EventSubscriber/SystemTestCloseSubscriber b/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/EventSubscriber/SystemTestCloseSubscriber
new file mode 100644
index 0000000..9a5a8ee
--- /dev/null
+++ b/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/EventSubscriber/SystemTestCloseSubscriber
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\system_test\EventSubscriber\SystemTestCloseSubscriber.
+ */
+
+namespace Drupal\system_test\EventSubscriber;
+
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\HttpKernel\Event\PostResponseEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Subscriber for all responses.
+ */
+class SystemTestCloseSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Performs end of request tasks.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\PostResponseEvent $event
+   *   The Event to process.
+   */
+  public function onTerminate(PostResponseEvent $event) {
+    watchdog('system_test', 'SystemTestCloseSubscriber');
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  static function getSubscribedEvents() {
+    $events[KernelEvents::TERMINATE][] = array('onTerminate', 50);
+
+    return $events;
+  }
+}
diff --git a/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/SystemTestBundle b/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/SystemTestBundle
new file mode 100644
index 0000000..3722b4d
--- /dev/null
+++ b/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/SystemTestBundle
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system_test\SystemTestBundle.
+ */
+
+namespace Drupal\system_test;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+/**
+ * Bundle class for the overlay module.
+ */
+class SystemTestBundle extends Bundle {
+
+  /**
+   * Implements \Symfony\Component\HttpKernel\Bundle\BundleInterface::build().
+   */
+  public function build(ContainerBuilder $container) {
+    $container->register('system_test_close_subscriber', 'Drupal\system_test\EventSubscriber\SystemTestCloseSubscriber')
+      ->addTag('event_subscriber');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module
index ee67964..f64ff34 100644
--- a/core/modules/system/tests/modules/system_test/system_test.module
+++ b/core/modules/system/tests/modules/system_test/system_test.module
@@ -236,13 +236,6 @@ function system_test_init() {
 }
 
 /**
- * Implements hook_exit().
- */
-function system_test_exit() {
-  watchdog('system_test', 'hook_exit');
-}
-
-/**
  * Implements hook_system_info_alter().
  */
 function system_test_system_info_alter(&$info, $file, $type) {
diff --git a/core/update.php b/core/update.php
index ff65c6e..9a7e058 100644
--- a/core/update.php
+++ b/core/update.php
@@ -39,7 +39,7 @@
  * Global flag indicating that update.php is being run.
  *
  * When this flag is set, various operations do not take place, such as invoking
- * hook_init() and hook_exit(), css/js preprocessing, and translation.
+ * hook_init(), event close subscribers, css/js preprocessing, and translation.
  *
  * This constant is defined using define() instead of const so that PHP
  * versions older than 5.3 can display the proper PHP requirements instead of
