JVM内存模型、对象结构、JVM性能优化

JVM内存模型

  1. 模型图
    在这里插入图片描述
  2. 各个内存块存放内容
    在这里插入图片描述

JVM内存结构主要有三大块:堆(heap space)、方法区(method area/ PermGen/ MetaSpace)、本地区(native area)。
堆内存

  • 堆内存(heap space)存放所有线程共享的对象和数组。在虚拟机启动时创建,Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。GC回收的策略是按代回收。
  • 对象在实例化后的整个运行周期内,都被存放在堆内存中。堆内存又被划分成不同的部分:是JVM中最大的一块由年轻代(Young Generation Space)和老年代(Old Generation Space)组成。而年轻代内存又被分成三部分,伊甸区(Eden)、From Survivor空间、To Survivor空间(幸存者区域(Survivor Sapce)包括From Survivor空间、To Survivor空间)。默认情况下年轻代按照8:1:1的比例来分配。

字符串常量池
在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;在JDK7.0版本,字符串常量池等被移到了堆中了。在HotSpot VM里实现的string pool功能的是一个StringTable类。在JDK7.0中,StringTable的长度可以通过参数指定。字符串常量池中的字符串只存在一份!

  • JVM对堆内存要求:堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

不同堆区域的存放的对象拥有不同的生命周期:
新建(New)或者短期的对象存放在Eden区域;
幸存的或者中期的对象将会从Eden区域拷贝到Survivor区域;
始终存在或者长期的对象将会从Survivor拷贝到Old Generation;
内存的释放(通过销毁对象)通过2种不同的GC实现:Young GC、Full GC。为了检查所有的对象是否能够被销毁,Young GC会标记不能销毁的对象,经过多次标记后,对象将会被移动到老年代中。

方法区是一个逻辑上的分区(元空间MetaSpace是JDK8及之后,HotSpot 虚拟机 对 方法区 的新的实现,是一个物理区域。)
方法区(method area)别名Non-Heap(非堆)。存储线程共享的类信息包括类名、属性(字段、方法)名、运行时常量池、静态变量、方法和构造方法经编译器编译后的代码等数据,方法区有时被称为永久或持久代(PermGen) 因为HotSpot把GC分代收集扩展至方法区。

  • JVM对堆内存要求:java虚拟机可以回收这个区也可以不回收这个区(类的卸载、常量池的回收)。方法区可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,可以扩展。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

class常量池(Class Constant Pool)
class常量池简介:每个class文件都有一个class常量池。 我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
什么是字面量和符号引用
字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。
运行时常量池(Runtime Constant Pool): 运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,即存放字面量和符号引用。不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用。一般的运行时常量池还存放这些直接引用。
JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

本地区
本地区(native area)包含java虚拟机栈、本地方法栈、程序计数器等内存区域。本地区是线程私有的,存放线程运行时的指向堆内存中的对象引用和局部基本变量。

  • 栈帧(Stack 、线程栈、java栈) 用于存储局部变量表(存放的是函数的入参,以及局部变量包括基本数据类型、对象引用如指针或句柄)、操作栈(存放调用过程中的计算结果的临时存放区域用来算术计算、传参等作用)、动态链接(方法引用,栈帧的方法引用为参数指向运行时常量池中的方法名)、方法出口(存放的是异常处理表和函数的返回,访问常量池的指针)等信息。以方法为单位,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 局部变量表存放了编译期可知的各种基本数据类型、对象引用。局部变量表所需的内存空间在编译期间完成分配。原始类型(primitive type)的本地变量以及引用都存放在线程栈中。
  • java虚拟机栈(JVM Stacks) 因为方法的执行都是伴随着线程的,线程执行每个方法的时候都会同时创建一个栈帧(Stack Frame)这个栈帧又叫线程栈或者java栈,是存放于虚拟机栈中的,先入后出。线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。方法退出的过程实际上等同于把当前栈帧出栈。
  • 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度(栈帧的数量),将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
  • 本地方法栈 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError、OutOfMemoryError异常
  • 程序计数器(Program Counter Register/ PC)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

常见控制参数
-Xms设置堆的最小空间大小。
-Xmx设置堆的最大空间大小。
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-Xss设置每个线程的堆栈大小。没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。老年代空间大小=堆空间大小-年轻代大空间大小

