java虚拟(一)--java内存区域和常量池概念

一、java运行时数据区

  也可以称为java内存区域,这是一种规范,具体实现和使用哪种虚拟机有关。运行时数据区和java内存模型不是一回事,不要弄混。

  官方文档地址:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

 

1.1、方法区

  线程共享,类装载过程中产生的java.lang.Class对象保存在方法区,而不是堆,请参考《深入理解java虚拟机》P215。

  jdk1.8之前HotSpot通过永久带实现方法区,为了对方法区的GC可以像堆一样管理内存,能够复用代码其他虚拟机没有永久带的概念,永久代的设计实现方法去并不是一个好的选择,因为更容易出现内存溢出。

  方法区主要存放类信息、常量、静态变量、即时编译后的代码等。

  垃圾回收主要是针对常量池回收和类型的卸载,这块区域的回收很难,尤其是类型卸载,可以选择不进行垃圾回收,但是回收很有必要的。

  PS:jdk1.8及以后,方法区被移除,通过Metaspace实现,而Metaspace使用的是直接内存,可以使用参数:-XX:MetaspaceSize来指定元

数据区的大小。

Tomcat中配置打印GC相关信息:可以证明MetaSpace的存在

JAVA_OPTS="-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log"

  然后直接打开,或者通过GC分析工具打开,就可以发现存在Metaspace内存,Server模式下默认使用Parallel垃圾收集器

MetaSpace实现方法区和之前通过PermGen Space实现的区别

  1、MetaSpace使用的是本地内存,PermGen使用的是jvm内存

  2、java.lang.OutOfMemoryError: PermGen Space这个异常不存在了

  3、字符串常量池存放在PermGen,容易出现性能问题和内存溢出,所以jdk1.7移动到了堆中

  4、类、方法等信息大小比较难确定,所以很难直接设置PermGen的大小

  5、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低

  6、还有一个很重要的点,要合并HotSpot和JRockit的代码,而JRockit没有MetaSpace,事实也证明了通过PermGen实现是一个错误的选择

1.2、虚拟机栈

  线程私有,生命周期和线程相同,每执行一个方法都会创建一个栈帧,从执行到结束,对应着栈帧在虚拟机栈的入栈到出栈过程,可以类比数据结构中的栈,java方法两种返回方式:

  1、return语句

  2、抛出异常

  这两种方式都会导致栈帧被弹出。

  栈帧:

    保存着局部变量表、操作数栈、方法出入口等。

  局部变量表:

    用来保存方法参数和返回值,也就是基本数据类型、对象的引用、returnAddress类型(指向一个字节码指令的地址)。

  double和long占用两个局部变量空间(variable slot),其余占用1个,slot空间大小在编译期间就确定,方法运行期间无法改变,也就是当程序发生异常,打印的堆栈信息,就是虚拟机栈。

举个栗子:解释局部变量表和操作数栈

public static Integer f1() {
    int a = 1;
    int b = 2;
    return a + b;
}

  我们通过javap进行反编译查看字节码知道,1和2这种int类型保存在局部变量表,而a+b的操作是从局部变量表中load数据到操作数栈,然后完成加法的操作。

  PS:可能出现Stack OverflowError、OutOfMemoryError错误。

1.3、本地方法栈

  线程私有,和虚拟机栈相似,一个为Java方法服务,一个为了本地方法服务,本地方法栈中方法实现的语言、方式等没有规定,由具体的虚拟机确定,在HotSpot中只有栈,没有虚拟机栈和本地方法栈的区别,其他的虚拟机如J9、JRocket等可能实现就不同,我们默认使用都是HotSpot。

  本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

  PS:可能出现Stack OverflowError、OutOfMemoryError错误

1.4、堆

线程共享,这是虚拟机内存最大的一块区域,也是GC的主要区域,几乎所有的对象和数组都保存在这里。

内存回收的角度分为:

新生代:Eden Space、From Survivor、To Survivor

