java内存区域
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存
程序计数器:记录当前线程执行的位置 当线程切换后能够知道该线程上次运行到哪了
java虚拟机栈:
每个线程在创建时都会创建一个虚拟机栈 保存一个个的stack frame,它保存方法的局部变量、部分结果,参与方法的调用和返回
通过参数-Xss设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
方法调用的数据通过栈进行传递,每次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,会有一个栈帧被弹出
也就是说,在这个线程上正在执行的每个方法都各子有对应的一个栈帧
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
栈由一个个栈帧组成,每个栈帧包括:局部变量表、操作数栈、动态连接、方法返回地址
栈帧如何工作
当前位于栈顶的栈帧是有效的,这个栈帧被成为当前栈帧,它对应的方法是当前方法,定义这个方法的类就是当前类
如果方法调用了另外一个方法,那么一个新的栈帧就会被创建出来放在栈顶,成为新的当前帧
不同线程中包含的栈帧没有办法相互引用,即多线程时不可能引用另外一个线程的栈帧
方法返回的时候,栈顶的栈帧被弹出,结果交给前一个栈帧
正常的return和抛出异常都会使得栈帧被弹出
栈帧的内部结构
局部变量表
Local Variables 主要存放了编译器可知的各种数据类型、对象引用
基本数据类型:boolean、byte、char、short、int、float、long、double
对象引用:reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其它与此相关的位置
局部变量表是线程的私有数据,不存在线程安全问题
局部变量表中的变量只在当前方法调用中有效
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
操作数栈
Operand stack 主要作为方法调用中的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中
动态连接
Dynamic linking 服务于一个方法需要调用其它方法的场景。用于将符号引用转换为调用方法的直接引用。
每一个栈帧内部包含一个指向运行时常量池中该栈顶所属方法的引用。
当java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在Class文件的常量池中,比如:描述一个方法调用了另外的方法,就是通过常量池中指向方法的符号引用来表示的
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
JVM如何执行方法调用?
一切方法调用在Class文件中都是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关。
- 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的方法在编译期可知,且运行期保持不变时,这种情况下将符号引用替换为直接引用的过程称为静态链接
- 动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能在程序运行期间将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接
对应方法的绑定机制为:早期绑定、晚期绑定。绑定时一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅只发生一次
方法返回地址
存放调用该方法的PC寄存器的值
java方法有两种返回方式,一种是return,一种是抛出异常,不论怎么返回,栈帧都会弹出。即栈帧随着方法调用而创建,随着方法结束而销毁。
OutOfMemoryError:HotSpot虚拟机的栈容量并不能动态扩展,线程申请栈空间只要成功了就不会OOM,但如果申请失败会抛出OOM
本地方法栈
虚拟机栈为虚拟机执行java方法服务,本地方法栈则为虚拟机使用到的Native方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。
总结:栈是运行时的单位,堆是存储的单位
堆
堆是java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。
几乎所有的对象实例以及数组都在这里分配内存
java堆时垃圾收集器管理的主要区域,因此也被称作GC堆。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以java堆还可以细分为:新生代和老年代
在jdk7及以前堆内存分为 新生代 老生代 永久代
在jdk8之后 永久代被元空间取代
大部分情况 对象首先在Eden区域分配,再一次新生代垃圾回收后,如果对象还存活,则会进入S0或S1,并且对象的年龄还会加1(Eden到Survivor区后对象的初始年龄变为1)
当它的年龄增加到一定程度(默认为15),就会被晋升到老年代
老年代垃圾回收称为主GC(Major GC),通常需要更多的时间
堆这里最容易出现的就是OutOfMemory错误
1、java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
JVM花大量时间执行垃圾回收并且只能回收很少的堆空间时
2、java.lang.OutOfMemoryError: Java heap space
创建新的对象时,堆内存中的空间不足以存放新创建的对象
如何设置堆内存大小
-Xms:表示堆的起始内存
-Xmx:表示堆的最大内存
默认情况下 初始堆的内存大小为电脑内存大小/64 最大堆内存大小为电脑内存大小/4
默认情况下,新生代和老年代的比例为1:2 通过 -xx: NewRatio
对象在堆中的生命周期
当创建一个新对象时,对象会被优先分配到新生代的Eden区
当Eden空间不足时,JVM执行Minor GC
JVM会把存活的对象转移到Survivor中,并且对象年龄+1
每经历一次MinorGC 对象的年龄就会+1
如果分配的对象超过了 -XX PetenureSizeThreshold 对象会直接分配到老年代
方法区
方法区属于时JVM运行时数据区域的一块逻辑区域
设置元空间:
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
方法区只是JVM规定的一个逻辑概念,用于存储类信息,常量池,静态变量等数据。并没有规定如何去实现它。不同的厂商有不同的实现方式
JAVA8中没有永久代了,取而代之的是元空间,元空间存在于本地内存,不受垃圾回收器管理
因此在物理上元空间并不是堆的一部分,虽然逻辑上它是堆的一部分
运行时常量池
Runtime Constant Pool是方法区的一部分
一个有效的.class文件中出了包含类的版本信息,字段,方法以及接口等描述信息外,还有一项信息就是常量池表,包含各种字面量和对类型、域、方法的符号引用
将编译后的类信息放入方法区中,用来动态获取类信息:class文件元信息描述、编译后的代码数据、引用类型数据、类文件常量池等
JVM为每个已记载的类型(类、接口)维护一个常量池
字符串常量池
为字符串专门开辟的一块区域,用于避免字符串的重复创建
字符串常量池为什么在堆里? – 方法区实现的gc回收效率太低,但字符串gc回收的频率非常高
直接内存
直接内存是一种特殊的内存缓冲区,并不在java堆或方法区中分配的。而是在本地内存中。
虚拟机对象创建过程
1、类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过,没有的话先执行相应的类加载过程
2、内存分配
为新生对象分配内存,对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来
3、初始化零值
将分配到的内存空间都初始化为零值
4、设置对象头
对对象进行必要的设置,如对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的gc分代年龄等信息。
5、执行init方法
对java来说,此时对象创建才刚刚开始,还需要执行init方法