在一开始学习java的时候,那时候是在网上看视频,老师就经常提到什么对象分配在堆区,什么在栈区,那时候和理解,后来理解了就想着写一篇文章好好的去梳理一下。

这个内存结构是基于java8的内存结构,最文章末尾也会和java7的内存结构进行一个比较,看看哪些地方进行了改变,这些改变对性能的影响是什么。

还有一点这个是基于Hotspot虚拟机来说的。

先给一张java8的内存结构图吧(我用Windows里面的画图工具画的)

图片

首先对这个图有一个认识,从上面可以看到java8的内存结构大致分了五个部分:PC寄存器,java虚拟机栈、本地方法栈、java堆、方法区。其中PC寄存器、java虚拟机栈和本地方法栈是所有线程共享的一块内存区域。java堆和方法区是每一个线程独享的一块区域,还有一个运行时常量池。

接下来看一看每一块区域里面存放的什么?

一、PC寄存器

在大学的时候学过计算机组成原理的时候都知道,内存里面有很多寄存器,大概几百个吧(目前),每一种寄存器的用途都不一样,其中有一个寄存器就是程序计数器。这个寄存器的主要作用就是存放下一条需要执行的指令。这是因为在一个时刻,一个处理器只能执行一条线程中的指令。就好比说我们的程序代码假如是一行一行执行的,程序计数器永远指向下一行需要执行的字节码指令。在循环结构中,我们就可以改变程序计数器中的值,来改变下一条需要执行的指令。并且每一个线程都有一个程序计数器。如果当前执行的是 Java 的方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native 方法,则PC寄存器中为空(Undefined)。PC寄存区区域就是存放了N多个这样的寄存区。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。因此可以把他的几个特点归纳如下。

  1. 程序计数器指定下一条需要执行的指令

  2. 每一个线程独享一个程序寄存器

  3. 执行java代码时,寄存器保存当前指令地址

  4. 执行native方法时候,寄存器为空。

  5. 不会造成OutOfMemoryError情况

二、Java虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型,每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。它的生命周期与线程相同。每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。

局部变量表里存放了编译期间可知的各种基本数据类型(8种)、对象引用、returnAddress类型(指向一条字节码指令的地址)。64位长度的long和double类型占用2个局部变量空间(Slot),其余数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误。如果扩展时无法申请到足够的内存,会抛出OutOfMemoryError异常。

来一张看一下比较直观吧。

图片

三、本地方法栈(Native Method Stack)

与虚拟机栈类似,区别是虚拟机栈执行java方法,本地方法站执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。本地方法栈可以抛出StackOverflowError和OutOfMemoryError异常。不过这块区域我们不怎么去关心。

四、Java堆

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,用来存放对象实例。是内存中最大的一块区域。垃圾收集器(GC)在该区域回收不使用的对象的内存空间。但是并不是所有的对象都在这保存,深入理解java虚拟机中说道,随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配、标量调换优化技术将会导致一些微妙的变化,所有的对象都分配在堆上也逐渐变得不那么绝对了。

堆的大小可以固定也可以动态扩展,可通过-Xms(最小值)和-Xmx(最大值)参数设置,如果在堆中没有内存完成实例分配,且堆也无法在扩展时,会抛出OutOfMemoryError异常。

五、方法区

方法区也是所有线程共享。主要用于存储类的信息、常量池、静态变量、及时编译器编译后的代码等数据。方法区逻辑上属于堆的一部分。通常又叫“Non-Heap(非堆)”。

一个例子理解全部

为了理解的比较深刻,先给一个例子。通过例子讲解印象更加深刻吧,假设我们在idea或者是任何IDE环境中定义了一个类。

有一个person类

public class Person{
int age;
String name;
Baby baby;
public void walk() {
System.out.println("我正在走路。。。。");
}
}

还有个Baby类

public class Baby{
   String babyname;
   int babyAge;
   public void cry(){
       System.out.println("我是孩子,我会哭");
  }
}

最后是一个测试类Test

public class Test {
public static void main(String[] args) {
Person person = new Person();
person.name = "冯冬冬的IT技术栈";
person.age = 18;
person.walk();

Baby baby= new Baby();
baby.babyname = "冯XX";
System.out.println(baby.babyname);

person.baby = baby;
System.out.println(pserson.baby.cry);
}
}

好了有了上面的环境,接下来就开始分析这些代码在运行时内存的变化。现在在我们的IDE开始运行。

  1. 第一步,JVM去方法区寻找Test类的代码信息,如果有直接调用,没有的话使用类的加载机制把类加载进来。同时把静态变量、静态方法、常量加载进来。这里加载的是(“冯冬冬的IT技术栈”,“冯XX”);这是因为字符串是常量,age中的18是基本类型。

  2. 第二步,jvm进入main方法,看到Person person=new Person()。首先分析Person这个类,同样的寻找Person类的代码信息,有就加载,没有的话类加载机制加载进来。同时也加载静态变量、静态方法、常量(“我正在走路。。。”)

  3. 第三步,jvm接下来看到了person,person在main方法内部,因而是局部变量,存放在栈空间中。

  4. 第四步,jvm接下来看到了new Person()。new出的对象(实例),存放在堆空间中。

  5. 第五步,jvm接下来看到了“=”,把new Person的地址告诉person变量,person通过四字节的地址(十六进制),引用该实例。 是不是有点晕,别着急,画个图看一下。

    图片

  6. 第六步,jvm看到person.name = "冯冬冬的IT技术栈";person通过引用new Person实例的name属性,该name属性通过地址指向常量池的"冯冬冬的IT技术栈"。

  7. 第七步,jvm看到person.age = 18; person的age属性是基本数据类型,直接赋值。

  8. 第八步,jvm看到person.walk(); 调用实例的方法时,并不会在实例对象中生成一个新的方法,而是通过地址指向方法区中类信息的方法。走到这一步再看看图怎么变化的。

    图片

  9. 第九步,jvm看到Baby baby=new Baby().这个过程和Person person = new Person()一样

  10. 第十步,jvm看到baby.babyname = "冯XX";这个过程也和person.name = "冯冬冬的IT技术栈";一样。

  11. 第十一步,jvm看到person.baby = baby;把baby对象引用赋值给Person实例的baby属性属性。

好了,到了这一步,应该对jvm的内存结构有一个详细的认识了。这就结束了,当然还没,我说过这是对java8的内存结构的分析,所以还要解释一下两个名词:永久代(PermGen)和元空间(Metaspace)。

首先是永久代:

我们常见的 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。

然后是元空间

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集

可以这样说在Java8中对永久代进行了完全删除。


更多相关文章

  1. Java实现定时任务的三种方法
  2. 一个Java对象到底占多大内存?
  3. sql.js:JS专用的内存型数据库[github项目精选0x01]
  4. 深入理解JVM—JVM内存模型
  5. Java内存模型-防止内存泄漏
  6. 使用 ThreadLocal 变量的时机和方法
  7. clone 方法是如何工作的
  8. Java内存模型-JMM简介
  9. Java内存模型-堆和栈

随机推荐

  1. Laravel数据库获取值的常用方法
  2. PHP过滤数组中的0、null、false和''等空
  3. php使用QueryList轻松采集JavaScript动态
  4. php JSON数据格式化(美化)的方法
  5. PHP替换回车换行符的三种方法
  6. PHP中explode和implode的使用
  7. 非常实用的PHP常用函数汇总-加密解密、字
  8. PHP运算符优先级的一个例外
  9. 自学php有哪些好的方法
  10. php三个函数、两种选择防止sql注入