【JVM】运行时数据区
按照不同的数据分区进行存储(方法区、堆、栈、本地方法栈、程序计数器)。
程序计数器
作用:用来记录程序执行的指令的位置。
特点:内存空间小,运行速度快,是线程私有的(每个线程都有一个计数器),生命周期与线程同步,不会出现内存溢出,没有垃圾回收。
虚拟机栈
栈是运行单位,管理方法(Java自己写的方法)的运行。调用方法入栈,执行结束出栈(口语描述:一个方法对应一个栈帧,如果方法A中调用了方法B,那么方法B入栈,成为当前栈帧,也就是处于运行状态,运行结束后出栈,方法A成为当前栈帧)。栈是线程私有的。没有垃圾回收,存在内存溢出的可能。
栈溢出错误:
package javaException;
public class Demo {
/*
常见的异常:
Error
java.lang.OutOfMemoryError 内存溢出错误,程序无法解决
java.lang..StackOverflowError 栈溢出错误,程序无法解决
*/
public static void main(String[] args) {
System.out.println(sum(1000000)); // java.lang..StackOverflowError
}
public static int sum(int n ) {
if (n == 1) {
return 1;
}
return n + sum(n - 1);
}
}
栈帧的构成
每个栈帧中存储着:
局部变量表(Local Variables)
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。
操作数栈(Operand Stack)(或表达式栈)
栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
方法返回地址(Retuen Address)(或方法正常退出或者异常退出的定义)
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
public void test() {
int a = 10;
int b = 5; // 局部变量表
int c = a++ + b; // 运算 操作数栈
//记录被调用方法的位置
}
本地方法栈
管理运行本地方法的地方。
本地方法
native 关键字修饰的方法,没有方法体,它是C语言写的、系统库中提供的方法。例如 Object 中的 hashCode()、getClass()、clone()、notify()、notifyAll()、wait(long timeout);Java语言是没有权力直接操作硬盘的,必须要经过操作系统,比如 FileInputStream 里面的 read0() 就是一个本地方法;Thread 中的 start0()等等······
是线程私有的。没有垃圾回收,存在内存溢出的可能。
堆
存放程序中产生的对象。是运行时数据区最大的一块空间,大小可以调节。线程是共享的。存在内存溢出的可能,会进行垃圾回收,并且是垃圾收集器重点回收区域。
堆空间分区
新生代:
伊甸园区:存放刚创建的对象。
幸存者1区:存放伊甸园区和另一个幸存者区经过垃圾回收后存活下来的对象。
幸存者2区:两个幸存者区可以交替使用,始终有一个区域是空闲的。
老年代:
存放生命周期长的/非常大的对象。经历过15次垃圾回收后依然存活的对象,就会被存放在老年代。
为什么要分区(代)?
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
对象创建以及在内存分布过程
1.新创建的对象存放到伊甸园区。
2.垃圾回收时,将伊甸园区存活的对象移入到幸存者0区。
3.程序继续运行,再次创建的对象还是保存到伊甸园区。
4.下一次垃圾回收到来时,将伊甸园区和幸存者0区存活的对象移入到幸存者1区,交替反复执行。
5.当一个对象经历过15次垃圾回收后仍然存活,那么就将此对象移入到老年代。在对象头中有4个 byte 位用来记录回收次数。回收次数可以设置,但是最大值是15。
Minor GC:是针对新生代进行的垃圾回收。频繁回收新生代。
Major GC:是针对老年区进行的垃圾回收。较少回收老年代。
FULL GC:整堆收集,实际开发中要尽量避免整堆收集。什么时候会触发整堆收集,这个我们后面再说。
空间比例
老年代和老年代比例 2 : 1
伊甸园区和两个幸存者区比例 8 : 1 : 1
堆空间的参数设置
JVM调优就是根据程序实际运行的需要设置参数,调整各个区间比例大小。
官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial 查看所有参数的默认初始值
-Xms:初始堆空间内存
-Xmx:最大堆空间内存
-Xmn:设置新生代的大小
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的 GC 处理日志
加入参数,Apply,然后ok:
运行,就能查看所有参数的默认初始值:
上面的演示示例只是在临时运行某个文件的时候可以调一调,看一看。正常情况肯定是在 JVM 的配置文件里面修改。
方法区
主要存储加载到虚拟机的类信息,大小可以调节,是线程共享的,存在内存溢出的可能。
方法区大小设置
Java 方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整。
元数据区大小可以使用参数 -XX:MetaspaceSize 指定。
方法区一旦沾满 就会触发 Full GC,因此为了减少 FullGC,可以给 -XX:MetaspaceSize 设置一个较高的值。
方法区的垃圾回收
方法区是存在垃圾回收的,只不过条件比较苛刻,一般情况下也可以认为类是不会被卸载的,需要同时满足3个条件,类才可以被卸载:
1.该类的所有对象以及子类对象都不存在。
2.加载该类的类加载器不存在了。
3.该类的 Class 对象没有被其他地方引用。