1-Java运行时数据区域和内存模型

一、普通进程内存布局 

我们知道,操作系统为每个进程都分配了一个虚拟的内存空间,进程会认为自己的操作都在这个空间上进行,这个空间的布局是操作系统控制的,每个进程都是一样的。JVM也是一个进程,他的内存布局肯定也是操作系统安排的,这是毋庸置疑的,那JVM运行时数据区域又是怎么一回事呢?
我们已经知道一个进程的内存布局是什么样的(看这篇文章),JVM进程的布局也遵循这个规则,但是JVM在这个布局内做了很多不属于应用程序业务逻辑的事情。
普通进程的空间大致如下:

JVM与普通进程的区别在于他在Heap上又划分了属于JVM的堆、方法区、虚拟机栈、程序计数器,JVM的本地方法栈应该就是进程的栈。

JVM这么做了以后就可以有一些普通进程没有的特点,或者称为好处,如:

一,减少系统调用的次数,JVM在给Java程序分配内存空间时不需要操作系统干预,仅仅在Java堆大小变化时需要向操作系统申请内存或通知回收,而普通程序每次内存空间的分配回收都需要系统调用参与;

二,减少内存泄漏,普通程序没有(或者没有及时)通知操作系统内存空间的释放是内存泄漏的重要原因之一,而由JVM统一管理,可以避免程序员带来的内存泄漏问题。

三,Java NIO,目的在于减少用于读写IO的系统调用的开销。

二、Java内存布局

jvm规定Java内存布局分为堆、方法区、虚拟机栈、本地方法栈、程序计数器五个区域,但是在实现上并不是完全和规范一致的。
JVM的规定很长一段时间都不会改变,但是JVM的实现会经常变化,我们需要了解不同版本的jdk在内存布局上的改变。
需要理解的是,Java的五个内存分布区域都是堆的逻辑表现形式而已。

  • Java堆是所有Java线程共享的一个区域,也是存储实例和数组对象的地方;
  • Java方法区是所有Java线程共享的一个区域,存储着类型的结构信息,如运行时常量池(runtime constant pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法;
  • Java栈为线程私有区域,用于存储局部变量与一些过程结果的地方。另外,它在方法调用和返回中也扮演了很重要的角色;
  • 本地方法栈,Java虚拟机实现可能会使用到传统的栈(通常称为C stack)来支持native方法(指使用Java以外的其他语言编写的方法)的执行,这个栈就是本地方法栈(nativemethod stack)。当Java虚拟机使用其他语言(例如C语言)来实现指令集解释器时,也会使用到本地方法栈。
  • Java虚拟机可以支持多条线程同时执行,每一条Java虚拟机线程都有自己的pc(program counter)寄存器。在任意时刻,一条Java虚拟机线程只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法(currentmethod)。如果这个方法不是native的,那pc寄存器就保存Java虚拟机正在执行的字节码指令的地址,如果该方法是native的,那pc寄存器的值是undefined。pc寄存器的容量至少应当能保存一个returnAddress类型的数据或者一个与平台相关的本地指针的值。

注意在上面堆上分配的解释时,提到实例和数组对象是在堆上分配,这里为什么将数组对象单独提出来说?为什么不提其他集合对象,比如链表对象,Map对象?看这篇文章
你在编写Java代码的时候,每写一句代码,定义一个变量的时候,你应该清楚的知道,Java会如何安放这些东西,这是一个基本的要求。

三、本地方法栈

Sun的jdk的解释器使用C语言实现的,jre大部分是用Java语言实现的,少部分调用了本地方法。本地方法是指已经写好的一些非Java语言库方法,他们存放在本地,可以为Java调用,JVM并没有限制本地方法实现的语言。
本地方法是为了融合不同语言为Java所用,初衷是为了使用c/c++程序。与操作系统交互也是本地方法的使用范畴。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,但不止如此,它还可以做任何它想做的事情。
任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
每一个方法的执行都需要栈帧,Java方法执行使用的是Java虚拟机栈,本地方法执行使用的是本地方法栈,Java会规定本地方法栈的大小,但无法规定本地方法如何使用本地方法栈,Java只会管理传递给本地方法的参数以及接收本地方法返回的参数。本地方法栈也会出现StackOverflowError和OutOfMemoryError,因为栈的大小是Java分配的,Java能知道不够用的情况。

四、运行时常量池

方法区是虚拟机规范定义的,运行时常量池也是虚拟机规范定义的。
每一个运行时常量池都由java虚拟机的方法区所分配,当java虚拟机创建类或者接口的时候,会对应的创建类或者接口的运行时常量池。
运行时常量池是每个类或者接口在运行时类文件中所代表的 `constant_pool table` (常量池表);其包含多种常量,范围从编译时已知的数字文字到必须在运行时解析的方法和字段引用。运行时常量池的功能类似于常规编程语言的 `symbol table` (符号表),尽管它包含的数据范围比典型的符号表还大;

五、HotSpot具体实现

我们可以看到上面的运行时数据区是JVM的规范,实际实现起来会有所不同,这里以我们经常使用的OpenJDK中HotSpot为例来说明。
Java8和Java8之前的内存布局也是不同的,主要是针对方法区的实现有了改变。
Java8之前,HotSpot对方法区的实现是永久代(PermGen space),也就是说,永久代中存放的是类方法信息以及运行时常量等。
Java8开始,HotSpot对方法区的实现是元空间(Metaspace),里面只存储了类方法信息等,运行时常量池在JVM堆上分配。
使用Meta space代替永久代的原因如下:

  • 字符串存在永久代中,容易出现性能问题和内存溢出。
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  • 将 HotSpot 与 JRockit 合二为一。

Java8之前的内存布局结构
堆和方法区相连,永久代是hotspot对方法区的实现

hotspot对应实现 

 

Java8之后运行时内存

永久代取消后,永久代的参数-XX:PermSize和-XX:MaxPermSize也随之被取消。

表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。

当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

更深层的原因还是要合并HotSpot和JRockit的代码,使用了元空间取代永久代,不用担心运行性能问题了,在覆盖到的测试中, 取代后程序启动和运行速度降低不超过1%,但是这点性能损失换来了更大的安全保障。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值