9. 栈
栈:8大基本类型+对象引用
栈运行原理:栈帧
程序正在执行的方法,一定在栈的顶部
9.1 JVM数据区
先上一张Java虚拟机运行时数据区中堆、栈以及方法区存储数据的概要图,如下所示:
9.2 堆
堆是存储时的单位,对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。
9.3 栈
栈是运行时的单位,Java 虚拟机栈,线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:他代表的
值就是数值本身;而引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象
本身存放在这个引用值所表示的地址的位置。
基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress
**引用类型包括:类类型,**接口类型和数组。
9.4 栈和堆的对比
- 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
- 栈因为是运行单位,因此里面存储的信息都是跟当前线程相关的信息。包括:局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
- 在方法中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。堆内存用于存放由new创建的对象和数组。
- 在Java中一个线程就会相应有一个线程栈与之对应,这点保证了程序的并发运行。而堆则是所有线程共享的,也可以理解为多个线程访问同一个对象,比如多线程去读写同一个对象的值。
- 栈内存溢出包括StackOverflowError和OutOfMemoryError。StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存;堆内存溢出是OutOfMemoryError。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出OutOfMemoryError异常。
9.5 堆栈分离的好处
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可
以动态变化的,但是在栈中,一个对象只对应了一个4btye的引用
9.6 为什么不把基本类型放堆中呢?
- 因为其占用的空间一般是1~8个字节——需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况——长度固定,因此栈中存储就够了,如果把他存在堆中是没有什么意义的
- 可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。
对象实例化(重点)
https://www.cnblogs.com/java-zzl/p/9899856.html
从字节码、执行步骤两个方面分析
1、字节码方面:
Object object = new Object();
,通过javap -verbose -p
查看对象创建的字节码指令:
- new:如果找不到Class对象就进行类加载,然后分配内存(本类路径上所有的属性都分配),其中对象的引用也是个变量也占内存(4个字节),这个指令执行完毕会把对象的压入虚拟机栈顶。
- dup:在栈顶复杂引用,如果有参数,把参数压入操作栈,两个引用,压入栈底的用来赋值或保存到局部变量表中,栈顶引用作为句柄调用相关方法。
- invokespecial:调用对象实例化方法,通过栈顶方法调用方法(也就是调用构造方法)。
2、执行步骤
- **确认类元信息是否存在:**接到new指令时,在metaspace检查类元信息是否存在,没有就在双亲委派模式下进行类加载,生成Class对象。
- **分配对象内存:**首先计算对象占用空间大小(成员变量是引用变量就分配4个字节大小的变量空间),在堆中划分内存空间给新对象(分配空间需要进行同步操作,如:cas)。
- **设定默认值:**成员变量设置不同形式的0值;
- **设置对象头:**设置对象的哈希码、锁信息、对象所属的类元信息,设置取决于JVM。
- **执行init方法:**初始化成员变量,执行代码块,调用类的构造方法,把堆对象首地址赋值给引用变量。
10. 堆
《Java 虚拟机规范》中对 Java 堆的描述是:所有的对象实例都应当在运行时分配在堆上,方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
Heap,一个JVM 只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把什么东西放到堆中?
类的实例、方法、常量、变量~,保存我们所有引用类型的真实对象
堆内存中还要细分为三个区域:
- 新生区 (伊甸园区) Young/New
- 养老区 old
- 永久区 Perm
1.GC 垃圾回收主要是在伊甸园区和养老区~
2.假设内存满了,OOM ,堆内存不够!
3.在JDK 8以后,永久存储区改了个名字(元空间)
10.1 新生区、老年区、永久区
新生区(伊甸园+幸存者区*2)
- 类诞生和成长甚至死亡的地方;
- 伊甸园,所有对象都是在伊甸园区new出来的!
- 幸存者区(0, 1),轻GC定期清理伊甸园,活下来的放入幸 存者区,幸存者区满了之后重GC清理 伊甸园+幸存者区,活下来的放入养老区。都满了就报OOM。
真理:经过研究,99%的对象都是临时对象!直接被清理了
老年区: 新生区剩下来的,轻GC杀不死了。
永久区:
这个区域常驻内存,用来存放JDK自身携带的Class对象,Interface元数据,存储的是java运行时的一些环境或类信息,该区域不存在垃圾回收GC。关闭虚拟机就会释放这个内存。
- jdk1.6之前:永久代,常量池在方法区
- jdk1.7:永久代,但是慢慢退化了(去永久代)常量池在堆中
- jdk1.8之后:无永久代,常量池在元空间
一句话:常量池一直在方法区,其中的字符串池 JDK1.7之后保存到了堆中。
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直
到内存满,就会出现OOM。
方法区又称非堆(non-heap),本质还是堆,只是为了区分概念。元空间逻辑上存在,物理上并不存在。
面试题:报OOM怎么办?
1.尝试扩大堆内存,如果还报错,说明有死循环代码 或垃圾代码
2.分析内存,看一下哪个地方有问题(专业工具)
扩大内存方法:
Edit Configration>add VM option>输入:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
10.2 为什么分区(代)?
- 将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及 GC 频率。
- 针对分类进行不同的垃圾回收算法,对算法扬长避短。
10.3 一个对象怎么从新生代变成老年代(重点)
JVM新创建的对象(除了大对象外)会被存放在新生代,默认占1/3堆内存空间,由于JVM频繁的创建对象,所以新生代会频繁触发Minor GC进行垃圾回收。新生代又分为Eden区,survivorForm区和survivorTo区。
- 当伊甸园的空间填满时,程序又需要创建对象时,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁.再加载新的对象放到伊甸园区
- 然后将伊甸园区中上一次垃圾回收的幸存者移动到幸存者 0 区(From)。
- 如果再次触发垃圾回收,此时上次幸存下来存放到幸存者 0 区的对象,如果没有回收, 就会被放到幸存者 1 区(To)。
- 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区,对象在幸存者区中每熬过一次MinorGC,年龄就加一。
- 什么时候去养老区呢?对象的年龄达到老年代标准,默认是 15 次,也可以设置参数(XX:MaxTenuringThreshold),最大值为 15
- 在老年区,相对悠闲,当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理.
- 若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常:Java.lang.OutOfMemoryError:Java heap space
10.4 总结:
- 大对象直接进入老年代:
目的就是为了防止大对象在Eden空间和Survivor空间来回大量复制。
- 长期存活的对象进入老年代:
对象在Survivor区中每熬过一次Minor GC,年龄就加一,当他的年龄增加到一定程度,就会被移动到老年代(年龄值默认为15)
- 动态年龄判断并进入老年代:
如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到MaxTensuringThreshold的要求年龄。
10.5 Jprofiler
在一个项目中,突然出现了OOM故障,该如何排除,研究为什么出错~
能够看到代码第几行出错:内存快照分析工具,MAT,Jprofiler
Debug,一行行分析代码!
MAT,Jprofiler作用:
分析Dump内存文件,快速定位内存泄漏;
获得堆中的数据
获得大的对象(大厂面试)
…
//-Xms 设置初始化内存分配大小 默认1/64
//-Xmx 设置最大分配内存,默认1/4
//-XX:+PrintGCDetails 打印GC垃圾回收信息
//-XX:+HeapDumpOnOutOfMemoryError oom DUMP
//-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
public class Demo03 {
byte[] array = new byte[1*1024*1024]; //1m
public static void main(String[] args) {
ArrayList<Demo03> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new Demo03()); //不停地把创建对象放进列表
count = count + 1;
}
} catch (Exception e) {
System.out.println("count: "+count);
e.printStackTrace();
}
}
}