当内存不足以引发OutOfMemoryError时会发生什么?


207

我知道每个对象都需要堆内存,而堆栈上的每个基本/引用都需要堆栈内存。

当我尝试在堆上创建对象并且没有足够的内存来执行此操作时,JVM 在堆上创建一个java.lang.OutOfMemoryError并将其扔给我。

因此,隐式地,这意味着JVM在启动时会保留一些内存。

当此保留的内存用完(肯定会用完,请阅读下面的讨论)并且JVM堆上没有足够的内存来创建java.lang.OutOfMemoryError实例时,会发生什么?

它只是挂了吗?还是null因为newOOM实例没有存储空间而将其扔给我?

try {
    Object o = new Object();
    // and operations which require memory (well.. that's like everything)
} catch (java.lang.OutOfMemoryError e) {
    // JVM had insufficient memory to create an instance of java.lang.OutOfMemoryError to throw to us
    // what next? hangs here, stuck forever?
    // or would the machine decide to throw us a "null" ? (since it doesn't have memory to throw us anything more useful than a null)
    e.printStackTrace(); // e.printStackTrace() requires memory too.. =X
}

==

JVM为什么不能保留足够的内存?

无论保留了多少内存,如果JVM无法“回收”该内存,则仍然有可能用完该内存:

try {
    Object o = new Object();
} catch (java.lang.OutOfMemoryError e) {
    // JVM had 100 units of "spare memory". 1 is used to create this OOM.
    try {
        e.printStackTrace();
    } catch (java.lang.OutOfMemoryError e2) {
        // JVM had 99 units of "spare memory". 1 is used to create this OOM.
        try {
            e.printStackTrace();
        } catch (java.lang.OutOfMemoryError e3) {
            // JVM had 98 units of "spare memory". 1 is used to create this OOM.
            try {
                e.printStackTrace();
            } catch (java.lang.OutOfMemoryError e4) {
                // JVM had 97 units of "spare memory". 1 is used to create this OOM.
                try {
                    e.printStackTrace();
                } catch (java.lang.OutOfMemoryError e5) {
                    // JVM had 96 units of "spare memory". 1 is used to create this OOM.
                    try {
                        e.printStackTrace();
                    } catch (java.lang.OutOfMemoryError e6) {
                        // JVM had 95 units of "spare memory". 1 is used to create this OOM.
                        e.printStackTrace();
                        //........the JVM can't have infinite reserved memory, he's going to run out in the end
                    }
                }
            }
        }
    }
}

或更简而言之:

private void OnOOM(java.lang.OutOfMemoryError e) {
    try {
        e.printStackTrace();
    } catch (java.lang.OutOfMemoryError e2) {
        OnOOM(e2);
    }
}

2
您的答案将在很大程度上取决于JVM
MozenRath 2012年

23
我曾经使用过的一个电话库(在90年代)用来捕获OutOfMemoryException,然后做一些涉及创建大缓冲区的事情……
汤姆·霍顿-粘性

@ TomHawtin-tackline如果这样做所涉及的操作引发了另一个OOM,该怎么办?
Pacerier

38
就像手机一样,它的电池电量用完了,但它有足够的电量来保持垃圾邮件的状态:“您的电池快用完了”。
万马

1
“当保留的内存用完时会发生什么”:只有程序捕获了第一个OutOfMemoryError并保留了对它的引用时,才会发生这种情况。这说明捕获a OutOfMemoryError并不像人们想象的那样有用,因为您几乎不能保证捕获程序的状态。见stackoverflow.com/questions/8728866/...
Raedwald

Answers:


145

JVM永远不会真正耗尽内存。它预先进行堆堆栈的内存计算。

JVM结构,第3章,第3.5.2节指出:

  • 如果可以动态扩展Java虚拟机堆栈,并且尝试进行扩展,但是可以提供足够的内存来实现扩展,或者如果没有足够的内存来为新线程创建初始Java虚拟机堆栈,则Java虚拟机机器抛出一个OutOfMemoryError

