1、概述
对于java程序员来说,在虚拟机内存的自动内存管理机制的帮助下,不再需要为每一个对象去写匹配的 delete/ free 代码,不容易出现内存泄漏或内存溢出的问题。
正是因为把内存控制权交给了java虚拟机,一旦出现了内存泄漏或者是溢出的问题,排查错误会变得困难。
2、运行时数据区域
2.1 程序计数器
程序计数器是一块较小的内存空间,可以看成是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、异常处理、线程恢复等基础功能都是依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换,分配处理器执行时间的方式来实现的。所以为了线程切换后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器。(“线程私有内存”)。
如果线程线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法简单地讲,一个Native Method就是一个java调用非java代码的接口,这个计数器为空(undefined)。
2.2 Java虚拟机栈
Java虚拟机栈也是线程私有的,它的生命周期与线程相同,虚拟机栈描述的是Java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。https://blog.csdn.net/zc375039901/article/details/79179465
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法需要在帧中分配多大的局部变量空间完全确定的,在方法执行期间不会改变局部变量表的大小。
在Java虚拟机规范中,对这个区域定了两种异常情况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。eg:无限递归
- 虚拟机栈进行动态的扩展时,如果无法申请到足够的内存会抛出OutOfMemoryError eg:创建 > GC回收。
//模拟StackOverflowError
public class Demo1 {
private int index = 1;
public void method() {
index++;
method();
}
@Test
public void testStackOverflowError() {
try {
method();
} catch (StackOverflowError e) {
System.out.println("程序所需要的栈大小 > 允许最大的栈大小,执行深度: " + index);
e.printStackTrace();
}
}
}
//模拟OutOfMemoryError
public class Demo2 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
while (true) {
list.add(new Heap());
}
}、
}
2.3 本地方法栈
本地方法栈与虚拟机所发挥的作用是非常相似的,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用到的native方法服务。虚拟机规范中对本地方法栈中使用的语言、方式并没有强制规定。因此虚拟机可以自有的实现它。
2.4 Java堆
对于大多数应用来说,java堆是java虚拟机所管理的内存中最大的一块。java堆是所有线程共享的一块区域,几乎所有的对象实例都在这里分配内存。
java堆是垃圾收集管理器管理的主要区域,也被称为GC堆(Garbage Collected heap)。
- 堆内存划分JDK1.7及以前
**JDK1.8
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError。
2.5 方法区
方法区存放类的信息(包括类的字节码,类的结构)、常量、静态变量等。字符串常量池就是在方法区中。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做
Non-Heap(非堆),目的是与Java堆区分开来。很多人都更愿意把方法区称为“永久代”(Permanent Generation)。从jdk1.7已经开始准备“去永久代”的规划,jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中。
ps:永久代和元空间
永久代
绝大部分 Java 程序员应该都见过 “java.lang.OutOfMemoryError: PermGen space “这个异常。这里的“PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。通过动态生成类来模拟 “PermGen space”的内存溢出
package com.paddx.test.memory;
public class Test {
}
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class PermGenOomMock{
public static void main(String[] args) {
URL url = null;
List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
try {
url = new File("/tmp").toURI().toURL();
URL[] urls = {url};
while (true){
ClassLoader loader = new URLClassLoader(urls);
classLoaderList.add(loader);
loader.loadClass("com.paddx.test.memory.Test");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
例中使用的 JDK 版本是 1.7,指定的 PermGen 区的大小为 8M。通过每次生成不同URLClassLoader对象来加载Test类,从而生成不同的类对象,这样就能看到我们熟悉的 “java.lang.OutOfMemoryError: PermGen space “ 异常了。
元空间
在JDK1.8中,永久代已经不存在,存储的类信息、编译后的代码数据等已经移动到了MetaSpace(元空间)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
元空间的大小仅受本地内存限制,可以通过以下参数来指定元空间大小:
- -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
- -XX:MaxMetaspaceSize,最大空间,默认是没有限制的
- -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
2.6 直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但是这部分也被频繁的使用,也可能导致outofMemoryError。
NIO:引入一种基于通道和缓冲区的io方式,可以使用native函数直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer 对象作为这块内存的引用来操作。避免了在java堆和native堆来回复制数据。
直接内存的分配不会受到java堆大小的限制,但是会受到本机总内存大小及处理器寻址空间的限制。在设置虚拟机参数的时候,一般会根据实际内存设置 -Xmx等参数。但会经常忽略掉直接内存,是的各内存区域的总和大于物理内存限制。2.7 对象访问
Object obj = new Object( );
假设这句代码出现在方法体中,” Object obj “ 这部分语义将会反应到java栈的本地变量表中,作为一个reference类型数据出现。而new Object( )这部分语义将会反应到java堆中。形成一块结构化内存。另外,在java堆中还必须包含能查到此对象类型数据( 如对象类型、父类、实现的接口、方法等 )
的地址信息,这些数据则存储在方法区中。
2.7.1 对象的访问定位
使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。通过句柄访问
Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。详情见图。
- 使用直接指针访问
reference 中直接存储对象地址
比较:使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。