欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

day11-java内存区域

程序员文章站 2024-02-26 18:01:58
...


参考书籍:[(深入理解JAVA虚拟机)]

概述

在内存管理领域,对C和C++程序开发人员来说,他们拥有每个对象的所有权,但是也担负着每个对象生命从开始到销毁的维护责任。对于java程序员来说,在jvm自动内存管理机制的帮助下,不在需要为每个new操作去管理,不容易出现内存泄漏和内存溢出的问题,但是正因为如此一旦出现了内存泄漏和内存溢出,如果不了解jvm是怎样管理使用内存的话,那么排查错误将是一个十分艰难的工作。
Java跨平台原因:在下载jdk时,我们选择了对应操作系统的jdk版本,不同的jdk对jvm有不同的实现,从软件层面屏蔽不同操作系统在底层硬件与指令上的区别
day11-java内存区域
我们写好一个类后,先编译为字节码文件,把字节码文件扔进jvm中去运行,先由类装载子系统,把字节码文件装载到运行时数据区,再由字节码执行引擎执行内存代码

day11-java内存区域

*Error&OutOfMemoryError

*Error代表的是,当栈深度超过虚拟机分配给线程的栈大小时就会出现此error
OutofMemoryError代表的是,当再申请新的内存时,虚拟机分配给线程的内存大小中无法再分配新的内存,就会出现此error

运行时数据区

Java虚拟机在执行Java程序的过程中,会把他所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途、以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而创建和销毁。

day11-java内存区域
线程私有的:程序计数器,虚拟机栈,本地方法栈
线程共享的:堆,方法区,直接内存

程序计数器

程序计数器是一块较小的内存空间,他可以看作是当前线程所执行的字节码的行号指示器,在java虚拟机的概念模型里,字节码实际工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,他是程序控制流的指示器分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流转换、分配处理器执行期间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置每条线程都需要有一个独立的程序计数器
各条线程之间计数器互不影响独立存储,我们称这类内存区域为线程私有的内存
注意,如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是本地方法,这个计数器值则为空。此内存区域是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError情况的区域。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

java虚拟机栈

java虚拟机栈是线程私有的,它的生命周期与线程相同,虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,java虚拟机都会同步创建一个栈帧。用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
先调用的方法,先开辟栈帧内存空间,后调用的方法,后开辟栈帧空间,同时也先执行完,先销毁栈帧空间,而先调用的方法最后执行完毕,才释放栈帧空间,符合大学是学的栈的思想:先进后出原则。
*Error代表的是,当栈深度超过虚拟机分配给线程的栈大小时就会出现此error,此时如果类似递归等操作就可能会出现这个错误。

局部变量表:存放局部变量,局部变量表存放了编译期可知的各种Java虚拟机基本数据类型、对象引用、返回值类型,这些数据类型在局部变量表中的存储空间以局部变量槽slot来表示。其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。(这里的大小指的是变量槽的数量,在虚拟机真正使用多大的内存空间来实现一个变量槽,还与具体的jvm实现有关。)
操作数栈:暂存程序运行过程中操作数的临时中转内存空间
动态链接:要执行方法在方法区的入口地址
方法出口:方法执行完后应该执行的位置。

public class Math {
	public static final int initData = 666; 
	public static User user = new User ();
	public int compute() { //-- 个方法对应一块栈帧内存区域
		int a = 1 :
		int b = 2;
		int c = ( a + b ) * 10 ;
		return c ;
	}
	public static void main(String[] args){
		Math math = new Math() ;
		math.compute ();
		System.out.println( test" );
	}
}

day11-java内存区域

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地Native方法服务。

方法区

方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

java堆

java堆是虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建此内存区域的唯一目的就是存放对象实例Java世界里 几乎 所有的对象实例都在这里分配内存。java堆是垃圾回收机制(垃圾收集器)所管理的内存区域,因此也称gc堆。
从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:其中新生代又分为:Eden空间、From Survivor、To Survivor空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。从内存分配的角度来看,线程共享的java堆中可能会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
day11-java内存区域
如图所示,JVM内存主要由新生代、老年代、永久代构成。

  • ① 新生代(Young Generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。

新生代内又分三个区:一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。

  • ② 老年代(Old Generation):在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。
  • ③ 永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。
在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域
(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。

运行时常量池

运行时常量池是方法区的一部分,class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池表:用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
day11-java内存区域
运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定有编译期才能产生,也就是说,并非预置入class文件中常量池的内容才能进入方法去运行时常量池。运行期间也可以将新的常量放入池中,这种特性被开发人员利用的较多的地方便是string类的intern()方法中。

直接内存

直接内存并不是虚拟机运行数据区的一部分,但是这部分内存被频繁使用,也可能导致OutOfMemoryError异常。
在JDK1.4中新加入了NOI(new input / out)类,引入了一种基于通道与缓冲区的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象最为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx参数信息,但是经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
例如idea就配置了这个参数:
day11-java内存区域


本文参考书籍:[(深入理解JAVA虚拟机)]