对于,请参阅第3.5.3节。

  • 如果计算需要的堆多于自动存储管理系统可以提供的堆,则Java虚拟机将抛出一个OutOfMemoryError

因此,它在分配对象之前预先进行了计算。


发生的情况是,JVM尝试在称为永久生成区域(或PermSpace)的内存中为对象分配内存。如果分配失败(即使在JVM调用垃圾回收器尝试并分配可用空间之后),也会抛出OutOfMemoryError。即使是异常也需要内存空间,因此错误将无限期抛出。

进一步阅读。?而且,OutOfMemoryError可以在不同的JVM结构中发生


10
我的意思是,但是Java虚拟机也不需要内存来引发OutOfMemoryError吗?当没有内存可以抛出OOM时会发生什么?
Pacerier

5
但是,如果JVM没有将引用返回到同一OOM实例,那么您是否同意最终它会用完其保留的内存?(如问题代码所示)
Pacerier,2012年

1
请允许我在这里把格雷厄姆的评论的引用:stackoverflow.com/questions/9261705/...
Pacerier

2
如果VM在所述极端情况下以及在核电站中保留了一个OOM异常单例,则可能会很好。
约翰K

8
@JohnK:我希望核电站不使用Java编程,就像航天飞机和波音757都不使用Java编程一样。
Dietrich Epp '02

64

Graham Borland似乎是对的:至少我的 JVM显然重用了OutOfMemoryErrors。为了对此进行测试,我编写了一个简单的测试程序:

class OOMTest {
    private static void test (OutOfMemoryError o) {
        try {
            for (int n = 1; true; n += n) {
                int[] foo = new int[n];
            }
        } catch (OutOfMemoryError e) {
            if (e == o)
                System.out.println("Got the same OutOfMemoryError twice: " + e);
            else test(e);
        }
    }
    public static void main (String[] args) {
        test(null);
    }
}

运行它会产生以下输出:

$ javac OOMTest.java && java -Xmx10m OOMTest 
Got the same OutOfMemoryError twice: java.lang.OutOfMemoryError: Java heap space

顺便说一句,我正在运行的JVM(在Ubuntu 10.04上)是这样的:

$ java -version
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)

编辑:我尝试查看如果使用以下程序强制 JVM完全耗尽内存,会发生什么情况:

class OOMTest2 {
    private static void test (int n) {
        int[] foo;
        try {
            foo = new int[n];
            test(n * 2);
        }
        catch (OutOfMemoryError e) {
            test((n+1) / 2);
        }
    }
    public static void main (String[] args) {
        test(1);
    }
}

事实证明,它似乎永远循环。但是,奇怪的是,尝试用Ctrl+ 终止程序C不起作用,而仅给出以下消息:

Java HotSpot(TM) 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated


不错的测试,对我来说,版本为“ 1.7.0_01” Java HotSpot(TM)64位服务器VM
Pacerier,2012年

有趣。这使JVM看起来似乎无法完全理解尾递归...(好像正在做一些尾递归堆栈重用,但还不足以清理所有内存...)
Izkata 2012年

修改您的代码以查看得到多少个不同的代码-我总是将else分支恰好执行5次。
Irfy 2012年

@Izkata:我想说这是一个有意识的决定,要预先分配nOOM,然后再重用其中的一个,以便始终可以抛出OOM 。Sun / Oracle的JVM根本不支持尾递归吗?
Irfy 2012年

10
@Izkata循环显然无休止地运行,因为JVM在内存用完后不断抛出一个和一个(第5个左右)OOM。因此,它n在堆栈上有框架,最终导致创建和破坏框架n+1永恒,给人以不断奔跑的感觉。
Irfy 2012年

41

大多数运行时环境会在启动时预先分配或保留足够的内存来处理内存不足的情况。我想象大多数理智的JVM实现都会做到这一点。


