blob: 179877e753af747276473e0a2199d7cb80a731d0 [file] [log] [blame]
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef V8_HEAP_MEMORY_ALLOCATOR_H_
#define V8_HEAP_MEMORY_ALLOCATOR_H_
#include <atomic>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "include/v8-platform.h"
#include "src/base/bounded-page-allocator.h"
#include "src/base/export-template.h"
#include "src/base/macros.h"
#include "src/base/platform/mutex.h"
#include "src/base/platform/semaphore.h"
#include "src/heap/heap.h"
#include "src/heap/memory-chunk.h"
#include "src/heap/spaces.h"
#include "src/tasks/cancelable-task.h"
#include "src/utils/allocation.h"
namespace v8 {
namespace internal {
class Heap;
class Isolate;
class ReadOnlyPage;
// The process-wide singleton that keeps track of code range regions with the
// intention to reuse free code range regions as a workaround for CFG memory
// leaks (see crbug.com/870054).
class CodeRangeAddressHint {
public:
// Returns the most recently freed code range start address for the given
// size. If there is no such entry, then a random address is returned.
V8_EXPORT_PRIVATE Address GetAddressHint(size_t code_range_size);
V8_EXPORT_PRIVATE void NotifyFreedCodeRange(Address code_range_start,
size_t code_range_size);
private:
base::Mutex mutex_;
// A map from code range size to an array of recently freed code range
// addresses. There should be O(1) different code range sizes.
// The length of each array is limited by the peak number of code ranges,
// which should be also O(1).
std::unordered_map<size_t, std::vector<Address>> recently_freed_;
};
// ----------------------------------------------------------------------------
// A space acquires chunks of memory from the operating system. The memory
// allocator allocates and deallocates pages for the paged heap spaces and large
// pages for large object space.
class MemoryAllocator {
public:
// Unmapper takes care of concurrently unmapping and uncommitting memory
// chunks.
class Unmapper {
public:
class UnmapFreeMemoryJob;
Unmapper(Heap* heap, MemoryAllocator* allocator)
: heap_(heap), allocator_(allocator) {
chunks_[kRegular].reserve(kReservedQueueingSlots);
chunks_[kPooled].reserve(kReservedQueueingSlots);
}
void AddMemoryChunkSafe(MemoryChunk* chunk) {
if (!chunk->IsLargePage() && chunk->executable() != EXECUTABLE) {
AddMemoryChunkSafe<kRegular>(chunk);
} else {
AddMemoryChunkSafe<kNonRegular>(chunk);
}
}
MemoryChunk* TryGetPooledMemoryChunkSafe() {
// Procedure:
// (1) Try to get a chunk that was declared as pooled and already has
// been uncommitted.
// (2) Try to steal any memory chunk of kPageSize that would've been
// unmapped.
MemoryChunk* chunk = GetMemoryChunkSafe<kPooled>();
if (chunk == nullptr) {
chunk = GetMemoryChunkSafe<kRegular>();
if (chunk != nullptr) {
// For stolen chunks we need to manually free any allocated memory.
chunk->ReleaseAllAllocatedMemory();
}
}
return chunk;
}
V8_EXPORT_PRIVATE void FreeQueuedChunks();
void CancelAndWaitForPendingTasks();
void PrepareForGC();
V8_EXPORT_PRIVATE void EnsureUnmappingCompleted();
V8_EXPORT_PRIVATE void TearDown();
size_t NumberOfCommittedChunks();
V8_EXPORT_PRIVATE int NumberOfChunks();
size_t CommittedBufferedMemory();
private:
static const int kReservedQueueingSlots = 64;
static const int kMaxUnmapperTasks = 4;
enum ChunkQueueType {
kRegular, // Pages of kPageSize that do not live in a CodeRange and
// can thus be used for stealing.
kNonRegular, // Large chunks and executable chunks.
kPooled, // Pooled chunks, already uncommited and ready for reuse.
kNumberOfChunkQueues,
};
enum class FreeMode {
kUncommitPooled,
kReleasePooled,
};
template <ChunkQueueType type>
void AddMemoryChunkSafe(MemoryChunk* chunk) {
base::MutexGuard guard(&mutex_);
chunks_[type].push_back(chunk);
}
template <ChunkQueueType type>
MemoryChunk* GetMemoryChunkSafe() {
base::MutexGuard guard(&mutex_);
if (chunks_[type].empty()) return nullptr;
MemoryChunk* chunk = chunks_[type].back();
chunks_[type].pop_back();
return chunk;
}
bool MakeRoomForNewTasks();
template <FreeMode mode>
void PerformFreeMemoryOnQueuedChunks(JobDelegate* delegate = nullptr);
void PerformFreeMemoryOnQueuedNonRegularChunks(
JobDelegate* delegate = nullptr);
Heap* const heap_;
MemoryAllocator* const allocator_;
base::Mutex mutex_;
std::vector<MemoryChunk*> chunks_[kNumberOfChunkQueues];
std::unique_ptr<v8::JobHandle> job_handle_;
friend class MemoryAllocator;
};
enum AllocationMode {
kRegular,
kPooled,
};
enum FreeMode {
kFull,
kAlreadyPooled,
kPreFreeAndQueue,
kPooledAndQueue,
};
V8_EXPORT_PRIVATE static intptr_t GetCommitPageSize();
// Computes the memory area of discardable memory within a given memory area
// [addr, addr+size) and returns the result as base::AddressRegion. If the
// memory is not discardable base::AddressRegion is an empty region.
V8_EXPORT_PRIVATE static base::AddressRegion ComputeDiscardMemoryArea(
Address addr, size_t size);
V8_EXPORT_PRIVATE MemoryAllocator(Isolate* isolate, size_t max_capacity,
size_t code_range_size);
V8_EXPORT_PRIVATE void TearDown();
// Allocates a Page from the allocator. AllocationMode is used to indicate
// whether pooled allocation, which only works for MemoryChunk::kPageSize,
// should be tried first.
template <MemoryAllocator::AllocationMode alloc_mode = kRegular,
typename SpaceType>
EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE)
Page* AllocatePage(size_t size, SpaceType* owner, Executability executable);
LargePage* AllocateLargePage(size_t size, LargeObjectSpace* owner,
Executability executable);
ReadOnlyPage* AllocateReadOnlyPage(size_t size, ReadOnlySpace* owner);
std::unique_ptr<::v8::PageAllocator::SharedMemoryMapping> RemapSharedPage(
::v8::PageAllocator::SharedMemory* shared_memory, Address new_address);
template <MemoryAllocator::FreeMode mode = kFull>
EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE)
void Free(MemoryChunk* chunk);
void FreeReadOnlyPage(ReadOnlyPage* chunk);
// Returns allocated spaces in bytes.
size_t Size() const { return size_; }
// Returns allocated executable spaces in bytes.
size_t SizeExecutable() const { return size_executable_; }
// Returns the maximum available bytes of heaps.
size_t Available() const {
const size_t size = Size();
return capacity_ < size ? 0 : capacity_ - size;
}
// Returns an indication of whether a pointer is in a space that has
// been allocated by this MemoryAllocator.
V8_INLINE bool IsOutsideAllocatedSpace(Address address) const {
return address < lowest_ever_allocated_ ||
address >= highest_ever_allocated_;
}
// Returns a BasicMemoryChunk in which the memory region from commit_area_size
// to reserve_area_size of the chunk area is reserved but not committed, it
// could be committed later by calling MemoryChunk::CommitArea.
V8_EXPORT_PRIVATE BasicMemoryChunk* AllocateBasicChunk(
size_t reserve_area_size, size_t commit_area_size,
Executability executable, BaseSpace* space);
// Returns a MemoryChunk in which the memory region from commit_area_size to
// reserve_area_size of the chunk area is reserved but not committed, it
// could be committed later by calling MemoryChunk::CommitArea.
V8_EXPORT_PRIVATE MemoryChunk* AllocateChunk(size_t reserve_area_size,
size_t commit_area_size,
Executability executable,
BaseSpace* space);
Address AllocateAlignedMemory(size_t reserve_size, size_t commit_size,
size_t alignment, Executability executable,
void* hint, VirtualMemory* controller);
void FreeMemory(v8::PageAllocator* page_allocator, Address addr, size_t size);
// Partially release |bytes_to_free| bytes starting at |start_free|. Note that
// internally memory is freed from |start_free| to the end of the reservation.
// Additional memory beyond the page is not accounted though, so
// |bytes_to_free| is computed by the caller.
void PartialFreeMemory(BasicMemoryChunk* chunk, Address start_free,
size_t bytes_to_free, Address new_area_end);
// Checks if an allocated MemoryChunk was intended to be used for executable
// memory.
bool IsMemoryChunkExecutable(MemoryChunk* chunk) {
return executable_memory_.find(chunk) != executable_memory_.end();
}
// Commit memory region owned by given reservation object. Returns true if
// it succeeded and false otherwise.
bool CommitMemory(VirtualMemory* reservation);
// Uncommit memory region owned by given reservation object. Returns true if
// it succeeded and false otherwise.
bool UncommitMemory(VirtualMemory* reservation);
// Zaps a contiguous block of memory [start..(start+size)[ with
// a given zap value.
void ZapBlock(Address start, size_t size, uintptr_t zap_value);
V8_WARN_UNUSED_RESULT bool CommitExecutableMemory(VirtualMemory* vm,
Address start,
size_t commit_size,
size_t reserved_size);
// Page allocator instance for allocating non-executable pages.
// Guaranteed to be a valid pointer.
v8::PageAllocator* data_page_allocator() { return data_page_allocator_; }
// Page allocator instance for allocating executable pages.
// Guaranteed to be a valid pointer.
v8::PageAllocator* code_page_allocator() { return code_page_allocator_; }
// Returns page allocator suitable for allocating pages with requested
// executability.
v8::PageAllocator* page_allocator(Executability executable) {
return executable == EXECUTABLE ? code_page_allocator_
: data_page_allocator_;
}
// A region of memory that may contain executable code including reserved
// OS page with read-write access in the beginning.
const base::AddressRegion& code_range() const {
// |code_range_| >= |optional RW pages| + |code_page_allocator_instance_|
DCHECK_IMPLIES(!code_range_.is_empty(), code_page_allocator_instance_);
DCHECK_IMPLIES(!code_range_.is_empty(),
code_range_.contains(code_page_allocator_instance_->begin(),
code_page_allocator_instance_->size()));
return code_range_;
}
Unmapper* unmapper() { return &unmapper_; }
// Performs all necessary bookkeeping to free the memory, but does not free
// it.
void UnregisterMemory(MemoryChunk* chunk);
void UnregisterMemory(BasicMemoryChunk* chunk,
Executability executable = NOT_EXECUTABLE);
void UnregisterSharedMemory(BasicMemoryChunk* chunk);
void RegisterReadOnlyMemory(ReadOnlyPage* page);
private:
void InitializeCodePageAllocator(v8::PageAllocator* page_allocator,
size_t requested);
// PreFreeMemory logically frees the object, i.e., it unregisters the
// memory, logs a delete event and adds the chunk to remembered unmapped
// pages.
void PreFreeMemory(MemoryChunk* chunk);
// PerformFreeMemory can be called concurrently when PreFree was executed
// before.
void PerformFreeMemory(MemoryChunk* chunk);
// See AllocatePage for public interface. Note that currently we only
// support pools for NOT_EXECUTABLE pages of size MemoryChunk::kPageSize.
template <typename SpaceType>
MemoryChunk* AllocatePagePooled(SpaceType* owner);
// Initializes pages in a chunk. Returns the first page address.
// This function and GetChunkId() are provided for the mark-compact
// collector to rebuild page headers in the from space, which is
// used as a marking stack and its page headers are destroyed.
Page* InitializePagesInChunk(int chunk_id, int pages_in_chunk,
PagedSpace* owner);
void UpdateAllocatedSpaceLimits(Address low, Address high) {
// The use of atomic primitives does not guarantee correctness (wrt.
// desired semantics) by default. The loop here ensures that we update the
// values only if they did not change in between.
Address ptr = lowest_ever_allocated_.load(std::memory_order_relaxed);
while ((low < ptr) && !lowest_ever_allocated_.compare_exchange_weak(
ptr, low, std::memory_order_acq_rel)) {
}
ptr = highest_ever_allocated_.load(std::memory_order_relaxed);
while ((high > ptr) && !highest_ever_allocated_.compare_exchange_weak(
ptr, high, std::memory_order_acq_rel)) {
}
}
void RegisterExecutableMemoryChunk(MemoryChunk* chunk) {
base::MutexGuard guard(&executable_memory_mutex_);
DCHECK(chunk->IsFlagSet(MemoryChunk::IS_EXECUTABLE));
DCHECK_EQ(executable_memory_.find(chunk), executable_memory_.end());
executable_memory_.insert(chunk);
}
void UnregisterExecutableMemoryChunk(MemoryChunk* chunk) {
base::MutexGuard guard(&executable_memory_mutex_);
DCHECK_NE(executable_memory_.find(chunk), executable_memory_.end());
executable_memory_.erase(chunk);
chunk->heap()->UnregisterUnprotectedMemoryChunk(chunk);
}
Isolate* isolate_;
// This object controls virtual space reserved for code on the V8 heap. This
// is only valid for 64-bit architectures where kRequiresCodeRange.
VirtualMemory code_reservation_;
// Page allocator used for allocating data pages. Depending on the
// configuration it may be a page allocator instance provided by
// v8::Platform or a BoundedPageAllocator (when pointer compression is
// enabled).
v8::PageAllocator* data_page_allocator_;
// Page allocator used for allocating code pages. Depending on the
// configuration it may be a page allocator instance provided by
// v8::Platform or a BoundedPageAllocator (when pointer compression is
// enabled or on those 64-bit architectures where pc-relative 32-bit
// displacement can be used for call and jump instructions).
v8::PageAllocator* code_page_allocator_;
// A part of the |code_reservation_| that may contain executable code
// including reserved page with read-write access in the beginning.
// See details below.
base::AddressRegion code_range_;
// This unique pointer owns the instance of bounded code allocator
// that controls executable pages allocation. It does not control the
// optionally existing page in the beginning of the |code_range_|.
// So, summarizing all above, the following conditions hold:
// 1) |code_reservation_| >= |code_range_|
// 2) |code_range_| >= |optional RW pages| +
// |code_page_allocator_instance_|. 3) |code_reservation_| is
// AllocatePageSize()-aligned 4) |code_page_allocator_instance_| is
// MemoryChunk::kAlignment-aligned 5) |code_range_| is
// CommitPageSize()-aligned
std::unique_ptr<base::BoundedPageAllocator> code_page_allocator_instance_;
// Maximum space size in bytes.
size_t capacity_;
// Allocated space size in bytes.
std::atomic<size_t> size_;
// Allocated executable space size in bytes.
std::atomic<size_t> size_executable_;
// We keep the lowest and highest addresses allocated as a quick way
// of determining that pointers are outside the heap. The estimate is
// conservative, i.e. not all addresses in 'allocated' space are allocated
// to our heap. The range is [lowest, highest[, inclusive on the low end
// and exclusive on the high end.
std::atomic<Address> lowest_ever_allocated_;
std::atomic<Address> highest_ever_allocated_;
VirtualMemory last_chunk_;
Unmapper unmapper_;
// Data structure to remember allocated executable memory chunks.
std::unordered_set<MemoryChunk*> executable_memory_;
base::Mutex executable_memory_mutex_;
friend class heap::TestCodePageAllocatorScope;
friend class heap::TestMemoryAllocatorScope;
DISALLOW_IMPLICIT_CONSTRUCTORS(MemoryAllocator);
};
extern template EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE)
Page* MemoryAllocator::AllocatePage<MemoryAllocator::kRegular, PagedSpace>(
size_t size, PagedSpace* owner, Executability executable);
extern template EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE)
Page* MemoryAllocator::AllocatePage<MemoryAllocator::kRegular, SemiSpace>(
size_t size, SemiSpace* owner, Executability executable);
extern template EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE)
Page* MemoryAllocator::AllocatePage<MemoryAllocator::kPooled, SemiSpace>(
size_t size, SemiSpace* owner, Executability executable);
extern template EXPORT_TEMPLATE_DECLARE(
V8_EXPORT_PRIVATE) void MemoryAllocator::
Free<MemoryAllocator::kFull>(MemoryChunk* chunk);
extern template EXPORT_TEMPLATE_DECLARE(
V8_EXPORT_PRIVATE) void MemoryAllocator::
Free<MemoryAllocator::kAlreadyPooled>(MemoryChunk* chunk);
extern template EXPORT_TEMPLATE_DECLARE(
V8_EXPORT_PRIVATE) void MemoryAllocator::
Free<MemoryAllocator::kPreFreeAndQueue>(MemoryChunk* chunk);
extern template EXPORT_TEMPLATE_DECLARE(
V8_EXPORT_PRIVATE) void MemoryAllocator::
Free<MemoryAllocator::kPooledAndQueue>(MemoryChunk* chunk);
} // namespace internal
} // namespace v8
#endif // V8_HEAP_MEMORY_ALLOCATOR_H_