Java运行时数据区域

146 阅读7分钟

Java基本概念

JVM:独立于硬件和操作系统,将字节码转换为特定机器代码,提供了内存管理/垃圾回收和安全机制等。JVM正是Java程序可以一次编写多处执行的原因。

JDK = Java语言 + JVM + Java API类库。JDK是用于支持Java程序开发的最小环境。

JRE = JVM + Java SE API子集。JRE是支持Java程序运行的标准环境。

  • Java ME:移动终端
  • Java SE:桌面级应用
  • Java EE:企业应用

Java运行时数据区域

  • 程序计数器:当前线程所执行的字节码的行号指示器。唯一一个不会出现OutOfMemoryError的内存区域。

  • 虚拟机栈:为虚拟机执行Java方法服务,描述方法运行过程,生命周期与线程相同。每个方法在执行的同时创建一个栈帧,每个方法从调用到执行完成,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

    • 每个方法的栈帧中都有局部变量表:存放方法中的局部变量,所需的内存空间在编译期间完成分配(64位的long double占用2个局部变量空间,其余数据类型只占用1个)

    局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建。而且,局部变量表的大小在编译时期就确定下来了,在创建的时候只需分配事先规定好的大小即可。此外,在方法运行的过程中局部变量表的大小是不会发生改变的。

  • 本地方法栈:为虚拟机使用到的Native方法服务(Native方法:调用非Java代码的接口)

  • Java堆:Java虚拟机所管理的内存中最大的一块,是垃圾收集器管理的主要区域。(创建:虚拟机启动时;目的:存放对象实例)可以处于物理上不连续的内存空间中,逻辑上连续即可。

    • 几乎所有的对象都在堆里。HotSpot虚拟机中Class类的对象存放在方法区里。数组也在堆里。
  • 方法区:存储已被虚拟机加载的类信息(类名、访问修饰符、常量池、字段描述、方法描述)、常量、静态变量、即时编译器编译后的代码等。方法区在逻辑上属于堆的一部分。

    • 运行时常量池:类加载后存放Class文件常量池(编译期生成的各种字面量和符号引用),运行期间也可能将新的常量放入池中,如String类的intern()方法就能在运行期间向常量池中添加字符串常量

PS: 虚拟机运行时数据区外还有直接内存

Java 堆 v.s. 栈

(1)栈内存用来存储局部变量和方法调用,堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。

(2)栈是线程私有的,堆是线程共享的。

(3)栈溢出,JVM抛出java.lang.StackOverFlowError;堆溢出,JVM抛出java.lang.OutOfMemoryError。

(4)栈的内存远远小于堆的内存大小。

方法区

(在Hotspot中,方法区只是在逻辑上独立,物理上还是包含在堆中)

Hotspot虚拟机用永久代实现方法区,其它JVM都没有永久代这个东西。

在Java 8中,永久代(在堆里)被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间。

移除永久代的原因

  1. 永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。但是有一个明显的问题,由于我们可以通过‑XX:MaxPermSize 设置永久代的大小,一旦类的元数据超过了设定的大小,程序就会耗尽内存,并出现内存溢出错误(OOM)。而元空间在本地内存,它的最大可分配空间就是系统可用内存空间,因此就不会遇到永久代存在时的内存溢出错误。
  2. 对永久代进行调优是很困难的。 ①永久代中的元数据可能会随着每一次Full GC发生而进行移动。 ②为永久代设置空间大小也是很难确定的,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等。 ③HotSpot虚拟机每种类型的垃圾回收器都需要特殊处理永久代中的元数据。 总结:将元数据从永久代剥离出来,不仅实现了对元空间的无缝管理,还可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化。

字符串常量池

字符串常量池存放的是对象引用,不是对象。在Java中,几乎所有对象都创建在堆内存中。
  1. 对于直接做+运算的两个字符串(字面量)常量,并不会放入String常量池中,而是直接把运算后的结果放入常量池中。
  2. 对于先声明的字符串字面量常量,会放入常量池,但是若使用字面量的引用进行运算就不会把运算后的结果放入常量池中了。
  3. 通过new操作符创建的字符串对象(只在堆里)不指向字符串池中的任何对象,但是可以通过使用字符串的intern()方法来指向其中的某一个。但是,new操作中的字面量会放入字符串常量池。
  4. JDK1.7中JVM把String常量池从方法区中移除了;JDK1.8中JVM把String常量池移入了堆中,同时取消了“永久代”,改用元空间代替。
  5. 更多参考:blog.csdn.net/sugar_rainb…

str.intern() 检查字符串常量池中是否存在str并返回池里的字符串引用;若池中不存在,则将其加入池中,并返回其引用。这样做主要是为了避免在堆中不断地创建新的字符串对象。

  • 不要在Java6中使用String.intern方法,由于Java6对JVM字符串常量池的存储是在一个固定内存区域(永久代区)进行的。
  • Java 7 和 8 将池实现于堆内存中。也就是说在Java 7和8中,你是由程序总内存大小所限制的
  • 请在Java 7 和 8 中使用-XX:StringTableSizeJVM参数以设置字符串常量池表大小。它大小固定,因为它是通过在存储单元存储了链表的哈希表实现的。为你程序中的不同字符串的数量进行预估(也就是那些你想缓存的字符串),并将池大小设为约等于此值的素数并乘上2(以减少可能发生的冲突)。这会让String.intern运行在一个常数时间内,并且每个缓存字符串所需内存会很小。
  • 在Java6以及Java 7 直到 Java7u40前,-XX:StringTableSize参数默认值是1009。在Java7u40中它增长为60013(在Java8中也是同样的值)。
  • 如果你不确定字符串常量池的使用情况,尝试使用 -XX:+PrintStringTableStatics 虚拟机参数。它将会在你程序结束时打印出你的字符串常量池的使用情况。

基本类型包装类常量池

  • Byte, Short, Integer, Long, Character, Boolean 这6种包装类实现了常量池技术,默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。
  • 两种浮点类型的包装类 Float, Double 没有实现常量池技术。