1
stackoverflow.com/questions/9261215/…:是的,但是如果JVM保留100个内存来执行此操作,并且在第一个OOM上花费了1个单元,那么如果在我的OOM catch块中执行e.printStackTrace()会怎样?e.printStackTrace()也需要内存。然后,JVM将花费另一个内存单元向我抛出另一个OOM(还剩下98个单元),然后我用e.printStackTrace()捕获了它,所以JVM向我抛出了另一个OOM(还剩下97个单元),而且被捕获了,所以我想..
Pacerier

3
这正是OOME从未使用过堆栈跟踪的原因-堆栈跟踪会占用内存!OOME仅开始尝试在Java 6中包含堆栈跟踪(blogs.oracle.com/alanb/entry/…)。我假设如果无法进行堆栈跟踪,则抛出异常而没有堆栈跟踪。
肖恩·赖利

1
@SeanReilly我的意思是没有堆栈跟踪的异常仍然是Object,它仍然需要内存。无论是否提供堆栈跟踪,都需要内存。是真的,如果在catch块中没有可用的内存来创建OOM(即使没有堆栈跟踪,也没有内存来创建一个OOM),那么我会捕获null吗?
Pacerier

17
JVM可以返回对OOM异常的单个静态实例的多个引用。因此,即使您的catch子句尝试使用更多的内存,JVM也可以一次又一次地抛出相同的OOM实例。
Graham Borland '02

1
@TheEliteGentleman我同意这些也是很好的答案,但是JVM驻留在物理计算机上,这些答案并未说明JVM如何神奇地具有足够的内存来始终提供OOM实例。“总是相同的实例”似乎解决了这个难题。
Pacerier

23

上一次我使用Java并使用调试器进行工作时,堆检查器显示JVM在启动时分配了OutOfMemoryError实例。换句话说,它在程序有机会开始消耗(更不用说耗尽)内存之前分配对象。


12

根据JVM Spec第3.5.2章:

如果可以动态扩展Java虚拟机堆栈,并且尝试进行扩展,但是可以提供足够的内存来实现扩展,或者如果没有足够的内存来为新线程创建初始Java虚拟机堆栈,则Java虚拟机机器抛出一个OutOfMemoryError

每个Java虚拟机都必须保证它将抛出一个OutOfMemoryError。这意味着,OutOfMemoryError即使没有堆空间,它也必须能够创建的实例(或必须事先创建)。

尽管不必保证,但仍有足够的内存来捕获它并打印一个不错的堆栈跟踪...

加成

您添加了一些代码来显示,如果JVM必须抛出多个以上的空间,则它可能会耗尽堆空间OutOfMemoryError。但是这样的实现将违反上面的要求。

并不需要抛出的实例OutOfMemoryError是唯一的或根据需要创建的。JVM可以OutOfMemoryError在启动期间准备好一个实例,并在堆空间用完时抛出该实例-在正常环境下,只有一次。换句话说:OutOfMemoryError我们看到的实例可能是单例。


我想要实现这一点,如果空间狭窄,就必须避免记录堆栈跟踪。
拉德瓦尔德

@Raedwald:事实上,这就是Oracle VM所做的事情,请参见我的回答。
sleske 2012年

11

有趣的问题:-)。当其他人对理论方面做出了很好的解释时,我决定尝试一下。这是在Oracle JDK 1.6.0_26,Windows 7 64位上。

测试设置

我写了一个简单的程序来耗尽内存(见下文)。

该程序只是创建一个static java.util.List,并不断向其中填充新的字符串,直到抛出OOM为止。然后,它将捕获并继续陷入无尽的循环(JVM不良...)。

测试结果

从输出中可以看到,前四次OOME被抛出,它带有堆栈跟踪。之后,后续的OOME仅java.lang.OutOfMemoryError: Java heap spaceprintStackTrace()被调用时打印。

