Skip to content

Commit b3d1da0

Browse files
author
Glyn Normington
committed
Implement memory ranges
To accommodate memory ranges, the weight balancing algorithm is completely overhauled. Each way in which a memory size may be specified is equivalent to a range. Omitting a memory size is equivalent to specifying the range `0..`. Specifying a single value is equivalent to specifying the corresponding degenerate range. In the four dimensional space of memory types (heap, permgen/maxspace, stack, and native), the Cartesian product of memory ranges is a (four dimensional) box B, all of whose edges start at a non-negative memory size but may extend to plus infinity in any dimension if the corresponding range is unbounded. The total available memory ($MEMORY_LIMIT) defines a hyperplane T in which each point has memory size coordinates which sum to the total available memory. T separates the whole space into two half spaces. The half space containing the origin consists of points which consume less than the total available memory. The other half space consists of points which consume more than the available memory. The memory weightings describe a line W which intersects T at a single point D (for default), which is the Cartesian product of the default memory sizes (in the absence of any memory ranges). In non-error cases, T intersects B in a "hypersegment" S of points which lie within all the memory ranges and have total memory equal to the total available memory. Note that S is convex. The weight balancing algorithm aims to find the closest point of S to the point D. If D lies within S, then the closest point is D itself. If D lies outside S, then since S is convex, there is a unique point in S which is closest to D. The weight balancing algorithm works as follows. It first sets a search point x to the point D. If x lies within S, the algorithm terminates. If x lies outside S, the algorithm determines in which dimensions x lies outside S. x is moved by constraining x to B in those dimensions. This effectively drops a perpendicular onto B in those dimensions. These dimensions of x are now fixed for the remainder of the algorithm and the corresponding memory sizes are deemed to have been allocated. If there are no remaining dimensions, the algorithm terminates. Otherwise, the remaining dimensions of x are recalculated by balancing the remaining memory among the remaining dimensions. x now either lies within S in those dimensions or it does not, in which case the above steps are repeated. It remains to be proved that this algorithm always determines the unique point in S which is closest to D. Native memory is allowed to have an upper bound. Even though this isn't particularly useful, there is no need to prohibit it and this simplifies the documentation. Stack size handling is improved. If a range of stack sizes is specified and the floor of the range is non-zero, the floor is used as the default stack size. Otherwise, the JVM default stack size is used. Also, the number of threads, determined from the weighted proportion of total available memory for the stack divided by the default stack size, is rounded and, if necessary, adjusted upwards to one to behave better in edge cases. Working in terms of a speculative number of threads is not ideal, but it does allow stack ranges and sizes to be converted back and forth between individual stack size and total stack memory. Memory weightings are generalised so that they need no longer add up to 1. The MemorySize type is extended to cope with a zero memory size with no unit. Set max permgen range based on default max permgen size. This will improve permgen for applications with less than 640M of total memory. Note that such applications will get less heap and a smaller stack than would be the case without the permgen range. [#58060090]
1 parent 171fcf1 commit b3d1da0

15 files changed

Lines changed: 736 additions & 356 deletions

config/openjdk.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
repository_root: "http://download.pivotal.io.s3.amazonaws.com/openjdk/{platform}/{architecture}"
1919
version: 1.7.0_+
2020
memory_sizes:
21+
permgen: 64m..
2122
memory_heuristics:
22-
heap: 0.75
23-
permgen: 0.1
24-
stack: 0.05
25-
native: 0.1
23+
heap: 75
24+
permgen: 10
25+
stack: 5
26+
native: 10

docs/jre-openjdk.md

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,54 @@ The JRE can be configured by modifying the [`config/openjdk.yml`][] file. The J
2121
| `repository_root` | The URL of the OpenJDK repository index ([details][repositories]).
2222
| `version` | The version of Java runtime to use. Candidate versions can be found in the listings for [lucid][], [mountainlion][], and [precise][].
2323
| `memory_sizes` | Optional memory sizes, described below under "Memory".
24-
| `memory_heuristics` | Default memory size proportions, described below under "Default Memory Sizes".
24+
| `memory_heuristics` | Default memory size weightings, described below under "Default Memory Sizes".
2525

