java程序在运行时会它会把它所管理的内存划分为多个区域,各有各的用途。下面我会一一解释这些比较重要的区域。
一,程序计数器
定义:
(Program Counter Register)是一块较小的区域,它是线程私有的(即每个线程都有一个独立的程序计数器,各线程之间的程序计数器互不影响,独立存储)。PC通过寄存器实现,因此读取比较快。
所谓计数就是通过这个计数器的值来记录下一条要执行的字节码指令的地址。
如一个简单的java程序反编译后的结果为上图,每一行就是一个jvm指令,前面是这个指令对应的地址。如在运行时当前执行的指令为第0行,则程序计数器里面存的是第3行的这个地址(当然不仅仅是3,在运行时会进行处理变为内存地址),然后再执行3,程序计数器继续指向5……
再如:在多线程下,当前线程时间片用完了,他肯定要保存当前执行到哪条指令,下次获得cpu时间片的时候还要接着执行的。
分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
它还是五个区域唯一一个不会内存溢出的区域。
二,虚拟机栈
定义:
也是线程私有的,每个线程都有一个虚拟机栈,在线程内,每个方法被执行的时候,java虚拟机都会创建一个栈帧(Stack Frame),用于存储对应方法的局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法执行完了,就会出栈。
代码演示:
public static void main(String[] args) {
f1();
}
public static void f1(){
f2();
}
public static void f2(){
f3();
}
public static void f3(){
}
}
如上的代码,main方法中调用了f1方法,f1方法又调用了f2方法,f2调用了f3方法,图如下
此时的状态为当前线程正在f1方法里,还未调用f2方法。用idea的debug工具可以看到
也是一个栈的结构,与之对应 。方法执行顺序为:main方法入栈->f1方法入栈->f2方法入栈->f3方法入栈->f3方法出栈->f2方法出栈->f1方法出栈->mian方法出栈
特点:
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧组成,对应着每次方法的调用时所占用的内存
- 每个线程只有一个活动栈帧,对应着当前正在执行的那个方法,栈顶即栈帧
- 栈不涉及到垃圾回收的处理
使用参数
-Xss
可以改变栈的大小,通过下面的步骤
来设置虚拟机参数(idea)
但是栈的大小并不是越大越好,当你让虚拟机栈的空间变大了,那么对应的最大线程数就会变少。
假设物理内存为500M,栈大小为1M,此时可以开500个线程,但是你设置栈大小为2M,只能开250个线程了。
栈内存溢出:
java.lang.StackOverflowError(深度大于虚拟机所允许的深度)
-
帧栈过多导致栈内存溢出,如无限调用递归,一直入栈但没有出栈,比如我们递归调用,没有设置终止条件的话,即使你栈空间再大,也会溢出。
-
栈帧过大导致栈内存溢出,如有一个方法存放的东西太多了,一个栈帧都放不下。
三,本地方法栈
(Native Method Stacks),java语言自身有一定的限制,不能和操作系统直接进行通信,需要用到C系语言编写的本地方法执行更底层的操作。如hashCode方法就是一个本地方法
public native int hashCode();
本地方法栈就是给本地方法提供内存。
四,堆
特点:
Heap,内存区域最大的一块内存,通过new关键字创建对象就会使用到堆内存。
- 堆是垃圾回收的主要区域。
- 堆是线程共享的,每个线程需要时就在堆的空间中挖一块区域存放本线程的数据,所以需要考虑线程安全的问题。
-
堆中可以划分出多个线程私有的 本地线程分配缓冲区(TLAB),提升对象分配时的效率
-
堆可以处在物理上不连续的内存空间中,但在逻辑上应该被视为连续的
堆内存溢出:
使用-Xmx设置
堆的最小值,使用-Xms设置
堆的最大值。
public static void main(String[] args) {
ArrayList<String> a=new ArrayList<>();
int count=0;
String s="hello";
try {
while (true){
a.add(s); //hello 加入到了list集合中,所以hello不能被回收掉
s+=s; //hellohello……
count++;
}
}catch (Throwable throwable){
throwable.printStackTrace();
System.out.println(count);
}
}//结果为:java.lang.OutOfMemoryError: Java heap space
五,方法区
定义:
(Method Area),同样是线程共享的区域存储,存放类的一些信息,如构造器,成员变量,方法,常量,静态变量,类型信息。反正和对象无关和类有关的都存放在方法区。
- 方法区在虚拟机启动时被创建。它是堆的一个逻辑部分(到底在不在堆这要看具体的jvm厂商怎么实现),但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
- 像永久代(Hotspot虚拟机jdk1.8之前的方法区实现)和元空间(1.8之后的实现)这种实现。元空间并不在虚拟机中,而是使用本地内存。
- 可以通过
-XX:MetaspaceSize
设置默认 和-XX:MaxMetaspaceSize
最大值 配置内存大小 -
运行时常量池(Runtime Constant Pool)是方法区的一部分。用于存放编译期生成的各种字面量和符号引用,受方法区内存的限制,当常量池无法再申请到内存时也会抛出 OutOfMemoryError 异常。
-
方法区的垃圾回收主要是针对常量池的回收和堆类型的卸载。
内存溢出:
里面的一些代码会在ClassLoader时讲解,现在只需知道他们的作用是,生成多个Class类,并把它加载到内存里,即存放到了元空间里,元空间放不下了,报OOM异常。需要加虚拟机参数,
//用来加载类的二进制字节码
public class test extends ClassLoader{
//-XX:MaxMetaspaceSize=10m
public static void main(String[] args) {
int j=0;
try {
test test=new test();
for (int i = 0; i < 10000; i++,j++) {
//ClassWriter作用是生成类的二进制字节码
ClassWriter cw=new ClassWriter(0);
//版本号,修饰符,类名,包名,父类,接口
cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
//返回byte[]
byte[] code=cw.toByteArray();
//执行了类的加载,生成Class对象
test.defineClass("Class"+i,code,0,code.length);
}
}finally {
System.out.println(j);
}
}
// 输出结果:
//3331
//Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space
spring和mybatis进程用到CGLib动态代理,就会生成很多类,需要注意。
关于方法区的常量池也是一个比较重要的概念,因为篇幅影响,在下一章继续讲解。