Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
- 程序计数器(线程隔离)
- 虚拟机栈(线程隔离)
- 本地方法栈(线程隔离)
- JAVA堆(线程共享)
- 方法区(线程共享)
(1)程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
- 如果线程正在执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
- 如果正在执行的是Native方法,这个计数器值为空;
- 此内存区域是唯一一个在JAVA虚拟机规范中没有规定任何OutOfMemoryError情况的区域
(2)虚拟机栈
虚拟机栈的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型;每个方法在执行的同时都会创建一个栈帧
用于存储局:
- 局部变量表(存放编译器可知的各种基本数据类型,对象引用类型)
- 操作数栈
- 动态链接
- 方法出口 等
对于虚拟机栈这个区域规定了两种异常情况:
- StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
- OutOfMemoryError:虚拟机栈在拓展时无法申请到足够的内存空间
(3)本地方法栈(与虚拟机栈所发挥的作用相似)
- 虚拟机栈:执行JAVA方法(字节码)服务
- 本地方法栈:执行为虚拟机使用到的Native方法服务
- 本地方法栈也会抛出
StackOverflowError
和OutOfMemoryError
异常
(4)JAVA堆
JAVA堆是JAVA虚拟机中内存最大的一块。JAVA堆在虚拟机启动时创建,被所有线程共享。
- 存放几乎
所有的对象实例
- 垃圾收集主要管理的就是JAVA堆
- 物理上不连续,逻辑上连续
(5)方法区
方法区域java堆一样,是各个线程共享的内存区域,它用于存储:
- 加载的类信息
- 常量
- 静态变量
- 即时编译器编译后的代码数据
运行时常量池位于方法区,存放运行时产生的常量,比如String对象
。方法区有两个值注意的地方:
- 物理上不需要连续的内存,逻辑上连续即可
- 可选择固定大小的内存区域
- 可选择不实现垃圾收集
实战:outofmemoryerror异常
(1)在单线程下,无论是由于栈帧太大还是由于虚拟机栈容量太小,当内存无法分配时,虚拟机抛出的都是java.lang.StackOverflowError异常。
package cn.test.two;
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
/**
* 运行结果表明;在单线程下,无论是由于栈帧太大还是由于虚拟机栈容量太小,当
* 内存无法分配时,虚拟机抛出的都是java.lang.StackOverflowError异常
* @param args
* @throws Throwable
*/
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try{
oo`这里写代码片`m.stackLeak();
}catch (Throwable e) {
System.out.println(oom.stackLength);
throw e;
}
}
}
(2)不断创建多线程导致发生OutOfMemory异常。
package cn.test.two;
/**
* 别运行,会死机
* 多线程导致内存溢出
* @author cjc
*
*/
public class JavaVMStackOOM {
private void dontStop(){
while(true){
}
}
public void stackLeakByThread(){
while(true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
(3)方法区的运行时常量池内存溢出
package cn.test.two;
import java.util.ArrayList;
import java.util.List;
/**
* 方法区的运行时常量池内存溢出
* @author cjc
*
*/
public class RuntimeConstraintPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i = 0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
}
在内存溢出还是栈溢出方面,关注点在于java栈和java堆的常量池。
JVM是如何判定对象是否应该被回收
引用计数法
原理:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
缺点:这种算法不能解决对象之间相互循环引用的问题。所以主流的JAVA虚拟机没有选用引用计数算法来管理内存。(比如:obja.obj = objb; objb.obj = obja;)可达性分析
原理:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即不可达)时,则证明此对象不可用。如图:
在Java中,可作为GC Roots的对象包括:
(1)虚拟机栈(栈帧中的本地变量表)
(2)方法区中类静态属性引用的对象
(3)方法区中常量引用的对象
(4)本地方法栈中JNI
再谈引用