一、java运行时虚拟机内存区域划分
JVM载执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
jdk 1.8之前:
jdk 1.8之后:
两者区别:元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
二、程序运行时每个部分都有什么作用?
1、程序计数器(Program Counter Register)
a.程序计数器是一块较小内存,它可以看作当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器的工作就是通过改变这个 计数器的值来选取下一条需要执行的字节码,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个计数器来完成。
b.程序计数器是一块 “线程私有” 的内存,每条线程都有一个独立的程序计数器,能够将切换后的线程恢复到正确的执行位置。
c.如果线程正在执行一个 Java 方法,这个计数器记录的正在执行的虚拟机字节码指令的地址,如果执行的是 Native 方法,这个计数器值为空。
Native 方法: 简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。
“A native method is a Java method whose implementation is provided by non-java code.”
2、java虚拟机栈(Java Virtual Machine Stacks)
1、Java虚拟机栈(Java Virtual Machine Stacks)描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame),栈帧中存储着局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,会对应一个栈帧在虚拟机栈中入栈到出栈的过程。与程序计数器一样,Java虚拟机栈也是线程私有的。
a.局部变量表主要存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置),returnAddress类型(指向了一条字节码指令的地址)。
b. 操作栈帧,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是 用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进 行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
c.指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时的常量池
d.方法返回地址,当一个方法执行完毕后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法的返回地址。
由于每个线程执行正在执行的方法可能不同,因此每个线程都有一个Java栈,互不干扰。
2、Java虚拟机规范中对这个区域规定了两种异常状况:
- StackOverflowError(栈溢出):若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。 ```java package com.lxh.memory;
public class StackOverFlowErrorTest {
private int count = 0;
// 方法自己调用自己,在虚拟机栈中建立一个又一个栈帧,直到到达栈深度
public void testStack() {
count++;
testStack();
}
public void test() {
try {
testStack();
} catch (Throwable e) {
System.out.println(e);
System.out.println("stack height:" + count);
}
}
public static void main(String[] args) {
new StackOverFlowErrorTest().test();
}
}
运行结果:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12870696/1616227991730-e07dd26c-b872-48ab-89b1-b68fb9e933a3.png#align=left&display=inline&height=93&margin=%5Bobject%20Object%5D&name=image.png&originHeight=186&originWidth=1269&size=196635&status=done&style=none&width=634.5)<br />**OutOfMemoryError(内存溢出)**:当可动态扩展的虚拟机栈在扩展时无法申请到足够的内存,就会抛出该异常。<br />**(这行代码不要轻易尝试,容易内存飚满 o(╥﹏╥)o)**<br />注意,特别提示一下,如果要尝试运行上面这段代码,记得要先保存当前的工作。由于在Windows平台的虚拟机,Java的线程是映射到操作系统的内核线程上的,因此上述代码执行时有较大的风险,可能会导致操作系统假死。
```java
package com.lxh.memory;
public class OutOfMemoryTest {
private void dontStop() {
while (true) {
}
}
//不断创建线程
public void stackLeakByThread() {
try {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
} catch (Throwable e) {
System.out.println(e);
System.out.println("stack height:" + count);
}
}
public static void main(String[] args) throws Throwable {
OutOfMemoryTest oom = new OutOfMemoryTest();
oom.stackLeakByThread();
}
}
运行结果:
3、对于虚拟机栈内存形象理解
可以把虚拟机栈内存看做一个弹夹上述testStack()方法相当于子弹,每执行一次该方法就相当于往弹夹里压入一颗子弹(栈帧),而栈深度就相当于弹夹的容量,即栈帧的数量,当方法执行过多时,弹夹容量达到限制没法再压入子弹,就会出现StackOverflowError,若 Java 虚拟机栈的内存大小允许动态扩展,就相当于把原来的弹夹换成了扩容弹夹,可以再压入子弹,当扩容弹夹再次压满时就会出现OutOfMemoryError
3、java本地方法栈(Native Method Stack)
本地方法栈(Native Method Stack)与Java虚拟机栈作用很相似,它们的区别在于虚拟机栈为虚拟机执行Java方法(即字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
在虚拟机规范中对本地方法栈中使用的语言、方式和数据结构并无强制规定,因此具体的虚拟机可实现它。甚至有的虚拟机(Sun HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一。与虚拟机一样,本地方法栈会抛出StackOverflowError和OutOfMemoryError异常。
4、堆(Heap)
对于大多数应用而言,Java堆(Heap)是Java虚拟机所管理的内存中最大的一块,它被所有线程共享的,在虚拟机启动时创建。此内存区域唯一的目的是存放对象实例,几乎所有的对象实例都在这里分配内存,且每次分配的空间是不定长的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。
Java堆是垃圾收集器管理的主要区域,因此也被称为 “GC堆(Garbage Collected Heap)” 。从内存回收的角度看内存空间可如下划分:
a.新生代(Young):新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。如果把新生代再分的细致一点,新生代又可细分为Eden空间、From Survivor空间、To Survivor空间,默认比例为8:1:1。
b.老年代(Tenured/Old):在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
c.永久代(Perm):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
其中新生代和老年代组成了Java堆的全部内存区域,而永久代不属于堆空间,其中新生代和老年代组成了Java堆的全部内存区域,而永久代不属于堆空间,它在JDK 1.8以前被Sun HotSpot虚拟机用作方法区的实现.
**
堆内存溢出演示:
package com.lxh.memory;
import java.util.ArrayList;
import java.util.List;
public class OutOfMemory {
public static void main(String[] args){
List list=new ArrayList();
for(;;){
int[] tmp=new int[1000000];
list.add(tmp);
}
}
}
5、方法区(Method Area)
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。Object Class Data(类定义数据)是存储在方法区的,此外,常量、静态变量、JIT编译后的代码也存储在方法区。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 Non-Heap。
JDK 1.8以前的永久代(PermGen)
Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集,也就是说,Java虚拟机规范只是规定了方法区的概念和它的作用,并没有规定如何去实现它。对于JDK 1.8之前的版本,HotSpot虚拟机设计团队选择把GC分代收集扩展至方法区,即用永久代来实现方法区,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他的虚拟机(如Oracle JRockit、IBM J9等)来说是不存在永久代的概念的。
如果运行时有大量的类产生,可能会导致方法区被填满,直至溢出。常见的应用场景如:
1.Spring和ORM框架使用CGLib操纵字节码对类进行增强,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。
2.大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)。
3.基于OSGi的应用(即使是同一个类文件,被不同的类加载器加载也会视为不同的类)。
这些都会导致方法区溢出,报出java.lang.OutOfMemoryError: PermGen space。
JDK 1.8的元空间(Metaspace)
整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 java.lang.OutOfMemoryError。你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
参考:https://blog.csdn.net/qq_31749835/article/details/88847193
https://blog.csdn.net/qq_20499001/article/details/100713737