Java OutOfMemoryError异常行为

Java OutOfMemoryError异常行为,java,out-of-memory,Java,Out Of Memory,假设我们的最大内存为256M,为什么该代码可以工作: public static void main(String... args) { for (int i = 0; i < 2; i++) { byte[] a1 = new byte[150000000]; } byte[] a2 = new byte[150000000]; } publicstaticvoidmain(字符串…参数){ 对于(int i=0;i:< /P>运行此代码。 static l

假设我们的最大内存为256M,为什么该代码可以工作:

public static void main(String... args) {
  for (int i = 0; i < 2; i++)
  {
      byte[] a1 = new byte[150000000];
  }
  byte[] a2 = new byte[150000000];
}
publicstaticvoidmain(字符串…参数){
对于(int i=0;i<2;i++)
{
字节[]a1=新字节[15000000];
}
字节[]a2=新字节[15000000];
}
但这一个扔了一个OOME

public static void main(String... args) {
  //for (int i = 0; i < 2; i++)
  {
      byte[] a1 = new byte[150000000];
  }
  byte[] a2 = new byte[150000000];
}
publicstaticvoidmain(字符串…参数){
//对于(int i=0;i<2;i++)
{
字节[]a1=新字节[15000000];
}
字节[]a2=新字节[15000000];
}

这是因为虽然
a1
不在括号后的范围内,但在方法返回之前,它处于称为不可见的状态

大多数现代JVM不会在变量
a1
离开作用域后立即将其设置为
null
(实际上,内部括号是否在那里甚至不会改变生成的字节码),因为它非常无效,通常也不重要。因此,在方法返回之前,
a1
不能被垃圾收集

您可以通过添加行来检查这一点

a1 = null;
在括号内,这使程序运行良好


不可见的术语和解释是从这篇旧论文中得出的:

< P>为了保持事物的透视性,请考虑用<代码> -XMX64 M< /代码>:< /P>运行此代码。
static long sum;
public static void main(String[] args) {
  System.out.println("Warming up...");
  for (int i = 0; i < 100_000; i++) test(1);
  System.out.println("Main call");
  test(5_500_000);
  System.out.println("Sum: " + sum);
}

static void test(int size) {
//  for (int i = 0; i < 1; i++)
  {
    long[] a2 = new long[size];
    sum += a2.length;
  }
  long[] a1 = new long[size];
  sum += a1.length;
}
没有:

static void test(int);
  Code:
   0: iload_0
   1: newarray long
   3: astore_1
   4: iload_0
   5: newarray long
   7: astore_1
   8: return
在这两种情况下都没有显式
null
ing out,但请注意,在No示例中,与示例相反,相同的内存位置实际上被重用。这将导致与观察到的行为相反的期望

扭曲。。。 根据我们从字节码中学到的内容,尝试运行以下命令:

public static void main(String[] args) {
  {
    long[] a1 = new long[5_000_000];
  }
  long[] a2 = new long[0];
  long[] a3 = new long[5_000_000];
}
没有抛出任何OOME。注释掉
a2
的声明,它就回来了。我们分配更多,但占用更少?看看字节码:

public static void main(java.lang.String[]);
  Code:
     0: ldc           #16                 // int 5000000
     2: istore_1      
     3: ldc           #16                 // int 5000000
     5: newarray       long
     7: astore_2      
     8: iconst_0      
     9: newarray       long
    11: astore_2      
    12: ldc           #16                 // int 5000000
    14: newarray       long
    16: astore_3      
    17: return        
用于
a1
的位置2可用于
a2
。OP的代码也是如此,但现在我们用一个对无害的零长度数组的引用覆盖该位置,并使用另一个位置存储对巨大数组的引用

总而言之。。。
Java语言规范没有规定必须收集任何垃圾对象,而JVM规范只说明在方法完成时,包含局部变量的“框架”作为一个整体被销毁。因此,我们所看到的所有行为都是由书本记录的。对象的不可见状态(在keppil链接的文档中提到)只是描述在某些实现和某些情况下发生的情况的一种方式,但决不是任何规范行为。

大多数现代JVM在最后一次使用结束后会将var设置为
null
,而不管范围如何,但这只适用于JITted代码。Keppil,这是JLS提供的吗?请注意,字节码与变量的是相同的。在这两种情况下都没有明确的
null
ing。该文档确实有解释,但由于没有指定,它只是重申了我们已经知道的:
a1
保留在块之外。这可以归结为这样一个事实:JLS对于对象的可达性范围实际上是沉默的。。。。但是Java虚拟机规范描述了VM的内部数据结构。特别是,它写入存储在一个帧中的数据,并且在方法调用完成时销毁。类似地,JVM规范要求使用“自动存储管理系统(称为垃圾收集器)”。不可否认,“Java虚拟机不假设任何特定类型的自动存储管理系统”。@meriton因此规范所要说的是,当整个方法完成时,对象变得不可访问。没有提到范围内或范围外的变量;此外,在任何特定时间,不需要对无法访问的对象执行任何特定操作。由于变量的作用域在转换为字节码时丢失,因此不会影响垃圾收集(除非编译器选择继续使用局部变量)。但是,是的,规范对影响垃圾收集的因素非常模糊。但实际上,所有主要JVM都通过可达性分析来识别垃圾,其根集包括活动帧。@meriton是的,从实际的角度来看,很清楚每种情况下都发生了什么,除了(至少对我而言)解释代码如何准确地释放循环中分配的对象,因为字节码并不表示它会发生。显然,作用域确实在某种程度上影响了这一点,尽管没有明显的影响。或者编译器甚至没有编译for循环,因为您没有使用a1,您可以自由更改代码以消除它。例如
System.out.println(a1.长度)。是的,这应该在两个代码段中的exmaple中完成。我不认为将变量设置为null并调用gc 5次或100次可以保证gc实际执行@Jerome,在第二个示例中,a1不是垃圾收集的,因为它作为静态变量保留在内存中,并且它可用于class@Gevorg这里没有涉及静态变量。
public static void main(java.lang.String[]);
  Code:
     0: ldc           #16                 // int 5000000
     2: istore_1      
     3: ldc           #16                 // int 5000000
     5: newarray       long
     7: astore_2      
     8: iconst_0      
     9: newarray       long
    11: astore_2      
    12: ldc           #16                 // int 5000000
    14: newarray       long
    16: astore_3      
    17: return