2626
### Memory
2727

2828
The following optional properties may be specified in the `memory_sizes` mapping.
2929

3030
| Name | Description
3131
| ---- | -----------
32-
| `heap` | The Java maximum heap size to use. For example, a value of `64m` will result in the Java command line option `-Xmx64m`. Values containing whitespace are rejected with an error, but all others values appear without modification on the Java command line appended to `-Xmx`.
33-
| `metaspace` | The Java maximum Metaspace size to use. This is applicable to versions of OpenJDK from 1.8 onwards. For example, a value of `128m` will result in the Java command line option `-XX:MaxMetaspaceSize=128m`. Values containing whitespace are rejected with an error, but all others values appear without modification on the Java command line appended to `-XX:MaxMetaspaceSize=`.
34-
| `permgen` | The Java maximum PermGen size to use. This is applicable to versions of OpenJDK earlier than 1.8. For example, a value of `128m` will result in the Java command line option `-XX:MaxPermSize=128m`. Values containing whitespace are rejected with an error, but all others values appear without modification on the Java command line appended to `-XX:MaxPermSize=`.
35-
| `stack` | The Java stack size to use. For example, a value of `256k` will result in the Java command line option `-Xss256k`. Values containing whitespace are rejected with an error, but all others values appear without modification on the Java command line appended to `-Xss`.
32+
| `heap` | The maximum heap size to use. It may be a single value such as `64m` or a range of acceptable values such as `128m..256m`. It is used to calculate the value of the Java command line option `-Xmx`.
33+
| `metaspace` | The maximum Metaspace size to use. It is applicable to versions of OpenJDK from 1.8 onwards. It may be a single value such as `64m` or a range of acceptable values such as `128m..256m`. It is used to calculate the value of the Java command line option `-XX:MaxMetaspaceSize=`.
34+
| `permgen` | The maximum PermGen size to use. It is applicable to versions of OpenJDK earlier than 1.8. It may be a single value such as `64m` or a range of acceptable values such as `128m..256m`. It is used to calculate the value of the Java command line option `-XX:MaxPermSize=`.
35+
| `stack` | The stack size to use. It may be a single value such as `2m` or a range of acceptable values such as `2m..4m`. It is used to calculate the value of the Java command line option `-Xss`.
36+
| `native` | The amount of memory to reserve for native memory allocation. It should normally be omitted or specified as a range with no upper bound such as `100m..`. It does not correspond to a switch on the Java command line.
3637

37-
### Default Memory Sizes
38+
#### Memory Sizes and Ranges
3839

39-
If some memory sizes are not specified using the above properties, default values are provided. For maximum heap, Metaspace, or PermGen size, the default value is based on a proportion of the total memory specified when the application was pushed. For stack size, the default value is one megabyte.
40+
Memory sizes together with _memory weightings_ (described in the next section) are used to calculate the amount of memory for each memory type. The calculation is described later.
4041

41-
If a memory size is specified which is not equal to the default value, the other default values are adjusted proportionately, except that the default stack size is never adjusted.
42+
Memory sizes consist of a non-negative integer followed by a unit (`k` for kilobytes, `m` for megabytes, `g` for gigabytes; the case is not significant). Only the memory size `0` may be specified without a unit.
4243

43-
The default memory size proportions are configured in the `memory_heuristics` mapping of [`config/openjdk.yml`][]. Each memory size is given a weighting between `0` and `1` corresponding to a proportion of the total memory specified when the application was pushed. The weightings should add up to `1`.
44+
The above memory size properties may be omitted, specified as a single value, or specified as a range. Ranges use the syntax `<lower bound>..<upper bound>`, although either bound may be omitted in which case the defaults of zero and the total available memory are used for the lower bound and upper bound, respectively. Examples of ranges are `100m..200m` (any value between 100 and 200 megabytes, inclusive) and `100m..` (any value greater than or equal to 100 megabytes).
45+
46+
Each form of memory size is equivalent to a range. Omitting a memory size is equivalent to specifying the range `0..`. Specifying a single value is equivalent to specifying the range with that value as both the lower and upper bound, for example `128m` is equivalent to the range `128m..128m`.
4447

