jvm-内存管理机制(一)

jvm-内存管理机制(一)

JVM内存区域主要包括如下部分:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。
JVM内存管理也可以理解为–jvm运行时数据区,jvm内存主要管理的就是这片内存区域。

堆(各线程共享区域)

java的堆是一个运行时的数据区,用来存储数据的单元,存放通过new关键字新建的对象和数组,对象从中分配内存。
在堆中声明的对象,是不能直接访问的,必须通过在栈中声明的指向该引用的变量来调用。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。

声明的对象是在堆内存中初始化的, 真正用来存储数据的,不能直接访问。
引用类型变量是保存在栈当中的,一个用来引用堆中对象的符号而已(指针)。

jvm堆的特点:

  1. java虚拟机所管理的内存中最大的一块。java堆时被所有线程共享的一块内存区域,在虚拟机启动时创建。
  2. 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。(几乎所有的对象实例以及数组都要在堆上分配。
  3. java堆实GC管理的就是该区域,也称为GC堆。
  4. java堆中还细分为:新生代,老年代;再细分一点有Eden空间,From Survivor(sərˈvaɪvə(r),幸存者)空间,To Survivor空间

方法区(各线程共享区域)

各线程共享区域,存储已被虚拟机加载的类信息常量静态变量,即时编译器编译后的代码等数据。
类信息有:每个类的全限定名、每个类的直接超类的全限定名、该类是类还是接口、该类型的访问修饰符、直接超接口的全限定名的有序列表。
每个已装载类的详细信息:运行时常量池、字段信息、方法信息、静态变量、到类classloader的引用、到类class的引用。

已装载类详细信息

  1. 运行时常量池:在方法区中,每个类型都对应一个常量池,存放该类型所用到的所有常量,常量池中存储了诸如文字字符串、final变量值、类名和方法名常量。
  2. 字段信息:字段信息存放类中声明的每一个字段的信息,包括字段的名、类型、修饰符。
  3. 字段名称:指的是类或接口的实例变量或类变量,字段的描述符是一个指示字段的类型的字符串,如private A a=null;则a为字段名,A为描述符,private为修饰符
  4. 方法信息:类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。
    (在编译的时候,就已经将方法的局部变量、操作数栈大小等确定并存放在字节码中,在装载的时候,随着类一起装入方法区。)
  5. 静态变量:就是类变量,类的所有实例都共享,在方法区有个静态区,静态区专门存放静态变量和静态块。
  6. 到类classloader的引用:到该类的类装载器的引用。
  7. 到类class 的引用:虚拟机为每一个被装载的类型创建一个class 实例,用来代表这个被装载的类。

java方法区中-运行时常量池

java 常量池实际上分为两种形态:静态常量池 和运行时常量池 。

  • 所谓静态常量池 ,即*.class文件中的常量池。class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,它们占用class文件绝大部分空间。
  • 而运行时常量池 ,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区 中,我们常说的常量池,就是指方法区中的运行时常量池。

运行时常量池(Constant Pool Table),用于存放编译期生成的各种字面量符号引用String字符串final变量值、类和结构的完全限定名,方法的名称和描述符,字段的名称和描述符,这部分内容将在类加载后存放到方法区的运行时常量池中。它们以数组形式通过索引被访问,是外部调用与类联系及类型对象化的桥梁.

在运行时,JVM从常量池中获得符号引用,然后在运行时解析成引用项的实际地址,最后通过常量池中的全限定名、方法和字段描述符,把当前类或接口中的代码与其它类或接口中的代码联系起来。
运行时常量池中的常量,基本来源于各个class文件中的常量池。
程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm不会自动添加常量到常量池

运行时常量池除了存放编译期产生的Class文件的常量外,还可存放在程序运行期间生成的新常量,比较常见增加新常量方法有String类的internd()方法。String.intern()是一个Native方法,它的作用是:如果运行时常量池中已经包含一个等于此String对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此String内容相同的字符串,并返回常量池中创建的字符串的引用。不过JDK7的intern()方法的实现有所不同,当常量池中没有该字符串时,不再是在常量池中创建与此String内容相同的字符串,而改为在常量池中记录堆中首次出现的该字符串的引用,并返回该引用。

由于运行时常量池在方法区中,我们可以通过jvm参数:-XX:PermSize、-XX:MaxPermSize来设置方法区大小,从而间接限制常量池大小。
在jdk8中,移除了方法区,转而用Metaspace区域替代,所以我们需要使用新的jvm参数:-XX:MaxMetaspaceSize

但是,JDK1.7之前运行时常量池是方法区的一部分,JDK1.7及之后版本已经将运行时常量池从方法区中移了出来,在堆(Heap)中开辟了一块区域存放运行时常量池。

PermGen(永久代)

绝大部分 Java 程序员应该都见过 “java.lang.OutOfMemoryError: PermGen space “这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。

Metaspace(元空间)

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
元空间的好处总结:
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

java栈 stack(私有)

每个方法在执行的同时都会创建一个栈帧用于存储局部变量操作数栈动态链接方法出口等信息,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

  • 局部变量区为一个以字为单位的数组,每个数组元素对应一个局部变量的值。调用方法时,将方法的局部变量组成一个数组,通过索引来访问。若为非静态方法,则加入一个隐含的引用参数this,该参数指向调用这个方法的对象。而静态方法则没有this参数。因此,对象无法调用静态方法。
  • 操作数栈也是一个数组,但是通过栈操作来访问。所谓操作数是那些被指令操作的数据。当需要对参数操作时如a=b+c,就将即将被操作的参数压栈,如将b 和c 压栈,然后由操作指令将它们弹出,并执行操作。虚拟机将操作数栈作为工作区。
  • 帧数据区处理常量池解析,异常处理等

如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出stackoverflowError;如果虚拟机可以动态扩展,但是无法申请到足够的内存时,就会抛出outOfMemoryError异常。

linux查看线程默认栈空间大小

1、通过命令 ulimit -s 查看linux的默认栈空间大小,默认情况下 为10240 即10M
2、通过命令 ulimit -s 设置大小值 临时改变栈空间大小:ulimit -s 102400, 即修改为100M
3、可以在/etc/rc.local 内 加入 ulimit -s 102400 则可以开机就设置栈空间大小
4、在/etc/security/limits.conf 中也可以改变栈空间大小
soft stack 102400

本地方法栈(私有)

虚拟机栈为虚拟机执行java方法提供服务,而本地方法栈为虚拟机使用到的本地方法服务。

程序计数器(私有)

  1. 是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。每条线程都需要一个独立的程序计数器,各线程之间互不影响
  2. 字节码解释器选取下一条指令,分支,循环,跳转,异常处理,线程恢复都需要依赖它

堆和栈的题外话

这里描述的是C,C++里面的堆和栈。

什么是堆,什么栈,为什么要有堆和栈?

从数据结构层次理解,栈是一种先进后出的线性表,只要符合先进后出的原则的线性表都是栈,至于采用的存储方式(实现方式)是顺序存储(顺序栈)还是链式存储(链式栈)是没有关系的。堆则是二叉树的一种,有最大堆最小堆,排序算法中有常用的堆排序。

从系统层次理解,栈是系统为运行的程序分配的先进后出的存储区域。在执行函数时,函数内部局部变量的存储单元可以在栈上创建(针对CISC架构而 言,RISC架构下,局部变量的存储单元是在寄存器上创建),函数执行结束时这些存储单元自动被释放。堆是系统管理的可以被程序利用的全局存储空间,动态 内存分配就是从堆上分配。

现在计算机(串行执行机制),都直接在代码层次支持栈这种数据结构。这体现在,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。比如ARM指令中的stmfd和ldmfd。因为栈内存分配运算内置于处理器的指令集中,所以效率很高,但是支持的数据有限,一般是整数、指 针、浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。
和栈不同,堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的。

栈和堆对比

  • 栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而堆是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。
  • 栈是系统数据结构,对于进程/线程是唯一的;堆是函数库内部数据结构,不一定唯一,不同堆分配的内存无法互相操作。
  • 栈空间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由alloc函数完成。栈的动态分配无需释放 (是自动的),也就没有释放函数。为可移植的程序起见,栈的动态分配操作是不被鼓励的!堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放 回系统,但是精确的申请内存/释放内存匹配是良好程序的基本要素。
  • stack:由系统自动分配,速度较快。但程序员是无法控制的。heap:由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

看了堆和栈的介绍,堆和栈都是内存,就好比一张桌子里面有两个抽屉,一个抽屉里面存放一种东西,另外一个抽屉存放另一种东西,这样方便存取方便,找东西也比较快捷方便。

栈和堆存储


  1. 图中描述的是操作系统分配的虚拟地址空间,不是实际的内存空间哦;堆和栈在地址空间上面是一定在一起的,但是物理内存则不一定。(虚拟地址空间只是存了一个地址,这个地址指向了一块实际的内存空间)。
  2. 堆的地址空间是从低到高存储,栈的地址空间是从高到底存储。(就是一个小区,有100栋楼,堆从1-100开始分房子;而栈是从100到1分房子)
  3. 局部变量,操作数都是放在栈里面的,使用时入栈占用栈内存空间,时候完毕后出栈释放栈内存空间;堆则不一样,new关键字有程序自己分配内存空间,使用完毕后自己回收空间垃圾值。

注意:每个线程都有自己的栈空间哦;所以多线程的时候栈是不用担心的,栈是线程私有的,不存在线程安全问题。
出栈后自动释放空间,所以这也就是后面要将的,垃圾收集从来没有说过要针对栈的,都是针对堆操作的。(java里面方法区和堆在这里都是堆空间)

顺带讲一下高低位

上面说的堆和栈的高低位是地址空间的高低位,就是楼层编号一样。这里讲的高低位则是字节序的高低位,是不一样的哦。
在汇编指令中,刚开始的cpu是16位的,分为高8位,低8位,分别用AH,AL指令操作高,低位。AH直接指向高位地址,Al直接指向低8位地址。
后来CPU支持32位了,为了向下兼容,32位的高位用EAX表示,那么64位的CPU呢?64位的高位还没有相应的指令,所以64位的二进制字节序取值需要使用位运算,向右移位操作。高低位的区分只是为了存取方便和读取效率高。

参考文献

http://blog.csdn.net/u011080472/article/details/51321769
《深入理解java虚拟机》
堆和栈:http://www.cnblogs.com/SinSay/archive/2008/11/12/1332076.html
http://zy77612.iteye.com/blog/1152225

猜你喜欢

转载自blog.csdn.net/piaoslowly/article/details/81458865