老年代:

  1、主要用来保存大对象(可以通过-XX:PretenureSizeThreshold 设置大对象的阀值)。

  2、或者从新生代经过15次 minor GC存活下来的对象(-XX:MaxTenuringThreshold)。

  3、第二条不是绝对的,VM动态判断,如果Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold要求的年龄。

  默认Eden Space:From Survivor:To Survivor=8:1:1,可以通过-XX:SurvivorRatio调节,不同垃圾收集器的调优策略不同的,所以不要百度到需要调节这个参数,就认为一定有效。

  -XX:NewRatio:为Young区和Old区的比例

  PS:进一步划分的目的是更好地回收内存,或者更快地分配内存。

1.5、程序计数器

  线程私有,是一块很小的内存区域,记录着当前虚拟机字节码指令的地址(对于JNI,值为undefined),字节码解释器通过改变计数器的值来选择下一条执行的字节码,分支、循环、跳转、异常处理、线程恢复等功能都要依靠计数器。

  线程上下文切换的时候,为了能够恢复到正确的执行位置,需要每个线程都拥有一个独立的线程计数器,互不影响。

  PS:唯一一个没有规定OOM的区域

1.6、直接内存

  不属于Java内存区域,有可能出现OOM,jdk1.4出现了NIO,它可以通过Native Libraries分配堆外内存,通过Java堆中的DirectByteBuffer对象作为引用进行操作,在某些场景明显提高性能,以为避免了Java堆和Native堆来回复制数据。

  直接内存的分配不受Java堆大小限制,而是收到本机总内存的限制。

二、jvm的内存结构

上面说了jvm运行时数据区是一种规范,而对于HotSpot来说,堆区和非堆区(就是jdk1.8之前的方法区)的内存结构如下:

堆区的结构在上面有介绍过,在jdk1.8中,方法区由Metaspace实现,包含CCS(压缩类空间),只有启用短指针才会存在这部分内存。

Metaspace:

  存放的就是方法区的的数据。包含Class、Package、Method、Field、字节码、常量池、符号引用等

CCS:

  堆中的对象都有一个指向class对象的指针,64位系统中每个指针都是64位,为了性能考虑,使用短指针32位的,如果使用短指针就会启用压缩类空间,将这些class对象保存在CCS当中。也就是保存着32位指针的Class。

CodeCache:

  JIT即使编译的Native代码、JNI使用的C代码

我们可以验证一下CCS的启用与关闭:默认开启

通过jps -l查找Java进程,然后通过

[root@iZuf6fkfhthmdm1nwdg5isZ bin]# jstat -gc 23631
 S0C    S1C    S0U    S1U      EC       EU        OC         OU         MC      MU      CCSC   CCSU       YGC     YGCT    FGC     FGCT     GCT   
20480.0 19968.0  0.0    0.0   283136.0 144816.6  52736.0    13931.5   35416.0 34236.6 4480.0   4208.6     10    0.215      2      0.220    0.435

然后在Catalina.sh中JAVA_OPTS加入-XX:-UseCompressedClassPointers进行禁用,然后重启

[root@iZuf6fkfhthmdm1nwdg5isZ bin]# jstat -gc 24008
 S0C    S1C      S0U    S1U      EC       EU        OC         OU       MC     MU      CCSC   CCSU     YGC     YGCT    FGC    FGCT     GCT   
20992.0 20480.0  0.0    0.0   282112.0 160448.0  51712.0    14654.7   35416.0 34133.3  0.0    0.0       10    0.192   2      0.153    0.345

验证CodeCache的存在:

  因为CodeCache保存的是即时编译的代码的代码,我们通过-Xint解释执行的方式启动,当然启动肯定会变慢的,因为默认以Mix方式启动然后重启Tomcat

 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
19456.0 20480.0 14800.5  0.0   282624.0 260677.0  52224.0    22069.4   32000.0 30489.7  0.0    0.0       10    0.140   1      0.065    0.205

我们发现MC(Metaspace Capacity)变小了,也证明了CodeCache的存在

