You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@impala.apache.org by jo...@apache.org on 2026/06/16 21:34:02 UTC

(impala) branch master updated: IMPALA-14702 (prep): Add an abstraction around malloc implementation details

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git


The following commit(s) were added to refs/heads/master by this push:
     new 0b8294b1a IMPALA-14702 (prep): Add an abstraction around malloc implementation details
0b8294b1a is described below

commit 0b8294b1a5b0ad4a817dee13b7fbb2ee53f534e2
Author: Joe McDonnell <jo...@cloudera.com>
AuthorDate: Wed May 13 23:09:31 2026 -0700

    IMPALA-14702 (prep): Add an abstraction around malloc implementation details
    
    This adds a MallocUtil abstraction around the malloc implementation
    to make it easier to try other mallocs. This has different
    MallocUtil libraries for different malloc implementations:
     - gperftools tcmalloc
     - sanitizer builds (which have their own malloc implementation)
     - libc malloc (used only for certain tests).
    
    Programs should set any relevant startup flags, then call
    MallocUtil::Init() to apply the settings.
    
    This introduces a tcmalloc_aggressive_memory_decommit startup
    flag, but limits the configurability and retain existing behavior.
    Impalad is hard coded to use tcmalloc_aggressive_memory_decommit=true
    and does not respect a user's value. Other binaries default to
    tcmalloc_aggressive_memory_decommit=false but will respect a
    custom value.
    
    Impalad's memory tracker has an entry for the malloc overhead.
    This converts it to a regular metric so that it is available
    for other daemons.
    
    Testing:
     - Ran core job
     - Ran pprof locally against the heap profiler endpoint
    
    Change-Id: I3ac7841cf279ac40141147d4be9192894d6dc908
    Reviewed-on: http://gerrit.cloudera.org:8080/24401
    Reviewed-by: Joe McDonnell <jo...@cloudera.com>
    Reviewed-by: Michael Smith <mi...@cloudera.com>
    Tested-by: Joe McDonnell <jo...@cloudera.com>
---
 be/CMakeLists.txt                             |  16 +-
 be/src/common/daemon-env.cc                   |   3 +
 be/src/common/global-flags.cc                 |   7 +
 be/src/common/init.cc                         |  11 +-
 be/src/runtime/bufferpool/system-allocator.cc |  15 +-
 be/src/runtime/data-stream-test.cc            |   5 +
 be/src/runtime/exec-env.cc                    |  83 ++-----
 be/src/runtime/exec-env.h                     |   7 -
 be/src/runtime/mem-tracker.cc                 |   1 -
 be/src/runtime/query-exec-mgr.cc              |   9 +-
 be/src/runtime/test-env.cc                    |  10 +-
 be/src/scheduling/admission-controller.cc     |  12 +-
 be/src/service/impala-hs2-server.cc           |   2 -
 be/src/util/CMakeLists.txt                    |  25 ++
 be/src/util/malloc-util-gperftools.h          | 314 ++++++++++++++++++++++++++
 be/src/util/malloc-util-libc.h                |  84 +++++++
 be/src/util/malloc-util-sanitizers.h          |  88 ++++++++
 be/src/util/malloc-util.cc                    |  59 +++++
 be/src/util/malloc-util.h                     | 120 ++++++++++
 be/src/util/memory-metrics.cc                 |  82 +------
 be/src/util/memory-metrics.h                  | 105 +--------
 be/src/util/memusage-path-handlers.cc         |  15 +-
 be/src/util/metrics-test.cc                   |   6 +
 be/src/util/pprof-path-handlers.cc            |  53 +++--
 common/thrift/metrics.json                    |  24 ++
 25 files changed, 833 insertions(+), 323 deletions(-)

diff --git a/be/CMakeLists.txt b/be/CMakeLists.txt
index fddf7f01c..6cbf9ad23 100644
--- a/be/CMakeLists.txt
+++ b/be/CMakeLists.txt
@@ -696,7 +696,6 @@ set (IMPALA_DEPENDENCIES
   krb5
   libev
   libunwind
-  pprof
   breakpad_client
   hdfs
   zlib
@@ -750,12 +749,15 @@ endif()
 # FE (for tests) the jvm has already made allocations before tcmalloc can be loaded. In
 # all other binaries, we can use tcmalloc except the address sanitizer build. Address
 # sanitizer is incompatible with tcmalloc (they both intercept malloc/free)
-set (IMPALA_LINK_LIBS_NO_TCMALLOC ${IMPALA_LINK_LIBS})
-if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "ADDRESS_SANITIZER" AND
-    NOT "${CMAKE_BUILD_TYPE}" STREQUAL "TSAN" AND
-    NOT "${CMAKE_BUILD_TYPE}" STREQUAL "TSAN_FULL")
-  set (IMPALA_LINK_LIBS ${IMPALA_LINK_LIBS} tcmallocstatic)
-  set (UNIFIED_TEST_LINK_LIBS ${UNIFIED_TEST_LINK_LIBS} tcmallocstatic)
+set (IMPALA_LINK_LIBS_NO_TCMALLOC ${IMPALA_LINK_LIBS} MallocUtilLibc)
+if ("${CMAKE_BUILD_TYPE}" STREQUAL "ADDRESS_SANITIZER" OR
+    "${CMAKE_BUILD_TYPE}" STREQUAL "TSAN" OR
+    "${CMAKE_BUILD_TYPE}" STREQUAL "TSAN_FULL")
+  set (IMPALA_LINK_LIBS ${IMPALA_LINK_LIBS} MallocUtilSanitizer)
+  set (UNIFIED_TEST_LINK_LIBS ${UNIFIED_TEST_LINK_LIBS} MallocUtilSanitizer)
+else()
+  set (IMPALA_LINK_LIBS ${IMPALA_LINK_LIBS} MallocUtilGperftools)
+  set (UNIFIED_TEST_LINK_LIBS ${UNIFIED_TEST_LINK_LIBS} MallocUtilGperftools)
 endif()
 
 # When we link statically, we need to replace the static libhdfs.a with the dynamic
diff --git a/be/src/common/daemon-env.cc b/be/src/common/daemon-env.cc
index 3f54b4480..1f0f36b36 100644
--- a/be/src/common/daemon-env.cc
+++ b/be/src/common/daemon-env.cc
@@ -21,6 +21,7 @@
 #include "rpc/rpc-trace.h"
 #include "util/common-metrics.h"
 #include "util/default-path-handlers.h"
+#include "util/malloc-util.h"
 #include "util/memory-metrics.h"
 #include "util/metrics.h"
 #include "util/thread.h"
