一、基础
栈帧(Stack Frame)栈空间的 基本元素,用于 方法的调用和方法的执行的数据结构
堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。
String是一个特殊的包装类数据。可以用: String str = new String("abc");String str = "abc";两种的形式来创建,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。而第二种是先在栈中创建一个对String类的对象引用变量str,然后查找栈中有没有存放"abc",如果没有,则将"abc"存放进栈,并令str指向"abc",如果已经有"abc" 则直接令str指向"abc"。比较类里面的数值是否相等时,用equals()方法;
程序计数器:指向当前线程所指向的字节码指令的(地址)行号。
本地方法栈:存放方法名有native修饰,public static native void sleep(long millis) throws InterruptedException
栈帧和方法的关系:每一个方法的执行都对应了一个栈帧入栈到出栈的过程。
什么时候分配栈帧的内存?分配多少内存?
在编译代码的时候栈帧中需要多大的局部变量表,多深的操作数据栈都已经完全确定了,因此一个栈帧需要分配多少内存,不会受程序运行期数据的影响,只取决于虚拟机的实现。
二、图解
2.1 栈帧详解
2.1.1 局部变量表(Local variable Table): 主要关注的栈内存,就是JVM栈中的局部变量表的部分。
局部变量表(Local variable Table)是一组储值空间,用于存放方法参数和方法的内部定义的局部变量,并且在Java编译为.class文件的时候就分配了该方法所需要的局部变量表的最大容量.
2.1.2 变量槽(Variable Slot)
是局部变量表容量的最小单位 4字节 32 位长度(4* 8) ;
blloean ,byte ,char short,int float ,【refrence】,double 和 long 8字节型需要2个Slot空间.
【reference】引用地址:一般来说虚拟机都能从直接引用或者间接引用中查找对象一下2点
在堆区存放的数据的开始索引
数据类型在方法区的数据类型
2.1.3 实例
方法执行时候,虚拟机使用局部变量表完成参数的传递,如果执行的方法是实例对象的方法,局部变量的0索引(比如x00001)就是在堆区对象实例的引用(通过this可以访问到这个地址)
其他参数按照顺序排列。
2.1.4 Slot复用
为了节省空间,Slot是可以复用的,也就是PC计数器的指令指已经超出了某个变量的作用域,(执行完毕)那么这个变量对应的Slot就会给其他变量使用,
优点:节省栈帧空间
缺点:影响垃圾回收:如果有大方法占用比较多的Slot,然后又不及时清除,或者设置为null,垃圾回收器就不能回收该内存.
2.1.5 动态连接(Dynamic Link)每个帧都包含一个指向运行时常量池中该帧所属于方法的引用,持有这个引用是为了支付方法调用过程中的动态连接。
静态解析:在类的加载的阶段的解析2.3阶段会将符号(PI)转化为直接引用(0x001),这种转化也成为静态解析。
动态连接:另外一部分,在每次运行的时候将符号转化为直接引用,这部分称之为动态连接
2.1.6 方法返回地址(父帧)1.执行方法返回的2种方法:
1.正常退出:执行到return ,就退出方法2.异常退出:发生异常且未处理,
2.无论采用 哪一种方法,在退出后都需要返回之前方法调用的位置,就是父帧
一般来说,方法正常退出时候,PC计数器的值可以作为放回的地址,栈帧中会保存这个计数器中的值,但是方法异常退出时候,返回的地址通过异常处理器表来确定的,栈帧中一般不会保存这部分的信息。
2.1.7 操作数栈:
操作数栈 和 局部变量表一致
编译为class就确定大小
Slot为基础储存单位
作用:当一个方法开始时候,方法数栈始空的,执行中,各种字节码文件指令往操作数栈中读取内容和写入内容,入栈和出栈的操作
比如算术运算就是通过操作数栈来进行的,或者调用其他方法时候是用操作数栈来传递参数的
三、 栈溢出模拟 SO(StackOverflowError)
public class Demo {
public static void main(String[] args) {
new Demo().a();
}
private void a(){
b();
}
private void b(){
a();
}
}
调试运行栈帧截图:
方法每次调用都会新增一个栈帧,a() 方法和 b()循环调用导致栈内存暴满。
方法的递归调用同理
四、堆Heap
新生区:
新生区是类的诞生、成长、消亡的区域,
一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区又分为两部分:伊甸区(Eden Space)和幸存者区(Survivor space),所有的类都是在伊甸区被new出来的。
幸村区有两个:0区(Survivor 0 space)和1区(Survivor 1space)。
当伊甸园的空间用完时,程序又需要创建对象,Jvm的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园区中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。
那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常"OutOfMemoryError"。
如果出现java.lang.OutOfMemoryError:Java heap space异常,说明Java虚拟机的对内存不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
Java堆从GC的角度还可以细分为:新生代(Eden区、From Survivor区和To Survivor区)和老年代。
MinorGC的过程(复制->清空->互换),其中,Eden:From:To = 8:1:1
1:eden、SurvivorFrom复制到survivorTo,年龄+1 首先,把Eden和SurvivorFrom区域中存活的对象复制到SurvivorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果SurvivorTo不够位置了就放到老年区)
2:清空eden、SurvivorFrom 然后,清空Eden和SurvivorFrom中的对象
3:SurvivorTo和SurvivorFrom互换 最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时SurvivorFrom区
Java1.8之后将最初的永久代取消了,由元空间取代。
在Java8中,永久代已经被移除了。被一个称为元空间的区域所取代。元空间的本质和永久代类似。
元空间与永久代之间最大的区别在于:
元空间并不在虚拟机中而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据当如native memeory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不在由MaxPermSize控制,而由系统的实际可用空间来控制。
通过下面实例可以观察Heap 中新生区[eden->surviorFrom0->surviorFrom1]->老年代区[old] 内存变化到内存报错
报错:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
public class HeapOom {
private byte[] b = new byte[1024];
public static void main(String[] args) throws InterruptedException {
ArrayList<HeapOom> list = new ArrayList<>();
while(true){
list.add(new HeapOom());
Thread.sleep(10);
}
}
}
堆内存空间调整参数
参数名称 描述
-Xms 设置初始分配大小,默认为物理内存的1/64
-Xmx 最大分配内存,默认为物理内存的1/4
-XX:+PrintGCDetails 输出详细的GC处理日志
-XX:+PrintGCTimeStamps 输出GC的时间戳信息
-XX:+PrintGCDateStamps 输出GC的时间戳信息(以日期的形式,如2019-09-15T16:24:24.155+0800)
-XX:+PrintHeapAtGC 在GC进行处理的前后打印堆内存信息
-Xloggc:保存路径 设置日志信息保存文件