虚拟机栈的内存大小会直接影响线程创建的数量。假定内存中的堆内存的大小不变都是512M,每个线程的虚拟机栈的大小会直接影响可创建线程数量的大小虚拟机栈内存越大可创建的数量越小。即:JVM内存 = 堆内存 + 线程数量 * 栈内存。

在不同的操作系统版本中,最大的地址空间是不一样的。32位的Windows最大是2G内存,因此可以得到这样的计算公式: 线程数 =(最大内存地址 - Xmx - 系统保留内存)/ Xss
当然线程数量和系统的内核配置还是有很大的关系

虚拟机栈和线程栈的关系

一、 虚拟机栈:与程序计数器、本地方法栈都是属于线程私有的JVM内存区域。虚拟机栈的生命周期是和线程相同的,是在JVM运行时创建的,在线程中,方法在执行的过程中会创建一个栈帧(Stack Frame)。主要用于存放局部变量表、操作栈、动态链接、方法出口等信息。一般将栈帧内存的大小称为宽度,而栈帧的数量被称为虚拟机栈的深度。虚拟机栈的大小可以通过参数-xss配置。因此在同等大小的虚拟机栈下,如果局部变量表等占用的内存越小,虚拟机栈的深度越大
二、虚拟机栈的内部结构
虚拟机栈是一个后入先出的栈。栈帧是保存在虚拟机栈中的,栈帧是用来存储数据和存储部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。
三、 栈帧

  1. 局部变量表
      局部变量表是一组局部变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java文件编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。

  2. 操作数栈
      操作数栈也常被称为操作栈,它是一个后入先出栈。JVM底层字节码指令集是基于栈类型的,所有的操作码都是对操作数栈上的数据进行操作,对于每一个方法的调用,JVM会建立一个操作数栈,以供计算使用。和局部变量一样。操作数栈的最大深度也是编译的时候写入到方法表的code属性的max_stacks数据项中。操作数栈每一个元素可以是任意的Java数据类型,包括long、double(64位)32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2栈容量的单位为**“字宽”,对于32位虚拟机来说,一个“字宽”占4个字节64位虚拟机来说,一个“字宽”占8个字节。当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递**的。
    另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了。

  3. 动态连接  
    每个栈帧都包含一个指向运行时常量池中该栈帧所属方法引用持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接

  4. 方法返回地址
      当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。 无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。

  5. 附加信息
      虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。

对象的结构图

在这里插入图片描述

对象的结构是由对象头和对象数据以及数据对齐(padding,凑够8字节的倍数 加快读取速度)3部分组成。

对象头
对象头由markword,klass,数组长度(只有数组有,记录数组的长度信息)。
1.markword记录存储自身运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit 4字节和64bit 8字节,官方称它为“MarkWord”。
2.klass 一个指针,指向方法区的类信息。
3.数组长度
实例数据
如果基本类型直接存储,引用类型存储的是引用指针。

建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种

句柄池中指针分别指向实例引用和方法去的对象类型,java栈(栈帧的本地变量表)中的reference指向的是句柄池中的对象指针,对象指针指向的是实例对象,java中的reference可以看成指针的指针,简接引用。
在这里插入图片描述
直接引用,在java栈中(栈帧的本地变量表)的reference直接指向存储指向java堆中实例对象。是直接引用,访问速度快。
在这里插入图片描述

对齐
HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍加快运行读取速度。
计算对象大小
1.在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节。
2.在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节。
3.64位开启指针压缩的情况下,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节。 数组长度4字节+数组对象头8字节(对象引用4字节(未开启指针压缩的64位为8字节)+数组markword为4字节(64位未开启指针压缩的为8字节))+对齐4=16字节。
4.静态属性不算在对象大小内。

