jvm内存管理
java的堆,栈,静态代码区 详解
一:在JAVA中,有六个不同的地方可以存储数据:
1. 程序计数器(Program Counter Register)。是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
------程序计数器在JVM中占用的空间很少,最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制。也是JVM规范里唯一规定没有任何OutOfMemoryError情况的区域。
2. 栈(stack)。与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程隔离的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)。栈帧是一个用于存储局部变量表、操作数栈、动态连接、方法出口等信息是一个数据结构。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
------存放基本类型的局部变量数据和对象,数组的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放在常量池中)
3. 堆(heap)。Java堆(Java Heap)是虛拟机所管理的内存中最大的一块。堆不同于栈的好处是:堆区是所有线程共享的一块内存区域,在虚拟机启动时创建。同时用堆进行存储分配比用栈进行存储存储需要更多的时间。此内存区域的唯一目的就是存放对象实例, Java世界里“几乎” 所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是:”所有的对象实例以及数组都应当在堆上分配”
------存放所有new出来的对象以及数组。
4. 方法区(非堆)。方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
说到方法区,不得不提一下”永久代”这个概念,尤其是在JDK 8以前,许多Java程序员都习惯在HotSpot虚拟机上开发、部署程序,很多人都更愿意把方法区称呼为”永久代”(PermanentGeneration),或将两者混为一谈。原因是当时的HotSpot虚拟机设计团队选择把垃圾收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。但是对于其他虚拟机实现,譬如JRockit、J9来说,是不存在永久代的概念的。
JDK6,JDK7,JDK8三个阶段关于“永久代”、“方法区”、“元空间”的关系变化过程给大家梳理一下!
JDK1.7中,符号引用(Symbols)转移到了native heap;字符串常量池(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。
但现在回头来看,当年使用永久代来实现方法区的决定并不是一个好主意,
a、这种设计导致了Java应用更容易遇到内存溢出的问题和性能问题。
b、类及方法的信息等比较难预估其大小,永久代要求配置参数-XX: MaxPermSize,即使不设置也有默认大小
因此对于永久代的大小指定比较困难,
太小容易出现永久代溢出,太大则容易导致老年代溢出。
c、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
到了JDK8,完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space) 来代替了。这样就没有参数-XX: MaxPermSize的大小限制,减少了OOM的机会。当然也不是无限大哈,受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,当方法区满足不了程序的空间需求,OOM仍然会出现的。
------存放已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
5. 运行时常量池。前面刚刚讲了JVM规范中描述方法区应该存储的数据:被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存。
常量,就进一步涉及一个叫“常量池”的概念,常量池主要可以分为以下几种:
(1)静态常量池:即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串/数字这些字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量和符号引用量。字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等;符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名,字段名称描述符,方法名称描述符
(2)运行时常量池:虚拟机会将在类加载后把各个class文件中的常量池载入到运行时常量池中,前面的静态常量池只是一个静态文件结构,运行时常量池是方法区的一部分,是一块内存区域。运行时常量池可以在运行期间将符号引用解析为直接引用,即把那些描述符(名字)替换为能直接定位到字段、方法的引用或句柄(地址)。
(3)字符串常量池 :字符串常量池可以理解为运行时常量池分出来的部分。
加载时,对于Class的静态常量池,如果是字符串会被装到字符串常量池中。
字符串池是 JVM 层面的技术。
在 JDK 1.6 以及以前的版本中,字符串池是放在 Perm 区(Permanent Generation,永久代)。
在 JDK 1.7 的版本中,字符串常量池移到Java Heap。
在 JDK 1.8 的版本中在永久代移除后,字符串常量池和运行时常量池也没有放到新的方法区---元空间里,
而是留在了Java堆里。元空间里只存储类和类加载器的元数据信息了!
(4)还有一类常量池,实质叫包装类的对象池(也有称常量池),但时他们和JVM的静态/运行时常量池没有任何关系。比如:我们应该听说过整型常量池,这类包装类的对象池也是池化技术的应用,
但,并非是虚拟机层面的东西,而是 Java 在类封装里实现的。
有IntegerCache 用于换成Integer对象
有ByteCache用于缓存Byte对象
有ShortCache用于缓存Short对象
有LongCache用于缓存Long对象
有CharacterCache用于缓存Character对象
Byte, Short, Long有固定范围: -128 到 127。对于Character, 范围是 0 到 127。除了Integer以外,这个范围都不能改变。
Java虛拟机对于Class文件每一部分的格式都有严格规定,如每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、加载和执行,但对于运行时常量池,《Java虚拟机规范》并没有做任何细节的要求,不同提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
String类的intern()方法的理解:
先看一段测试:
String str1 = new String("1")+ new String("1"); System.out.println(str1.intern() == str1); System.out.println(str1 == "11");
JDK版本1.8,输出结果为:
true
true
再将上面的例子加上一行代码:
String str2 = "11";//新加的一行代码,其余不变 String str1 = new String("1")+ new String("1"); System.out.println(str1.intern() == str1); System.out.println(str1 == "11");
再运行,结果为:
false
false
是不是感觉莫名其妙!新定义的str2好像和str1没有半毛钱的关系,怎么会影响到有关str1的输出结果呢?intern方法的作用是把字符串加载到常量池中,返回在常量池里的对象的引用。
第一种情况: String str1 = new String("1")+ new String("1"); 这行代码在字符串常量池中生成“1”,并在堆空间中生成str1 引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。
str1.intern()这一行代码,是将 str1中的“11”字符串放入 String 常量池中。此时常量池中不存在“11”字符串,JDK8的常量池中不会再重复新创建一份对象了,直接存储堆中"11"的引用,常量池中放的是引用。
System.out.println(str1.intern() == str1); 返回true。 str1 == "11" 这一行代码会直接去常量池中创建 "11" ,但是发现已经有这个对象了,还是维持原来的引用。System.out.println(str1 == "11"); 返回true。
第二种情况: str2先在常量池中创建了"11",常量池中放的是"11"这个字符串对象本身。那么str1.intern(),想将 str1中的“11”字符串放入 String 常量池中,但是发现已经有这个对象了,常量池中不需要再重复创建一份对象了,返回了指向常量池"11"的引用。System.out.println(str1.intern() == str1); 返回false。str1 == "11" 这一行代码会直接去常量池中创建 "11" ,但是发现已经有这个对象了,还是维持原来的引用str2。
System.out.println(str1 == "11"); 返回false。
6. 直接内存(Direct Memory)。并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。
在JDK 1.4中新加入了NIO类,引入了一种基于通道(Channel) 与缓冲区(Buffer) 的I/0方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置方法区、堆区等大小的参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。说这个点儿,也就是说以后配置虚拟机参数时,要考虑到预留部分直接内存出来!
就速度来说,有如下关系:
程序计数器< 栈 < 堆 < 其他
1、所有new出来的对象,都会放到堆内存中,这点没疑问!
2、class字节码,对应的类的结构信息,构造器,static修饰的变量值放在方法区中的静态变量区,jdk1.7后放在这个静态变量区转移到了堆内存里,这点也基本没问题!
3、但是有些最常用的东西,基本数据类型(int、long、boolean、byte、short、char、double、float)和对应的封装类(Byte,Short,Integer,Long,Character,Boolean,Double,Float)不用new关键,直接赋的值是放在哪里的。
首先要明白一点:只有方法执行的时候所用到的各种指令参数才会入栈出栈!
所以:
① 在方法执行的时候定义的参数或临时变量,如果是基本数据类型,那么这些变量本身的信息
和对应的值毫无疑问会放到栈帧里!
② 在类的属性上定义的变量,static修饰的,启动jvm时就加载到了方法区的静态变量区
(jdk1.7以后这个静态变量区转移到了堆内存里了)
③ 普通的属性变量,会放到堆内存里,作为对象的属性值
④ 只不过这里有一个需要我们注意的知识点,方法执行时基本数据类型的对应的包装中Byte,Short,Integer,Long,Character这5种整型的包装类型直接赋值时在对应值小于等于127且大于等于-128时会和string类型的值一样被放入常量池
超出了这个范围,自动装箱,new成对象放在堆里