文章目录
1. 运行时数据区域
1.1 程序计数器(线程私有)
程序计数器是一块较小的内存空间,可以理解为是当前线程所执行的字节码文件的行号指示器,在分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖该计数器完成;
程序计数器是线程私有的数据区,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,目的是为了线程切换后能恢复到正确的执行位置;
程序计数器在线程执行的是一个本地(Native)方法时,其计数器的值为空;
程序计数器是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域;
实战案例:
package cn.jackiegu.jvm.study.memory;
public class ProgramCounterRegisterTest {
public static void main(String[] args) {
Thread thread0 = new Thread(new R0());
Thread thread1 = new Thread(new R1());
thread0.start();
thread1.start();
}
}
class R0 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 打上断点
System.out.println("thread0: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class R1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 打上断点
System.out.println("thread1: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
分别在R0和R1类的run方法中打上以线程方式暂停的断点,然后以Debug方式运行main函数;
这里的Thread-0和Thread-1就是在main函数中创建的那两个线程;
选择其中一个线程断点,然后下面显示的内容(红框里面的内容)可以理解为当前线程正执行的Java类和代码行号,也正是程序计数器所记录的内容,在线程切换时其内容也将随之改变;如:run:37, R1(cn.jackiegu.jvm.study.memory)
表示当前线程正执行到cn.jackiegu.jvm.study.memory包下的R1类中的run方法里,代码行号37;
1.2 Java虚拟机栈(线程私有)
Java虚拟机栈也是线程私有的数据区,其生命周期和线程相同;
Java虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量、操作数栈、动态连接、方法出口等信息;每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程;
Java虚拟机栈中,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError
异常;如果Java虚拟机栈容量可以动态扩展(HotSpot虚拟机的栈容量是不可以动态扩展的),当栈扩展时无法申请到足够的内存会抛出OutOfMemory
异常;
Java虚拟机栈中每个线程的堆栈大小可以通过-Xss
进行设置;
实战案例:
测试一:
package cn.jackiegu.jvm.study.memory;
/**
* VM Args: 不限制
*/
public class JavaVirtualMachineStackTest {
private Integer number = 1;
public void overflow() {
this.number++;
overflow();
}
public static void main(String[] args) {
JavaVirtualMachineStackTest javaVirtualMachineStackTest = new JavaVirtualMachineStackTest();
try {
// StackOverflowError异常测试
javaVirtualMachineStackTest.overflow();
} catch (Error e) {
System.out.println(javaVirtualMachineStackTest.number);
System.out.println(e);
}
}
}
// 运行结果
11667
java.lang.StackOverflowError
测试二:
重载overflow
方法,观察执行结果
package cn.jackiegu.jvm.study.memory;
/**
* VM Args: 不限制
*/
public class JavaVirtualMachineStackTest {
private Integer number = 1;
public void overflow(String message) {
this.number++;
char[] chars = new char[message.length()];
for (int i = 0; i < chars.length; i++) {
chars[i] = message.charAt(i);
}
String newMessage = new String(chars);
overflow(newMessage);
}
public static void main(String[] args) {
JavaVirtualMachineStackTest javaVirtualMachineStackTest = new JavaVirtualMachineStackTest();
try {
// StackOverflowError异常测试
javaVirtualMachineStackTest.overflow("java vm stack");
} catch (Error e) {
System.out.println(javaVirtualMachineStackTest.number);
System.out.println(e);
}
}
}
// 运行结果
4696
java.lang.StackOverflowError
测试三:
设置线程堆栈大小为128k
,观察执行结果
package cn.jackiegu.jvm.study.memory;
/**
* VM Args: -Xss128k
*/
public class JavaVirtualMachineStackTest {
private Integer number = 1;
public void overflow(String message) {
this.number++;
char[] chars = new char[message.length()];
for (int i = 0; i < chars.length; i++) {
chars[i] = message.charAt(i);
}
String newMessage = new String(chars);
overflow(newMessage);
}
public static void main(String[] args) {
JavaVirtualMachineStackTest javaVirtualMachineStackTest = new JavaVirtualMachineStackTest();
try {
// StackOverflowError异常测试
javaVirtualMachineStackTest.overflow("java vm stack");
} catch (Error e) {
System.out.println(javaVirtualMachineStackTest.number);
System.out.println(e);
}
}
}
// 运行结果
723
java.lang.StackOverflowError
总结:当方法内部定义的变量越多,线程请求的栈深度就越浅;同时线程的堆栈大小设置得越小,线程请求的栈深度也越浅;
1.3 本地方法栈(线程私有)
本地方法栈与Java虚拟机栈所发挥的作用是非常相似的,其区别只是Java虚拟机栈是为虚拟机执行Java方法服务,而本地方法栈是为虚拟机使用到的本地方法服务;
在HotSpot虚拟机中,Java虚拟机栈和本地方法栈是被合二为一的;
本地方法栈同Java虚拟机栈一样,在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常;
1.4 Java堆(线程共享)
Java堆是虚拟机所管理的内存中最大的一块,其是被所有线程共享的,并且在虚拟机启动时创建;
Java中”所有的“对象实例以及数组都应当在堆上分配;
Java堆是垃圾收集器管理的内存区域;
Java堆对于当前主流的Java虚拟机都是按照可扩展来实现的,可通过参数-Xms
和-Xmx
分别设置初始内存大小和最大可用内存大小,如果在Java堆中没有内存完成实例分配,且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常;
实战案例:
package cn.jackiegu.jvm.study.memory;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* VM Args: -Xms32m -Xmx32m
*/
public class JavaHeapTest {
private final Integer randomNumber;
private final String randomString;
public JavaHeapTest(Integer randomNumber) {
this.randomNumber = randomNumber;
this.randomString = String.valueOf(randomNumber);
}
public static void main(String[] args) {
Random random = new SecureRandom();
List<JavaHeapTest> list = new ArrayList<>();
try {
while (true) {
list.add(new JavaHeapTest(random.nextInt(99999)));
}
} catch (Error e) {
System.out.println(list.size());
System.out.println(e);
}
}
}
// 运行结果
303979
java.lang.OutOfMemoryError: GC overhead limit exceeded
1.5 方法区(线程共享)
方法区与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据;
方法区有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来;
永久代与元空间(Meta-space)是HotSpot虚拟机不同版本对方法区实现的方式:
- 在Java8之前采用的是永久代实现,在内存溢出时会抛出
java.lang.OutOfMemoryError:PermGen space
异常,可通过-XX:PermSize
与-XX:MaxPermSize
两个配置参数进行限制永久代大小; - Java8及之后采用元空间实现,在内存溢出时会抛出
java.lang.OutOfMemoryError:Metaspace
异常,可通过-XX:MetaspaceSize
与-XX:MaxMetaspaceSize
两个配置参数进行限制元空间大小;
当方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常;
运行时常量池
运行时常量池是方法区的一部分,在Class文件中有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中,同时由符号引用翻译出来的直接引用也存储在运行时常量池中;
运行时常量池并非预置入Class文件中的常量池的内容才能进入,运行期间也可以将新的常量放入池中,如String.intern()函数;
运行时常量池在无法申请到内存时会抛出OutOfMemoryError异常;
实战案例:
设置初始元空间和最大元空间大小为32m
,观察执行结果
package cn.jackiegu.jvm.study.memory;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
/**
* VM Args: -XX:MetaspaceSize=32m -XX:MaxMetaspaceSize=32m
*/
public class MethodAreaTest {
public static void main(String[] args) {
int i = 0;
try {
while (true) {
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOM.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(o, args));
enhancer.create();
}
} catch (Error e) {
System.out.println(i);
System.out.println(e);
}
}
}
class OOM {
}
// 运行结果
3176
java.lang.OutOfMemoryError: Metaspace
2. 非运行时数据区域
2.1 直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域;它是在JDK1.4中新加入的NIO中引入的,是一种基于通道与缓冲区的 I/O 方式,它可以使用Native函数库直接分配堆外内存,然后通过一个储存在Java堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作;
直接内存的容量大小可通过-XX:MaxDirectMemorySize
参数来指定,如果不指定,则默认与Java堆最大值(由-Xmx
指定)一致;
package cn.jackiegu.jvm.study.memory;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class DirectMemoryTest {
private static final long _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field field = Unsafe.class.getDeclaredFields()[0];
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
// 运行结果
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at cn.jackiegu.jvm.study.memory.DirectMemoryTest.main(DirectMemoryTest.java:22)
3. 备注
部分内容摘抄于《深入理解Java虚拟机》-周志明(第3版)
。