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实现,而元空间使用的是直接内存,可以使用参数:-XX:MetaspaceSize来指定元数据区

的大小。

我们在Tomcat中配置:

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

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

即时编译和解释执行:

  在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设置编译方式,不过一般情况下不需要修改。

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中只有栈,没有虚拟机栈和本地方法栈的区别。

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

  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函数库分配堆外内存,通过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的存在

三、常量池

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()发生变化,返回对象的引用,因此除了字符串常量,也可以存放堆中字符串常量的引用

  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)中开辟了一块区域存放运行时常量池

转载于:https://www.cnblogs.com/huigelaile/p/diamondshine.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值