JVM内存模型图
-
栈:存放基本数据类型和引用(可理解为一个地址),实例方法。主要异常会有(StackOverFlowError,OutOfMemoryError).
线程私有,生命周期与线程相同,每个方法执行时会创建一个栈帧结构。用于存放局部变量表,操作数栈,动态链接,方法出口。 -
堆:(Heap)存放对象实例,所有对象的内存地址在此分配。
它是线程共享的,生命周期与虚拟机相同。主要异常有(OutOfMemoryError) -
程序计数器:线程私有,特点:占用内存小,记录的是线程执行的行号,负责分支,循环,跳转,异常,线程恢复等
-
方法区:线程共享,存储类加载信息(Class对象),常量,静态变量,主要异常(OutOfMenmoryError)
-
本地方栈:为虚拟机使用到的native方法服务,主要异常会有(StackOverFlowError,OutOfMemoryError).
当有native关键字时,表明java的作用区域达不到了,会去调用底层的C语言的方法库。 即进入本地方法栈,调用本地方法接口(JNI),JNI作用:拓展java的使用,融合不同的编程语言为java所用。 -
虚拟机栈:为虚拟机执行java 方法(字节码)而服务。
-
运行时常量区:属于方法区的一部分,存放编译期生成的各种字面量和符号引用。
举个例子:
public class Math {
public static final Integer COASTAT = 666;
public int compute() {//一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
Math math2 = new Math();
math2.compute();
System.out.println("test");
}
}
查看jvm指令 Javap -v Mathclass.
上图对应的是compute()方法中对应的代码块执行步骤。
iconst_1 将int类型常量1压入栈,
程序计数器加1,istore_1将int类型值存入局部变量1(也就是a中),
iconst_2 将int类型常量2压入栈 ,
istore_2将int类型值存入局部变量2(也就是b)中,
以此类推,int c = (a + b) * 10;这一步对应先做加法,再将3存入操作数栈,: bipush 10将10压进操作数栈,
imul 做乘法运算,得到30,
istore_3 将int类型值存入局部变量3(也就是c),
iload_3 从局部变量3中装载int类型值c=30
动态链接:
栈帧中保存了一个引用,相当于C语言中的指针,指向该方法在运行时常量池中的位置,通过运行时常量池的符号引用(指向堆),完成将符号引用转化为直接引用。
#4地址在常量池存放,在执行compute()方法时,是通过#4地址调用Class类的compute()方法、
堆区分类
堆区新生区主要有伊甸园区(Eden),幸存0区和幸存1区,也叫作From区和To区,
老年代(jdk8叫做元空间):这里的对象一直有被引用,在幸存区经过15次full gc 操作之后一直有指针指向,eg:静态变量,数据库连接池对象等。
在创建对象时, 对象优先在Eden区分配
大多数情况下,对象会在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机(执行引擎)会发起一次 Minor GC。Minor GC相比Major GC更频繁,回收速度也更快。通过Minor GC之后,Eden区中绝大部分对象会被回收,而那些存活对象,将会送到Survivor的From区(若From区空间不够,则直接进入Old区) 。
== 注意==:
- Minor GC是清理年轻代
- Major GC 是清理老年代。
- Full GC 是清理整个堆空间—包括年轻代和老年代。
Survivor区
Survivor区相当于是Eden区和Old区的一个缓冲,Survivor又分为2个区,一个是From区,一个是To区。每次执行Minor GC,会将Eden区中存活的对象放到Survivor的From区,而在From区中,仍存活的对象会根据他们的年龄值来决定去向。(From Survivor和To Survivor的逻辑关系会发生颠倒: From变To , To变From,目的是保证有连续的空间存放对方,避免碎片化的发生)
Survivor区存在的意义
如果没有Survivor区,Eden区每进行一次Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次Minor GC没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。所以,Survivor的存在意义就是减少被送到老年代的对象,进而减少Major GC的发生。Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。
举个例子:模拟栈内存爆满的情况
package com.ztb.entity;
import java.util.ArrayList;
public class HeapTest {
byte[] a= new byte[1024*100];//创建一个100kb的数组
public static void main(String[] args) throws InterruptedException {
ArrayList<HeapTest> heapTests=new ArrayList<>();
while(true){//死循环,模拟栈内存爆满
heapTests.add(new HeapTest());//都是有引用的对象,不会被GC释放
Thread.sleep(50);//休眠 防止系统卡死
}
}
}
打开JavaVisal VM查看Java进程
对象刚开始创建时存放在伊甸园区,内存满后做Minor GC,释放空间。但是这个程序中的对象都是有引用的,所以会在幸存区堆积 ,经过十五次FULL GC之后,老年代中会一直有堆积对象,直到栈内存爆满。程序抛出StackOverFlowError异常。
总结:JVM调优的目的
1.== 将转移到老年代的对象数量降低到最小,减少fullGC的次数==
2.== 减少full GC的执行时间==
原因:
做full GC时,会停掉当前应用线程的执行,影响程序的性能
打印GC日志,一个程序在几秒中之内就做了几次FULLGC,都是由于元空间不足导致的,很有可能是加载的类太多或者静态变量太多。
调优思路:在java程序启动之前,就更改元空间的设置。