三、常量池

  因为大家一般都是通过周志明老师的书里面学习jvm的内容,对于常量池的部分的讲解很容易让人搞懵逼,最开始说Class文件中包含常量池,指的是Class常量池,又说运行时常量池可以通过intern()动态添加字符串,看到这里我都蒙了,intern()是操作字符串常量池的,难道字符串常量池是运行时常量池的一部分吗?

  而实现方法区的内存溢出是就是通过intern()实现,说的也是运行时常量池导致的内存溢出,所以我只能认为字符串常量池是运行时常量池的一部分,而且jdk1.7之后,运行时常量池和字符串常量池都从方法区移到堆中,所以,我不得不相信

  不知道理解的对不对,有不同意见的可以评论提出来。。。。

2.1、字符串常量池

  在HotSpot中通过StringTable类实现功能,StringTable是一个hash表,默认长度大小1009,被所有类共享。字符串常量由字符组成,保存在StringTable上面。在jdk1.6当中,StringTable的长度是固定1009,如果存放在StringTable中的字符串很多,造成hash冲突的几率很大,链表过长,当通过String.intern()查找String Pool时,性能就会降低。

  在jdk1.7当中,StringTable的长度可以通过-XX:StringTableSize设置

存放的内容:

  String.intern()主要是为了复用字符串常量,节省内存空间

  在jdk1.6及以前的版本,存放的都是字符串常量,使用""声明的字符串都存储在这,例如:String str = "abc";

  在jdk1.7之后,String.intern()发生变化,如果字符串常量池不存在这个String对象,如果堆区存在这个对象,直接复制到字符串常量池,否则还是要创建字符串,然后返回字符串对象的引用。因此除了字符串常量,也可以存放堆中字符串常量的引用

  PS:在jdk1.7之后,字符串常量池从方法区转移到堆中

2.2、class常量池

  首先java代码通过javac编译成class文件,class文件中保存着类的相关信息(版本,类、字段、方法、接口等信息),除此之外,还有Class常量池,用来存放编译器产生的各种字面量(Literal)和符号引用(Symbolic References),每个class文件都有一个class常量池。

  字面量:1.String  2.基本数据类型  3.声明final的常量

  符号引用:1.类和方法的全限定名(类似于com.cfets.**.**这种)  2.字段的名称和描述符  3.方法的名称和描述符

2.3、运行时常量池

  就是class常量池被加载到方法区之后的版本,区别就是:字面量可以通过String.intern()动态添加,符号引用解析为直接引用(类加载的解析阶段)

  当类加载到内存之中,jvm会把class常量池的内存存放到运行时常量池,所以,运行时常量池也是每个class都有的。

  符号引用:上述有说明。以一组符号来描述所引用的目标,只要能定位到目标,无论是任何形式的字面量,和jvm实现的内存布局无关。

  直接引用:直接指向目标的指针、相对偏移量或者间接定位到目标的句柄,句柄和指针对应对象的访问定位方式,和jvm实现的内存布局有关。

  PS:JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池.所以jdk1.7之后,运行时常量池和字符串常量池都从方法区移到堆中

四、HotSpot中编译代码的方式

解释执行:

  逐条翻译字节码为可运行的机器码,优势在于不用等待。

即时编译:

  以方法为单位将字节码翻译成机器码,实际运行当中效率更高。

  在HotSpot中默认采用混合模式,其先解释执行字节码,然后将其中的热点代码(多次执行,循环等)直接编译成机器码,下次就不用再编译了,让其更快速地运行。使用混合模式的原因有二八定律和jvm优化的考虑在里面

通过java -version命令可以查看:

# java -version
java version "1.8.0_102"
Java(TM) SE Runtime Environment (build 1.8.0_102-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode)

通过-Xint(解释执行), -Xcomp(即时编译), 和-Xmixed设置编译方式,不过一般情况下不需要修改。

原文地址:https://www.cnblogs.com/huigelaile/p/diamondshine.html