介绍下Java内存区域(运行时内存区)
Java虚拟机在执行Java程序时,会将它管理的内存划分为若干个不同的数据区域,且JDK1.8和之前的版本也有所不同。
线程私有:
(生命周期与线程相同)
程序计数器、虚拟机栈、本地方法栈
线程共享:
堆、方法区、直接内存
程序计数器
程序计数器是一块较小的内存空间,可以看做是当前线程的一个字节码行号指示器。
两大作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
- 在多线程情况下,程序计数器用来记录当前线程的执行位置,当线程被切换回来时,能够知道线程上次运行到哪一步。
注:程序计数器是唯一一个不会出现OutOfMemoryError
(内存溢出错误)的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
Java虚拟机栈
Java虚拟机栈描述的是Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
- Java内存可以大致分为堆(Heap)内存和栈(Stack)内存,其中栈就是虚拟机栈,或者说是虚拟机栈中的局部变量表部分。实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都包含:局部变量表、操作数栈(保存计算过程的中间结果)、动态链接、方法出口信息。
- 局部变量表主要存放了编译期可知的各种基本数据类型以及对象引用(它与对象本身不同,可能是指向对象的起始地址的引用指针,也可能是指向一个其他与对象有关的位置。)
注:Java虚拟机栈会出现两种错误:
StackOverFolwError
:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前虚拟机栈的最大深度的时候,就抛出StackOverFlowError
错误OutOfMemoryError
:若Java虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出OutOfMemoryError
错误。
问题:
那么方法/函数如何调用?
Java栈可以类比于数据结构中的栈,Java栈中保存 的主要内容是栈帧,每一次函数的调用都会有一个对应的栈帧被压入Java栈,每一个函数调用结束(return语句、抛出异常
)后,都会有一个栈帧被弹出。
扩展:
每一个栈帧都包含stack1中的东西,且通过父帧和子帧相互指引。
本地方法栈
本地方法栈和虚拟机栈的作用类似,区别是:虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 本地方法在被执行时,在本地方法栈中也会有栈帧的创建,方法执行结束后,相关栈帧也会出栈。
native:Java Native Interface(JNI) Java本地接口
- 为什么使用native?
凡是带了native关键字的,说明Java的作用范围已经达不到了,会去调用底层C语言的库!
会进入本地方法栈,调用本地方法本地接口(JNI) - JNI的作用?
扩展Java的使用,融合不同的编译语言为Java作用!最初:融合c和c++。 - 为什么最初选择融合c、c++?
在Java诞生之初,c、c++横行,想要立足,就不需要根c、c++扯上关系(要有调用C、C++的程序) - 执行过程
它在内存区域专门开辟了一块标记区域:Native Method Stack,用来登记native方法,并在最终执行的时候,通过JNI加载本地方法库中的方法 - 使用范畴:
在企业级应用开发中很少见,通常用于驱动打印机,管理系统等。
堆
Java虚拟机管理的内存中最大的一块,JVM堆是左右线程锁共享的一块区域,在虚拟机启动时创建。此内存的唯一目标就是存放对象实例,几乎所有对象实例和数组都在这里分配内存。
一个JVM只有一个对内存,堆内存的大小是可以调节的。同时,堆也是进行垃圾回收的重点区域,也称为GC堆。
JDK1.8之前,堆内存通常被分为三部分:新生代、老年代、永生代
JDK1.8,堆内存通常被分为三部分:新生代、老年代、元空间
进⼀步划分的目的是更好地回收内存,或者更快地分配内存。
当堆内存满了,会报OOM(OutOfMemoryError)错误,堆内存溢出。
-
新生区
类:诞生和成长的地方,甚至死亡;
伊甸园,所有的对象都是在伊甸园区new出来的
幸存者区(0,1) -
老年区
-
永生区
JDK1.6之前:永久代,常量池在方法区
JDK1.7:永久代,但是慢慢退化了,常量池在堆中
JDK1.8开始:去永久化,常量池在元空间
这个区域作为常驻内存,用来存放JDK自身携带的Class对象。interface元数据,存储的是Java运行时的一些环境或类信息。
这个区域不存在垃圾回收!关闭VM虚拟机就会释放这个区域的内存。
OOM出现原因?
1、一个启动类,加载大量的第三方jar包。
2、Tomcat部署了太多的应用。
3、大量动态生成的反射类,不断被加载,直到内存满。
问题: 为什么要将永久代替换为元空间?
- 整个永久代有⼀个 JVM 本身设置固定⼤⼩上限,⽆法进⾏调整,⽽元空间使⽤的是直接内存,受本机可⽤内存的限制,虽然元空间仍旧可能溢出,但是⽐原来出现的⼏率会更⼩。
你可以使⽤ -XX MaxMetaspaceSize 标志设置最⼤元空间⼤⼩,默认值为 unlimited,这意味着它只受系统内存的限制。 -XX MetaspaceSize 调整标志定义元空间的初始⼤⼩如果未指定此标志,则 Metaspace 将根据运⾏时的应⽤程序需求动态地重新调整⼤⼩。
- 元空间⾥⾯存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了,⽽由系统的实际可⽤空间来控制,这样能加载的类就更多了。
- 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有⼀个叫永久代的东⻄, 合并之后就没有必要额外的设置这么⼀个永久代的地⽅了。
方法区
方法区是被所有线程共享的,所有的字段和方法字节码,以及一些特殊的方法(构造方法、接口代码)也在此定义,简而言之,所有定义的方法的信息都保存在该区域,此区域属于共享空间。
静态变量(static)、常量(final)、类信息(Class)、运行时常量池等存在方法区中,但实例变量存在堆内存中,和方法区无关。
扩展
堆、栈、方法区:交互
运行时常量池
运行时常量池时方法区的一部分。Class类中不仅有类的版本、字段、方法、接口等信息,还有常量池表,用于存放编译期生成的各种各种字面量和符号引用。当然,它作为方法区的一部分,自然也受到方法区内存的限制,当常量池无法在申请到内存时,也会报OOM错误。
直接内存
直接内存并不是虚拟机运行时内存的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁的使用。也会导致OOM错误。