48+
#### Memory Weightings
49+
50+
Memory weightings are configured in the `memory_heuristics` mapping of [`config/openjdk.yml`][]. Each weighting is a non-negative number and represents a proportion of the total available memory (represented by the sum of all the weightings). For example, the following weightings:
51+
52+
```
53+
memory_heuristics:
54+
heap: 15
55+
permgen: 5
56+
stack: 1
57+
native: 2
58+
```
59+
60+
represent a maximum heap size three times as large as the maximum PermGen size, and so on.
61+
62+
Memory weightings are used together with memory ranges to calculate the amount of memory for each memory type, as follows.
63+
64+
#### Memory Calculation
65+
66+
The total available memory is allocated into heap, Metaspace or PermGen (depending on the version of OpenJDK), stack, and native memory types.
67+
68+
The total available memory is allocated to each memory type in proportion to its weighting. If the resultant size of a memory type lies outside its range, the size is constrained to
69+
the range, the constrained size is excluded from the remaining memory, and no further calculation is required for the memory type. If the resultant size of a memory size lies within its range, the size is included in the remaining memory. The remaining memory is then allocated to the remaining memory types in a similar fashion. Allocation terminates when none of the sizes of the remaining memory types is constrained by the corresponding range.
70+
71+
Termination is guaranteed since there is a finite number of memory types and in each iteration either none of the remaining memory sizes is constrained by the corresponding range and allocation terminates or at least one memory size is constrained by the corresponding range and is omitted from the next iteration.
4572

4673
[`config/openjdk.yml`]: ../config/openjdk.yml
4774
[Configuration and Extension]: ../README.md#Configuration-and-Extension

lib/java_buildpack/jre/memory/memory_bucket.rb

Lines changed: 16 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
require 'java_buildpack/diagnostics/logger_factory'
1818
require 'java_buildpack/jre'
19-
require 'java_buildpack/jre/memory/memory_size'
19+
require 'java_buildpack/jre/memory/memory_range'
2020

2121
module JavaBuildpack::Jre
2222

@@ -28,71 +28,29 @@ class MemoryBucket
2828
# defaulted
2929
attr_reader :size
3030

31+
# @!attribute [r] range
32+
# @return [MemoryRange] the permissible range of the memory bucket
33+
attr_accessor :range
34+
35+
# @!attribute [r] weighting
36+
# @return [Numeric] the weighting of the memory bucket
37+
attr_reader :weighting
38+
3139
# Constructs a memory bucket.
3240
#
3341
# @param [String] name a non-empty, human-readable name for this memory bucket, used only in diagnostics
3442
# @param [Numeric] weighting a number between 0 and 1 corresponding to the proportion of total memory which this
3543
# memory bucket should consume by default
36-
# @param [MemorySize, nil] size a user-specified size of the memory bucket or nil if the user did not specify a
37-
# size
38-
# @param [Boolean] adjustable whether the size of this memory bucket can grow/shrink or is fixed. If the user
39-
# specified the size of the memory bucket, the size is fixed, regardless of the value of
40-
# this parameter, although the parameter value must still be valid. If total_memory is
41-
# +nil+, the size is fixed since no defaulting will occur.
42-
# @param [Numeric, nil] total_memory the total virtual memory size of the operating system process in KB or +nil+ if
43-
# this is not known
44-
def initialize(name, weighting, size, adjustable, total_memory)
44+
# @param [MemoryRange, nil] range a user-specified range for the memory bucket or nil if the user did not specify a
45+
# range
46+
def initialize(name, weighting, range)
4547
@name = MemoryBucket.validate_name name
4648
@weighting = validate_weighting weighting
47-
@size_specified = size ? validate_memory_size(size, 'size') : nil
48-
@adjustable = (validate_adjustable adjustable) && !@size_specified && total_memory
49-
@total_memory = total_memory ? validate_memory_size(total_memory, 'total_memory') : nil
50-
@size = @size_specified || default_size
49+
@range = range ? validate_memory_range(range) : nil
5150
logger = JavaBuildpack::Diagnostics::LoggerFactory.get_logger
5251
logger.debug { inspect }
5352
end
5453

