内存划分
JAVA虚拟机会在执行java程序过程中把其管理的内存划分为若干个不同的数据区域。
为什么方法区和堆是所有线程共享的?
而为什么程序计数器,栈是线程隔离的?
程序计数器
实质:一块小的内存空间
作用:当前线程执行的字节码的行号指示器,决定下一条执行的指令
由于多线程是通过线程轮流切换并且分配处理器执行时间来实现的,任何时刻一个处理器只会执行一条线程的指令,因此为了线程切换后能够恢复到正确的执行位置,每个线程都要有一个独立的PC
如果线程执行JAVA方法: PC记录的是执行指令的地址
执行Native方法: 计数器值为空,此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域
VM stack
与程序计数器一样,Java虚拟机栈也是线程私有的,生命周期与线程相同。
实质:描述java方法执行的内存模型
作用:每个方法执行的时候创建一个栈帧,每一个方法调用到执行完成过程,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
我们常把内存划分为heap与stack,实质上这里的栈就是指虚拟机栈中的局部变量表部分。这个部分所需的内存空间是编译期间完成分配的。
StackOvetflowError
线程请求的栈深度大于虚拟机所允许的深度
OutOfMemoryError
虚拟机可以扩展,如果扩展申请不到足够的内存就会抛出此异常
本地方法栈
与虚拟机栈类似,不过这是为虚拟机所使用到的Native方法服务的。
对了,什么叫本地方法?
简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C
Java堆
- Java虚拟机中管理内存最大的一块
- 所有线程共享,虚拟机启动的时候创建
- 作用是存放对象实例
所有对象实例以及数组都要在堆上分配内存
- 垃圾收集管理的主要区域
- 可以处在物理上不连续的内存空间,只要逻辑上是连续的即可
方法区 (Non-heap 非堆)
- 各个线程共享的内存区域,用于存储被虚拟机加载的各类信息,常量,静态变量,编译器编译后的代码等数据。
运行时常量池
- 方法区的一部分,用于存放编译期生成的各种字面量与符号引用,类加载完毕后这些内容会存放在运行的常量池中
直接内存
本机直接分配内存不会受到java 堆大小的限制。
既然是内存,肯定还是会受到本机总内存的大小以及处理器寻址空间的限制。
对象访问
Object obj = new Object();
Object obj 这部分将会反映到Java栈的本地变量表中,作为一个reference类型数据出现。
new Object()会反映到Java 堆中,形成一块存储了Object类型所有实例数据值的结构化内存。
Java堆中还要包含能够查到此对象类型数据(对象类型,父类,实现接口,方法等)的地址信息,这些类型数据存储在方法区中。
如何从引用类型访问具体的对象呢?
使用句柄
- Java堆划分一块内存作为句柄池
- reference 存储对象的句柄地址
- 句柄包含了对象实例和类型数据各自的具体地址信息
使用直接指针访问
Java堆对象的布局必须考虑如何放置访问类型数据的相关信息。
reference直接存储的就是对象地址
两种访问方式比较
1.句柄访问:reference中存储的是稳定的句柄地址,对象被移动的时候只会改变句柄中的实例数据指针,reference本身不改变
2.指针访问: 节省了一次指针定位的时间开销,速度更快
实战 OutOfMemory异常
Java堆溢出
import java.util.ArrayList;
import java.util.List;
public class demo{
static class OOMObject {
}
public static void main(String[] args){
List<OOMObject> list = new ArrayList<>();
while(true){
list.add(new OOMObject());
}
}
}
虚拟机参数:
-Xss规定了每个线程堆栈的大小。一般情况下256K是足够了。影响了此进程中并发线程数大小。
-Xms初始的Heap的大小。
-Xmx最大Heap的大小。
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
虚拟机栈或者本地方法栈溢出
import java.util.ArrayList;
import java.util.List;
-Xss256k
public class demo{
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args){
demo oom = new demo();
try{
oom.stackLeak();
}catch (Throwable e){
System.out.println(oom.stackLength);
throw e;
}
}
}
虚拟机栈容量太小的时候当内存无法分配,跑出栈溢出的异常。
import java.util.ArrayList;
import java.util.List;
public class demo{
public void donstop(){
while(true){
}
}
public void stackLeakByThread(){
while(true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
donstop();
}
});
thread.start();
}
}
public static void main(String [] args) throws Throwable{
demo oom = new demo();
oom.stackLeakByThread();
}
}
上面这段代码谨慎执行。如果是建立多线程导致的内存溢出,可以通过减少最大堆和减少栈容量来换取更多的线程。
因为给每个线程的栈分配的内存越大,可建立的内存会越小。反而会更容易产生内存溢出异常。
虚拟机栈与本地方法栈内存 = 操作系统内存-最大堆-最大方法区容量