一段函数和图片,说明类的加载,对象初始化内存分配,函数执行过程
首先AppClassLoader加载MainApp类型,找到入口main方法,MainApp类会被初始化,执行main字节码,会有一个线程来执行字节码。此时生成一个栈帧入java虚拟机栈,栈帧包括局部变量表,操作数栈、动态解析、方法出入口等信息)线程执行过程中,需要用到Animal类,这时发现Animal对象还未加载,那么接下来会执行< client >方法加载Animal类的class文件到方法区(这时系统会再堆中生成一个单例Class对象,对应方法区的Animal类型结构)。然后执行< init >无参构造器,进行对象的实例化。实例化前先在堆中分配内存,根据方法区的Animal类信息,Animal类对象内存大小是实现确定的。(堆内存的分配有不同JVM实现方式,如果使用和空闲的内存空间分开是指针碰撞,如果使用和空闲的内存空间不分开则是空闲列表。)animal对象初始化后,根据对象头的Klass指针,找到方法区对应的类结构信息,根据方法表运行printFrame()函数,接下来,又生成一个栈帧(Animal对象引用animal加载进栈帧中的局部变量表中,指向堆内存中的Animal对象)入java虚拟机栈的栈顶。Java虚拟机会机将常量池内的符号引用(函数方法名,类型名还是其他字面量)替换为直接引用如指针,从而可以进行方法,类型定位,这个过程是解析。

MainApp类在方法区以及Animal在方法区都分别有运行时常量池、类型信息、方法名、变量名等信息,图中没有画全。堆中的pointer即是对象头中存储的Kcass指针,指向了方法区。animal对象引用存储在java栈中。

package com.example.ABReference;

public class MainApp {
    public static void main(String[] args) {
        Animal animal=new Animal();
        animal.printFrame();
    }
}

class Animal {
    void printFrame(){
        System.out.println("打印");
    }
}

在这里插入图片描述

JVM性能优化

如果JVM频繁发生FULL GC,那么将会是致命的危险,不仅仅会造成网站响应迟钝,更严重的时候会导致系统崩溃,这对用户体验来讲,都是我们不愿意看到的。
在这里插入图片描述

  • Eden区域是用来存放使用new或者newInstance等方式创建的对象,默认都是存放在Eden区,除非这个对象太大,或者超过了设定的阈值-XX:PretenureSizeThresold,这样的对象会被直接分配到Old区域。
  • 2个Survivor(幸存)区,一般称S0,S1,理论上他们是一样大的,解释一下,他们是如何工作的:
  • 在不断创建对象的过程中,Eden区会满,这时候会开始做Young GC也叫Minor GC,而Young空间的第一次GC就是找出Eden区中,幸存活着的对象,然后将这些对象,放到S0,或S1区中的其中一个, 假设第一次选择了S0,它会逐步将活着的对象拷贝到S0区域,但是如果S0区域满了,剩下活着的对象只能放old区域了,接下来要做的是,将Eden区域 清空,此时时候S1区域也是空的。
  • 当第二次Eden区域满的时候,就将Eden区域中活着的对象+S0区域中活着的对象,迁移到S1中,如果S1放不下,就会将剩下的部门,放到Old区域中,只是这次对象来源区域增加了S0,最后会将Eden区+S0区域,清空
  • 第三次和第四次依次类推,始终保证S0和S1有一个是空的,用来存储临时对象,用于交换空间的目的,反反复复多次没有被淘汰的对象,将会放入old区域中,默认是15次。具体的交换过程就和上图中的信息相似。

问题一:怎么定义活着的对象?
1.引用计数
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题
2. 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
问题二:什么是根(GC Roots)?
本地变量引用,操作数栈引用,PC寄存器,本地方法栈引用等这些都是根。(虚拟机栈(存放栈帧)中引用的对象、本地方法栈中JNI引用的对象、方法区中类静态属性实体引用的对象、方法区中常量引用的对象。)
问题三:对象进入Old区域有什么坏处?
old区域一般称为老年代,老年代与新生代不一样,年轻代,我们可以认为存活下来的对象很少(经常被清空),而老年代则相反,存活下来的对象很多(老顽固的对象,15次都清理不掉)。发生一次FULL GC,来找出来所有存活的对象是非常耗时的,因此,我们应该尽量避免FULL GC的发生。
问题四:S0和S1一般多大,靠什么参数来控制,有什么变化?
一般来说很小,我们大概知道它与Young差不多相差一倍的比例,设置的的参数主要有两个: -XX:SurvivorRatio=8 -XX:InitialSurvivorRatio=8 第一个参数是Eden和Survivor区域比重,注意是一个Survivor的的大小,如果将其设置为8,则说明Eden区是一个Survivor区的8倍,换句话说S0或S1空间是整个Young空间的1/10,剩余的80%由Eden区域来使用。第二个参数Young/S0的比值,当其设置为8时,表示s0或s1占整个Young空间的12.5%。
问题五:一个对象每次MinorGc时,活着的对象都会在s0和s1区域转移,经过经过Minor GC多少次后,会进入Old区域呢?
默认是15次,参数设置-XX:MaxTenuringThreshold=15,计数器会在对象的头部记录它交换的次数