因此,显然JVM会尽力打印堆栈跟踪,但是如果内存确实很紧,它会忽略该跟踪,就像其他答案所建议的那样。

同样有趣的是OOME的哈希码。请注意,前几个OOME都有不同的哈希值。JVM开始忽略堆栈跟踪后,哈希值始终相同。这表明JVM将尽可能长地使用新鲜的(预分配的?)OOME实例,但是如果推入推挤,它将仅重用同一实例,而不会抛出任何异常。

输出量

注意:我截断了一些堆栈跟踪以使输出更易于阅读(“ [...]”)。

iteration 0
iteration 100000
iteration 200000
iteration 300000
iteration 400000
iteration 500000
iteration 600000
iteration 700000
iteration 800000
iteration 900000
iteration 1000000
iteration 1100000
iteration 1200000
iteration 1300000
iteration 1400000
iteration 1500000
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1069480624
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.ArrayList.ensureCapacity(Unknown Source)
    at java.util.ArrayList.add(Unknown Source)
    at testsl.Div.gobbleUpMemory(Div.java:23)
    at testsl.Div.exhaustMemory(Div.java:12)
    at testsl.Div.main(Div.java:7)
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 616699029
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 2136955031
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1535562945
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
[...]

该程序

public class Div{
    static java.util.List<String> list = new java.util.ArrayList<String>();

    public static void main(String[] args) {
        exhaustMemory();
    }

    private static void exhaustMemory() {
        try {
            gobbleUpMemory();
        } catch (OutOfMemoryError e) {
            System.out.println("Ouch: " + e+"; hash: "+e.hashCode());
            e.printStackTrace();
            System.out.println("Keep on trying...");
            exhaustMemory();
        }
    }

    private static void gobbleUpMemory() {
        for (int i = 0; i < 10000000; i++) {
            list.add(new String("some random long string; use constructor to force new instance"));
            if (i % 10000000== 0) {
                System.out.println("iteration "+i);
            }
        }

    }
}

推送时,它无法为OOME分配内存,因此它将刷新已创建的内存。
Buhake Sindi

1
小注释:您的某些输出似乎是乱序的,大概是因为您要打印到,System.out但是默认情况下printStackTrace()使用System.err。始终使用任一流,可能会获得更好的结果。
Ilmari Karonen '02

@IlmariKaronen:是的,我注意到了。这只是一个例子,所以没关系。显然,您不会在生产代码中使用它。
sleske

没错,当我第一次查看输出时,花了我一段时间才弄清楚****是怎么回事。
Ilmari Karonen '02


4

表示尝试违反托管内存环境边界的异常由所述环境的运行时处理,在这种情况下为JVM。JVM是它自己的进程,正在运行应用程序的IL。如果程序尝试进​​行调用以将调用堆栈扩展到限制之外,或者分配的内存超出JVM可以保留的内存,则运行时本身将注入异常,这将导致调用堆栈被取消。无论您的程序当前需要多少内存,或者其调用堆栈有多深,JVM都会在其自己的进程范围内分配足够的内存来创建所述异常并将其注入到您的代码中。


“ JVM将在其自己的进程范围内分配足够的内存来创建所述异常”,但是如果您的代码保留了对该异常的引用,那么它不能被重用,如何创建另一个?或者您是否建议它具有特殊的Singleton OOME对象?
拉德瓦尔德'02

我不建议那样。如果您的程序捕获并挂起了所有已创建的异常,包括由JVM或OS创建和注入的异常,那么最终JVM本身将超出操作系统设置的边界,并且操作系统将其关闭以获取GPF或类似的错误。但是,这首先是不好的设计。异常应该被处理然后超出范围,或者被抛出。而且,您永远不应试图追赶并继续进行SOE或OOME;除了“清理”之外,您可以正常退出,在这种情况下您无能为力。
KeithS 2012年