55-
# Returns the excess memory in this memory bucket.
56-
#
57-
# @return [Numeric] the excess memory in KB
58-
def excess
59-
if @total_memory
60-
@size_specified ? @size_specified - default_size : MemorySize::ZERO
61-
else
62-
MemorySize::ZERO
63-
end
64-
end
65-
66-
# Returns the adjustable weighting of this memory bucket.
67-
#
68-
# @return [Numeric] the adjustable weighting
69-
def adjustable_weighting
70-
@adjustable ? @weighting : 0
71-
end
72-
73-
# Adjusts the size by the appropriate proportion for this memory bucket.
74-
#
75-
# @param [Numeric] total_excess
76-
# @param [Numeric] total_adjustable_weighting
77-
def adjust(total_excess, total_adjustable_weighting)
78-
if @adjustable
79-
if total_adjustable_weighting == 0
80-
@size = MemorySize::ZERO
81-
else
82-
@size = default_size - (total_excess - excess) * @weighting / total_adjustable_weighting
83-
end
84-
end
85-
end
86-
87-
# Returns the default memory size as a weighted proportion of total memory.
88-
#
89-
# @return [MemorySize, nil] the default memory size or nil if there is no default
90-
def default_size
91-
@total_memory ? @total_memory * @weighting : nil
92-
end
93-
94-
protected
95-
9654
attr_writer :size
9755

9856
private
@@ -105,7 +63,6 @@ def self.validate_name(name)
10563
def validate_weighting(weighting)
10664
raise diagnose_weighting(weighting, 'not numeric') unless MemoryBucket.is_numeric weighting
10765
raise diagnose_weighting(weighting, 'negative') if weighting < 0
108-
raise diagnose_weighting(weighting, 'greater than 1') if weighting > 1
10966
weighting
11067
end
11168

@@ -121,14 +78,9 @@ def identify
12178
"MemoryBucket #{@name}"
12279
end
12380

124-
def validate_memory_size(size, parameter_name)
125-
raise "Invalid '#{parameter_name}' parameter of class '#{size.class}' for #{identify} : not a MemorySize" unless size.is_a? MemorySize
126-
size
127-
end
128-
129-
def validate_adjustable(adjustable)
130-
raise "Invalid 'adjustable' parameter for #{identify} : not true or false" unless !!adjustable == adjustable
131-
adjustable
81+
def validate_memory_range(range)
82+
raise "Invalid 'range' parameter of class '#{range.class}' for #{identify} : not a MemoryRange" unless range.is_a? MemoryRange
83+
range
13284
end
13385

13486
end