遇到实际的内存溢出异常时,能根据异常的信息快速判断是哪个区域的内存溢出,知道什么样的代码可能会导致这些区域内存溢出,以及出现这些异常后该如何处理。

-Xms 堆内存的最小大小,默认为物理内存的1/64
-Xmx 堆内存的最大大小,默认为物理内存的1/4
-Xmn 堆内新生代的大小。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn
-Xss 设置每个线程可使用的内存大小,即栈的大小。在相同物理内存下,减小这个值能生成更多的线程,当然操作系统对一个进程内的线程数还是有限制的,不能无限生成。线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。
比较全面的控制参数

堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-Xmn -XX:NewSize:新生代大小
-XX:MaxNewSize设置新生代最大空间大小。
-XX:NewRatio:设置新生代和老年代的比值,老年代是新生代的几倍。如:为3,表示年轻代与老年代比值为1:3
-XX:SurvivorRatio:新生代中Eden区与两个Survivor区的比值,伊甸区是幸存区0或者1的几倍。注意Survivor区有两个。如:为3,表示Eden:Survivor=3:2,一个Survivor区占整个新生代的1/5 。
-XX:MaxTenuringThreshold:设置转入老年代的存活次数。如果是0,则直接跳过新生代进入老年代
-XX:PermSize、-XX:MaxPermSize:分别设置永久代最小大小与最大大小(Java8以前)
-XX:MetaspaceSize、-XX:MaxMetaspaceSize:分别设置元空间最小大小与最大大小(Java8以后)
-Xss设置每个线程的堆栈大小。没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。老年代空间大小=堆空间大小-年轻代大空间大小
收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行老年代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器新生代收集方式为并行收集时,使用的CPU数。并行收集线程数。

java堆溢出

虚拟机参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

package com.example.MemoryManagement;

import java.util.ArrayList;
import java.util.List;

public class HeapOutOfMemory {
    static class OOMObject{}

    public static void main(String[] args) {
        List<OOMObject> list=new ArrayList<>();// list保证了新建对象的可达性(GCroots),存的对象OOMObject不会被回收

        while (true){
            list.add(new OOMObject());
        }
    }
}

结果
[GC (Allocation Failure) [PSYoungGen: 8192K->1008K(9216K)] 8192K->4831K(19456K), 0.0058751 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) --[PSYoungGen: 9200K->9200K(9216K)] 13023K->19437K(19456K), 0.0127700 secs] [Times: user=0.04 sys=0.00, real=0.02 secs]
[Full GC (Ergonomics) [PSYoungGen: 9200K->0K(9216K)] [ParOldGen: 10237K->10078K(10240K)] 19437K->10078K(19456K), [Metaspace: 3104K->3104K(1056768K)], 0.1177786 secs] [Times: user=0.29 sys=0.00, real=0.11 secs]
[Full GC (Ergonomics) [PSYoungGen: 8044K->7826K(9216K)] [ParOldGen: 10078K->7966K(10240K)] 18122K->15793K(19456K), [Metaspace: 3104K->3104K(1056768K)], 0.1436159 secs] [Times: user=0.56 sys=0.01, real=0.14 secs]
[Full GC (Allocation Failure) [PSYoungGen: 7826K->7826K(9216K)] [ParOldGen: 7966K->7948K(10240K)] 15793K->15775K(19456K), [Metaspace: 3104K->3104K(1056768K)], 0.1077415 secs] [Times: user=0.55 sys=0.00, real=0.11 secs]
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.example.MemoryManagement.HeapOutOfMemory.main(HeapOutOfMemory.java:13)
Heap
PSYoungGen total 9216K, used 8069K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
eden space 8192K, 98% used [0x00000007bf600000,0x00000007bfde15f0,0x00000007bfe00000)
from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
to space 1024K, 48% used [0x00000007bff00000,0x00000007bff7bf00,0x00000007c0000000)
ParOldGen total 10240K, used 7948K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
object space 10240K, 77% used [0x00000007bec00000,0x00000007bf3c3360,0x00000007bf600000)
Metaspace used 3136K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 342K, capacity 388K, committed 512K, reserved 1048576K

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值