diff --git a/core/includes/session.inc b/core/includes/session.inc
index b9b8fbc3ee897aa9f5d2cbdc9335e18709281e27..9ffd3d13cc1c2efa1e9e8b716ed273675375e650 100644
--- a/core/includes/session.inc
+++ b/core/includes/session.inc
@@ -105,9 +105,6 @@ function _drupal_session_read($sid) {
   // We found the client's session record and they are an authenticated,
   // active user.
   if ($user && $user->uid > 0 && $user->status == 1) {
-    // This is done to unserialize the data member of $user.
-    $user->data = unserialize($user->data);
-
     // Add roles element to $user.
     $user->roles = array();
     $user->roles[DRUPAL_AUTHENTICATED_RID] = DRUPAL_AUTHENTICATED_RID;
diff --git a/core/modules/block/block.install b/core/modules/block/block.install
index 56b91c3ba9039d050e62485120447a0d4c42b37f..a5bd3c456f2bf0b4aacf91e1aed593c633cdf1de 100644
--- a/core/modules/block/block.install
+++ b/core/modules/block/block.install
@@ -233,6 +233,10 @@ function block_update_dependencies() {
   $dependencies['block'][8002] = array(
     'user' => 8002,
   );
+  // Migrate users.data after User module prepared the tables.
+  $dependencies['block'][8005] = array(
+    'user' => 8011,
+  );
   return $dependencies;
 }
 
@@ -353,6 +357,28 @@ function block_update_8004() {
   }
 }
 
+/**
+ * Migrate {users}.data into {users_data}.
+ */
+function block_update_8005() {
+  $query = db_select('_d7_users_data', 'ud');
+  $query->addField('ud', 'uid');
+  $query->addExpression("'block'", 'module');
+  $query->addExpression("'block'", 'name');
+  // Take over the extracted and serialized value in {_d7_users_data} as-is.
+  $query->addField('ud', 'value');
+  $query->addExpression('1', 'serialized');
+  $query->condition('name', 'block');
+
+  db_insert('users_data')
+    ->from($query)
+    ->execute();
+
+  db_delete('_d7_users_data')
+    ->condition('name', 'block')
+    ->execute();
+}
+
 /**
  * @} End of "addtogroup updates-7.x-to-8.x".
  * The next series of updates should start at 9000.
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index 20191b70052c80d8036368230cf79bada63b0a8e..26bcb2530d339af25630af5953c656c1a6b862e4 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -599,6 +599,7 @@ function block_form_user_profile_form_alter(&$form, &$form_state) {
   $account = $form_state['controller']->getEntity($form_state);
   $rids = array_keys($account->roles);
   $result = db_query("SELECT DISTINCT b.* FROM {block} b LEFT JOIN {block_role} r ON b.module = r.module AND b.delta = r.delta WHERE b.status = 1 AND b.custom <> 0 AND (r.rid IN (:rids) OR r.rid IS NULL) ORDER BY b.weight, b.module", array(':rids' => $rids));
+  $account_data = drupal_container()->get('user.data')->get('block', $account->id(), 'block');
 
   $blocks = array();
   foreach ($result as $block) {
@@ -607,7 +608,7 @@ function block_form_user_profile_form_alter(&$form, &$form_state) {
       $blocks[$block->module][$block->delta] = array(
         '#type' => 'checkbox',
         '#title' => check_plain($data[$block->delta]['info']),
-        '#default_value' => isset($account->data['block'][$block->module][$block->delta]) ? $account->data['block'][$block->module][$block->delta] : ($block->custom == 1),
+        '#default_value' => isset($account_data[$block->module][$block->delta]) ? $account_data[$block->module][$block->delta] : ($block->custom == 1),
       );
     }
   }
@@ -639,11 +640,11 @@ function block_field_extra_fields() {
 }
 
 /**
- * Implements hook_user_presave().
+ * Implements hook_user_update().
  */
-function block_user_presave($account) {
+function block_user_update($account) {
   if (isset($account->block)) {
-    $account->data['block'] = $account->block;
+    drupal_container()->get('user.data')->set('block', $account->id(), 'block', $account->block);
   }
 }
 
@@ -805,6 +806,10 @@ function block_block_list_alter(&$blocks) {
     $block_langcodes[$record->module][$record->delta][$record->type][$record->langcode] = TRUE;
   }
 
+  if ($user->uid) {
+    $user_data = drupal_container()->get('user.data')->get('block', $user->uid, 'block');
+  }
+
   foreach ($blocks as $key => $block) {
     if (!isset($block->theme) || !isset($block->status) || $block->theme != $theme_key || $block->status != 1) {
       // This block was added by a contrib module, leave it in the list.
@@ -822,8 +827,8 @@ function block_block_list_alter(&$blocks) {
 
     // Use the user's block visibility setting, if necessary.
     if ($block->custom != BLOCK_CUSTOM_FIXED) {
-      if ($user->uid && isset($user->data['block'][$block->module][$block->delta])) {
-        $enabled = $user->data['block'][$block->module][$block->delta];
+      if ($user->uid && isset($user_data[$block->module][$block->delta])) {
+        $enabled = $user_data[$block->module][$block->delta];
       }
       else {
         $enabled = ($block->custom == BLOCK_CUSTOM_ENABLED);
diff --git a/core/modules/contact/contact.install b/core/modules/contact/contact.install
index 9ea5451c3cbc0051bc23bbbde046c867fed9a80b..8f97f0c802eb41e96acac5bc716bd657f9c50de3 100644
--- a/core/modules/contact/contact.install
+++ b/core/modules/contact/contact.install
@@ -21,6 +21,17 @@ function contact_install() {
  * @{
  */
 
+/**
+ * Implements hook_update_dependencies().
+ */
+function contact_update_dependencies() {
+  // Migrate users.data after User module prepared the tables.
+  $dependencies['contact'][8003] = array(
+    'user' => 8011,
+  );
+  return $dependencies;
+}
+
 /**
  * Moves contact setting from variable to config.
  *
@@ -68,6 +79,26 @@ function contact_update_8002() {
   db_drop_table('contact');
 }
 
+/**
+ * Migrate {users}.data into {users_data}.
+ */
+function contact_update_8003() {
+  $query = db_select('_d7_users_data', 'ud');
+  $query->condition('name', 'contact');
+  $query->addField('ud', 'uid');
+  $query->addExpression("'contact'", 'module');
+  $query->addExpression("'enabled'", 'name');
+  $query->addField('ud', 'value', 'value');
+  $query->addExpression(1, 'serialized');
+
+  db_insert('users_data')
+    ->from($query)
+    ->execute();
+  db_delete('_d7_users_data')
+    ->condition('name', 'contact')
+    ->execute();
+}
+
 /**
  * @} End of "defgroup updates-7.x-to-8.x".
  * The next series of updates should start at 9000.
diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module
index 4e1ec6b421076cc174c4a72e13a9b85c875f619b..053df01d44d8be7e7c4820edc78a84f74b1f9c39 100644
--- a/core/modules/contact/contact.module
+++ b/core/modules/contact/contact.module
@@ -140,14 +140,20 @@ function _contact_personal_tab_access($account) {
     return TRUE;
   }
 
-  // If the requested user has disabled their contact form, or this preference
-  // has not yet been saved, do not allow users to contact them.
-  if (empty($account->data['contact'])) {
+  // If requested user has been blocked, do not allow users to contact them.
+  if (empty($account->status)) {
     return FALSE;
   }
 
-  // If requested user has been blocked, do not allow users to contact them.
-  if (empty($account->status)) {
+  // If the requested user has disabled their contact form, do not allow users
+  // to contact them.
+  $account_data = drupal_container()->get('user.data')->get('contact', $account->id(), 'enabled');
+  if (isset($account_data) && empty($account_data)) {
+    return FALSE;
+  }
+  // If the requested user did not save a preference yet, deny access if the
+  // configured default is disabled.
+  elseif (!config('contact.settings')->get('user_default_enabled')) {
     return FALSE;
   }
 
@@ -290,19 +296,22 @@ function contact_form_user_profile_form_alter(&$form, &$form_state) {
     '#collapsible' => TRUE,
   );
   $account = $form_state['controller']->getEntity($form_state);
+  $account_data = drupal_container()->get('user.data')->get('contact', $account->id(), 'enabled');
   $form['contact']['contact'] = array(
     '#type' => 'checkbox',
     '#title' => t('Personal contact form'),
-    '#default_value' => !empty($account->data['contact']) ? $account->data['contact'] : FALSE,
+    '#default_value' => isset($account_data) ? $account_data : config('contact.settings')->get('user_default_enabled'),
     '#description' => t('Allow other users to contact you via a personal contact form which keeps your e-mail address hidden. Note that some privileged users such as site administrators are still able to contact you even if you choose to disable this feature.'),
   );
 }
 
 /**
- * Implements hook_user_presave().
+ * Implements hook_user_update().
  */
-function contact_user_presave($account) {
-  $account->data['contact'] = isset($account->contact) ? $account->contact : config('contact.settings')->get('user_default_enabled');
+function contact_user_update($account) {
+  if (isset($account->contact)) {
+    drupal_container()->get('user.data')->set('contact', $account->id(), 'enabled', (int) $account->contact);
+  }
 }
 
 /**
diff --git a/core/modules/openid/lib/Drupal/openid/Tests/OpenIDRegistrationTest.php b/core/modules/openid/lib/Drupal/openid/Tests/OpenIDRegistrationTest.php
index debb9cdb512f7a47dbaf0341388e726d78868a4a..51c216012a7dfbcf2d6f298bfec3dc48ac6e2806 100644
--- a/core/modules/openid/lib/Drupal/openid/Tests/OpenIDRegistrationTest.php
+++ b/core/modules/openid/lib/Drupal/openid/Tests/OpenIDRegistrationTest.php
@@ -77,7 +77,6 @@ function testRegisterUserWithEmailVerification() {
     $this->assertEqual($user->mail, 'john@example.com', 'User was registered with right email address.');
     $this->assertEqual($user->timezone, 'Europe/London', 'User was registered with right timezone.');
     $this->assertEqual($user->preferred_langcode, 'pt', 'User was registered with right language.');
-    $this->assertFalse($user->data, 'No additional user info was saved.');
 
     $this->submitLoginForm($identity);
     $this->assertRaw(t('You must validate your email address for this account before logging in via OpenID.'));
@@ -126,7 +125,6 @@ function testRegisterUserWithoutEmailVerification() {
     $this->assertEqual($user->mail, 'john@example.com', 'User was registered with right email address.');
     $this->assertEqual($user->timezone, 'Europe/London', 'User was registered with right timezone.');
     $this->assertEqual($user->preferred_langcode, 'pt-br', 'User was registered with right language.');
-    $this->assertFalse($user->data, 'No additional user info was saved.');
 
     $this->drupalLogout();
 
@@ -171,7 +169,6 @@ function testRegisterUserWithInvalidSreg() {
     $user = user_load_by_name('john');
     $this->assertTrue($user, 'User was registered with right username.');
     $this->assertEqual($user->preferred_langcode, language_default()->langcode, 'User language is site default.');
-    $this->assertFalse($user->data, 'No additional user info was saved.');
 
     // Follow the one-time login that was sent in the welcome e-mail.
     $this->drupalGet($reset_url);
@@ -211,7 +208,6 @@ function testRegisterUserWithoutSreg() {
     $user = user_load_by_name('john');
     $this->assertTrue($user, 'User was registered with right username.');
     $this->assertEqual($user->preferred_langcode, language_default()->langcode, 'User language is site default.');
-    $this->assertFalse($user->data, 'No additional user info was saved.');
 
     // Follow the one-time login that was sent in the welcome e-mail.
     $this->drupalGet($reset_url);
diff --git a/core/modules/overlay/overlay.install b/core/modules/overlay/overlay.install
index 2df860b5e6ed78cdaaf2199e4bdf4a5c256c9d88..588f871e6ebfb28634976ad3ae2c5c934a8dcdf9 100644
--- a/core/modules/overlay/overlay.install
+++ b/core/modules/overlay/overlay.install
@@ -17,3 +17,48 @@ function overlay_enable() {
     $_SESSION['overlay_enable_redirect'] = 1;
   }
 }
+
+/**
+ * Implements hook_update_dependencies().
+ */
+function overlay_update_dependencies() {
+  // Migrate users.data after User module prepared the tables.
+  $dependencies['overlay'][8000] = array(
+    'user' => 8011,
+  );
+  return $dependencies;
+}
+
+/**
+ * Migrate {users}.data into {users_data}.
+ */
+function overlay_update_8000() {
+  $query = db_select('_d7_users_data', 'ud');
+  $query->condition('name', 'overlay');
+  $query->addField('ud', 'uid');
+  $query->addExpression("'overlay'", 'module');
+  $query->addExpression("'enabled'", 'name');
+  $query->addField('ud', 'value', 'value');
+  $query->addExpression(1, 'serialized');
+
+  db_insert('users_data')
+    ->from($query)
+    ->execute();
+
+  // Migrate 'overlay_message_dismissed'.
+  $query = db_select('_d7_users_data', 'ud');
+  $query->condition('name', 'overlay_message_dismissed');
+  $query->addField('ud', 'uid');
+  $query->addExpression("'overlay'", 'module');
+  $query->addExpression("'message_dismissed'", 'name');
+  $query->addField('ud', 'value', 'value');
+  $query->addExpression(1, 'serialized');
+
+  db_insert('users_data')
+    ->from($query)
+    ->execute();
+
+  db_delete('_d7_users_data')
+    ->condition('name', array('overlay', 'overlay_message_dismissed'))
+    ->execute();
+}
diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module
index 06214b2d825ef8c9aa7f015b4b786ba5576e9a35..986391540319d19c30a1799acd1ac25ac2a31913 100644
--- a/core/modules/overlay/overlay.module
+++ b/core/modules/overlay/overlay.module
@@ -87,6 +87,7 @@ function overlay_theme() {
 function overlay_form_user_profile_form_alter(&$form, &$form_state) {
   $account = $form_state['controller']->getEntity($form_state);
   if (user_access('access overlay', $account)) {
+    $account_data = drupal_container()->get('user.data')->get('overlay', $account->id(), 'enabled');
     $form['overlay_control'] = array(
       '#type' => 'details',
       '#title' => t('Administrative overlay'),
@@ -97,17 +98,17 @@ function overlay_form_user_profile_form_alter(&$form, &$form_state) {
       '#type' => 'checkbox',
       '#title' => t('Use the overlay for administrative pages.'),
       '#description' => t('Show administrative pages on top of the page you started from.'),
-      '#default_value' => isset($account->data['overlay']) ? $account->data['overlay'] : 1,
+      '#default_value' => isset($account_data) ? $account_data : 1,
     );
   }
 }
 
 /**
- * Implements hook_user_presave().
+ * Implements hook_user_update().
  */
-function overlay_user_presave($account) {
+function overlay_user_update($account) {
   if (isset($account->overlay)) {
-    $account->data['overlay'] = $account->overlay;
+    drupal_container()->get('user.data')->set('overlay', $account->id(), 'enabled', (int) $account->overlay);
   }
 }
 
@@ -126,7 +127,8 @@ function overlay_init() {
 
   // Only act if the user has access to the overlay and a mode was not already
   // set. Other modules can also enable the overlay directly for other uses.
-  $use_overlay = !isset($user->data['overlay']) || $user->data['overlay'];
+  $user_data = drupal_container()->get('user.data')->get('overlay', $user->uid, 'enabled');
+  $use_overlay = !isset($user_data) || $user_data;
   if (empty($mode) && user_access('access overlay') && $use_overlay) {
     $current_path = current_path();
     // After overlay is enabled on the modules page, redirect to
@@ -354,9 +356,7 @@ function overlay_user_dismiss_message() {
     throw new AccessDeniedHttpException();
   }
 
-  $account = user_load($user->uid);
-  $account->data['overlay_message_dismissed'] = 1;
-  $account->save();
+  drupal_container()->get('user.data')->set('overlay', $user->uid, 'message_dismissed', 1);
   drupal_set_message(t('The message has been dismissed. You can change your overlay settings at any time by visiting your profile page.'));
   // Destination is normally given. Go to the user profile as a fallback.
   drupal_goto('user/' . $user->uid . '/edit');
@@ -378,7 +378,13 @@ function overlay_user_dismiss_message() {
 function overlay_disable_message() {
   global $user;
 
-  if (!empty($user->uid) && empty($user->data['overlay_message_dismissed']) && (!isset($user->data['overlay']) || $user->data['overlay']) && user_access('access overlay')) {
+  $build = array();
+  if (empty($user->uid) || !user_access('access overlay')) {
+    return $build;
+  }
+
+  $user_data = drupal_container()->get('user.data')->get('overlay', $user->uid);
+  if (empty($user_data['message_dismissed']) && (!isset($user_data['enabled']) || $user_data['enabled'])) {
     $build = array(
       '#theme' => 'overlay_disable_message',
       '#weight' => -99,
@@ -418,9 +424,6 @@ function overlay_disable_message() {
       )
     );
   }
-  else {
-    $build = array();
-  }
 
   return $build;
 }
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
index 60a9a7707b2e977158cf9ec8d67dc03c3976b74f..72ec6287bda6f0494dce2525bce7512066f6465e 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
@@ -819,13 +819,13 @@ protected function prepareEnvironment() {
     $this->originalContainer = clone drupal_container();
     $this->originalLanguage = $language_interface;
     $this->originalConfigDirectories = $GLOBALS['config_directories'];
-    $this->originalThemeKey = $GLOBALS['theme_key'];
-    $this->originalTheme = $GLOBALS['theme'];
+    $this->originalThemeKey = isset($GLOBALS['theme_key']) ? $GLOBALS['theme_key'] : NULL;
+    $this->originalTheme = isset($GLOBALS['theme']) ? $GLOBALS['theme'] : NULL;
 
     // Save further contextual information.
     $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files');
     $this->originalProfile = drupal_get_profile();
-    $this->originalUser = clone $user;
+    $this->originalUser = isset($user) ? clone $user : NULL;
 
     // Ensure that the current session is not changed by the new environment.
     drupal_save_session(FALSE);
diff --git a/core/modules/system/lib/Drupal/system/Tests/Upgrade/FilledStandardUpgradePathTest.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/FilledStandardUpgradePathTest.php
index e1df73e26dcbac6c1242322294d5645bbf8fc617..41b3d84974d1cf46cc9eca8fcd0cb8bd7b90e0d7 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Upgrade/FilledStandardUpgradePathTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/FilledStandardUpgradePathTest.php
@@ -29,6 +29,7 @@ public function setUp() {
     // Path to the database dump files.
     $this->databaseDumpFiles = array(
       drupal_get_path('module', 'system') . '/tests/upgrade/drupal-7.filled.standard_all.database.php.gz',
+      drupal_get_path('module', 'system') . '/tests/upgrade/drupal-7.user_data.database.php',
     );
     parent::setUp();
   }
@@ -96,5 +97,28 @@ public function testFilledStandardUpgrade() {
     $blog_type = node_type_load('blog');
     $this->assertEqual($blog_type->module, 'node', "Content type 'blog' has been reassigned from the blog module to the node module.");
     $this->assertEqual($blog_type->base, 'node_content', "The base string used to construct callbacks corresponding to content type 'Blog' has been reassigned to 'node_content'.");
+
+    // Check that user data has been migrated correctly.
+    $query = db_query('SELECT * FROM {users_data}');
+
+    $userdata = array();
+    $i = 0;
+    foreach ($query as $row) {
+      $i++;
+      $userdata[$row->uid][$row->module][$row->name] = $row;
+    }
+    // Check that the correct amount of rows exist.
+    $this->assertEqual($i, 5);
+    // Check that the data has been converted correctly.
+    $this->assertEqual(unserialize($userdata[1]['contact']['enabled']->value), 1);
+    $this->assertEqual($userdata[1]['contact']['enabled']->serialized, 1);
+    $this->assertEqual(unserialize($userdata[2]['contact']['enabled']->value), 0);
+    $this->assertEqual(unserialize($userdata[1]['overlay']['enabled']->value), 1);
+    $this->assertEqual(unserialize($userdata[2]['overlay']['enabled']->value), 1);
+    $this->assertEqual(unserialize($userdata[1]['overlay']['message_dismissed']->value), 1);
+    $this->assertFalse(isset($userdata[2]['overlay']['message_dismissed']));
+
+    // Make sure that only the garbage is remaining in the helper table.
+    $this->assertEqual(db_query('SELECT COUNT(*) FROM {_d7_users_data}')->fetchField(), 2);
   }
 }
diff --git a/core/modules/system/tests/upgrade/drupal-7.user_data.database.php b/core/modules/system/tests/upgrade/drupal-7.user_data.database.php
new file mode 100644
index 0000000000000000000000000000000000000000..d55f4bf1b34789e142c3e9549a60687faf5cfc66
--- /dev/null
+++ b/core/modules/system/tests/upgrade/drupal-7.user_data.database.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Database additions for role tests. Used in
+ * \Drupal\system\Tests\Upgrade\FilledStandardUpgradePathTest.
+ *
+ * This dump only contains data and schema components relevant for user data
+ * upgrade tests. The drupal-7.filed.database.php.gz file is imported before
+ * this dump, so the two form the database structure expected in tests
+ * altogether.
+ */
+
+db_update('users')
+  ->condition('uid', 1)
+  ->fields(array(
+    'data' => serialize(array(
+      'contact' => 1,
+      'overlay_message_dismissed' => '1',
+      'overlay' => '1',
+      'garbage' => 'data',
+    )),
+  ))
+  ->execute();
+
+db_update('users')
+  ->condition('uid', 2)
+  ->fields(array(
+    'data' => serialize(array(
+      'contact' => '0',
+      'overlay' => 1,
+      'more' => array('garbage', 'data'),
+    )),
+  ))
+  ->execute();
diff --git a/core/modules/user/lib/Drupal/user/UserBundle.php b/core/modules/user/lib/Drupal/user/UserBundle.php
index 9ae8936a1dd71b324c2ee5c838698dfb5e3860b2..a4e7d8d01b29403d2fe5694ab073ab44b23813a6 100644
--- a/core/modules/user/lib/Drupal/user/UserBundle.php
+++ b/core/modules/user/lib/Drupal/user/UserBundle.php
@@ -2,12 +2,13 @@
 
 /**
  * @file
- * Contains Drupal\system\UserBundle.
+ * Contains Drupal\user\UserBundle.
  */
 
 namespace Drupal\user;
 
 use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\HttpKernel\Bundle\Bundle;
 
 /**
@@ -16,10 +17,13 @@
 class UserBundle extends Bundle {
 
   /**
-   * Overrides Bundle::build().
+   * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
    */
   public function build(ContainerBuilder $container) {
     $container->register('access_check.user.register', 'Drupal\user\Access\RegisterAccessCheck')
       ->addTag('access_check');
+    $container
+      ->register('user.data', 'Drupal\user\UserData')
+      ->addArgument(new Reference('database'));
   }
 }
diff --git a/core/modules/user/lib/Drupal/user/UserData.php b/core/modules/user/lib/Drupal/user/UserData.php
new file mode 100644
index 0000000000000000000000000000000000000000..e607d8755a25c55225e5a2dc38c986b809007f96
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/UserData.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\user\UserData.
+ */
+
+namespace Drupal\user;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * Defines the user data service.
+ */
+class UserData implements UserDataInterface {
+
+  /**
+   * The database connection to use.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Constructs a new user data service.
+   *
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection to use.
+   */
+  public function __construct(Connection $connection) {
+    $this->connection = $connection;
+  }
+
+  /**
+   * Implements \Drupal\user\UserDataInterface::get().
+   */
+  public function get($module, $uid = NULL, $name = NULL) {
+    $query = $this->connection->select('users_data', 'ud')
+      ->fields('ud')
+      ->condition('module', $module);
+    if (isset($uid)) {
+      $query->condition('uid', $uid);
+    }
+    if (isset($name)) {
+      $query->condition('name', $name);
+    }
+    $result = $query->execute();
+    // If $module, $uid, and $name was passed, return the value.
+    if (isset($name) && isset($uid)) {
+      $result = $result->fetchAllAssoc('uid');
+      if (isset($result[$uid])) {
+        return $result[$uid]->serialized ? unserialize($result[$uid]->value) : $result[$uid]->value;
+      }
+      return NULL;
+    }
+    // If $module and $uid was passed, return the name/value pairs.
+    elseif (isset($uid)) {
+      $return = array();
+      foreach ($result as $record) {
+        $return[$record->name] = ($record->serialized ? unserialize($record->value) : $record->value);
+      }
+      return $return;
+    }
+    // If $module and $name was passed, return the uid/value pairs.
+    elseif (isset($name)) {
+      $return = array();
+      foreach ($result as $record) {
+        $return[$record->uid] = ($record->serialized ? unserialize($record->value) : $record->value);
+      }
+      return $return;
+    }
+    // If only $module was passed, return data keyed by uid and name.
+    else {
+      $return = array();
+      foreach ($result as $record) {
+        $return[$record->uid][$record->name] = ($record->serialized ? unserialize($record->value) : $record->value);
+      }
+      return $return;
+    }
+  }
+
+  /**
+   * Implements \Drupal\user\UserDataInterface::set().
+   */
+  public function set($module, $uid, $name, $value) {
+    $serialized = 0;
+    if (!is_scalar($value)) {
+      $value = serialize($value);
+      $serialized = 1;
+    }
+    $this->connection->merge('users_data')
+      ->key(array(
+        'uid' => $uid,
+        'module' => $module,
+        'name' => $name,
+      ))
+      ->fields(array(
+        'value' => $value,
+        'serialized' => $serialized,
+      ))
+      ->execute();
+  }
+
+  /**
+   * Implements \Drupal\user\UserDataInterface::delete().
+   */
+  public function delete($module = NULL, $uid = NULL, $name = NULL) {
+    $query = $this->connection->delete('users_data');
+    if (isset($module)) {
+      $query->condition('module', $module);
+    }
+    if (isset($uid)) {
+      $query->condition('uid', $uid);
+    }
+    if (isset($name)) {
+      $query->condition('name', $name);
+    }
+    $query->execute();
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/UserDataInterface.php b/core/modules/user/lib/Drupal/user/UserDataInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..b56baf49a4957483254be0237a0f0624635f01bf
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/UserDataInterface.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\user\UserDataInterface.
+ */
+
+namespace Drupal\user;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * Defines the user data service interface.
+ */
+interface UserDataInterface {
+
+  /**
+   * Returns data stored for a user account.
+   *
+   * @param string $module
+   *   The name of the module the data is associated with.
+   * @param integer $uid
+   *   (optional) The user account ID the data is associated with.
+   * @param string $name
+   *   (optional) The name of the data key.
+   *
+   * @return mixed|array
+   *   The requested user account data, depending on the arguments passed:
+   *   - For $module, $name, and $uid, the stored value is returned, or NULL if
+   *     no value was found.
+   *   - For $module and $uid, an associative array is returned that contains
+   *     the stored data name/value pairs.
+   *   - For $module and $name, an associative array is returned whose keys are
+   *     user IDs and whose values contain the stored values.
+   *   - For $module only, an associative array is returned that contains all
+   *     existing data for $module in all user accounts, keyed first by user ID
+   *     and $name second.
+   */
+  public function get($module, $uid = NULL, $name = NULL);
+
+  /**
+   * Stores data for a user account.
+   *
+   * @param string $module
+   *   The name of the module the data is associated with.
+   * @param integer $uid
+   *   The user account ID the data is associated with.
+   * @param string $name
+   *   The name of the data key.
+   * @param mixed $value
+   *   The value to store. Non-scalar values are serialized automatically.
+   *
+   * @return void
+   */
+  public function set($module, $uid, $name, $value);
+
+  /**
+   * Deletes data stored for a user account.
+   *
+   * @param string|array $module
+   *   (optional) The name of the module the data is associated with. Can also
+   *   be an array to delete the data of multiple modules.
+   * @param integer|array $uid
+   *   (optional) The user account ID the data is associated with. If omitted,
+   *   all data for $module is deleted. Can also be an array of IDs to delete
+   *   the data of multiple user accounts.
+   * @param string $name
+   *   (optional) The name of the data key. If omitted, all data associated with
+   *   $module and $uid is deleted.
+   *
+   * @return void
+   */
+  public function delete($module = NULL, $uid = NULL, $name = NULL);
+
+}
diff --git a/core/modules/user/lib/Drupal/user/UserStorageController.php b/core/modules/user/lib/Drupal/user/UserStorageController.php
index cf0322d33fb44e994336d5c6a343ed7ba56e6956..2b532c90c79944e537f51ece85b67690c6620a7f 100644
--- a/core/modules/user/lib/Drupal/user/UserStorageController.php
+++ b/core/modules/user/lib/Drupal/user/UserStorageController.php
@@ -24,7 +24,6 @@ class UserStorageController extends DatabaseStorageController {
    */
   function attachLoad(&$queried_users, $load_revision = FALSE) {
     foreach ($queried_users as $key => $record) {
-      $queried_users[$key]->data = unserialize($record->data);
       $queried_users[$key]->roles = array();
       if ($record->uid) {
         $queried_users[$record->uid]->roles[DRUPAL_AUTHENTICATED_RID] = DRUPAL_AUTHENTICATED_RID;
@@ -96,10 +95,10 @@ protected function preSave(EntityInterface $entity) {
       $entity->roles = array_filter($entity->roles);
     }
 
-    // Move account cancellation information into $entity->data.
+    // Store account cancellation information.
     foreach (array('user_cancel_method', 'user_cancel_notify') as $key) {
       if (isset($entity->{$key})) {
-        $entity->data[$key] = $entity->{$key};
+        drupal_container()->get('user.data')->set('user', $entity->id(), substr($key, 5), $entity->{$key});
       }
     }
   }
@@ -179,5 +178,6 @@ protected function postDelete($entities) {
     db_delete('authmap')
       ->condition('uid', array_keys($entities), 'IN')
       ->execute();
+    drupal_container()->get('user.data')->delete(NULL, array_keys($entities));
   }
 }
diff --git a/core/modules/user/user.api.php b/core/modules/user/user.api.php
index cb7b4e5f64b0f7718fd3738f3bcf3830450ab04f..3b5f4ca068b61cae15ba2f4a9f9294ad7bc73323 100644
--- a/core/modules/user/user.api.php
+++ b/core/modules/user/user.api.php
@@ -229,11 +229,6 @@ function hook_user_operations() {
  *
  * This hook is invoked before the user account is saved to the database.
  *
- * Modules that want to store properties in the serialized {users}.data column,
- * which is automatically loaded whenever a user account object is loaded, may
- * add their properties to $account->data in order to have their data serialized
- * on save.
- *
  * @param $account
  *   The user account object.
  *
@@ -241,10 +236,9 @@ function hook_user_operations() {
  * @see hook_user_update()
  */
 function hook_user_presave($account) {
-  // Make sure that our form value 'mymodule_foo' is stored as
-  // 'mymodule_bar' in the 'data' (serialized) column.
+  // Ensure that our value is an array.
   if (isset($account->mymodule_foo)) {
-    $account->data['mymodule_bar'] = $account->mymodule_foo;
+    $account->mymodule_foo = (array) $account->mymodule_foo;
   }
 }
 
diff --git a/core/modules/user/user.install b/core/modules/user/user.install
index 28511d3561342f4d9b9dd45d1b98e20306aef727..058ebbd76b3410c94c7c9c8d91b9e74d07035505 100644
--- a/core/modules/user/user.install
+++ b/core/modules/user/user.install
@@ -129,13 +129,6 @@ function user_schema() {
         'default' => '',
         'description' => 'E-mail address used for initial account creation.',
       ),
-      'data' => array(
-        'type' => 'blob',
-        'not null' => FALSE,
-        'size' => 'big',
-        'serialize' => TRUE,
-        'description' => 'A serialized array of name value pairs that are related to the user. Any form values posted during user edit are stored and are loaded into the $user object during user_load(). Use of this field is discouraged and it will likely disappear in a future version of Drupal.',
-      ),
     ),
     'indexes' => array(
       'access' => array('access'),
@@ -267,6 +260,54 @@ function user_schema() {
     ),
   );
 
+  $schema['users_data'] = array(
+    'description' => 'Stores module data as key/value pairs per user.',
+    'fields' => array(
+      'uid' => array(
+        'description' => 'Primary key: {users}.uid for user.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'module' => array(
+        'description' => 'The name of the module declaring the variable.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'name' => array(
+        'description' => 'The identifier of the data.',
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'value' => array(
+        'description' => 'The value.',
+        'type' => 'blob',
+        'not null' => FALSE,
+        'size' => 'big',
+      ),
+      'serialized' => array(
+        'description' => 'Whether value is serialized.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'unsigned' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('uid', 'module', 'name'),
+    'indexes' => array(
+      'module' => array('module'),
+      'name' => array('name'),
+    ),
+    'foreign keys' => array(
+      'uid' => array('users' => 'uid'),
+    ),
+  );
+
   $schema['users_roles'] = array(
     'description' => 'Maps users to roles.',
     'fields' => array(
@@ -326,7 +367,6 @@ function user_install() {
       'mail' => 'placeholder-for-uid-1',
       'created' => REQUEST_TIME,
       'status' => 1,
-      'data' => NULL,
     ))
     ->execute();
 
@@ -787,6 +827,142 @@ function user_update_8013() {
   db_drop_field('users', 'picture');
 }
 
+/**
+ * Create new {users_data} table.
+ */
+function user_update_8014() {
+  // Create the {users_data} table.
+  db_create_table('users_data', array(
+    'description' => 'Stores module data as key/value pairs per user.',
+    'fields' => array(
+      'uid' => array(
+        'description' => 'Primary key: {users}.uid for user.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'module' => array(
+        'description' => 'The name of the module declaring the variable.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'name' => array(
+        'description' => 'The identifier of the data.',
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'value' => array(
+        'description' => 'The value.',
+        'type' => 'blob',
+        'not null' => FALSE,
+        'size' => 'big',
+      ),
+      'serialized' => array(
+        'description' => 'Whether value is serialized.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'unsigned' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('uid', 'module', 'name'),
+    'indexes' => array(
+      'module' => array('module'),
+      'name' => array('name'),
+    ),
+    'foreign keys' => array(
+      'uid' => array('users' => 'uid'),
+    ),
+  ));
+
+  // Create backup table for data migration.
+  // Since the origin/owner of individual values in {users}.data is unknown,
+  // other modules need to migrate their existing values from {_d7_users_data}.
+  db_create_table('_d7_users_data', array(
+    'description' => 'Backup of {users}.data for migration.',
+    'fields' => array(
+      'uid' => array(
+        'description' => 'Primary Key: {users}.uid for user.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'name' => array(
+        'description' => 'The name of the variable.',
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'value' => array(
+        'description' => 'The serialized value of the variable.',
+        'type' => 'blob',
+        'not null' => FALSE,
+        'size' => 'big',
+        'serialize' => TRUE,
+      ),
+    ),
+    'primary key' => array('uid', 'name'),
+    'foreign keys' => array(
+      'uid' => array('users' => 'uid'),
+    ),
+  ));
+}
+
+/**
+ * Move existing {users}.data into {_d7_users_data} migration table.
+ */
+function user_update_8015(&$sandbox) {
+  if (!isset($sandbox['progress'])) {
+    $sandbox['progress'] = 0;
+    // The anonymous user cannot have data, so start with uid 1.
+    $sandbox['last'] = 0;
+    $sandbox['max'] = db_query('SELECT COUNT(uid) FROM {users} WHERE uid > 0')->fetchField();
+  }
+
+  // Process 20 user records at a time. E.g., if there are 10 data keys per user
+  // record, that leads to an insert query with 200 values.
+  $result = db_query_range('SELECT uid, data FROM {users} WHERE uid > :uid ORDER BY uid ASC', 0, 20, array(':uid' => $sandbox['last']))->fetchAllKeyed();
+  $query = db_insert('_d7_users_data')->fields(array('uid', 'name', 'value'));
+  $has_values = FALSE;
+  foreach ($result as $uid => $data) {
+    $sandbox['progress']++;
+    $sandbox['last'] = $uid;
+    if (empty($data)) {
+      continue;
+    }
+    $data = unserialize($data);
+    if (!empty($data) && is_array($data)) {
+      $has_values = TRUE;
+      foreach ($data as $name => $value) {
+        $query->values(array(
+          'uid' => $uid,
+          'name' => $name,
+          'value' => serialize($value),
+        ));
+      }
+    }
+  }
+  if ($has_values) {
+    $query->execute();
+  }
+
+  $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+}
+
+/**
+ * Drop {users}.data column.
+ */
+function user_update_8016() {
+  db_drop_field('users', 'data');
+}
+
 /**
  * @} End of "addtogroup updates-7.x-to-8.x".
  */
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 93c48e14ec6b8bf036c056161994e0a6010b8026..8d73b8259662470e176a2d4234fa59e58a340d00 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -2805,12 +2805,11 @@ function user_node_load($nodes, $types) {
   }
 
   // Fetch name and data for these users.
-  $user_fields = db_query("SELECT uid, name, data FROM {users} WHERE uid IN (:uids)", array(':uids' => $uids))->fetchAllAssoc('uid');
+  $user_names = db_query("SELECT uid, name FROM {users} WHERE uid IN (:uids)", array(':uids' => $uids))->fetchAllKeyed();
 
   // Add these values back into the node objects.
   foreach ($uids as $nid => $uid) {
-    $nodes[$nid]->name = $user_fields[$uid]->name;
-    $nodes[$nid]->data = $user_fields[$uid]->data;
+    $nodes[$nid]->name = $user_names[$uid];
   }
 }
 
@@ -2927,6 +2926,8 @@ function user_modules_uninstalled($modules) {
    db_delete('role_permission')
      ->condition('module', $modules, 'IN')
      ->execute();
+  // Remove any potentially orphan module data stored for users.
+  drupal_container()->get('user.data')->delete($modules);
 }
 
 /**
diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc
index 20af863271320e8c8cf1acf1be7d7451540cc05b..d45112d539fb07e1fc56bac0ad1340c8a054b75a 100644
--- a/core/modules/user/user.pages.inc
+++ b/core/modules/user/user.pages.inc
@@ -388,13 +388,14 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') {
   $current = REQUEST_TIME;
 
   // Basic validation of arguments.
-  if (isset($account->data['user_cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) {
+  $account_data = drupal_container()->get('user.data')->get('user', $account->id());
+  if (isset($account_data['cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) {
     // Validate expiration and hashed password/login.
     if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) {
       $edit = array(
-        'user_cancel_notify' => isset($account->data['user_cancel_notify']) ? $account->data['user_cancel_notify'] : config('user.settings')->get('notify.status_canceled'),
+        'user_cancel_notify' => isset($account_data['cancel_notify']) ? $account_data['cancel_notify'] : config('user.settings')->get('notify.status_canceled'),
       );
-      user_cancel($edit, $account->uid, $account->data['user_cancel_method']);
+      user_cancel($edit, $account->id(), $account_data['cancel_method']);
       // Since user_cancel() is not invoked via Form API, batch processing needs
       // to be invoked manually and should redirect to the front page after
       // completion.
diff --git a/core/modules/user/user.views.inc b/core/modules/user/user.views.inc
index 170759a91c85fc0abb7ec9d2a3a9d9e6c49b1ae1..52e34022cd09d9598713872b53d07f4cfe9a3e84 100644
--- a/core/modules/user/user.views.inc
+++ b/core/modules/user/user.views.inc
@@ -316,14 +316,6 @@ function user_views_data() {
     ),
   );
 
-  $data['users']['data'] = array(
-    'title' => t('Data'),
-    'help' => t('Provide serialized data of the user'),
-    'field' => array(
-      'id' => 'serialized',
-    ),
-  );
-
   // users_roles table
 
   $data['users_roles']['table']['group']  = t('User');
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 5029d13359c927afbc577bf84d22dbbf96e5839f..e728932979f58f73d09978af3b15b88fd3ee5c90 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -369,7 +369,7 @@ function simpletest_script_run_one_test($test_id, $test_class) {
 
   try {
     // Bootstrap Drupal.
-    drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+    drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
 
     simpletest_classloader_register();