企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
## 1. 内存结构 ### 1.1 堆内存 ![](https://img.kancloud.cn/da/18/da18761d3197e58670c9dffbb2c9abc8_598x411.png) * 永久代在jdk8之后被废弃掉,取而代之的是元空间(MetaSpace)。 * 元空间与永久代类似,都是方法区的实现,最大的区别是:元空间不在jvm中,而使用本地内存 1. JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。 2. 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。 3. 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。 **4. 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等**。 ### 1.2 方法区 方法区是java jvm规范,具体实现 jdk 1.7 :永久代,jdk1.8:元空间 > 方法区是jvm规范的一个概念定义,每一个jvm都有自己的实现。存储着**类信息**、**常量**、**静态变量** 1. 在Java官方的HotSpot 虚拟机中,Java8版本以后,是用元空间来实现的方法区;在Java8之前的版本,则是用永久代实现的方法区; 2. 也就是说,“元空间” 和 “方法区”,一个是HotSpot 的具体实现技术,一个是JVM规范的抽象定义; 所以,并不能说“JVM的元空间是方法区”,但是可以说在Java8以后的HotSpot 中“元空间用来实现了方法区”。 这个元空间是使用本地内存(Native Memory)实现的,也就是说它的内存是不在虚拟机内的,所以可以理论上物理机器还有多个内存就可以分配,而不用再受限于JVM本身分配的内存了。 #### 1.2.1 常量池 * 常量池是方法区中的一部分,一个jvm只有一个常量池,线程共享 * 保存了在编译期间就已经确定的数据。包括final常量值(局部常量、成员常量以及引用常量)和对象字面值; * final常量:一切经过final关键字修饰的变量均为常量,final常量在定义时必须赋初值,否则编译不通过; * **Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值\[-128,127\] 的相应类型的缓存数据,Character创建了数值在\[0,127\]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象.** **两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。** ``` Integer i1 = 33; Integer i2 = 33; System.out.println(i1 == i2);// 输出 true Integer i11 = 333; Integer i22 = 333; System.out.println(i11 == i22);// 输出 false Double i3 = 1.2; Double i4 = 1.2; System.out.println(i3 == i4);// 输出 false ``` `Integer i1=40`;Java 在编译的时候会直接将代码封装成 `Integer i1=Integer.valueOf(40)`;,从而使用常量池中的对象。 `Integer i1 = new Integer(40);`这种情况下会创建新的对象。 ``` Integer i1 = 40; Integer i2 = new Integer(40); System.out.println(i1==i2);//输出 false ``` ``` Integer i1 = 40; Integer i2 = 40; Integer i3 = 0; Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0); System.out.println("i1=i2 " + (i1 == i2));//true System.out.println("i1=i2+i3 " + (i1 == i2 + i3));//true System.out.println("i1=i4 " + (i1 == i4));//false System.out.println("i4=i5 " + (i4 == i5));//false System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); //true 拆箱实际上等同40==40 System.out.println("40=i5+i6 " + (40 == i5 + i6)); //true ``` 语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。 #### 1.2.2 字符串常量池 常量池存在于方法区当中。 * **String s1 = new String("abc");这句话创建了几个字符串对象?** **将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。** ``` String s1 = new String("abc");// 堆内存的地址值 String s2 = "abc"; System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。 System.out.println(s1.equals(s2));// 输出 true ``` ## 2. 堆分代 1. 新生成的对象首先放到年轻代Eden区 2. 当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区 3. Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。 4. 老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。 Minor GC : 清理年轻代 Major GC : 清理老年代 Full GC : 清理整个堆空间,包括年轻代和永久代 **所有GC都会停止应用所有线程** ## 3.为什么会堆内存溢出? > **在年轻代中经过GC后还存活的对象会被复制到老年代中。当老年代空间不足时,JVM会对老年代进行完全的垃圾回收(Full GC)。如果GC后,还是无法存放从Survivor区复制过来的对象,就会出现OOM(Out of Memory)。** **OOM(Out of Memory)异常常见有以下几个原因:** 1)老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace 2)永久代内存不足:java.lang.OutOfMemoryError:PermGenspace 3)代码bug,占用内存无法及时回收。 OOM在这几个内存区都有可能出现,实际遇到OOM时,能根据异常信息定位到哪个区的内存溢出。 ## 4. 内存溢出排查方法 1 . 分析堆内存快照 可以通过添加个参数-XX:+HeapDumpOnOutMemoryError,让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。 2. Java工具 **jstat分析各块内存占用率和gc时间** ![](https://img.kancloud.cn/16/b1/16b1918061c06dd0d9384de8caef222c_860x301.png) 查询结果表明:这台服务器的新生代Eden区(E,表示Eden)使用了28.30%(最后)的空间,两个Survivor区(S0、S1,表示Survivor0、Survivor1)分别是0和8.93%,老年代(O,表示Old)使用了87.33%。程序运行以来共发生Minor GC(YGC,表示Young GC)101次,总耗时1.961秒,发生Full GC(FGC,表示Full GC)7次,Full GC总耗时3.022秒,总的耗时(GCT,表示GC Time)为4.983秒。 jmap查看对象存活状态,实例个数和占用内存 ``` jmap -histo:live 1002 ``` ![](https://img.kancloud.cn/dc/9a/dc9ad6c116faa38af80f9348b1803df9_813x411.png)