“首先是糟糕的设计”:糟糕的设计。但是,以传统的方式,如果JVM失败了,它会符合规范吗?
拉德瓦尔德

4

您似乎将JVM在其中运行Java程序的JVM保留的虚拟内存与主机OS在其中将JVM作为本地进程运行的本地内存混淆了。您机器上的JVM在操作系统管理的内存中运行,而不是在JVM保留用于运行Java程序的内存中运行。

进一步阅读:

而作为最后一点,试图赶上一个java.lang.Error的(和它的子类),以打印堆栈跟踪可能不会给你任何有用的信息。您需要堆转储。


4

为了从功能上进一步阐明@Graham Borland的答案,JVM在启动时执行了以下操作:

private static final OutOfMemoryError OOME = new OutOfMemoryError();

后来,JVM执行以下Java字节码之一:“ new”,“ anewarray”或“ multianewarray”。该指令使JVM在内存不足的情况下执行许多步骤:

  1. 调用本地函数,例如allocate()allocate()尝试为特定类或数组的新实例分配内存。
  2. 该分配请求失败,因此JVM调用了另一个本机函数,例如doGC(),尝试进行垃圾回收。
  3. 当该函数返回时,allocate()尝试再次为该实例分配内存。
  4. 如果失败(*),则JVM在allocate()中仅执行一个throw OOME;,引用其在启动时实例化的OOME。注意,它不必分配该OOME,而只是引用它。

显然,这些不是文字上的步骤;在实现中,它们因JVM而异,但这是高级概念。

(*)在失败之前,这里需要进行大量工作。JVM将尝试清除SoftReference对象,使用世代收集器时尝试将其直接分配到使用权的世代中,并可能进行其他操作(例如终结处理)。


3

JVM将预分配的答案OutOfMemoryErrors确实是正确的。
除了通过引起内存不足的情况进行测试外,我们还可以检查任何JVM的堆(我使用了一个只做睡眠的小程序,使用Java 8 update 31中的Oracle Hotspot JVM运行它)。

使用jmap我们可以看到似乎有9个OutOfMemoryError实例(即使我们有足够的内存):

> jmap -histo 12103 | grep OutOfMemoryError
 71:9288 java.lang.OutOfMemoryError
170:1 32 [Ljava.lang.OutOfMemoryError;

然后我们可以生成一个堆转储:

> jmap -dump:format = b,file = heap.hprof 12315

并使用Eclipse Memory Analyzer打开它,其中的OQL查询显示JVM实际上似乎已OutOfMemoryErrors为所有可能的消息预分配:

在此处输入图片说明

实际上可以预先分配它们的Java 8 Hotspot JVM的代码可以在此处找到,看起来像这样(省略了一些部分):

...
// Setup preallocated OutOfMemoryError errors
k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_OutOfMemoryError(), true, CHECK_false);
k_h = instanceKlassHandle(THREAD, k);
Universe::_out_of_memory_error_java_heap = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_metaspace = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_class_metaspace = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_array_size = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_gc_overhead_limit =
  k_h->allocate_instance(CHECK_false);

...