@@ -49,6 +50,8 @@ DaemonEnv::DaemonEnv(const string& name)
 }
 
 Status DaemonEnv::Init(bool init_jvm) {
+  RETURN_IF_ERROR(MallocUtil::GetInstance()->Init());
+
   if (FLAGS_enable_webserver) {
     AddDefaultUrlCallbacks(webserver_.get(), metrics_.get());
     RETURN_IF_ERROR(metrics_->RegisterHttpHandlers(webserver_.get()));
diff --git a/be/src/common/global-flags.cc b/be/src/common/global-flags.cc
index 236c02ce9..757a36ca7 100644
--- a/be/src/common/global-flags.cc
+++ b/be/src/common/global-flags.cc
@@ -110,6 +110,13 @@ DEFINE_int64(tcmalloc_max_total_thread_cache_bytes, 0, "(Advanced) Bound on the
     "amount of bytes allocated to TCMalloc thread caches. If left at 0 (default), use "
     "the default value in TCMalloc library.");
 
+DEFINE_bool(tcmalloc_aggressive_memory_decommit, false, "(Advanced) Whether to use "
+    "tcmalloc's aggressive memory decommit mode. Setting this to true frees memory "
+    "immediately to minimize memory overhead. Setting this to false can improve "
+    "performance at the cost of extra memory use. Impalad does not respect the value "
+    "of this flag and hard codes this to true. Other daemons respect the value of the "
+    "flag and default to false.");
+
 DEFINE_bool(abort_on_config_error, true, "Abort Impala startup if there are improper "
     "configs or running on unsupported hardware.");
 
diff --git a/be/src/common/init.cc b/be/src/common/init.cc
index 1c78c15c7..388c17cc1 100644
--- a/be/src/common/init.cc
+++ b/be/src/common/init.cc
@@ -20,7 +20,6 @@
 #include <csignal>
 #include <regex>
 #include <boost/filesystem.hpp>
-#include <gperftools/heap-profiler.h>
 #include <third_party/lss/linux_syscall_support.h>
 
 #include "common/global-flags.h"
@@ -49,6 +48,7 @@
 #include "util/filesystem-util.h"
 #include "util/jni-util.h"
 #include "util/logging-support.h"
+#include "util/malloc-util.h"
 #include "util/mem-info.h"
 #include "util/memory-metrics.h"
 #include "util/minidump.h"
@@ -717,12 +717,11 @@ void impala::InitCommonRuntime(int argc, char** argv, bool init_jvm,
 
   if (impala::KuduIsAvailable()) impala::InitKuduLogging();
 
-#if !defined(ADDRESS_SANITIZER) && !defined(THREAD_SANITIZER)
-  // tcmalloc and address sanitizer can not be used together
-  if (FLAGS_enable_process_lifetime_heap_profiling) {
-    HeapProfilerStart(FLAGS_heap_profile_dir.c_str());
+  // Start lifetime heap profiling if it is supported and configured
+  if (MallocUtil::GetInstance()->SupportsHeapProfiling() &&
+      FLAGS_enable_process_lifetime_heap_profiling) {
+    MallocUtil::GetInstance()->HeapProfilerStart(FLAGS_heap_profile_dir.c_str());
   }
-#endif
 
   // Signal handler for handling the SIGTERM. We want to log a message when catalogd or
   // impalad or statestored is being shutdown using a SIGTERM.
diff --git a/be/src/runtime/bufferpool/system-allocator.cc b/be/src/runtime/bufferpool/system-allocator.cc
index e6b037634..5992909a7 100644
--- a/be/src/runtime/bufferpool/system-allocator.cc
+++ b/be/src/runtime/bufferpool/system-allocator.cc
@@ -19,10 +19,9 @@
 
 #include <sys/mman.h>
 
-#include <gperftools/malloc_extension.h>
-
 #include "gutil/strings/substitute.h"
 #include "util/bit-util.h"
+#include "util/malloc-util.h"
 
 #include "common/names.h"
 
@@ -46,13 +45,11 @@ static int64_t HUGE_PAGE_SIZE = 2LL * 1024 * 1024;
 SystemAllocator::SystemAllocator(int64_t min_buffer_len)
   : min_buffer_len_(min_buffer_len) {
   DCHECK(BitUtil::IsPowerOf2(min_buffer_len));
-#if !defined(ADDRESS_SANITIZER) && !defined(THREAD_SANITIZER)
-  // Free() assumes that aggressive decommit is enabled for TCMalloc.
-  size_t aggressive_decommit_enabled;
-  MallocExtension::instance()->GetNumericProperty(
-      "tcmalloc.aggressive_memory_decommit", &aggressive_decommit_enabled);
-  CHECK_EQ(true, aggressive_decommit_enabled);
-#endif
+  if (FLAGS_madvise_huge_pages) {
+    MallocUtil::HugePageSupport support =
+        MallocUtil::GetInstance()->GetHugePageSupport();
+    CHECK_EQ(support, MallocUtil::HugePageSupport::MADVISE_COMPATIBLE);
+  }
 }
 
 Status SystemAllocator::Allocate(int64_t len, BufferPool::BufferHandle* buffer) {
diff --git a/be/src/runtime/data-stream-test.cc b/be/src/runtime/data-stream-test.cc
index b3cd12508..b5e44a327 100644
--- a/be/src/runtime/data-stream-test.cc
+++ b/be/src/runtime/data-stream-test.cc
@@ -47,6 +47,7 @@
 #include "util/debug-util.h"
 #include "util/thread.h"
 #include "util/time.h"
+#include "util/malloc-util.h"
 #include "util/mem-info.h"
 #include "util/parse-util.h"
 #include "util/scope-exit-trigger.h"
@@ -83,6 +84,7 @@ DECLARE_int32(datastream_service_deserialization_queue_size);
 DECLARE_string(datastream_service_queue_mem_limit);
 DECLARE_int32(datastream_service_num_svc_threads);
 DECLARE_string(debug_actions);
+DECLARE_bool(tcmalloc_aggressive_memory_decommit);
 
 static const PlanNodeId DEST_NODE_ID = 1;
 static const int BATCH_CAPACITY = 100;  // rows
@@ -184,6 +186,9 @@ class DataStreamTest : public testing::Test {
   virtual void SetUp() {
     exec_env_.reset(new ExecEnv());
     ABORT_IF_ERROR(exec_env_->InitForFeSupport());
+    // Always use aggressive decommit for backend tests
+    FLAGS_tcmalloc_aggressive_memory_decommit = true;
+    ABORT_IF_ERROR(MallocUtil::GetInstance()->Init());
     exec_env_->InitBufferPool(32 * 1024, 1024 * 1024 * 1024, 32 * 1024);
     runtime_state_.reset(new RuntimeState(TQueryCtx(), exec_env_.get()));
     TPlanFragment* fragment = runtime_state_->obj_pool()->Add(new TPlanFragment());
diff --git a/be/src/runtime/exec-env.cc b/be/src/runtime/exec-env.cc
index 4d359ad9f..43f74b118 100644
--- a/be/src/runtime/exec-env.cc
+++ b/be/src/runtime/exec-env.cc
@@ -63,6 +63,7 @@
 #include "util/impalad-metrics.h"
 #include "util/jwt-util-internal.h"
 #include "util/jwt-util.h"
+#include "util/malloc-util.h"
 #include "util/mem-info.h"
 #include "util/memory-metrics.h"
 #include "util/metrics.h"
@@ -144,6 +145,7 @@ DECLARE_bool(is_executor);
 DECLARE_string(webserver_interface);
 DECLARE_int32(webserver_port);
 DECLARE_int64(tcmalloc_max_total_thread_cache_bytes);
+DECLARE_bool(tcmalloc_aggressive_memory_decommit);
 DECLARE_string(admission_service_host);
 DECLARE_int32(admission_service_port);
 DECLARE_string(catalog_service_host);
@@ -415,6 +417,22 @@ Status ExecEnv::Init() {
   LOG(INFO) << "Admit memory limit: "
             << PrettyPrinter::Print(admit_mem_limit_, TUnit::BYTES);
 
+  // Initialize malloc settings
+  // This needs to happen before initializing the buffer pool, because the buffer pool
+  // verifies the support for huge pages.
+
+  // Bump thread cache to 1GB to reduce contention for TCMalloc central
+  // list's spinlock.
+  if (FLAGS_tcmalloc_max_total_thread_cache_bytes == 0) {
+    FLAGS_tcmalloc_max_total_thread_cache_bytes = 1 << 30;
+  }
+  // Aggressive decommit is required so that unused pages in the TCMalloc page heap are
+  // not backed by physical pages and do not contribute towards memory consumption.
+  // Enable it unconditionally for Impalad.
+  FLAGS_tcmalloc_aggressive_memory_decommit = true;
+
+  RETURN_IF_ERROR(MallocUtil::GetInstance()->Init());
+
   int64_t buffer_pool_limit = ParseUtil::ParseMemSpec(FLAGS_buffer_pool_limit,
       &is_percent, admit_mem_limit_);
   if (buffer_pool_limit <= 0) {
@@ -481,55 +499,13 @@ Status ExecEnv::Init() {
   RETURN_IF_ERROR(data_svc_->Init());
   RETURN_IF_ERROR(stream_mgr_->Init(data_svc_->mem_tracker()));
 
-  // Bump thread cache to 1GB to reduce contention for TCMalloc central
-  // list's spinlock.
-  if (FLAGS_tcmalloc_max_total_thread_cache_bytes == 0) {
-    FLAGS_tcmalloc_max_total_thread_cache_bytes = 1 << 30;
+  // A MemTracker for malloc overhead
+  IntGauge* overhead_gauge = MallocUtil::GetInstance()->GetOverheadBytesMetric();
+  if (overhead_gauge != nullptr) {
+    obj_pool_->Add(
+        new MemTracker(overhead_gauge, -1, Substitute("$0 Overhead",
+            MallocUtil::GetInstance()->GetName()), mem_tracker_.get()));
   }
-
-#if !defined(ADDRESS_SANITIZER) && !defined(THREAD_SANITIZER)
-  const static char* TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES =
-    "tcmalloc.max_total_thread_cache_bytes";
-  // Change the total TCMalloc thread cache size if necessary.
-  if (FLAGS_tcmalloc_max_total_thread_cache_bytes > 0 &&
-      !MallocExtension::instance()->SetNumericProperty(
-          TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES,
-          FLAGS_tcmalloc_max_total_thread_cache_bytes)) {
-    return Status(Substitute("Failed to change {0}",
-        TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES));
-  }
-  // Read the value back from tcmalloc to verify it matches what we set.
-  size_t actual_max_total_thread_cache_bytes = 0;
-  bool retval = MallocExtension::instance()->GetNumericProperty(
-    TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES,
-    &actual_max_total_thread_cache_bytes);
-  if (!retval) {
-    return Status(Substitute("Could not retrieve value of {0}.",
-        TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES));
-  }
-  if (actual_max_total_thread_cache_bytes !=
-      FLAGS_tcmalloc_max_total_thread_cache_bytes) {
-    LOG(WARNING) << "Set " << TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES << " to "
-                 << FLAGS_tcmalloc_max_total_thread_cache_bytes << " bytes but actually "
-                 << "using " << actual_max_total_thread_cache_bytes << " bytes.";
-  }
-
-  // A MemTracker for TCMalloc overhead which is the difference between the physical bytes
-  // reserved (TcmallocMetric::PHYSICAL_BYTES_RESERVED) and the bytes in use
-  // (TcmallocMetrics::BYTES_IN_USE). This overhead accounts for all the cached freelists
-  // used by TCMalloc.
-  IntGauge* negated_bytes_in_use = obj_pool_->Add(new NegatedGauge(
-      MakeTMetricDef("negated_tcmalloc_bytes_in_use", TMetricKind::GAUGE, TUnit::BYTES),
-      TcmallocMetric::BYTES_IN_USE));
-  vector<IntGauge*> overhead_metrics;
-  overhead_metrics.push_back(negated_bytes_in_use);
-  overhead_metrics.push_back(TcmallocMetric::PHYSICAL_BYTES_RESERVED);
-  SumGauge* tcmalloc_overhead = obj_pool_->Add(new SumGauge(
-      MakeTMetricDef("tcmalloc_overhead", TMetricKind::GAUGE, TUnit::BYTES),
-      overhead_metrics));
-  obj_pool_->Add(
-      new MemTracker(tcmalloc_overhead, -1, "TCMalloc Overhead", mem_tracker_.get()));
-#endif
   mem_tracker_->RegisterMetrics(metrics_.get(), "mem-tracker.process");
 
   RETURN_IF_ERROR(disk_io_mgr_->Init());
@@ -651,19 +627,8 @@ void ExecEnv::SetImpalaServer(ImpalaServer* server) {
   }
 }
 
-void ExecEnv::InitTcMallocAggressiveDecommit() {
-#if !defined(ADDRESS_SANITIZER) && !defined(THREAD_SANITIZER)
-  // Aggressive decommit is required so that unused pages in the TCMalloc page heap are
-  // not backed by physical pages and do not contribute towards memory consumption.
-  // Enable it in TCMalloc before InitBufferPool().
-  MallocExtension::instance()->SetNumericProperty(
-      "tcmalloc.aggressive_memory_decommit", 1);
-#endif
-}
-
 void ExecEnv::InitBufferPool(int64_t min_buffer_size, int64_t capacity,
     int64_t clean_pages_limit) {
-  InitTcMallocAggressiveDecommit();
   buffer_pool_.reset(
       new BufferPool(metrics_.get(), min_buffer_size, capacity, clean_pages_limit));
   buffer_reservation_.reset(new ReservationTracker());
diff --git a/be/src/runtime/exec-env.h b/be/src/runtime/exec-env.h
index 18aa27317..22b73915c 100644
--- a/be/src/runtime/exec-env.h
+++ b/be/src/runtime/exec-env.h
@@ -358,13 +358,6 @@ class ExecEnv {
   /// Initialize ExecEnv based on Hadoop config from frontend.
   Status InitHadoopConfig();
 
-  /// Set tcmalloc's aggressive_memory_decommit=1. This needs to be called before
-  /// initializing the buffer pool, because the buffer pool asserts that this
-  /// property is set and newer versions of tcmalloc do not set it by default.
-  /// InitBufferPool() calls this automatically, so this is only used directly by
-  /// TestEnv.
-  void InitTcMallocAggressiveDecommit();
-
   /// Initialise 'buffer_pool_' and 'buffer_reservation_' with given capacity.
   void InitBufferPool(int64_t min_page_len, int64_t capacity, int64_t clean_pages_limit);
 
diff --git a/be/src/runtime/mem-tracker.cc b/be/src/runtime/mem-tracker.cc
index 8818861cb..eec9d5898 100644
--- a/be/src/runtime/mem-tracker.cc
+++ b/be/src/runtime/mem-tracker.cc
@@ -19,7 +19,6 @@
 
 #include <boost/algorithm/string/join.hpp>
 #include <boost/lexical_cast.hpp>
-#include <gperftools/malloc_extension.h>
 #include <gutil/strings/substitute.h>
 
 #include "runtime/bufferpool/reservation-tracker-counters.h"
diff --git a/be/src/runtime/query-exec-mgr.cc b/be/src/runtime/query-exec-mgr.cc
index d162b7822..53a4ae55d 100644
--- a/be/src/runtime/query-exec-mgr.cc
+++ b/be/src/runtime/query-exec-mgr.cc
@@ -25,7 +25,6 @@
 #include <utility>
 
 #include <gflags/gflags.h>
-#include <gperftools/malloc_extension.h>
 #include <gutil/strings/substitute.h>
 
 #include "common/compiler-util.h"
@@ -37,6 +36,7 @@
 #include "util/container-util.h"
 #include "util/debug-util.h"
 #include "util/impalad-metrics.h"
+#include "util/malloc-util.h"
 #include "util/metrics.h"
 #include "util/network-util.h"
 #include "util/thread-pool.h"
@@ -169,18 +169,13 @@ void QueryExecMgr::ExecuteQueryHelper(QueryState* qs) {
   // Start the query fragment instances and wait for completion or errors.
   if (LIKELY(qs->StartFInstances())) qs->MonitorFInstances();
 
-#if !defined(ADDRESS_SANITIZER) && !defined(THREAD_SANITIZER)
-  // tcmalloc and address or thread sanitizer cannot be used together
   if (FLAGS_log_mem_usage_interval > 0) {
     uint64_t num_complete = ImpaladMetrics::IMPALA_SERVER_NUM_FRAGMENTS->GetValue();
     if (num_complete % FLAGS_log_mem_usage_interval == 0) {
-      char buf[2048];
       // This outputs how much memory is currently being used by this impalad
-      MallocExtension::instance()->GetStats(buf, 2048);
-      LOG(INFO) << buf;
+      LOG(INFO) << MallocUtil::GetInstance()->GetTextDescription();
     }
   }
-#endif
 
   // decrement refcount taken in QueryState::Init();
   qs->ReleaseBackendResourceRefcount();
diff --git a/be/src/runtime/test-env.cc b/be/src/runtime/test-env.cc
index 084cd998f..d1edd03f1 100644
--- a/be/src/runtime/test-env.cc
+++ b/be/src/runtime/test-env.cc
@@ -31,6 +31,7 @@
 #include "service/control-service.h"
 #include "util/disk-info.h"
 #include "util/impalad-metrics.h"
+#include "util/malloc-util.h"
 #include "util/memory-metrics.h"
 #include "util/uid-util.h"
 
@@ -40,6 +41,7 @@ using boost::scoped_ptr;
 using std::numeric_limits;
 
 DECLARE_string(hostname);
+DECLARE_bool(tcmalloc_aggressive_memory_decommit);
 
 namespace impala {
 
@@ -51,6 +53,9 @@ TestEnv::TestEnv()
     buffer_pool_capacity_(0) {}
 
 Status TestEnv::Init() {
+  // Always use aggressive decommit for backend tests
+  FLAGS_tcmalloc_aggressive_memory_decommit = true;
+  RETURN_IF_ERROR(MallocUtil::GetInstance()->Init());
   if (static_metrics_ == NULL) {
     static_metrics_.reset(new MetricGroup("test-env-static-metrics"));
     ImpaladMetrics::CreateMetrics(static_metrics_.get());
@@ -73,11 +78,6 @@ Status TestEnv::Init() {
   if (enable_buffer_pool_) {
     exec_env_->InitBufferPool(buffer_pool_min_buffer_len_, buffer_pool_capacity_,
         static_cast<int64_t>(0.1 * buffer_pool_capacity_));
-  } else {
-    // The buffer pool requires tcmalloc's aggressive_memory_decommit to be set.
-    // Since this codepath will never call InitBufferPool(), this needs to manually
-    // set it.
-    exec_env_->InitTcMallocAggressiveDecommit();
   }
   if (process_mem_tracker_use_metrics_) {
     exec_env_->InitMemTracker(process_mem_limit_);
diff --git a/be/src/scheduling/admission-controller.cc b/be/src/scheduling/admission-controller.cc
index 7a233593c..76096c41c 100644
--- a/be/src/scheduling/admission-controller.cc
+++ b/be/src/scheduling/admission-controller.cc
@@ -39,6 +39,7 @@
 #include "util/collection-metrics.h"
 #include "util/debug-util.h"
 #include "util/histogram-metric.h"
+#include "util/malloc-util.h"
 #include "util/memory-metrics.h"
 #include "util/metrics.h"
 #include "util/pretty-printer.h"
@@ -365,13 +366,10 @@ static bool RejectForAdmissionServiceMemory(
   int64_t mem_limit = AdmissiondEnv::GetInstance()->admission_service_mem_limit();
   if (mem_limit <= 0) return false;
 
-#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER)
-  DCHECK(SanitizerMallocMetric::BYTES_ALLOCATED != nullptr);
-  int64_t bytes_inuse = SanitizerMallocMetric::BYTES_ALLOCATED->GetValue();
-#else
-  DCHECK(TcmallocMetric::BYTES_IN_USE != nullptr);
-  int64_t bytes_inuse = TcmallocMetric::BYTES_IN_USE->GetValue();
-#endif
+  IntGauge* used_bytes_gauge = MallocUtil::GetInstance()->GetUsedBytesMetric(
+      /*include_overhead*/ false);
+  DCHECK(used_bytes_gauge != nullptr);
+  int64_t bytes_inuse = used_bytes_gauge->GetValue();
 
   if (bytes_inuse + additional_mem > mem_limit) {
     LOG(INFO) << "Rejected admission with additional_mem: " << additional_mem
diff --git a/be/src/service/impala-hs2-server.cc b/be/src/service/impala-hs2-server.cc
index 75d9b0292..e28c5392b 100644
--- a/be/src/service/impala-hs2-server.cc
+++ b/be/src/service/impala-hs2-server.cc
@@ -25,8 +25,6 @@
 #include <boost/algorithm/string/join.hpp>
 #include <boost/bind.hpp>
 #include <boost/unordered_set.hpp>
-#include <gperftools/heap-profiler.h>
-#include <gperftools/malloc_extension.h>
 #include <gtest/gtest.h>
 #include <gutil/strings/substitute.h>
 
diff --git a/be/src/util/CMakeLists.txt b/be/src/util/CMakeLists.txt
index 7dc2c68d2..c20ffdc53 100644
--- a/be/src/util/CMakeLists.txt
+++ b/be/src/util/CMakeLists.txt
@@ -178,6 +178,31 @@ target_link_libraries(ProfileToolUtil PRIVATE GlobalFlags)
 set_target_properties(ProfileToolUtil PROPERTIES
   COMPILE_FLAGS "-ffunction-sections -fdata-sections")
 
+# Build a variant of MallocUtil for each separate implementation
+
+add_library(MallocUtilLibc
+  malloc-util.cc
+)
+add_dependencies(MallocUtilLibc gen-deps)
+target_compile_definitions(MallocUtilLibc PRIVATE MALLOC_UTIL_LIBC_IMPL)
+
+add_library(MallocUtilGperftools
+  malloc-util.cc
+)
+add_dependencies(MallocUtilGperftools gen-deps)
+target_compile_definitions(MallocUtilGperftools PRIVATE MALLOC_UTIL_GPERFTOOLS_IMPL)
+target_link_libraries(MallocUtilGperftools tcmallocstatic pprof)
+
+if ("${CMAKE_BUILD_TYPE}" STREQUAL "ADDRESS_SANITIZER" OR
+    "${CMAKE_BUILD_TYPE}" STREQUAL "TSAN" OR
+    "${CMAKE_BUILD_TYPE}" STREQUAL "TSAN_FULL")
+  add_library(MallocUtilSanitizer
+    malloc-util.cc
+  )
+  add_dependencies(MallocUtilSanitizer gen-deps)
+  target_compile_definitions(MallocUtilSanitizer PRIVATE MALLOC_UTIL_SANITIZER_IMPL)
+endif()
+
 # Squeasel requires C99 compatibility to build.
 SET_SOURCE_FILES_PROPERTIES(${SQUEASEL_SRC_DIR}/squeasel.c
   PROPERTIES COMPILE_FLAGS -std=c99)
diff --git a/be/src/util/malloc-util-gperftools.h b/be/src/util/malloc-util-gperftools.h
new file mode 100644
index 000000000..2adce2534
--- /dev/null
+++ b/be/src/util/malloc-util-gperftools.h
@@ -0,0 +1,314 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <gflags/gflags.h>
+#include <glog/logging.h>
+#include <gperftools/heap-profiler.h>
+#include <gperftools/malloc_extension.h>
+#include <gperftools/profiler.h>
+
+#include "gutil/strings/substitute.h"
+#include "util/malloc-util.h"
+#include "util/metrics.h"
+
+DECLARE_int64(tcmalloc_max_total_thread_cache_bytes);
+DECLARE_bool(tcmalloc_aggressive_memory_decommit);
+
+using strings::Substitute;
+
+namespace impala {
+
+/// Specialised metric which exposes numeric properties from tcmalloc.
+/// These metrics line up with the text description used for /memz:
+/// MALLOC:     3388499040 ( 3231.5 MiB) Bytes in use by application (#1)
+/// MALLOC: +            0 (    0.0 MiB) Bytes in page heap freelist (#2)
+/// MALLOC: +     98309992 (   93.8 MiB) Bytes in central cache freelist (#3)
+/// MALLOC: +      4746496 (    4.5 MiB) Bytes in transfer cache freelist (#4)
+/// MALLOC: +    177297208 (  169.1 MiB) Bytes in thread cache freelists (#5)
+/// MALLOC: +     14942208 (   14.2 MiB) Bytes in malloc metadata
+/// MALLOC:   ------------
+/// MALLOC: =   3683794944 ( 3513.1 MiB) Actual memory used (physical + swap) (#6)
+/// MALLOC: +    149757952 (  142.8 MiB) Bytes released to OS (aka unmapped) (#7)
+/// MALLOC:   ------------
+/// MALLOC: =   3833552896 ( 3656.0 MiB) Virtual address space used (#8)
+class TcmallocMetric : public IntGauge {
+ public:
+  /// #1: Number of bytes allocated by tcmalloc, currently used by the application.
+  static TcmallocMetric* BYTES_IN_USE;
+
+  /// #2: Number of bytes reserved and still mapped by tcmalloc that are not allocated
+  /// to the application.
+  static TcmallocMetric* PAGEHEAP_FREE_BYTES;
+
+  /// #3: Number of free bytes in the central cache assigned to size classes.
+  static TcmallocMetric* CENTRAL_CACHE_FREE_BYTES;
+
+  /// #4: Number of free bytes waiting to be transferred between central and thread
+  /// caches.
+  static TcmallocMetric* TRANSFER_CACHE_FREE_BYTES;
+
+  /// #5: Number of bytes used across all thread caches.
+  static TcmallocMetric* CURRENT_TOTAL_THREAD_CACHE_BYTES;
+
+  /// #6: Number of physical bytes in use (including any tcmalloc caches and metadata)
+  static TcmallocMetric* PHYSICAL_BYTES_RESERVED;
+
+  /// #7: Number of bytes once reserved by tcmalloc, but released back to the operating
+  /// system so that their use incurs a pagefault. Contributes to the total amount of
+  /// virtual address space used, but not to the physical memory usage.
+  static TcmallocMetric* PAGEHEAP_UNMAPPED_BYTES;
+
+  /// #8: Derived metric computing the amount of virtual memory (in bytes) used by the
+  /// process, including all different types of tcmalloc memory including unmapped
+  /// virtual memory.
+  class TotalBytesReservedMetric : public IntGauge {
+  public:
+    TotalBytesReservedMetric(const TMetricDef& def) : IntGauge(def, 0) { }
+
+    virtual int64_t GetValue() override {
+      return PHYSICAL_BYTES_RESERVED->GetValue() + PAGEHEAP_UNMAPPED_BYTES->GetValue();
+    }
+  };
+  static TotalBytesReservedMetric* TOTAL_BYTES_RESERVED;
+
+  /// Derived metric computing the amount of memory (in bytes) used by tcmalloc for
+  /// overhead (thread caches, unused memory in page heap, etc).
+  class OverheadBytesMetric : public IntGauge {
+  public:
+    OverheadBytesMetric(const TMetricDef& def) : IntGauge(def, 0) { }
+
+    int64_t GetValue() override {
+      return PHYSICAL_BYTES_RESERVED->GetValue() - BYTES_IN_USE->GetValue();
+    }
+  };
+
+  static OverheadBytesMetric* OVERHEAD_BYTES;
+
+  static TcmallocMetric* CreateAndRegister(MetricGroup* metrics, const std::string& key,
+      const std::string& tcmalloc_var) {
+    return metrics->RegisterMetric(
+        new TcmallocMetric(MetricDefs::Get(key), tcmalloc_var));
+  }
+
+  int64_t GetValue() override {
+    int64_t retval = 0;
+    MallocExtension::instance()->GetNumericProperty(tcmalloc_var_.c_str(),
+      reinterpret_cast<size_t*>(&retval));
+    return retval;
+  }
+
+ private:
+  /// Name of the tcmalloc property this metric should fetch.
+  const std::string tcmalloc_var_;
+
+  TcmallocMetric(const TMetricDef& def, const std::string& tcmalloc_var)
+    : IntGauge(def, 0), tcmalloc_var_(tcmalloc_var) { }
+};
+
+TcmallocMetric* TcmallocMetric::BYTES_IN_USE = nullptr;
+TcmallocMetric* TcmallocMetric::PAGEHEAP_FREE_BYTES = nullptr;
+TcmallocMetric* TcmallocMetric::CENTRAL_CACHE_FREE_BYTES = nullptr;
+TcmallocMetric* TcmallocMetric::TRANSFER_CACHE_FREE_BYTES = nullptr;
+TcmallocMetric* TcmallocMetric::CURRENT_TOTAL_THREAD_CACHE_BYTES = nullptr;
+TcmallocMetric* TcmallocMetric::PHYSICAL_BYTES_RESERVED = nullptr;
+TcmallocMetric* TcmallocMetric::PAGEHEAP_UNMAPPED_BYTES = nullptr;
+TcmallocMetric::TotalBytesReservedMetric* TcmallocMetric::TOTAL_BYTES_RESERVED = nullptr;
+TcmallocMetric::OverheadBytesMetric* TcmallocMetric::OVERHEAD_BYTES = nullptr;
+
+class GperftoolsMallocUtil : public MallocUtil {
+ public:
+  Status Init() override {
+    if (initialized_) { return Status::OK(); }
+
+    if (FLAGS_tcmalloc_aggressive_memory_decommit) {
+      // By default tcmalloc does not use aggressive decommit. Set it if it is enabled.
+      MallocExtension::instance()->SetNumericProperty(
+          "tcmalloc.aggressive_memory_decommit", 1);
+    }
+
+    const static char* TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES =
+      "tcmalloc.max_total_thread_cache_bytes";
+    // Change the total TCMalloc thread cache size if necessary.
+    if (FLAGS_tcmalloc_max_total_thread_cache_bytes > 0 &&
+        !MallocExtension::instance()->SetNumericProperty(
+            TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES,
+            FLAGS_tcmalloc_max_total_thread_cache_bytes)) {
+      return Status(Substitute("Failed to change {0}",
+          TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES));
+    }
+    // Read the value back from tcmalloc to verify it matches what we set.
+    size_t actual_max_total_thread_cache_bytes = 0;
+    bool retval = MallocExtension::instance()->GetNumericProperty(
+      TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES,
+      &actual_max_total_thread_cache_bytes);
+    if (!retval) {
+      return Status(Substitute("Could not retrieve value of {0}.",
+          TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES));
+    }
+    if (actual_max_total_thread_cache_bytes !=
+        FLAGS_tcmalloc_max_total_thread_cache_bytes) {
+      LOG(WARNING) << "Set " << TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES << " to "
+                   << FLAGS_tcmalloc_max_total_thread_cache_bytes << " bytes but "
+                   << "actually using " << actual_max_total_thread_cache_bytes
+                   << " bytes.";
+    }
+    initialized_ = true;
+    return Status::OK();
+  }
+
+  std::string GetTextDescription() const override {
+    char buf[2048];
+    MallocExtension::instance()->GetStats(buf, 2048);
+    return std::string(buf);
+  }
+
+  std::string GetName() const override { return "TCMalloc"; }
+
+  Status RegisterMemoryMetrics(MetricGroup* metrics) override {
+    DCHECK(initialized_);
+    MetricGroup* tcmalloc_metrics = metrics->GetOrCreateChildGroup("tcmalloc");
+
+    /// These metrics line up with the text description used for /memz:
+    /// MALLOC:     3388499040 ( 3231.5 MiB) Bytes in use by application (#1)
+    /// MALLOC: +            0 (    0.0 MiB) Bytes in page heap freelist (#2)
+    /// MALLOC: +     98309992 (   93.8 MiB) Bytes in central cache freelist (#3)
+    /// MALLOC: +      4746496 (    4.5 MiB) Bytes in transfer cache freelist (#4)
+    /// MALLOC: +    177297208 (  169.1 MiB) Bytes in thread cache freelists (#5)
+    /// MALLOC: +     14942208 (   14.2 MiB) Bytes in malloc metadata
+    /// MALLOC:   ------------
+    /// MALLOC: =   3683794944 ( 3513.1 MiB) Actual memory used (physical + swap) (#6)
+    /// MALLOC: +    149757952 (  142.8 MiB) Bytes released to OS (aka unmapped) (#7)
+    /// MALLOC:   ------------
+    /// MALLOC: =   3833552896 ( 3656.0 MiB) Virtual address space used (#8)
+
+    /// #1
+    TcmallocMetric::BYTES_IN_USE = TcmallocMetric::CreateAndRegister(
+        tcmalloc_metrics, "tcmalloc.bytes-in-use", "generic.current_allocated_bytes");
+
+    /// #2
+    TcmallocMetric::PAGEHEAP_FREE_BYTES = TcmallocMetric::CreateAndRegister(
+        tcmalloc_metrics, "tcmalloc.pageheap-free-bytes", "tcmalloc.pageheap_free_bytes");
+
+    /// #3
+    TcmallocMetric::CENTRAL_CACHE_FREE_BYTES = TcmallocMetric::CreateAndRegister(
+        tcmalloc_metrics, "tcmalloc.central-cache-free-bytes",
+        "tcmalloc.central_cache_free_bytes");
+
+    /// #4
+    TcmallocMetric::TRANSFER_CACHE_FREE_BYTES = TcmallocMetric::CreateAndRegister(
+        tcmalloc_metrics, "tcmalloc.transfer-cache-free-bytes",
+        "tcmalloc.transfer_cache_free_bytes");
+
+    /// #5
+    TcmallocMetric::CURRENT_TOTAL_THREAD_CACHE_BYTES = TcmallocMetric::CreateAndRegister(
+        tcmalloc_metrics, "tcmalloc.current-total-thread-cache-bytes",
+        "tcmalloc.current_total_thread_cache_bytes");
+
+    /// #6
+    TcmallocMetric::PHYSICAL_BYTES_RESERVED = TcmallocMetric::CreateAndRegister(
+        tcmalloc_metrics, "tcmalloc.physical-bytes-reserved",
+        "generic.total_physical_bytes");
+
+    /// #7
+    TcmallocMetric::PAGEHEAP_UNMAPPED_BYTES =
+        TcmallocMetric::CreateAndRegister(tcmalloc_metrics,
+            "tcmalloc.pageheap-unmapped-bytes", "tcmalloc.pageheap_unmapped_bytes");
+
+    /// #8
+    TcmallocMetric::TOTAL_BYTES_RESERVED =
+        tcmalloc_metrics->RegisterMetric(new TcmallocMetric::TotalBytesReservedMetric(
+            MetricDefs::Get("tcmalloc.total-bytes-reserved")));
+
+    /// Additional overhead metric (#6 - #1)
+    TcmallocMetric::OVERHEAD_BYTES =
+        tcmalloc_metrics->RegisterMetric(new TcmallocMetric::OverheadBytesMetric(
+            MetricDefs::Get("tcmalloc.overhead-bytes")));
+
+    return Status::OK();
+  }
+
+  IntGauge* GetUsedBytesMetric(bool include_overhead) const override {
+    DCHECK(initialized_);
+    if (include_overhead) {
+      DCHECK(TcmallocMetric::PHYSICAL_BYTES_RESERVED != nullptr);
+      return TcmallocMetric::PHYSICAL_BYTES_RESERVED;
+    } else {
+      DCHECK(TcmallocMetric::BYTES_IN_USE != nullptr);
+      return TcmallocMetric::BYTES_IN_USE;
+    }
+  }
+
+  IntGauge* GetOverheadBytesMetric() const override {
+    DCHECK(initialized_);
+    DCHECK(TcmallocMetric::OVERHEAD_BYTES != nullptr);
+    return TcmallocMetric::OVERHEAD_BYTES;
+  }
+
+  HugePageSupport GetHugePageSupport() const override {
+    DCHECK(initialized_);
+    // As an extra precaution, read the actual value from tcmalloc rather than simply
+    // reading the startup parameter.
+    size_t aggressive_decommit_enabled;
+    MallocExtension::instance()->GetNumericProperty(
+        "tcmalloc.aggressive_memory_decommit", &aggressive_decommit_enabled);
+    if (FLAGS_tcmalloc_aggressive_memory_decommit) {
+      DCHECK(aggressive_decommit_enabled);
+    } else {
+      DCHECK(!aggressive_decommit_enabled);
+    }
+    if (aggressive_decommit_enabled) {
+      // With aggressive decommit, large allocations are immediately freed,
+      // so it is compatible with using madvise to get huge pages.
+      return HugePageSupport::MADVISE_COMPATIBLE;
+    } else {
+      return HugePageSupport::MADVISE_INCOMPATIBLE;
+    }
+  }
+
+  bool SupportsHeapProfiling() const override { return true; }
+  void HeapProfilerStart(const std::string& tmp_prof_file_name) override {
+    ::HeapProfilerStart(tmp_prof_file_name.c_str());
+  }
+
+  void HeapProfilerStop() override {
+    ::HeapProfilerStop();
+  }
+
+  char* GetHeapProfile() override {
+    return ::GetHeapProfile();
+  }
+
+  bool SupportsHeapGrowthStacks() const override { return true; }
+  void GetHeapGrowthStacks(std::string* heap_growth_stacks) override {
+    MallocExtension::instance()->GetHeapGrowthStacks(heap_growth_stacks);
+  }
+
+  bool SupportsCPUProfiling() const override { return true; }
+  void CPUProfilerStart(const std::string& tmp_prof_file_name) override {
+    ::ProfilerStart(tmp_prof_file_name.c_str());
+  }
+  void CPUProfilerStop() override {
+    ::ProfilerStop();
+  }
+
+ private:
+  bool initialized_ = false;
+};
+
+} // namespace impala
diff --git a/be/src/util/malloc-util-libc.h b/be/src/util/malloc-util-libc.h
new file mode 100644
index 000000000..16127d437
--- /dev/null
+++ b/be/src/util/malloc-util-libc.h
@@ -0,0 +1,84 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <sstream>
+
+#include <glog/logging.h>
+
+#include "malloc-util.h"
+
+#include "util/metrics.h"
+#include "util/process-state-info.h"
+
+namespace impala {
+
+/// Libc doesn't have a simple API for tracking memory use from malloc, so we use
+/// resident set size.
+class LibcMallocMetric : public IntGauge {
+ public:
+  LibcMallocMetric(const TMetricDef& def) : IntGauge(def, 0) {}
+
+  static LibcMallocMetric* RESIDENT_SET_SIZE;
+
+  int64_t GetValue() override {
+    // There may be more efficient ways to get this from the OS
+    ProcessStateInfo proc_state(false);
+    return proc_state.GetRss();
+  }
+};
+
+LibcMallocMetric* LibcMallocMetric::RESIDENT_SET_SIZE = nullptr;
+
+class LibcMallocUtil : public MallocUtil {
+ public:
+  Status Init() override { return Status::OK(); }
+
+  std::string GetName() const override {
+    return "Libc";
+  }
+
+  std::string GetTextDescription() const override {
+    std::stringstream ss;
+    ss << GetName() << " doesn't support a text description.";
+    return ss.str();
+  }
+
+  Status RegisterMemoryMetrics(MetricGroup* metrics) override {
+    LibcMallocMetric::RESIDENT_SET_SIZE = metrics->RegisterMetric(
+        new LibcMallocMetric(MetricDefs::Get("libc-resident-set-size")));
+    return Status::OK();
+  }
+
+  IntGauge* GetUsedBytesMetric(bool include_overhead) const override {
+    DCHECK(LibcMallocMetric::RESIDENT_SET_SIZE != nullptr);
+    return LibcMallocMetric::RESIDENT_SET_SIZE;
+  }
+
+  IntGauge* GetOverheadBytesMetric() const override {
+    return nullptr;
+  }
+
+  HugePageSupport GetHugePageSupport() const override {
+    // libc doesn't retain large chunks of memory, so this is compatible with using
+    // madvise to get huge pages.
+    return HugePageSupport::MADVISE_COMPATIBLE;
+  }
+};
+
+} // namespace impala
diff --git a/be/src/util/malloc-util-sanitizers.h b/be/src/util/malloc-util-sanitizers.h
new file mode 100644
index 000000000..8e87e9810
--- /dev/null
+++ b/be/src/util/malloc-util-sanitizers.h
@@ -0,0 +1,88 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <sstream>
+
+#include <glog/logging.h>
+#include <sanitizer/allocator_interface.h>
+
+#include "malloc-util.h"
+
+#include "util/metrics.h"
+#include "util/pretty-printer.h"
+
+namespace impala {
+
+/// The sanitizers replace malloc() and track memory usage themselves.
+class SanitizerMallocMetric : public IntGauge {
+ public:
+  SanitizerMallocMetric(const TMetricDef& def) : IntGauge(def, 0) {}
+
+  static SanitizerMallocMetric* BYTES_ALLOCATED;
+
+  int64_t GetValue() override {
+    return __sanitizer_get_current_allocated_bytes();
+  }
+};
+
+SanitizerMallocMetric* SanitizerMallocMetric::BYTES_ALLOCATED = nullptr;
+
+class SanitizerMallocUtil : public MallocUtil {
+ public:
+  Status Init() override { return Status::OK(); }
+
+  std::string GetName() const override {
+#if defined(ADDRESS_SANITIZER)
+    return "AddressSanitizer";
+#else
+    return "ThreadSanitizer";
+#endif
+  }
+
+  std::string GetTextDescription() const override {
+    std::stringstream ss;
+    ss << GetName() << " Total Bytes Allocated: "
+       << PrettyPrinter::Print(
+              SanitizerMallocMetric::BYTES_ALLOCATED->GetValue(), TUnit::BYTES);
+    return ss.str();
+  }
+
+  Status RegisterMemoryMetrics(MetricGroup* metrics) override {
+    SanitizerMallocMetric::BYTES_ALLOCATED = metrics->RegisterMetric(
+        new SanitizerMallocMetric(MetricDefs::Get("sanitizer-total-bytes-allocated")));
+    return Status::OK();
+  }
+
+  IntGauge* GetUsedBytesMetric(bool include_overhead) const override {
+    DCHECK(SanitizerMallocMetric::BYTES_ALLOCATED != nullptr);
+    return SanitizerMallocMetric::BYTES_ALLOCATED;
+  }
+
+  IntGauge* GetOverheadBytesMetric() const override {
+    return nullptr;
+  }
+
+  HugePageSupport GetHugePageSupport() const override {
+    // Sanitizers don't retain large chunks of memory, so this is compatible
+    // with using madvise to get huge pages
+    return HugePageSupport::MADVISE_COMPATIBLE;
+  }
+};
+
+} // namespace impala
diff --git a/be/src/util/malloc-util.cc b/be/src/util/malloc-util.cc
new file mode 100644
index 000000000..feacb1cbe
--- /dev/null
+++ b/be/src/util/malloc-util.cc
@@ -0,0 +1,59 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <glog/logging.h>
+
+#include "util/malloc-util.h"
+
+#if defined(MALLOC_UTIL_SANITIZER_IMPL)
+#include "util/malloc-util-sanitizers.h"
+#elif defined(MALLOC_UTIL_GPERFTOOLS_IMPL)
+#include "util/malloc-util-gperftools.h"
+#elif defined(MALLOC_UTIL_LIBC_IMPL)
+#include "util/malloc-util-libc.h"
+#else
+#error "Unknown implementation"
+#endif
+
+namespace impala {
+
+MallocUtil* MallocUtil::GetInstance() {
+  // With C++11, static variables are initialized the first time a function is called,
+  // and initialization is thread safe. These all have trivial constructors.
+#if defined(MALLOC_UTIL_SANITIZER_IMPL)
+  static MallocUtil* malloc_util_instance = new SanitizerMallocUtil();
+#elif defined(MALLOC_UTIL_GPERFTOOLS_IMPL)
+  static MallocUtil* malloc_util_instance = new GperftoolsMallocUtil();
+#elif defined(MALLOC_UTIL_LIBC_IMPL)
+  static MallocUtil* malloc_util_instance = new LibcMallocUtil();
+#endif
+  return malloc_util_instance;
+}
+
+std::ostream& operator<<(std::ostream& out, const MallocUtil::HugePageSupport& h) {
+  switch (h) {
+  case MallocUtil::HugePageSupport::MADVISE_COMPATIBLE:
+    out << "MADVISE_COMPATIBLE";
+    break;
+  case MallocUtil::HugePageSupport::MADVISE_INCOMPATIBLE:
+    out << "MADVISE_INCOMPATIBLE";
+    break;
+  }
+  return out;
+}
+
+} // namespace impala
diff --git a/be/src/util/malloc-util.h b/be/src/util/malloc-util.h
new file mode 100644
index 000000000..889e0c70e
--- /dev/null
+++ b/be/src/util/malloc-util.h
@@ -0,0 +1,120 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <string>
+
+#include <glog/logging.h>
+
+#include "common/status.h"
+#include "util/metrics-fwd.h"
+
+namespace impala {
+  class MallocUtil {
+  public:
+    // Gets the MallocUtil singleton. Always a non-nullptr.
+    static MallocUtil* GetInstance();
+
+    // Initialize the settings for the malloc implementation. This should be called once
+    // at startup. Since backend tests may call this repeatedly, any additional calls
+    // are ignored.
+    virtual Status Init() = 0;
+
+    // Gets a human readable description of malloc state / statistics
+    virtual std::string GetTextDescription() const = 0;
+
+    // The name of the malloc implementation
+    virtual std::string GetName() const = 0;
+
+    // Add malloc implementation specific metrics in the provided metrics group.
+    virtual Status RegisterMemoryMetrics(MetricGroup* metrics) = 0;
+
+    // Get the metric for used memory. This can only be called after initialization.
+    // This must be non-null for all malloc implementations.
+    virtual IntGauge* GetUsedBytesMetric(bool include_overhead) const = 0;
+
+    // Get the metric for the malloc implementation's overhead. This can only be
+    // called after initialization. This will return null if the malloc
+    // implementation does not have an overhead metric.
+    virtual IntGauge* GetOverheadBytesMetric() const = 0;
+
+    // Get information about whether the malloc's implementation is compatible with
+    // madvising huge pages. If a malloc implementation holds on to memory and can
+    // break it up into smaller pieces, then it must return MADVISE_INCOMPATIBLE.
+    // An implementation that will either immediately free a huge page should return
+    // MADVISE_COMPATIBLE. This can only be called after initialization.
+    enum class HugePageSupport {
+      MADVISE_COMPATIBLE,
+      MADVISE_INCOMPATIBLE
+    };
+    virtual HugePageSupport GetHugePageSupport() const = 0;
+    friend std::ostream& operator<<(std::ostream& os, const HugePageSupport& h);
+
+    // Profiler interfaces
+    // This will need to evolve as other malloc implementations are added. Right now,
+    // this only needs to handle Gperftools TCMalloc and the sanitizers. To simplify
+    // implementations that don't support profiling, these default to returning false
+    // and DCHECKing if any are used.
+    //
+    // Currently, all the different types of profiling return a binary structure for
+    // use with the pprof utility. These are the current types of profiling:
+    // Heap profiling: profile all allocations for a period of time. The call pattern
+    //    is HeapProfilerStart(), wait for some samples to be collected,
+    //    GetHeapProfile(), then HeapProfilerStop().
+    // Heap growth stacks: return stacks for allocations that caused the address space
+    //    to grow.
+    // CPU profiling: send a signal and sample stacks. The call pattern is
+    //    CPUProfileStart(), wait for some samples to be collected, then
+    //    CPUProfileStop(). This writes the profile to a temporary file that can be
+    //    read back.
+
+    virtual bool SupportsHeapProfiling() const { return false; }
+    virtual void HeapProfilerStart(const std::string& tmp_prof_file_name) {
+      DCHECK(false);
+    }
+    virtual void HeapProfilerStop() { DCHECK(false); }
+    // Returns a binary heap profile. The returned pointer is a '\0'-terminated
+    // string allocated using malloc() and should be free()-ed as soon as the
+    // caller does not need it anymore.
+    virtual char* GetHeapProfile() {
+      DCHECK(false);
+      return nullptr;
+    }
+
+    virtual bool SupportsHeapGrowthStacks() const {
+      return false;
+    }
+    virtual void GetHeapGrowthStacks(std::string* heap_growth_stacks) {
+      DCHECK(false);
+    }
+
+    virtual bool SupportsCPUProfiling() const { return false; }
+    virtual void CPUProfilerStart(const std::string& tmp_prof_file_name) {
+      DCHECK(false);
+    }
+    virtual void CPUProfilerStop() {
+      DCHECK(false);
+    }
+  protected:
+    // Restrict access to the constructor / destructor to protect the singleton.
+    MallocUtil() = default;
+    ~MallocUtil() = default;
+  private:
+    DISALLOW_COPY_AND_ASSIGN(MallocUtil);
+  };
+} // namespace impala
diff --git a/be/src/util/memory-metrics.cc b/be/src/util/memory-metrics.cc
index 4b665c9a5..372919131 100644
--- a/be/src/util/memory-metrics.cc
+++ b/be/src/util/memory-metrics.cc
@@ -24,6 +24,7 @@
 #include "runtime/bufferpool/reservation-tracker.h"
 #include "runtime/mem-tracker.h"
 #include "util/jni-util.h"
+#include "util/malloc-util.h"
 #include "util/mem-info.h"
 #include "util/process-state-info.h"
 #include "util/time.h"
@@ -52,17 +53,6 @@ StringProperty* AggregateMemoryMetrics::THP_ENABLED = nullptr;
 StringProperty* AggregateMemoryMetrics::THP_DEFRAG = nullptr;
 StringProperty* AggregateMemoryMetrics::THP_KHUGEPAGED_DEFRAG = nullptr;
 
-TcmallocMetric* TcmallocMetric::BYTES_IN_USE = nullptr;
-TcmallocMetric* TcmallocMetric::PAGEHEAP_FREE_BYTES = nullptr;
-TcmallocMetric* TcmallocMetric::CENTRAL_CACHE_FREE_BYTES = nullptr;
-TcmallocMetric* TcmallocMetric::TRANSFER_CACHE_FREE_BYTES = nullptr;
-TcmallocMetric* TcmallocMetric::CURRENT_TOTAL_THREAD_CACHE_BYTES = nullptr;
-TcmallocMetric* TcmallocMetric::PHYSICAL_BYTES_RESERVED = nullptr;
-TcmallocMetric* TcmallocMetric::PAGEHEAP_UNMAPPED_BYTES = nullptr;
-TcmallocMetric::TotalBytesReservedMetric* TcmallocMetric::TOTAL_BYTES_RESERVED = nullptr;
-
-SanitizerMallocMetric* SanitizerMallocMetric::BYTES_ALLOCATED = nullptr;
-
 bool JvmMemoryMetric::initialized_ = false;
 JvmMemoryMetric* JvmMemoryMetric::HEAP_MAX_USAGE = nullptr;
 JvmMemoryMetric* JvmMemoryMetric::NON_HEAP_COMMITTED = nullptr;
@@ -84,11 +74,6 @@ BufferPoolMetric* BufferPoolMetric::CLEAN_PAGES_LIMIT = nullptr;
 BufferPoolMetric* BufferPoolMetric::NUM_CLEAN_PAGES = nullptr;
 BufferPoolMetric* BufferPoolMetric::CLEAN_PAGE_BYTES = nullptr;
 
-TcmallocMetric* TcmallocMetric::CreateAndRegister(
-    MetricGroup* metrics, const string& key, const string& tcmalloc_var) {
-  return metrics->RegisterMetric(new TcmallocMetric(MetricDefs::Get(key), tcmalloc_var));
-}
-
 Status impala::RegisterMemoryMetrics(MetricGroup* metrics, bool register_jvm_metrics,
     ReservationTracker* global_reservations, BufferPool* buffer_pool) {
   if (global_reservations != nullptr) {
@@ -112,68 +97,9 @@ Status impala::RegisterMemoryMetrics(MetricGroup* metrics, bool register_jvm_met
       used_metrics.push_back(JvmMemoryMetric::NON_HEAP_COMMITTED);
     }
   }
-#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER)
-  SanitizerMallocMetric::BYTES_ALLOCATED = metrics->RegisterMetric(
-      new SanitizerMallocMetric(MetricDefs::Get("sanitizer-total-bytes-allocated")));
-  used_metrics.push_back(SanitizerMallocMetric::BYTES_ALLOCATED);
-#else
-  MetricGroup* tcmalloc_metrics = metrics->GetOrCreateChildGroup("tcmalloc");
-  // We rely on TCMalloc for our global memory metrics, so skip setting them up
-  // if we're not using TCMalloc.
-
-  /// These metrics line up with the text description used for /memz:
-  /// MALLOC:     3388499040 ( 3231.5 MiB) Bytes in use by application (#1)
-  /// MALLOC: +            0 (    0.0 MiB) Bytes in page heap freelist (#2)
-  /// MALLOC: +     98309992 (   93.8 MiB) Bytes in central cache freelist (#3)
-  /// MALLOC: +      4746496 (    4.5 MiB) Bytes in transfer cache freelist (#4)
-  /// MALLOC: +    177297208 (  169.1 MiB) Bytes in thread cache freelists (#5)
-  /// MALLOC: +     14942208 (   14.2 MiB) Bytes in malloc metadata
-  /// MALLOC:   ------------
-  /// MALLOC: =   3683794944 ( 3513.1 MiB) Actual memory used (physical + swap) (#6)
-  /// MALLOC: +    149757952 (  142.8 MiB) Bytes released to OS (aka unmapped) (#7)
-  /// MALLOC:   ------------
-  /// MALLOC: =   3833552896 ( 3656.0 MiB) Virtual address space used (#8)
-
-  /// #1
-  TcmallocMetric::BYTES_IN_USE = TcmallocMetric::CreateAndRegister(
-      tcmalloc_metrics, "tcmalloc.bytes-in-use", "generic.current_allocated_bytes");
-
-  /// #2
-  TcmallocMetric::PAGEHEAP_FREE_BYTES = TcmallocMetric::CreateAndRegister(
-      tcmalloc_metrics, "tcmalloc.pageheap-free-bytes", "tcmalloc.pageheap_free_bytes");
-
-  /// #3
-  TcmallocMetric::CENTRAL_CACHE_FREE_BYTES = TcmallocMetric::CreateAndRegister(
-      tcmalloc_metrics, "tcmalloc.central-cache-free-bytes",
-      "tcmalloc.central_cache_free_bytes");
-
-  /// #4
-  TcmallocMetric::TRANSFER_CACHE_FREE_BYTES = TcmallocMetric::CreateAndRegister(
-      tcmalloc_metrics, "tcmalloc.transfer-cache-free-bytes",
-      "tcmalloc.transfer_cache_free_bytes");
-
-  /// #5
-  TcmallocMetric::CURRENT_TOTAL_THREAD_CACHE_BYTES = TcmallocMetric::CreateAndRegister(
-      tcmalloc_metrics, "tcmalloc.current-total-thread-cache-bytes",
-      "tcmalloc.current_total_thread_cache_bytes");
-
-  /// #6
-  TcmallocMetric::PHYSICAL_BYTES_RESERVED = TcmallocMetric::CreateAndRegister(
-      tcmalloc_metrics, "tcmalloc.physical-bytes-reserved",
-      "generic.total_physical_bytes");
-
-  /// #7
-  TcmallocMetric::PAGEHEAP_UNMAPPED_BYTES =
-      TcmallocMetric::CreateAndRegister(tcmalloc_metrics,
-          "tcmalloc.pageheap-unmapped-bytes", "tcmalloc.pageheap_unmapped_bytes");
-
-  /// #8
-  TcmallocMetric::TOTAL_BYTES_RESERVED =
-      tcmalloc_metrics->RegisterMetric(new TcmallocMetric::TotalBytesReservedMetric(
-          MetricDefs::Get("tcmalloc.total-bytes-reserved")));
-
-  used_metrics.push_back(TcmallocMetric::PHYSICAL_BYTES_RESERVED);
-#endif
+  RETURN_IF_ERROR(MallocUtil::GetInstance()->RegisterMemoryMetrics(metrics));
+  used_metrics.push_back(MallocUtil::GetInstance()->GetUsedBytesMetric(
+      /*include_overhead*/ true));
   MetricGroup* aggregate_metrics = metrics->GetOrCreateChildGroup("memory");
   AggregateMemoryMetrics::TOTAL_USED = aggregate_metrics->RegisterMetric(
       new SumGauge(MetricDefs::Get("memory.total-used"), used_metrics));
diff --git a/be/src/util/memory-metrics.h b/be/src/util/memory-metrics.h
index 4c9c3d0fc..b5fd7602c 100644
--- a/be/src/util/memory-metrics.h
+++ b/be/src/util/memory-metrics.h
@@ -20,10 +20,6 @@
 #include "util/metrics.h"
 
 #include <boost/thread/shared_mutex.hpp>
-#include <gperftools/malloc_extension.h>
-#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER)
-#include <sanitizer/allocator_interface.h>
-#endif
 
 #include "gen-cpp/Frontend_types.h"
 
@@ -88,97 +84,6 @@ class AggregateMemoryMetrics {
   static void Refresh();
 };
 
-/// Specialised metric which exposes numeric properties from tcmalloc.
-/// These metrics line up with the text description used for /memz:
-/// MALLOC:     3388499040 ( 3231.5 MiB) Bytes in use by application (#1)
-/// MALLOC: +            0 (    0.0 MiB) Bytes in page heap freelist (#2)
-/// MALLOC: +     98309992 (   93.8 MiB) Bytes in central cache freelist (#3)
-/// MALLOC: +      4746496 (    4.5 MiB) Bytes in transfer cache freelist (#4)
-/// MALLOC: +    177297208 (  169.1 MiB) Bytes in thread cache freelists (#5)
-/// MALLOC: +     14942208 (   14.2 MiB) Bytes in malloc metadata
-/// MALLOC:   ------------
-/// MALLOC: =   3683794944 ( 3513.1 MiB) Actual memory used (physical + swap) (#6)
-/// MALLOC: +    149757952 (  142.8 MiB) Bytes released to OS (aka unmapped) (#7)
-/// MALLOC:   ------------
-/// MALLOC: =   3833552896 ( 3656.0 MiB) Virtual address space used (#8)
-class TcmallocMetric : public IntGauge {
- public:
-  /// #1: Number of bytes allocated by tcmalloc, currently used by the application.
-  static TcmallocMetric* BYTES_IN_USE;
-
-  /// #2: Number of bytes reserved and still mapped by tcmalloc that are not allocated
-  /// to the application.
-  static TcmallocMetric* PAGEHEAP_FREE_BYTES;
-
-  /// #3: Number of free bytes in the central cache assigned to size classes.
-  static TcmallocMetric* CENTRAL_CACHE_FREE_BYTES;
-
-  /// #4: Number of free bytes waiting to be transferred between central and thread
-  /// caches.
-  static TcmallocMetric* TRANSFER_CACHE_FREE_BYTES;
-
-  /// #5: Number of bytes used across all thread caches.
-  static TcmallocMetric* CURRENT_TOTAL_THREAD_CACHE_BYTES;
-
-  /// #6: Number of physical bytes in use (including any tcmalloc caches and metadata)
-  static TcmallocMetric* PHYSICAL_BYTES_RESERVED;
-
-  /// #7: Number of bytes once reserved by tcmalloc, but released back to the operating
-  /// system so that their use incurs a pagefault. Contributes to the total amount of
-  /// virtual address space used, but not to the physical memory usage.
-  static TcmallocMetric* PAGEHEAP_UNMAPPED_BYTES;
-
-  /// #8: Derived metric computing the amount of virtual memory (in bytes) used by the
-  /// process, including all different types of tcmalloc memory including unmapped
-  /// virtual memory.
-  class TotalBytesReservedMetric : public IntGauge {
-  public:
-    TotalBytesReservedMetric(const TMetricDef& def) : IntGauge(def, 0) { }
-
-    virtual int64_t GetValue() override {
-      return PHYSICAL_BYTES_RESERVED->GetValue() + PAGEHEAP_UNMAPPED_BYTES->GetValue();
-    }
-  };
-  static TotalBytesReservedMetric* TOTAL_BYTES_RESERVED;
-
-  static TcmallocMetric* CreateAndRegister(MetricGroup* metrics, const std::string& key,
-      const std::string& tcmalloc_var);
-
-  virtual int64_t GetValue() override {
-    int64_t retval = 0;
-#if !defined(ADDRESS_SANITIZER) && !defined(THREAD_SANITIZER)
-    MallocExtension::instance()->GetNumericProperty(tcmalloc_var_.c_str(),
-        reinterpret_cast<size_t*>(&retval));
-#endif
-    return retval;
-  }
-
- private:
-  /// Name of the tcmalloc property this metric should fetch.
-  const std::string tcmalloc_var_;
-
-  TcmallocMetric(const TMetricDef& def, const std::string& tcmalloc_var)
-    : IntGauge(def, 0), tcmalloc_var_(tcmalloc_var) { }
-};
-
-/// Alternative to TCMallocMetric if we're running under a sanitizer that replaces
-/// malloc(), e.g. address or thread sanitizer.
-class SanitizerMallocMetric : public IntGauge {
- public:
-  SanitizerMallocMetric(const TMetricDef& def) : IntGauge(def, 0) {}
-
-  static SanitizerMallocMetric* BYTES_ALLOCATED;
-
-  virtual int64_t GetValue() override {
-#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER)
-    return __sanitizer_get_current_allocated_bytes();
-#else
-    return 0;
-#endif
-
-  }
-};
-
 // A singleton for caching the gathering of JVM Metrics for 1 second, to amortize
 // the cost of getting all the JVM metrics.
 //
@@ -233,7 +138,7 @@ class JvmMemoryMetric : public IntGauge {
 
   /// Searches through jvm_metrics_response_ for a matching memory pool and pulls out the
   /// right value from that structure according to metric_type_.
-  virtual int64_t GetValue() override;
+  int64_t GetValue() override;
 
   // Expose the total memory metrics that may be counted against process memory limit.
   // Initialised by InitMetrics().
@@ -267,7 +172,7 @@ class JvmMemoryMetric : public IntGauge {
 // data via JniUtil::GetJvmMemoryMetrics() via JvmMetricCache.
 class JvmMemoryCounterMetric : public IntCounter {
   public:
-    virtual int64_t GetValue() override;
+    int64_t GetValue() override;
 
     static JvmMemoryCounterMetric* GC_COUNT;
     static JvmMemoryCounterMetric* GC_TIME_MILLIS;
@@ -304,7 +209,7 @@ class BufferPoolMetric : public IntGauge {
   static BufferPoolMetric* NUM_CLEAN_PAGES;
   static BufferPoolMetric* CLEAN_PAGE_BYTES;
 
-  virtual int64_t GetValue() override;
+  int64_t GetValue() override;
 
  private:
   friend class ReservationTrackerTest;
@@ -344,7 +249,7 @@ class MemTrackerMetric : public IntGauge {
   static void CreateMetrics(MetricGroup* metrics, MemTracker* mem_tracker,
       const std::string& name);
 
-  virtual int64_t GetValue() override;
+  int64_t GetValue() override;
 
  private:
   enum class MemTrackerMetricType {
@@ -359,7 +264,7 @@ class MemTrackerMetric : public IntGauge {
   const MemTracker* mem_tracker_;
 };
 
-/// Registers common tcmalloc memory metrics. If 'register_jvm_metrics' is true, the JVM
+/// Registers common malloc memory metrics. If 'register_jvm_metrics' is true, the JVM
 /// memory metrics are also registered. If 'global_reservations' and 'buffer_pool' are
 /// not NULL, also register buffer pool metrics.
 Status RegisterMemoryMetrics(MetricGroup* metrics, bool register_jvm_metrics,
diff --git a/be/src/util/memusage-path-handlers.cc b/be/src/util/memusage-path-handlers.cc
index d8bc889bf..101fb99a6 100644
--- a/be/src/util/memusage-path-handlers.cc
+++ b/be/src/util/memusage-path-handlers.cc
@@ -17,10 +17,9 @@
 
 #include "util/memusage-path-handlers.h"
 
-#include <gperftools/malloc_extension.h>
-
 #include "runtime/mem-tracker.h"
 #include "util/common-metrics.h"
+#include "util/malloc-util.h"
 #include "util/mem-info.h"
 #include "util/metrics.h"
 #include "util/pretty-printer.h"
@@ -56,16 +55,8 @@ void AddMemTracker(MemTracker* mem_tracker,
 /// - Freelist of central cache, each class.
 /// - Page heap freelist.
 void AddTCmallocOverview(const Webserver::WebRequest& req, Document* document) {
-  stringstream ss;
-#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER)
-  ss << "Memory tracking is not available with address or thread sanitizer builds.";
-#else
-  char buf[2048];
-  MallocExtension::instance()->GetStats(buf, 2048);
-  ss << string(buf);
-#endif
-
-  Value overview(ss.str().c_str(), document->GetAllocator());
+  string s = MallocUtil::GetInstance()->GetTextDescription();
+  Value overview(s, document->GetAllocator());
   document->AddMember("overview", overview, document->GetAllocator());
 }
 
diff --git a/be/src/util/metrics-test.cc b/be/src/util/metrics-test.cc
index 4c8eb6938..958e9cbfa 100644
--- a/be/src/util/metrics-test.cc
+++ b/be/src/util/metrics-test.cc
@@ -22,6 +22,7 @@
 
 #include "testutil/gtest-util.h"
 #include "util/collection-metrics.h"
+#include "util/malloc-util.h"
 #include "util/memory-metrics.h"
 #include "util/metrics.h"
 #include "util/histogram-metric.h"
@@ -31,6 +32,8 @@
 
 using namespace rapidjson;
 
+DECLARE_bool(tcmalloc_aggressive_memory_decommit);
+
 namespace impala {
 
 template <typename M, typename T>
@@ -246,6 +249,9 @@ TEST_F(MetricsTest, StatsMetricsSingle) {
 
 TEST_F(MetricsTest, MemMetric) {
 #if !defined(ADDRESS_SANITIZER) && !defined(THREAD_SANITIZER)
+  // Always use aggressive decommit for backend tests
+  FLAGS_tcmalloc_aggressive_memory_decommit = true;
+  ASSERT_OK(MallocUtil::GetInstance()->Init());
   MetricGroup metrics("MemMetrics");
   ASSERT_OK(RegisterMemoryMetrics(&metrics, false, nullptr, nullptr));
   // Smoke test to confirm that tcmalloc metrics are returning reasonable values.
diff --git a/be/src/util/pprof-path-handlers.cc b/be/src/util/pprof-path-handlers.cc
index be37edc1a..bb3b5eb76 100644
--- a/be/src/util/pprof-path-handlers.cc
+++ b/be/src/util/pprof-path-handlers.cc
@@ -20,11 +20,9 @@
 #include <boost/bind.hpp>
 #include <fstream>
 #include <sys/stat.h>
-#include <gperftools/profiler.h>
-#include <gperftools/heap-profiler.h>
-#include <gperftools/malloc_extension.h>
 
 #include "common/logging.h"
+#include "util/malloc-util.h"
 #include "util/webserver.h"
 
 #include "common/names.h"
@@ -54,10 +52,13 @@ void PprofCmdLineHandler(const Webserver::WebRequest& req, stringstream* output)
 // by calling HeapProfileStart(filename), continue to do work, and then, some number of
 // seconds later, call GetHeapProfile() followed by HeapProfilerStop().
 void PprofHeapHandler(const Webserver::WebRequest& req, stringstream* output) {
-#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER)
-  (void)PPROF_DEFAULT_SAMPLE_SECS; // Avoid unused variable warning.
-  (*output) << "Heap profiling is not available with address/thread sanitizer builds.";
-#else
+  MallocUtil* mallocUtil = MallocUtil::GetInstance();
+  if (!mallocUtil->SupportsHeapProfiling()) {
+    (*output) << "Heap profiling is not available with "
+              << mallocUtil->GetName()
+              << " builds.";
+    return;
+  }
   const auto& args = req.parsed_args;
   Webserver::ArgumentMap::const_iterator it = args.find("seconds");
   int seconds = PPROF_DEFAULT_SAMPLE_SECS;
@@ -65,23 +66,27 @@ void PprofHeapHandler(const Webserver::WebRequest& req, stringstream* output) {
     seconds = atoi(it->second.c_str());
   }
 
-  HeapProfilerStart(FLAGS_heap_profile_dir.c_str());
+  mallocUtil->HeapProfilerStart(FLAGS_heap_profile_dir);
   // Sleep to allow for some samples to be collected.
   sleep(seconds);
-  const char* profile = GetHeapProfile();
-  HeapProfilerStop();
+  const char* profile = mallocUtil->GetHeapProfile();
+  mallocUtil->HeapProfilerStop();
   (*output) << profile;
-  delete profile;
-#endif
+  // free() doesn't understand const, so cast the const away
+  free(const_cast<char*>(profile));
 }
 
 // pprof asks for the url /pprof/profile?seconds=XX to get cpu-profiling information.
 // The server should respond by calling ProfilerStart(), continuing to do its work,
 // and then, XX seconds later, calling ProfilerStop().
 void PprofCpuProfileHandler(const Webserver::WebRequest& req, stringstream* output) {
-#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER)
-  (*output) << "CPU profiling is not available with address/thread sanitizer builds.";
-#else
+  MallocUtil* mallocUtil = MallocUtil::GetInstance();
+  if (!mallocUtil->SupportsCPUProfiling()) {
+    (*output) << "CPU profiling is not available with "
+              << mallocUtil->GetName()
+              << " builds.";
+    return;
+  }
   const auto& args = req.parsed_args;
   Webserver::ArgumentMap::const_iterator it = args.find("seconds");
   int seconds = PPROF_DEFAULT_SAMPLE_SECS;
@@ -91,9 +96,9 @@ void PprofCpuProfileHandler(const Webserver::WebRequest& req, stringstream* outp
   ostringstream tmp_prof_file_name;
   // Build a temporary file name that is hopefully unique.
   tmp_prof_file_name << "/tmp/impala_cpu_profile." << getpid() << "." << rand();
-  ProfilerStart(tmp_prof_file_name.str().c_str());
+  mallocUtil->CPUProfilerStart(tmp_prof_file_name.str());
   sleep(seconds);
-  ProfilerStop();
+  mallocUtil->CPUProfilerStop();
   ifstream prof_file(tmp_prof_file_name.str().c_str(), ios::in);
   if (!prof_file.is_open()) {
     (*output) << "Unable to open cpu profile: " << tmp_prof_file_name.str();
@@ -101,20 +106,22 @@ void PprofCpuProfileHandler(const Webserver::WebRequest& req, stringstream* outp
   }
   (*output) << prof_file.rdbuf();
   prof_file.close();
-#endif
 }
 
 // pprof asks for the url /pprof/growth to get heap-profiling delta (growth) information.
 // The server should respond by calling:
 // MallocExtension::instance()->GetHeapGrowthStacks(&output);
 void PprofGrowthHandler(const Webserver::WebRequest& req, stringstream* output) {
-#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER)
-  (*output) << "Growth profiling is not available with address/thread sanitizer builds.";
-#else
+  MallocUtil* mallocUtil = MallocUtil::GetInstance();
+  if (!mallocUtil->SupportsHeapGrowthStacks()) {
+    (*output) << "Growth profiling is not available with "
+              << mallocUtil->GetName()
+              << " builds.";
+    return;
+  }
   string heap_growth_stack;
-  MallocExtension::instance()->GetHeapGrowthStacks(&heap_growth_stack);
+  mallocUtil->GetHeapGrowthStacks(&heap_growth_stack);
   (*output) << heap_growth_stack;
-#endif
 }
 
 // pprof asks for the url /pprof/symbol to map from hex addresses to variable names.
diff --git a/common/thrift/metrics.json b/common/thrift/metrics.json
index d72768e38..4c88ee697 100644
--- a/common/thrift/metrics.json
+++ b/common/thrift/metrics.json
@@ -2644,6 +2644,18 @@
     "kind": "GAUGE",
     "key": "tcmalloc.transfer-cache-free-bytes"
   },
+  {
+    "description": "Number of bytes overhead for TCMalloc. This is memory allocated by TCMalloc that is not used by the application. It can include thread caches, free memory in the page heap, etc.",
+    "contexts": [
+      "STATESTORE",
+      "CATALOGSERVER",
+      "IMPALAD"
+    ],
+    "label": "TCMalloc Overhead Bytes",
+    "units": "BYTES",
+    "kind": "GAUGE",
+    "key": "tcmalloc.overhead-bytes"
+  },
   {
     "description": "Bytes allocated from the sanitizer malloc (Sanitizer debug builds only)",
     "contexts": [
@@ -2656,6 +2668,18 @@
     "kind": "GAUGE",
     "key": "sanitizer-total-bytes-allocated"
   },
+  {
+    "description": "Resident set size using the libc malloc (Default malloc debug builds only)",
+    "contexts": [
+      "STATESTORE",
+      "CATALOGSERVER",
+      "IMPALAD"
+    ],
+    "label": "Libc Resident Set Size",
+    "units": "BYTES",
+    "kind": "GAUGE",
+    "key": "libc-resident-set-size"
+  },
   {
     "description": "Maximum allowed bytes allocated by the buffer pool.",
     "contexts": [