diff --git a/modules/user/user.module b/modules/user/user.module
index 14e1459..38b5807 100644
--- a/modules/user/user.module
+++ b/modules/user/user.module
@@ -2741,46 +2741,49 @@ function user_mail_tokens(&$replacements, $data, $options) {
 function user_roles($membersonly = FALSE, $permission = NULL) {
   $user_roles = &drupal_static(__FUNCTION__);
 
-  // Do not cache roles for specific permissions. This data is not requested
-  // frequently enough to justify the additional memory use.
-  if (empty($permission)) {
-    $cid = $membersonly ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID;
-    if (isset($user_roles[$cid])) {
-      return $user_roles[$cid];
-    }
-  }
-
-  $query = db_select('role', 'r');
-  $query->addTag('translatable');
-  $query->fields('r', array('rid', 'name'));
-  $query->orderBy('weight');
-  $query->orderBy('name');
-  if (!empty($permission)) {
-    $query->innerJoin('role_permission', 'p', 'r.rid = p.rid');
-    $query->condition('p.permission', $permission);
-  }
-  $result = $query->execute();
-
-  $roles = array();
-  foreach ($result as $role) {
-    switch ($role->rid) {
-      // We only translate the built in role names
-      case DRUPAL_ANONYMOUS_RID:
-        if (!$membersonly) {
+  // We don't cache roles for specific permissions, as most sites have enough
+  // permissions to make it very unlikely that this data will be requested
+  // frequently enough to justify the additional memory use. So if we have
+  // a permission, or $user_roles is empty, we need to hit the database.
+  if ($permission || !$user_roles) {
+    $query = db_select('role', 'r');
+    $query->addTag('translatable');
+    $query->fields('r', array('rid', 'name'));
+    $query->orderBy('weight');
+    $query->orderBy('name');
+    if ($permission) {
+      $query->innerJoin('role_permission', 'p', 'r.rid = p.rid');
+      $query->condition('p.permission', $permission);
+      if ($membersonly) {
+        $query->condition('r.rid', DRUPAL_ANONYMOUS_RID, '<>');
+      }
+    }
+    $result = $query->execute();
+
+    $roles = array();
+    foreach ($result as $role) {
+      switch ($role->rid) {
+        // We only translate the built in role names
+        case DRUPAL_ANONYMOUS_RID:
+        case DRUPAL_AUTHENTICATED_RID:
           $roles[$role->rid] = t($role->name);
-        }
-        break;
-      case DRUPAL_AUTHENTICATED_RID:
-        $roles[$role->rid] = t($role->name);
-        break;
-      default:
-        $roles[$role->rid] = $role->name;
+          break;
+        default:
+          $roles[$role->rid] = $role->name;
+      }
+    }
+    if (!$permission) {
+      // The $roles array is sorted by key so that the DRUPAL_ANONYMOUS_RID
+      // role is always the first element in the array, allowing us to use less
+      // memory by keeping one list of roles. The sort makes it easy to return
+      // a list with or without the anonymous role using array_slice().
+      ksort($roles);
+      $user_roles = $roles;
     }
   }
 
-  if (empty($permission)) {
-    $user_roles[$cid] = $roles;
-    return $user_roles[$cid];
+  if (!$permission) {
+    return $membersonly ? array_slice($user_roles, 1, count($user_roles), TRUE) : $user_roles;
   }
 
   return $roles;
@@ -2855,16 +2858,20 @@ function user_role_save($role) {
 
   if (!empty($role->rid) && $role->name) {
     $status = drupal_write_record('role', $role, 'rid');
-    module_invoke_all('user_role_update', $role);
+    $user_hook = 'user_role_update';
   }
   else {
     $status = drupal_write_record('role', $role);
-    module_invoke_all('user_role_insert', $role);
+    $user_hook = 'user_role_insert';
   }
 
-  // Clear the user access cache.
+  // Clear the user_* static caches before we invoke hook_user_role_*(),
+  // so that implementing modules don't have to work with a stale cache.
   drupal_static_reset('user_access');
   drupal_static_reset('user_role_permissions');
+  drupal_static_reset('user_roles');
+
+  module_invoke_all($user_hook, $role);
 
   return $status;
 }
@@ -2894,11 +2901,13 @@ function user_role_delete($role) {
     ->condition('rid', $role->rid)
     ->execute();
 
-  module_invoke_all('user_role_delete', $role);
-
-  // Clear the user access cache.
+  // Clear the user_* static caches before we invoke hook_user_role_delete(),
+  // so that implementing modules don't have to work with a stale cache.
   drupal_static_reset('user_access');
   drupal_static_reset('user_role_permissions');
+  drupal_static_reset('user_roles');
+
+  module_invoke_all('user_role_delete', $role);
 }
 
 /**
diff --git a/modules/user/user.test b/modules/user/user.test
index 6ecbfac..ad7e813 100644
--- a/modules/user/user.test
+++ b/modules/user/user.test
@@ -1795,7 +1795,64 @@ class UserEditedOwnAccountTestCase extends DrupalWebTestCase {
 }
 
 /**
- * Test case to test adding, editing and deleting roles.
+ * Test case to test adding, editing and deleting roles via the API.
+ */
+class UserRoleTestCase extends DrupalWebTestCase {
+  protected $profile = 'testing';
+
+  public static function getInfo() {
+    return array(
+      'name' => 'User role API',
+      'description' => 'Test adding, editing and deleting user roles via the API.',
+      'group' => 'User',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp('user_static_cache_test');
+  }
+
+  /**
+   * Test adding, renaming and deleting roles.
+   */
+  function testUserRoles() {
+    // Prime the user_roles() static cache.
+    user_roles();
+
+    // Add a new role, and check that it appears in the user_roles() list.
+    $role = new stdClass();
+    $role->name = 'original role name';
+    user_role_save($role);
+    $rid = $role->rid;
+    $roles = user_roles();
+    $this->assertTrue(isset($roles[$rid]) && $roles[$rid] == 'original role name', t('A newly saved role appears in the list returned by user_roles().'));
+    $status_messages = drupal_get_messages('status');
+    $this->assertTrue(in_array('hook_user_role_insert', $status_messages['status']), t('A newly saved role appears in the list returned by user_roles() in hook_user_role_insert().'));
+
+    // Rename the role, and check that the new name appears in the user_roles()
+    // list.
+    $role->name = 'new role name';
+    user_role_save($role);
+    $roles = user_roles();
+    $this->assertTrue(isset($roles[$rid]) && $roles[$rid] == 'new role name', t('A renamed role appears in the list returned by user_roles().'));
+    $status_messages = drupal_get_messages('status');
+    $this->assertTrue(in_array('hook_user_role_update', $status_messages['status']), t('A renamed role appears in the list returned by user_roles() in hook_user_role_update().'));
+
+    // Clear the static cache, then reload it, so we can be sure that the role
+    // we're about to delete is there, otherwise we're not realing testing for
+    // staleness after the delete.
+    drupal_static_reset('user_roles');
+    user_roles();
+    user_role_delete((int) $rid);
+    $roles = user_roles();
+    $this->assertFalse(isset($roles[$rid]), t('A deleted role does not appear in the list returned by user_roles().'));
+    $status_messages = drupal_get_messages('status');
+    $this->assertTrue(in_array('hook_user_role_delete', $status_messages['status']), t('A deleted role does not appear in the list returned by user_roles() in hook_user_role_delete().'));
+  }
+}
+
+/**
+ * Test case to test adding, editing and deleting roles via the admin UI.
  */
 class UserRoleAdminTestCase extends DrupalWebTestCase {
 
@@ -2169,3 +2226,4 @@ class UserValidateCurrentPassCustomForm extends DrupalWebTestCase {
     $this->assertText(t('The password has been validated and the form submitted successfully.'));
   }
 }
+
diff --git a/modules/user/user_static_cache_test.info b/modules/user/user_static_cache_test.info
index e69de29..9d2563d 100644
--- a/modules/user/user_static_cache_test.info
+++ b/modules/user/user_static_cache_test.info
@@ -0,0 +1,6 @@
+name = User static caches module test
+description = Support module for user module's static cache testing.
+package = Testing
+version = VERSION
+core = 8.x
+hidden = TRUE
diff --git a/modules/user/user_static_cache_test.module b/modules/user/user_static_cache_test.module
index e69de29..02b6563 100644
--- a/modules/user/user_static_cache_test.module
+++ b/modules/user/user_static_cache_test.module
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * Implements hook_user_role_delete().
+ */
+function user_static_cache_test_user_role_delete($role) {
+  $roles = user_roles();
+  if (!isset($roles[$role->rid])) {
+    drupal_set_message('hook_user_role_delete');
+  }
+}
+
+/**
+ * Implements hook_user_role_insert().
+ */
+function user_static_cache_test_user_role_insert($role) {
+  $roles = user_roles();
+  if (isset($roles[$role->rid]) && $roles[$role->rid] == 'original role name') {
+    drupal_set_message('hook_user_role_insert');
+  }
+}
+
+/**
+ * Implements hook_user_role_update().
+ */
+function user_static_cache_test_user_role_update($role) {
+  $roles = user_roles();
+  if (isset($roles[$role->rid]) && $roles[$role->rid] == 'new role name') {
+    drupal_set_message('hook_user_role_update');
+  }
+}
+