if (!DumpSharedSpaces) {
  // These are the only Java fields that are currently set during shared space dumping.
  // We prefer to not handle this generally, so we always reinitialize these detail messages.
  Handle msg = java_lang_String::create_from_str("Java heap space", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_java_heap, msg());

  msg = java_lang_String::create_from_str("Metaspace", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_metaspace, msg());
  msg = java_lang_String::create_from_str("Compressed class space", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_class_metaspace, msg());

  msg = java_lang_String::create_from_str("Requested array size exceeds VM limit", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_array_size, msg());

  msg = java_lang_String::create_from_str("GC overhead limit exceeded", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_gc_overhead_limit, msg());

  msg = java_lang_String::create_from_str("/ by zero", CHECK_false);
  java_lang_Throwable::set_message(Universe::_arithmetic_exception_instance, msg());

  // Setup the array of errors that have preallocated backtrace
  k = Universe::_out_of_memory_error_java_heap->klass();
  assert(k->name() == vmSymbols::java_lang_OutOfMemoryError(), "should be out of memory error");
  k_h = instanceKlassHandle(THREAD, k);

  int len = (StackTraceInThrowable) ? (int)PreallocatedOutOfMemoryErrorCount : 0;
  Universe::_preallocated_out_of_memory_error_array = oopFactory::new_objArray(k_h(), len, CHECK_false);
  for (int i=0; i<len; i++) {
    oop err = k_h->allocate_instance(CHECK_false);
    Handle err_h = Handle(THREAD, err);
    java_lang_Throwable::allocate_backtrace(err_h, CHECK_false);
    Universe::preallocated_out_of_memory_errors()->obj_at_put(i, err_h());
  }
  Universe::_preallocated_out_of_memory_error_avail_count = (jint)len;
}
...

并且此代码显示JVM将首先尝试使用预先分配的错误之一来为堆栈跟踪留出空间,然后回落到没有堆栈跟踪的错误中:

oop Universe::gen_out_of_memory_error(oop default_err) {
  // generate an out of memory error:
  // - if there is a preallocated error with backtrace available then return it wth
  //   a filled in stack trace.
  // - if there are no preallocated errors with backtrace available then return
  //   an error without backtrace.
  int next;
  if (_preallocated_out_of_memory_error_avail_count > 0) {
    next = (int)Atomic::add(-1, &_preallocated_out_of_memory_error_avail_count);
    assert(next < (int)PreallocatedOutOfMemoryErrorCount, "avail count is corrupt");
  } else {
    next = -1;
  }
  if (next < 0) {
    // all preallocated errors have been used.
    // return default
    return default_err;
  } else {
    // get the error object at the slot and set set it to NULL so that the
    // array isn't keeping it alive anymore.
    oop exc = preallocated_out_of_memory_errors()->obj_at(next);
    assert(exc != NULL, "slot has been used already");
    preallocated_out_of_memory_errors()->obj_at_put(next, NULL);

    // use the message from the default error
    oop msg = java_lang_Throwable::message(default_err);
    assert(msg != NULL, "no message");
    java_lang_Throwable::set_message(exc, msg);

    // populate the stack trace and return it.
    java_lang_Throwable::fill_in_stack_trace_of_preallocated_backtrace(exc);
    return exc;
  }
}

好的帖子,我将在一周内将此答案作为答案,以便在返回之前的答案之前提供更多可见性。
Pacerier,2015年

在Java 8及更高版本中,永久生成空间已被完全删除,并且您不再可以从Java 8及更高版本中指定堆空间分配内存大小,因为它们引入了称为的动态类元数据内存管理Metaspace。如果您可以显示一段适合PermGen的代码并将其与Metaspace进行比较,那就太好了。
Buhake Sindi

@BuhakeSindi-我看不到永久一代与此有关。如您在回答中所述,不会在永久代中分配新对象。您也永远不会提到OutOfMemoryErrors已预先分配的事实(这是问题的实际答案)。
约翰·卡文

1
好的,我的意思是,从Java 8开始,对象分配是动态计算的,而对象分配是在预先定义的堆空间上分配的,因此可能是预先分配的。即使OOME已预先分配,也要进行“计算”以确定是否需要抛出OOME(因此,为什么要引用JLS规范)。
Buhake Sindi

1
Java堆大小与Java 8中的预定义一样。永久代是包含类元数据,内部字符串和类静态变量的堆的一部分。它的大小有限,需要与总堆大小分开进行调整。在Java 8中,类元数据已移至本机内存,并且已将实习生字符串和类静态变量移至常规Java堆(请参见例如此处的infoq.com/articles/Java-PERMGEN-Removed)。
Johan Kaving
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.