| 1 | /* |
| 2 | * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. |
| 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| 4 | * |
| 5 | * This code is free software; you can redistribute it and/or modify it |
| 6 | * under the terms of the GNU General Public License version 2 only, as |
| 7 | * published by the Free Software Foundation. |
| 8 | * |
| 9 | * This code is distributed in the hope that it will be useful, but WITHOUT |
| 10 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| 11 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 12 | * version 2 for more details (a copy is included in the LICENSE file that |
| 13 | * accompanied this code). |
| 14 | * |
| 15 | * You should have received a copy of the GNU General Public License version |
| 16 | * 2 along with this work; if not, write to the Free Software Foundation, |
| 17 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| 18 | * |
| 19 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| 20 | * or visit www.oracle.com if you need additional information or have any |
| 21 | * questions. |
| 22 | * |
| 23 | */ |
| 24 | |
| 25 | #include "precompiled.hpp" |
| 26 | #include "memory/allocation.hpp" |
| 27 | #include "memory/allocation.inline.hpp" |
| 28 | #include "memory/metaspaceShared.hpp" |
| 29 | #include "memory/resourceArea.hpp" |
| 30 | #include "runtime/atomic.hpp" |
| 31 | #include "runtime/os.hpp" |
| 32 | #include "runtime/task.hpp" |
| 33 | #include "runtime/threadCritical.hpp" |
| 34 | #include "services/memTracker.hpp" |
| 35 | #include "utilities/ostream.hpp" |
| 36 | |
| 37 | //-------------------------------------------------------------------------------------- |
| 38 | // ChunkPool implementation |
| 39 | |
| 40 | // MT-safe pool of chunks to reduce malloc/free thrashing |
| 41 | // NB: not using Mutex because pools are used before Threads are initialized |
| 42 | class ChunkPool: public CHeapObj<mtInternal> { |
| 43 | Chunk* _first; // first cached Chunk; its first word points to next chunk |
| 44 | size_t _num_chunks; // number of unused chunks in pool |
| 45 | size_t _num_used; // number of chunks currently checked out |
| 46 | const size_t _size; // size of each chunk (must be uniform) |
| 47 | |
| 48 | // Our four static pools |
| 49 | static ChunkPool* _large_pool; |
| 50 | static ChunkPool* _medium_pool; |
| 51 | static ChunkPool* _small_pool; |
| 52 | static ChunkPool* _tiny_pool; |
| 53 | |
| 54 | // return first element or null |
| 55 | void* get_first() { |
| 56 | Chunk* c = _first; |
| 57 | if (_first) { |
| 58 | _first = _first->next(); |
| 59 | _num_chunks--; |
| 60 | } |
| 61 | return c; |
| 62 | } |
| 63 | |
| 64 | public: |
| 65 | // All chunks in a ChunkPool has the same size |
| 66 | ChunkPool(size_t size) : _size(size) { _first = NULL; _num_chunks = _num_used = 0; } |
| 67 | |
| 68 | // Allocate a new chunk from the pool (might expand the pool) |
| 69 | NOINLINE void* allocate(size_t bytes, AllocFailType alloc_failmode) { |
| 70 | assert(bytes == _size, "bad size" ); |
| 71 | void* p = NULL; |
| 72 | // No VM lock can be taken inside ThreadCritical lock, so os::malloc |
| 73 | // should be done outside ThreadCritical lock due to NMT |
| 74 | { ThreadCritical tc; |
| 75 | _num_used++; |
| 76 | p = get_first(); |
| 77 | } |
| 78 | if (p == NULL) p = os::malloc(bytes, mtChunk, CURRENT_PC); |
| 79 | if (p == NULL && alloc_failmode == AllocFailStrategy::EXIT_OOM) { |
| 80 | vm_exit_out_of_memory(bytes, OOM_MALLOC_ERROR, "ChunkPool::allocate" ); |
| 81 | } |
| 82 | return p; |
| 83 | } |
| 84 | |
| 85 | // Return a chunk to the pool |
| 86 | void free(Chunk* chunk) { |
| 87 | assert(chunk->length() + Chunk::aligned_overhead_size() == _size, "bad size" ); |
| 88 | ThreadCritical tc; |
| 89 | _num_used--; |
| 90 | |
| 91 | // Add chunk to list |
| 92 | chunk->set_next(_first); |
| 93 | _first = chunk; |
| 94 | _num_chunks++; |
| 95 | } |
| 96 | |
| 97 | // Prune the pool |
| 98 | void free_all_but(size_t n) { |
| 99 | Chunk* cur = NULL; |
| 100 | Chunk* next; |
| 101 | { |
| 102 | // if we have more than n chunks, free all of them |
| 103 | ThreadCritical tc; |
| 104 | if (_num_chunks > n) { |
| 105 | // free chunks at end of queue, for better locality |
| 106 | cur = _first; |
| 107 | for (size_t i = 0; i < (n - 1) && cur != NULL; i++) cur = cur->next(); |
| 108 | |
| 109 | if (cur != NULL) { |
| 110 | next = cur->next(); |
| 111 | cur->set_next(NULL); |
| 112 | cur = next; |
| 113 | |
| 114 | // Free all remaining chunks while in ThreadCritical lock |
| 115 | // so NMT adjustment is stable. |
| 116 | while(cur != NULL) { |
| 117 | next = cur->next(); |
| 118 | os::free(cur); |
| 119 | _num_chunks--; |
| 120 | cur = next; |
| 121 | } |
| 122 | } |
| 123 | } |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | // Accessors to preallocated pool's |
| 128 | static ChunkPool* large_pool() { assert(_large_pool != NULL, "must be initialized" ); return _large_pool; } |
| 129 | static ChunkPool* medium_pool() { assert(_medium_pool != NULL, "must be initialized" ); return _medium_pool; } |
| 130 | static ChunkPool* small_pool() { assert(_small_pool != NULL, "must be initialized" ); return _small_pool; } |
| 131 | static ChunkPool* tiny_pool() { assert(_tiny_pool != NULL, "must be initialized" ); return _tiny_pool; } |
| 132 | |
| 133 | static void initialize() { |
| 134 | _large_pool = new ChunkPool(Chunk::size + Chunk::aligned_overhead_size()); |
| 135 | _medium_pool = new ChunkPool(Chunk::medium_size + Chunk::aligned_overhead_size()); |
| 136 | _small_pool = new ChunkPool(Chunk::init_size + Chunk::aligned_overhead_size()); |
| 137 | _tiny_pool = new ChunkPool(Chunk::tiny_size + Chunk::aligned_overhead_size()); |
| 138 | } |
| 139 | |
| 140 | static void clean() { |
| 141 | enum { BlocksToKeep = 5 }; |
| 142 | _tiny_pool->free_all_but(BlocksToKeep); |
| 143 | _small_pool->free_all_but(BlocksToKeep); |
| 144 | _medium_pool->free_all_but(BlocksToKeep); |
| 145 | _large_pool->free_all_but(BlocksToKeep); |
| 146 | } |
| 147 | }; |
| 148 | |
| 149 | ChunkPool* ChunkPool::_large_pool = NULL; |
| 150 | ChunkPool* ChunkPool::_medium_pool = NULL; |
| 151 | ChunkPool* ChunkPool::_small_pool = NULL; |
| 152 | ChunkPool* ChunkPool::_tiny_pool = NULL; |
| 153 | |
| 154 | void chunkpool_init() { |
| 155 | ChunkPool::initialize(); |
| 156 | } |
| 157 | |
| 158 | void |
| 159 | Chunk::clean_chunk_pool() { |
| 160 | ChunkPool::clean(); |
| 161 | } |
| 162 | |
| 163 | |
| 164 | //-------------------------------------------------------------------------------------- |
| 165 | // ChunkPoolCleaner implementation |
| 166 | // |
| 167 | |
| 168 | class ChunkPoolCleaner : public PeriodicTask { |
| 169 | enum { CleaningInterval = 5000 }; // cleaning interval in ms |
| 170 | |
| 171 | public: |
| 172 | ChunkPoolCleaner() : PeriodicTask(CleaningInterval) {} |
| 173 | void task() { |
| 174 | ChunkPool::clean(); |
| 175 | } |
| 176 | }; |
| 177 | |
| 178 | //-------------------------------------------------------------------------------------- |
| 179 | // Chunk implementation |
| 180 | |
| 181 | void* Chunk::operator new (size_t requested_size, AllocFailType alloc_failmode, size_t length) throw() { |
| 182 | // requested_size is equal to sizeof(Chunk) but in order for the arena |
| 183 | // allocations to come out aligned as expected the size must be aligned |
| 184 | // to expected arena alignment. |
| 185 | // expect requested_size but if sizeof(Chunk) doesn't match isn't proper size we must align it. |
| 186 | assert(ARENA_ALIGN(requested_size) == aligned_overhead_size(), "Bad alignment" ); |
| 187 | size_t bytes = ARENA_ALIGN(requested_size) + length; |
| 188 | switch (length) { |
| 189 | case Chunk::size: return ChunkPool::large_pool()->allocate(bytes, alloc_failmode); |
| 190 | case Chunk::medium_size: return ChunkPool::medium_pool()->allocate(bytes, alloc_failmode); |
| 191 | case Chunk::init_size: return ChunkPool::small_pool()->allocate(bytes, alloc_failmode); |
| 192 | case Chunk::tiny_size: return ChunkPool::tiny_pool()->allocate(bytes, alloc_failmode); |
| 193 | default: { |
| 194 | void* p = os::malloc(bytes, mtChunk, CALLER_PC); |
| 195 | if (p == NULL && alloc_failmode == AllocFailStrategy::EXIT_OOM) { |
| 196 | vm_exit_out_of_memory(bytes, OOM_MALLOC_ERROR, "Chunk::new" ); |
| 197 | } |
| 198 | return p; |
| 199 | } |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | void Chunk::operator delete(void* p) { |
| 204 | Chunk* c = (Chunk*)p; |
| 205 | switch (c->length()) { |
| 206 | case Chunk::size: ChunkPool::large_pool()->free(c); break; |
| 207 | case Chunk::medium_size: ChunkPool::medium_pool()->free(c); break; |
| 208 | case Chunk::init_size: ChunkPool::small_pool()->free(c); break; |
| 209 | case Chunk::tiny_size: ChunkPool::tiny_pool()->free(c); break; |
| 210 | default: |
| 211 | ThreadCritical tc; // Free chunks under TC lock so that NMT adjustment is stable. |
| 212 | os::free(c); |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | Chunk::Chunk(size_t length) : _len(length) { |
| 217 | _next = NULL; // Chain on the linked list |
| 218 | } |
| 219 | |
| 220 | void Chunk::chop() { |
| 221 | Chunk *k = this; |
| 222 | while( k ) { |
| 223 | Chunk *tmp = k->next(); |
| 224 | // clear out this chunk (to detect allocation bugs) |
| 225 | if (ZapResourceArea) memset(k->bottom(), badResourceValue, k->length()); |
| 226 | delete k; // Free chunk (was malloc'd) |
| 227 | k = tmp; |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | void Chunk::next_chop() { |
| 232 | _next->chop(); |
| 233 | _next = NULL; |
| 234 | } |
| 235 | |
| 236 | void Chunk::start_chunk_pool_cleaner_task() { |
| 237 | #ifdef ASSERT |
| 238 | static bool task_created = false; |
| 239 | assert(!task_created, "should not start chuck pool cleaner twice" ); |
| 240 | task_created = true; |
| 241 | #endif |
| 242 | ChunkPoolCleaner* cleaner = new ChunkPoolCleaner(); |
| 243 | cleaner->enroll(); |
| 244 | } |
| 245 | |
| 246 | //------------------------------Arena------------------------------------------ |
| 247 | |
| 248 | Arena::Arena(MEMFLAGS flag, size_t init_size) : _flags(flag), _size_in_bytes(0) { |
| 249 | size_t round_size = (sizeof (char *)) - 1; |
| 250 | init_size = (init_size+round_size) & ~round_size; |
| 251 | _first = _chunk = new (AllocFailStrategy::EXIT_OOM, init_size) Chunk(init_size); |
| 252 | _hwm = _chunk->bottom(); // Save the cached hwm, max |
| 253 | _max = _chunk->top(); |
| 254 | MemTracker::record_new_arena(flag); |
| 255 | set_size_in_bytes(init_size); |
| 256 | } |
| 257 | |
| 258 | Arena::Arena(MEMFLAGS flag) : _flags(flag), _size_in_bytes(0) { |
| 259 | _first = _chunk = new (AllocFailStrategy::EXIT_OOM, Chunk::init_size) Chunk(Chunk::init_size); |
| 260 | _hwm = _chunk->bottom(); // Save the cached hwm, max |
| 261 | _max = _chunk->top(); |
| 262 | MemTracker::record_new_arena(flag); |
| 263 | set_size_in_bytes(Chunk::init_size); |
| 264 | } |
| 265 | |
| 266 | Arena *Arena::move_contents(Arena *copy) { |
| 267 | copy->destruct_contents(); |
| 268 | copy->_chunk = _chunk; |
| 269 | copy->_hwm = _hwm; |
| 270 | copy->_max = _max; |
| 271 | copy->_first = _first; |
| 272 | |
| 273 | // workaround rare racing condition, which could double count |
| 274 | // the arena size by native memory tracking |
| 275 | size_t size = size_in_bytes(); |
| 276 | set_size_in_bytes(0); |
| 277 | copy->set_size_in_bytes(size); |
| 278 | // Destroy original arena |
| 279 | reset(); |
| 280 | return copy; // Return Arena with contents |
| 281 | } |
| 282 | |
| 283 | Arena::~Arena() { |
| 284 | destruct_contents(); |
| 285 | MemTracker::record_arena_free(_flags); |
| 286 | } |
| 287 | |
| 288 | void* Arena::operator new(size_t size) throw() { |
| 289 | assert(false, "Use dynamic memory type binding" ); |
| 290 | return NULL; |
| 291 | } |
| 292 | |
| 293 | void* Arena::operator new (size_t size, const std::nothrow_t& nothrow_constant) throw() { |
| 294 | assert(false, "Use dynamic memory type binding" ); |
| 295 | return NULL; |
| 296 | } |
| 297 | |
| 298 | // dynamic memory type binding |
| 299 | void* Arena::operator new(size_t size, MEMFLAGS flags) throw() { |
| 300 | return (void *) AllocateHeap(size, flags, CALLER_PC); |
| 301 | } |
| 302 | |
| 303 | void* Arena::operator new(size_t size, const std::nothrow_t& nothrow_constant, MEMFLAGS flags) throw() { |
| 304 | return (void*)AllocateHeap(size, flags, CALLER_PC, AllocFailStrategy::RETURN_NULL); |
| 305 | } |
| 306 | |
| 307 | void Arena::operator delete(void* p) { |
| 308 | FreeHeap(p); |
| 309 | } |
| 310 | |
| 311 | // Destroy this arenas contents and reset to empty |
| 312 | void Arena::destruct_contents() { |
| 313 | if (UseMallocOnly && _first != NULL) { |
| 314 | char* end = _first->next() ? _first->top() : _hwm; |
| 315 | free_malloced_objects(_first, _first->bottom(), end, _hwm); |
| 316 | } |
| 317 | // reset size before chop to avoid a rare racing condition |
| 318 | // that can have total arena memory exceed total chunk memory |
| 319 | set_size_in_bytes(0); |
| 320 | _first->chop(); |
| 321 | reset(); |
| 322 | } |
| 323 | |
| 324 | // This is high traffic method, but many calls actually don't |
| 325 | // change the size |
| 326 | void Arena::set_size_in_bytes(size_t size) { |
| 327 | if (_size_in_bytes != size) { |
| 328 | long delta = (long)(size - size_in_bytes()); |
| 329 | _size_in_bytes = size; |
| 330 | MemTracker::record_arena_size_change(delta, _flags); |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | // Total of all Chunks in arena |
| 335 | size_t Arena::used() const { |
| 336 | size_t sum = _chunk->length() - (_max-_hwm); // Size leftover in this Chunk |
| 337 | Chunk *k = _first; |
| 338 | while( k != _chunk) { // Whilst have Chunks in a row |
| 339 | sum += k->length(); // Total size of this Chunk |
| 340 | k = k->next(); // Bump along to next Chunk |
| 341 | } |
| 342 | return sum; // Return total consumed space. |
| 343 | } |
| 344 | |
| 345 | void Arena::signal_out_of_memory(size_t sz, const char* whence) const { |
| 346 | vm_exit_out_of_memory(sz, OOM_MALLOC_ERROR, "%s" , whence); |
| 347 | } |
| 348 | |
| 349 | // Grow a new Chunk |
| 350 | void* Arena::grow(size_t x, AllocFailType alloc_failmode) { |
| 351 | // Get minimal required size. Either real big, or even bigger for giant objs |
| 352 | size_t len = MAX2(x, (size_t) Chunk::size); |
| 353 | |
| 354 | Chunk *k = _chunk; // Get filled-up chunk address |
| 355 | _chunk = new (alloc_failmode, len) Chunk(len); |
| 356 | |
| 357 | if (_chunk == NULL) { |
| 358 | _chunk = k; // restore the previous value of _chunk |
| 359 | return NULL; |
| 360 | } |
| 361 | if (k) k->set_next(_chunk); // Append new chunk to end of linked list |
| 362 | else _first = _chunk; |
| 363 | _hwm = _chunk->bottom(); // Save the cached hwm, max |
| 364 | _max = _chunk->top(); |
| 365 | set_size_in_bytes(size_in_bytes() + len); |
| 366 | void* result = _hwm; |
| 367 | _hwm += x; |
| 368 | return result; |
| 369 | } |
| 370 | |
| 371 | |
| 372 | |
| 373 | // Reallocate storage in Arena. |
| 374 | void *Arena::Arealloc(void* old_ptr, size_t old_size, size_t new_size, AllocFailType alloc_failmode) { |
| 375 | if (new_size == 0) return NULL; |
| 376 | #ifdef ASSERT |
| 377 | if (UseMallocOnly) { |
| 378 | // always allocate a new object (otherwise we'll free this one twice) |
| 379 | char* copy = (char*)Amalloc(new_size, alloc_failmode); |
| 380 | if (copy == NULL) { |
| 381 | return NULL; |
| 382 | } |
| 383 | size_t n = MIN2(old_size, new_size); |
| 384 | if (n > 0) memcpy(copy, old_ptr, n); |
| 385 | Afree(old_ptr,old_size); // Mostly done to keep stats accurate |
| 386 | return copy; |
| 387 | } |
| 388 | #endif |
| 389 | char *c_old = (char*)old_ptr; // Handy name |
| 390 | // Stupid fast special case |
| 391 | if( new_size <= old_size ) { // Shrink in-place |
| 392 | if( c_old+old_size == _hwm) // Attempt to free the excess bytes |
| 393 | _hwm = c_old+new_size; // Adjust hwm |
| 394 | return c_old; |
| 395 | } |
| 396 | |
| 397 | // make sure that new_size is legal |
| 398 | size_t corrected_new_size = ARENA_ALIGN(new_size); |
| 399 | |
| 400 | // See if we can resize in-place |
| 401 | if( (c_old+old_size == _hwm) && // Adjusting recent thing |
| 402 | (c_old+corrected_new_size <= _max) ) { // Still fits where it sits |
| 403 | _hwm = c_old+corrected_new_size; // Adjust hwm |
| 404 | return c_old; // Return old pointer |
| 405 | } |
| 406 | |
| 407 | // Oops, got to relocate guts |
| 408 | void *new_ptr = Amalloc(new_size, alloc_failmode); |
| 409 | if (new_ptr == NULL) { |
| 410 | return NULL; |
| 411 | } |
| 412 | memcpy( new_ptr, c_old, old_size ); |
| 413 | Afree(c_old,old_size); // Mostly done to keep stats accurate |
| 414 | return new_ptr; |
| 415 | } |
| 416 | |
| 417 | |
| 418 | // Determine if pointer belongs to this Arena or not. |
| 419 | bool Arena::contains( const void *ptr ) const { |
| 420 | #ifdef ASSERT |
| 421 | if (UseMallocOnly) { |
| 422 | // really slow, but not easy to make fast |
| 423 | if (_chunk == NULL) return false; |
| 424 | char** bottom = (char**)_chunk->bottom(); |
| 425 | for (char** p = (char**)_hwm - 1; p >= bottom; p--) { |
| 426 | if (*p == ptr) return true; |
| 427 | } |
| 428 | for (Chunk *c = _first; c != NULL; c = c->next()) { |
| 429 | if (c == _chunk) continue; // current chunk has been processed |
| 430 | char** bottom = (char**)c->bottom(); |
| 431 | for (char** p = (char**)c->top() - 1; p >= bottom; p--) { |
| 432 | if (*p == ptr) return true; |
| 433 | } |
| 434 | } |
| 435 | return false; |
| 436 | } |
| 437 | #endif |
| 438 | if( (void*)_chunk->bottom() <= ptr && ptr < (void*)_hwm ) |
| 439 | return true; // Check for in this chunk |
| 440 | for (Chunk *c = _first; c; c = c->next()) { |
| 441 | if (c == _chunk) continue; // current chunk has been processed |
| 442 | if ((void*)c->bottom() <= ptr && ptr < (void*)c->top()) { |
| 443 | return true; // Check for every chunk in Arena |
| 444 | } |
| 445 | } |
| 446 | return false; // Not in any Chunk, so not in Arena |
| 447 | } |
| 448 | |
| 449 | |
| 450 | #ifdef ASSERT |
| 451 | void* Arena::malloc(size_t size) { |
| 452 | assert(UseMallocOnly, "shouldn't call" ); |
| 453 | // use malloc, but save pointer in res. area for later freeing |
| 454 | char** save = (char**)internal_malloc_4(sizeof(char*)); |
| 455 | return (*save = (char*)os::malloc(size, mtChunk)); |
| 456 | } |
| 457 | |
| 458 | // for debugging with UseMallocOnly |
| 459 | void* Arena::internal_malloc_4(size_t x) { |
| 460 | assert( (x&(sizeof(char*)-1)) == 0, "misaligned size" ); |
| 461 | check_for_overflow(x, "Arena::internal_malloc_4" ); |
| 462 | if (_hwm + x > _max) { |
| 463 | return grow(x); |
| 464 | } else { |
| 465 | char *old = _hwm; |
| 466 | _hwm += x; |
| 467 | return old; |
| 468 | } |
| 469 | } |
| 470 | #endif |
| 471 | |
| 472 | |
| 473 | //-------------------------------------------------------------------------------------- |
| 474 | // Non-product code |
| 475 | |
| 476 | #ifndef PRODUCT |
| 477 | |
| 478 | julong Arena::_bytes_allocated = 0; |
| 479 | |
| 480 | void Arena::inc_bytes_allocated(size_t x) { inc_stat_counter(&_bytes_allocated, x); } |
| 481 | |
| 482 | // debugging code |
| 483 | inline void Arena::free_all(char** start, char** end) { |
| 484 | for (char** p = start; p < end; p++) if (*p) os::free(*p); |
| 485 | } |
| 486 | |
| 487 | void Arena::free_malloced_objects(Chunk* chunk, char* hwm, char* max, char* hwm2) { |
| 488 | assert(UseMallocOnly, "should not call" ); |
| 489 | // free all objects malloced since resource mark was created; resource area |
| 490 | // contains their addresses |
| 491 | if (chunk->next()) { |
| 492 | // this chunk is full, and some others too |
| 493 | for (Chunk* c = chunk->next(); c != NULL; c = c->next()) { |
| 494 | char* top = c->top(); |
| 495 | if (c->next() == NULL) { |
| 496 | top = hwm2; // last junk is only used up to hwm2 |
| 497 | assert(c->contains(hwm2), "bad hwm2" ); |
| 498 | } |
| 499 | free_all((char**)c->bottom(), (char**)top); |
| 500 | } |
| 501 | assert(chunk->contains(hwm), "bad hwm" ); |
| 502 | assert(chunk->contains(max), "bad max" ); |
| 503 | free_all((char**)hwm, (char**)max); |
| 504 | } else { |
| 505 | // this chunk was partially used |
| 506 | assert(chunk->contains(hwm), "bad hwm" ); |
| 507 | assert(chunk->contains(hwm2), "bad hwm2" ); |
| 508 | free_all((char**)hwm, (char**)hwm2); |
| 509 | } |
| 510 | } |
| 511 | |
| 512 | #endif // Non-product |
| 513 | |