请选择 进入手机版 | 继续访问电脑版

热点推荐

    查看: 17|回复: 0

    JVM内存区域之常量池

    [复制链接]

    该用户从未签到

    671

    主题

    671

    帖子

    2013

    积分

    金牌会员

    Rank: 6Rank: 6

    积分
    2013
    发表于 7 天前 | 显示全部楼层 |阅读模式
    Java常量池的划分
    / H. Z( g( N. H8 y1 G! s
    0 T7 f/ @8 U0 {6 ~( u1 HJava常量池是一个经久不衰的话题,Java的内存分配中,实际上分为两种形态:静态常量池和运行时常量池。
    ! X! W4 x/ [1 X9 _9 [. I所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
    , i! \1 x3 X% S* f而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。1 s/ S4 N5 o7 J+ I
    也就是说静态常量池还可以分为一些基础类型和字符串的开启常量池,其中重点关注字符串常量池;另外一类是所谓的类信息相关的class常量池。这样我们的常量池部分可以划分为字符串常量池,class(类信息)常量池和运行时常量池三类了,以下分别看一下:
    ) e5 G0 v, Q+ R5 A- S1. 字符串常量池(String Constant Pool):, I" s% Z% t7 e9 C' `) M; ~5 q

    2 |, M9 G3 G$ O+ i1 b1.1:字符串常量池在Java内存区域的哪个位置?2 Z: n8 J, |/ ^: t! v
    在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;在JDK7.0版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。
    * y- Z! U' q, _/ f7 j5 F. Y1.2:字符串常量池是什么?
    / M& i  Y5 t, J2 o+ m' C在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。
    8 W$ B  T& ^; R3 P3 ?在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
    - m( O6 @, d4 `/ o( k在JDK7.0中,StringTable的长度可以通过参数指定:- S& @4 m( z  |( f5 \/ H
    -XX:StringTableSize=66666) }% {2 [4 ]) M4 l, w# k; y
    1.3:字符串常量池里放的是什么?7 s' g. H$ E/ T9 \4 y3 w
    在JDK6.0及之前版本中,String Pool里放的都是字符串常量;
    ! |  w( R2 y4 n2 I, X( S在JDK7.0中,由于String.intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。) z& S  O1 i8 G5 G9 I4 {
    需要说明的是:字符串常量池中的字符串只存在一份!! O6 E" J3 @& b0 h
    如:2 u! o4 R, e: h. h# o
    String s1 = "hello,world!";
    " G  l) W+ X# f" M0 y: k& N, w- KString s2 = "hello,world!";6 D& k0 c7 u' z7 L9 s. R7 X
    1+ {+ R5 S1 b5 b% s
    2
    8 t! E: b2 ]+ y/ Q% p5 Y即执行完第一行代码后,常量池中已存在 “hello,world!”,那么 s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。(这里具体的字符串如何分配就不细说了,可以看我的另一篇博客)! O1 y2 C+ C, s4 n4 c# v* C+ }- t( {! l
    2.class常量池(Class Constant Pool):
    " ~" ^0 J( l+ }2 [. v. k" C5 J3 u. V% x4 B; B5 `7 [0 N4 u% M
    2.1:class常量池简介:" ?* b, r& I' s2 g
    我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);7 ~) x) O9 X, ~
    每个class文件都有一个class常量池。
    . ^% c2 S5 U0 c% J6 W2.2:什么是字面量和符号引用:, d- {0 c/ o8 [) i* K
    字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;1 F  c9 _' m. ^) G$ Q, a
    符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。+ O8 |$ A' R0 K* z7 [7 E
    3.运行时常量池(Runtime Constant Pool):
    3 ^- N: f" ^* C2 y& v( A9 h6 |
    ( x0 N7 H" w6 K. a/ b/ Y: h4 b运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用% ?0 R) G" G* [# [, m0 |8 v. ^1 O
    JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。
    ) l2 t; h4 E% @. b+ L在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
    & U% w, @7 j9 n. L我们来看一个字符串常量池的例子:
    ! e; O0 I/ S2 l: ^4 b0 lString s1 = "Hello";String s2 = "Hello";String s3 = "Hel" + "lo";String s4 = "Hel" + new String("lo");String s5 = new String("Hello");String s6 = s5.intern();String s7 = "H";String s8 = "ello";String s9 = s7 + s8;       System.out.println(s1 == s2);  // trueSystem.out.println(s1 == s3);  // trueSystem.out.println(s1 == s4);  // falseSystem.out.println(s1 == s9);  // falseSystem.out.println(s4 == s5);  // falseSystem.out.println(s1 == s6);  // true首先说明一点,在java 中,直接使用==操作符,比较的是两个字符串的引用地址,并不是比较内容,比较内容请用String.equals()。  x* B& @" z8 d2 p( d
    s1 == s2这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。; k0 r: M' Z; B: W- M; \
    s1 == s3这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,因此String s3 = "Hel" + "lo";在class文件中被优化成String s3 = "Hello";,所以s1 == s3成立。/ V( [% r! `9 Y  Z0 P7 Y: P7 E
    s1 == s4当然不相等,s4虽然也是拼接出来的,但new String("lo")这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,必须等到运行时才可以确定结果,结合字符串不变定理,鬼知道s4被分配到哪去了,所以地址肯定不同。配上一张简图理清思路:% g- W7 X, ]7 ~7 K+ Q4 r* a( h' `
    ' O  a# t1 }; X, ]  H* }* K
    3 _0 o" P% M$ z) @& b) b5 j, h- ^
    s1 == s9也不相等,道理差不多,虽然s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,编译器毕竟是编译器,不可能当解释器用,所以不做优化,等到运行时,s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。
    - }& @, d/ T, ^2 F, b: G1 P
    9 x+ T  L7 D1 {/ e! G
    & K! x# t7 R9 E) a
    s4 == s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。# R8 b; q5 n- R. a# A
    s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。
    ) q4 c3 l) n2 e, S至此,我们可以得出三个非常重要的结论:. t4 I" Y; P: O, c5 u
    1)必须要关注编译期的行为,才能更好的理解常量池。
    * ?0 ?% g. p+ p! J) P! P  s' B2)运行时常量池中的常量,基本来源于各个class文件中的常量池。5 [% ?$ V& B1 \: N1 M! k
    3)程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm不会自动添加常量到常量池。6 E2 D3 ~- _: |( M) C* B3 k
    以上所讲仅涉及字符串常量池,实际上还有整型常量池、浮点型常量池等等,但都大同小异,只不过数值类型的常量池不可以手动添加常量,程序启动时常量池中的常量就已经确定了,比如整型常量池中的常量范围:-128~127,只有这个范围的数字可以用到常量池。; C# l% J3 {& ]0 a) ^
    常量池中变量的底层形态
    " k( x) `/ _& ^8 K- t, s
    8 R4 Y0 P' x9 l! n8 [以下我们通过查看底层class文件结构来深入的看一下常量池区域,前文提到过,class文件中存在一个静态常量池,这个常量池是由编译器生成的,用来存储Java源文件中的字面量(本文仅仅关注字面量),假设我们有代码就是如下一行:
    - L+ N% i4 L8 ^- I3 O: G8 VString s = "hi";0 A7 p) j6 u$ j* [: L" }
    将代码编译成class文件后,用winhex打开二进制格式的class文件。如图:  c; W! |6 l# H; f1 I
    2 w9 J9 I" {% D  d) n

    % `$ r; Z* _4 @& r( K& r简单讲解一下class文件的结构,开头的4个字节是class文件魔数,用来标识这是一个class文件,说白话点就是文件头,既:CA FE BA BE。
    5 Z4 M  E/ x# f% _8 y# _紧接着4个字节是java的版本号,这里的版本号是34,因为笔者是用jdk8编译的,版本号的高低和jdk版本的高低相对应,高版本可以兼容低版本,但低版本无法执行高版本。所以,如果哪天读者想知道别人的class文件是用什么jdk版本编译的,就可以看这4个字节。$ e3 S4 L. `: f6 S. p
    接下来就是常量池入口,入口处用2个字节标识常量池常量数量,本例中数值为001A,翻译成十进制是26,也就是有25个常量,其中第0个常量是特殊值,所以只有25个常量。
    6 j* a& H7 A4 h. Y1 A' X( z常量池中存放了各种类型的常量,他们都有自己的类型,并且都有自己的存储规范,本文只关注字符串常量,字符串常量以01开头(1个字节),接着用2个字节记录字符串长度,然后就是字符串实际内容。本例中为:01 00 02 68 69。
    8 Q# D0 R' ^) p# u: u接下来再说说运行时常量池,由于运行时常量池在方法区中,我们可以通过jvm参数:-XX:PermSize、-XX:MaxPermSize来设置方法区大小,从而间接限制常量池大小。假设jvm启动参数为:-XX:PermSize=2M -XX:MaxPermSize=2M,然后运行如下代码:
    0 ^9 ]! d4 o" m//保持引用,防止自动垃圾回收
    7 H: G% J( Z* k8 A. HList list = new ArrayList();  J" {# ]) c) c9 f: v
    int i = 0;5 d0 ^/ ?  o, O. x
    while(true){+ ~- |$ K* c. ?7 F4 m
    //通过intern方法向常量池中手动添加常量; `/ ~3 a. y( ?/ z  n
    list.add(String.valueOf(i++).intern());
    ! l) t) w5 {" J8 M}& m/ l; P/ Q8 _
    程序立刻会抛出:Exception in thread "main" java.lang.outOfMemoryError: PermGen space异常。PermGen space正是方法区,足以说明常量池在方法区中。
    % y7 q. \' E0 D: W9 J
    ; E3 m$ w1 S7 b* r6 mJava吧 收集整理 java论坛 www.java8.com
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    快速回复 返回顶部 返回列表