diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index 0c18a60..826915d 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -2670,8 +2670,7 @@ function menu_router_rebuild() {
   $transaction = db_transaction();
 
   try {
-    list($menu, $masks) = menu_router_build();
-    _menu_router_save($menu, $masks);
+    list($menu, $masks) = menu_router_build(TRUE);
     _menu_navigation_links_rebuild($menu);
     // Clear the menu, page and block caches.
     menu_cache_clear_all();
@@ -2690,8 +2689,11 @@ function menu_router_rebuild() {
 
 /**
  * Collect and alter the menu definitions.
+ *
+ * @param bool $save
+ *   (Optional) Save the new router to the database. Defaults to FALSE.
  */
-function menu_router_build() {
+function menu_router_build($save = FALSE) {
   // We need to manually call each module so that we can know which module
   // a given item came from.
   $callbacks = array();
@@ -2706,7 +2708,7 @@ function menu_router_build() {
   }
   // Alter the menu as defined in modules, keys are like user/%user.
   drupal_alter('menu', $callbacks);
-  list($menu, $masks) = _menu_router_build($callbacks);
+  list($menu, $masks) = _menu_router_build($callbacks, $save);
   _menu_router_cache($menu);
 
   return array($menu, $masks);
@@ -3486,11 +3488,12 @@ function _menu_link_parents_set(&$item, $parent) {
 /**
  * Helper function to build the router table based on the data from hook_menu.
  */
-function _menu_router_build($callbacks) {
+function _menu_router_build($callbacks, $save = FALSE) {
   // First pass: separate callbacks from paths, making paths ready for
   // matching. Calculate fitness, and fill some default values.
   $menu = array();
   $masks = array();
+  $path_roots = array();
   foreach ($callbacks as $path => $item) {
     $load_functions = array();
     $to_arg_functions = array();
@@ -3498,6 +3501,7 @@ function _menu_router_build($callbacks) {
     $move = FALSE;
 
     $parts = explode('/', $path, MENU_MAX_PARTS);
+    $path_roots[$parts[0]] = $parts[0];
     $number_parts = count($parts);
     // We store the highest index of parts here to save some work in the fit
     // calculation loop.
@@ -3707,6 +3711,17 @@ function _menu_router_build($callbacks) {
   $masks = array_keys($masks);
   rsort($masks);
 
+  if ($save) {
+    $path_roots = array_values($path_roots);
+    // Update the path roots variable and reset the path alias whitelist cache
+    // if the list has changed.
+    if ($path_roots != variable_get('menu_path_roots', array())) {
+      variable_set('menu_path_roots', array_values($path_roots));
+      cache()->delete('path_alias_whitelist');
+    }
+    _menu_router_save($menu, $masks);
+  }
+
   return array($menu, $masks);
 }
 
diff --git a/core/includes/path.inc b/core/includes/path.inc
index 0c7e4d4..00e5a3b 100644
--- a/core/includes/path.inc
+++ b/core/includes/path.inc
@@ -9,6 +9,8 @@
  * executing "drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);".
  */
 
+use Drupal\Core\Utility\PathAliasWhitelist;
+
 /**
  * Initializes the current path to the proper normal path.
  */
@@ -63,21 +65,13 @@ function drupal_lookup_path($action, $path = '', $langcode = NULL) {
     $cache = array(
       'map' => array(),
       'no_source' => array(),
-      'whitelist' => NULL,
+      'whitelist' => new PathAliasWhitelist('path_alias_whitelist', 'cache'),
       'system_paths' => array(),
       'no_aliases' => array(),
       'first_call' => TRUE,
     );
   }
 
-  // Retrieve the path alias whitelist.
-  if (!isset($cache['whitelist'])) {
-    $cache['whitelist'] = variable_get('path_alias_whitelist', NULL);
-    if (!isset($cache['whitelist'])) {
-      $cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
-    }
-  }
-
   // If no language is explicitly specified we default to the current URL
   // language. If we used a language different from the one conveyed by the
   // requested URL, we might end up being unable to check if there is a path
@@ -88,7 +82,7 @@ function drupal_lookup_path($action, $path = '', $langcode = NULL) {
     $cache = array();
     $cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
   }
-  elseif ($cache['whitelist'] && $path != '') {
+  elseif ($path != '') {
     if ($action == 'alias') {
       // During the first call to drupal_lookup_path() per language, load the
       // expected system paths for the page from cache.
@@ -368,27 +362,20 @@ function current_path() {
  *   An optional system path for which an alias is being inserted.
  *
  * @return
- *   An array containing a white list of path aliases.
+ *   An instance of the PathAliasWhitelist class.
  */
 function drupal_path_alias_whitelist_rebuild($source = NULL) {
   // When paths are inserted, only rebuild the whitelist if the system path
   // has a top level component which is not already in the whitelist.
   if (!empty($source)) {
-    $whitelist = variable_get('path_alias_whitelist', NULL);
+    $static = &drupal_static('drupal_lookup_path');
+    $whitelist = $static['whitelist'];
     if (isset($whitelist[strtok($source, '/')])) {
       return $whitelist;
     }
   }
-  // For each alias in the database, get the top level component of the system
-  // path it corresponds to. This is the portion of the path before the first
-  // '/', if present, otherwise the whole path itself.
-  $whitelist = array();
-  $result = db_query("SELECT DISTINCT SUBSTRING_INDEX(source, '/', 1) AS path FROM {url_alias}");
-  foreach ($result as $row) {
-    $whitelist[$row->path] = TRUE;
-  }
-  variable_set('path_alias_whitelist', $whitelist);
-  return $whitelist;
+  cache()->delete('path_alias_whitelist');
+  $whitelist = new PathAliasWhitelist('path_alias_whitelist', 'cache');
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Utility/PathAliasWhitelist.php b/core/lib/Drupal/Core/Utility/PathAliasWhitelist.php
new file mode 100644
index 0000000..127844e
--- /dev/null
+++ b/core/lib/Drupal/Core/Utility/PathAliasWhitelist.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @file
+ * Definition of PathAliasWhitelist
+ */
+
+namespace Drupal\Core\Utility;
+
+use Drupal\Core\Utility\CacheArray;
+
+/*
+ * Extends DrupalCacheArray to build the path alias whitelist over time.
+ */
+class PathAliasWhitelist extends CacheArray {
+
+  public function __construct($cid, $bin) {
+    parent::__construct($cid, $bin);
+
+    // On a cold start $this->storage will be empty and the whitelist will
+    // need to be rebuilt from scratch. The whitelist is initialized from the
+    // list of all valid path roots stored in the 'menu_path_roots' variable,
+    // with values initialized to NULL.During the request, each path requested
+    // that matches one of these keys will be looked up and the array value set
+    // to either TRUE or FALSE. This ensures that paths which do not exist in
+    // the router are not looked up, and that paths that do exist in the router
+    // are only looked up once.
+    if (empty($this->storage)) {
+      foreach (variable_get('menu_path_roots', array()) as $root) {
+        $this->storage[$root] = NULL;
+        $this->persist($root);
+      }
+    }
+  }
+
+  public function offsetGet($offset) {
+    // url() may be called with paths that are not represented by menu router
+    // items such as paths that will be rewritten by hook_url_outbound_alter().
+    // Therefore internally TRUE is used to indicate whitelisted paths. FALSE is
+    // used to indicate paths that have already been checked but are not
+    // whitelisted, and NULL indicates paths that have not been checked yet.
+    if (isset($this->storage[$offset])) {
+      if ($this->storage[$offset]) {
+        return TRUE;
+      }
+    }
+    elseif (array_key_exists($offset, $this->storage)) {
+      return $this->resolveCacheMiss($offset);
+    }
+  }
+
+  function resolveCacheMiss($root) {
+    $query = db_select('url_alias', 'u');
+    $query->addExpression(1);
+    $query->condition('u.source', db_like($root) . '%', 'LIKE') ->range(0, 1);
+    $exists = (bool) $query->execute()->fetchField();
+    $this->storage[$root] = $exists;
+    $this->persist($root);
+    if ($exists) {
+      return TRUE;
+    }
+  }
+
+  public function set($cid, $data, $bin, $lock = TRUE) {
+    $lock_name = $cid . ':' . $bin;
+    if (!$lock || lock_acquire($lock_name)) {
+      if ($cached = cache($bin)->get($cid)) {
+        // Use array merge instead of union so that filled in values in $data
+        // overwrite empty values in the current cache.
+        $data = array_merge($cached->data, $data);
+      }
+      cache($bin)->set($cid, $data);
+      if ($lock) {
+        lock_release($lock_name);
+      }
+    }
+  }
+}