lib/java_buildpack/jre/memory/memory_limit.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def self.memory_limit
2929
memory_limit = ENV['MEMORY_LIMIT']
3030
return nil unless memory_limit
3131
memory_limit_size = MemorySize.new(memory_limit)
32-
raise "Invalid negative $MEMORY_LIMIT #{memory_limit}" if memory_limit_size < MemorySize::ZERO
32+
raise "Invalid negative $MEMORY_LIMIT #{memory_limit}" if memory_limit_size < 0
3333
memory_limit_size
3434
end
3535

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Encoding: utf-8
2+
# Cloud Foundry Java Buildpack
3+
# Copyright (c) 2013 the original author or authors.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
require 'java_buildpack/jre'
18+
19+
module JavaBuildpack::Jre
20+
21+
# A class representing a permissible range of memory sizes.
22+
class MemoryRange
23+
24+
# @!attribute [r] floor
25+
# @return [MemorySize] the lower bound of this memory range
26+
attr_reader :floor
27+
28+
# @!attribute [r] ceiling
29+
# @return [MemorySize, nil] the upper bound of this memory range or +nil+ if there is no upper bound
30+
attr_reader :ceiling
31+
32+
# Creates a memory range based on either a memory range string or lower and upper bounds expressed as MemorySizes.
33+
#
34+
# @param [MemorySize, String] value the lower bound of the range or a string range
35+
# @param [MemorySize, nil] ceiling the upper bound of the range
36+
def initialize(value, ceiling = nil)
37+
if value.is_a? String
38+
raise "Invalid combination of parameter types #{value.class} and #{ceiling.class}" unless ceiling.nil?
39+
lower_bound, upper_bound = MemoryRange.get_bounds(value)
40+
@floor = create_memory_size lower_bound
41+
@ceiling = upper_bound ? create_memory_size(upper_bound) : nil
42+
else
43+
MemoryRange.validate_memory_size value
44+
MemoryRange.validate_memory_size ceiling unless ceiling.nil?
45+
@floor = value
46+
@ceiling = ceiling
47+
end
48+
raise "Invalid range: floor #{@floor} is higher than ceiling #{@ceiling}" if @ceiling && @floor > @ceiling
49+
end
50+
51+
# Determines whether or not this range is bounded. Reads better than testing for a +nil+ ceiling.
52+
#
53+
# @return [Boolean] +true+ if and only if this range is bounded
54+
def bounded?
55+
!@ceiling.nil?
56+
end
57+
58+
# Determines whether a given memory size falls in this range.
59+
#
60+
# @param [MemorySize] size the memory size to be checked
61+
# @return [Boolean] +true+ if and only if the given memory size falls in this range
62+
def contains?(size)
63+
@floor <= size && (@ceiling.nil? || size <= @ceiling)
64+
end
65+
66+
# Constrains a given memory size to this range. If the size falls within the range, returns the size.
67+
# If the size is below the range, return the floor of the range. If the size is above the range,
68+
# return the ceiling of the range.
69+
#
70+
# @param [MemorySize] size the memory size to be constrained
71+
# @return [MemorySize] the constrained memory size
72+
def constrain(size)
73+
if size < @floor
74+
@floor
75+
else
76+
!@ceiling.nil? && size > @ceiling ? @ceiling : size
77+
end
78+
end
79+
80+
# Returns true if and only if this range consists of a single value.
81+
#
82+
# @return [Boolean] whether or not this range consists of a single value
83+
def degenerate?
84+
@floor == @ceiling
85+
end
86+
87+
# Multiply this memory range by a numeric factor.
88+
#
89+
# @param [Numeric] other the factor to multiply by
90+
# @return [MemoryRange] the result
91+
def *(other)
92+
raise "Cannot multiply a MemoryRange by an instance of #{other.class}" unless other.is_a? Numeric
93+
raise 'Cannot multiply an unbounded MemoryRange by 0' if !bounded? && other == 0
94+
MemoryRange.new(@floor * other, bounded? ? @ceiling * other : nil)
95+
end
96+
97+
# Compare this memory range for equality with another memory range
98+
#
99+
# @param [MemoryRange] other
100+
# @return [Boolean] the result
101+
def ==(other)
102+
@floor == other.floor && @ceiling == other.ceiling
103+
end
104+
105+
# Returns a string representation of this range.
106+
#
107+
# @return [String] the string representation of this range
108+
def to_s
109+
"#{@floor}..#{@ceiling ? @ceiling : ''}"
110+
end
111+
112+
private
113+
114+
RANGE_SEPARATOR = '..'
115+
116+
def self.get_bounds(range)
117+
if range.index(RANGE_SEPARATOR)
118+
lower_bound, upper_bound = range.split(RANGE_SEPARATOR)
119+
lower_bound = '0' if lower_bound.nil? || lower_bound == ''
120+
return lower_bound, upper_bound
121+
else
122+
return range, range
123+
end
124+
end
125+
126+
def create_memory_size(size)
127+
MemorySize.new(size)
128+
end
129+
130+
def self.validate_memory_size(size)
131+
raise "Invalid MemorySize parameter of type #{size.class}" unless size.is_a? MemorySize
132+
end
133+
134+
end
135+
136+
end

0 commit comments

Comments
 (0)