澳门新葡萄京娱乐场 8

澳门新葡萄京娱乐场提高 Java 代码性能的各种技巧

Java 6,7,8 中的 String.intern – 字符串池

这篇文章将要讨论 Java 6
中是如何实现 String.intern 方法的,以及这个方法在 Java 7 以及 Java 8
中做了哪些调整。

缘起

开始介绍 intern()方法前,先看一个简单的 Java程序吧!下面是一段
Java代码,代码内容比较简单,简而言之,就是比较几个字符串是否相等并输出比较结果。然而,看似简单的字符串比较操作,却暗含玄机,聪明的你,能一字不差的说出最后的输出结果么?如果你知道答案并理解原因的话,那么你就可以选择跳过此篇博文去干更有意义的事了。若是不能的话,要不就跟随小编一起探明究竟吧!

public class Intern {
    // 测试 String.intern()的使用
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";
        String str3 = "a";
        String str4 = "bc";
        String str5 = str3 + str4;
        String str6 = new String(str1);

        print("------no intern------");
        printnb("str1 == str2 ? ");
        print( str1 == str2);
        printnb("str1 == str5 ? ");
        print(str1 == str5);
        printnb("str1 == str6 ? ");
        print(str1 == str6);
        print();

        print("------intern------");
        printnb("str1.intern() == str2.intern() ? ");
        print(str1.intern() == str2.intern());
        printnb("str1.intern() == str5.intern() ? ");
        print(str1.intern() == str5.intern());
        printnb("str1.intern() == str6.intern() ? ");
        print(str1.intern() == str6.intern());
        printnb("str1 == str6.intern() ? ");
        print(str1 == str6.intern());
    }
}

Duang, the true answer is over here:

------no intern------
str1 == str2 ? true
str1 == str5 ? false
str1 == str6 ? false

------intern------
str1.intern() == str2.intern() ? true
str1.intern() == str5.intern() ? true
str1.intern() == str6.intern() ? true
str1 == str6.intern() ? true

** 初步解析 **
——no intern——
Java语言会使用 常量池
保存那些在编译器就已确定的已编译的class文件中的一份数据,主要有类、接口、方法中的常量,以及一些以文本形式出现的符号引用,如:类和接口的全限定名;字段的名称和描述符;方法和名称和描述符等。因此在编译完Intern类后,生成的class文件中会在常量池中保存“abc”、“a”和“bc”三个String常量。

  • 变量str1和str2均保存的是常量池中“abc”的引用,所以str1==str2成立;
  • 在执行 str5 = str3 +
    str4这句时,JVM会先创建一个StringBuilder对象,通过StringBuilder.append()方法将str3与str4的值拼接,然后通过StringBuilder.toString()返回一个String对象,赋值给str5,因此str1和str5指向的不是同一个String对象,str1
    == str5不成立;
  • String str6 = new
    String(str1)一句显式创建了一个新的String对象,因此str1 ==
    str6不成立便是显而易见的事了。

——intern——
上面没有使用intern()方法的字符串比较相对比较好理解,然而下面这部分使用了intern()方法的字符串比较操作才是本文的重点。看到答案的你有没有一脸懵逼?

一、JVM 内存模型

字符串池

字符串池(有名字符串标准化)是通过使用唯一的共享 String 对象来使用相同的值不同的地址表示字符串的过程。你可以使用自己定义的 Map<String, String> (根据需要使用
weak 引用或者 soft 引用)并使用 map
中的值作为标准值来实现这个目标,或者你也可以使用 JDK
提供的 String.intern()

很多标准禁止在 Java 6
中使用 String.intern() 因为如果频繁使用池会市区控制,有很大的几率触发 OutOfMemoryException。Oracle
Java 7
对字符串池做了很多改进,你可以通过以下地址进行了解 以及 

String.intern()使用原理

查看 Java String类源码,可以看到 intern()方法的定义如下:

public native String intern();

String.intern()是一个Native方法,底层调用C++的
StringTable::intern方法实现。

当通过语句str.intern()调用intern()方法后,JVM
就会在当前类的常量池中查找是否存在与str等值的String,若存在则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用。因此,只要是等值的String对象,使用intern()方法返回的都是常量池中同一个String引用,所以,这些等值的String对象通过intern()后使用==是可以匹配的。

由此就可以理解上面代码中——intern——部分的结果了。因为str1、str5和str6是三个等值的String,所以通过intern()方法,他们均会指向常量池中的同一个String引用,因此str1.intern()
== str5.intern() == str6.intern()均为true。

根据 JVM 规范,JVM
内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。

Java 6 中的 String.intern()

在美好的过去所有共享的 String 对象都存储在 PermGen 中 —
堆中固定大小的部分主要用于存储加载的类对象和字符串池。除了明确的共享字符串,PermGen
字符串池还包含所有程序中使用过的字符串(这里要注意是使用过的字符串,如果类或者方法从未加载或者被条用,在其中定义的任何常量都不会被加载)

Java 6 中字符串池的最大问题是它的位置 — PermGen。PermGen
的大小是固定的并且在运行时是无法扩展的。你可以使用 -XX:MaxPermSize=N 配置来调整它的大小。据我了解,对于不同的平台默认的
PermGen 大小在 32M 到 96M
之间。你可以扩展它的大小,不过大小使用都是固定的。这个限制需要你在使用 String.intern 时需要非常小心
— 你最好不要使用这个方法 intern 任何无法控制的用户输入。这是为什么在
JAVA6 中大部分使用手动管理 Map 来实现字符串池

String.intern() in Java 6

Java
6中常量池位于PermGen(永久代)中,PermGen是一块主要用于存放已加载的类信息和字符串池的大小固定的区域。执行intern()方法时,若常量池中不存在等值的字符串,JVM就会在常量池中***
创建一个等值的字符串***,然后返回该字符串的引用。除此以外,JVM
会自动在常量池中保存一份之前已使用过的字符串集合。

** Java 6中使用intern()方法的主要问题就在于常量池被保存在PermGen中
**

  • 首先,PermGen是一块大小固定的区域,一般,不同的平台PermGen的默认大小也不相同,大致在32M到96M之间。所以不能对不受控制的运行时字符串(如用户输入信息等)使用intern()方法,否则很有可能会引发PermGen内存溢出;

  • 其次,String对象保存在
    Java堆区,Java堆区与PermGen是物理隔离的,因此,如果对多个不等值的字符串对象执行intern操作,则会导致内存中存在许多重复的字符串,会造成性能损失。

澳门新葡萄京娱乐场 1

Java 7 中的 String.intern()

Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的改变 —
字符串池的位置被调整到 heap
中了。这意味着你再也不会被固定的内存空间限制了。所有的字符串都保存在堆(heap)中同其他普通对象一样,这使得你在调优应用时仅需要调整堆大小。这
个改动使得我们有足够的理由让我们重新考虑在 Java 7 中使用
String.intern()。

String.intern() in Java 7

Java
7将常量池从PermGen区移到了Java堆区,执行intern操作时,如果常量池已经存在该字符串,则直接返回字符串引用,否则***
复制该字符串对象的引用*** 到常量池中并返回。

堆区的大小一般不受限,所以将常量池从PremGen区移到堆区使得常量池的使用不再受限于固定大小。除此之外,位于堆区的常量池中的对象可以被垃圾回收。当常量池中的字符串不再存在指向它的引用时,JVM就会回收该字符串。

可以使用 -XX:StringTableSize
虚拟机参数设置字符串池的map大小。字符串池内部实现为一个HashMap,所以当能够确定程序中需要intern的字符串数目时,可以将该map的size设置为所需数目*2(减少hash冲突),这样就可以使得String.intern()每次都只需要常量时间和相当小的内存就能够将一个String存入字符串池中。

-XX:StringTableSize的默认值:Java 7u40以前为:1009,Java 7u40以后:60013

1、虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个恒定的值,我们通过下面这段程序可以测试一下这个结果:

字符串池中的数据会被垃圾收集

没错,在 JVM
字符串池中的所有字符串会被垃圾收集,如果这些值在应用中没有任何引用。这是用于所有版本的
Java,这意味着如果 interned 的字符串在作用域外并且没有任何引用 —
它将会从 JVM 的字符串池中被垃圾收集掉。

因为被重新定位到堆中以及会被垃圾收集,JVM
的字符串池看上去是存放字符串的合适位置,是吗?理论上是 —
违背使用的字符串会从池中收集掉,当外部输入一个字符传且池中存在时可以节省内存。看起来是一个完美的节省内存的策略?在你回答这个之前,可以肯定的是你
需要知道字符串池是如何实现的。

intern()适用场景

Java
6中常量池位于PermGen区,大小受限,所以不建议适用intern()方法,当需要字符串池时,需要自己使用HashMap实现。

Java7、8中,常量池由PermGen区移到了堆区,还可以通过-XX:StringTableSize参数设置StringTable的大小,常量池的使用不再受限,由此可以重新考虑使用intern()方法。

intern()方法优点:

  • 执行速度非常快,直接使用==进行比较要比使用equals()方法快很多;
  • 内存占用少。

虽然intern()方法的优点看上去很诱人,但若不是在恰当的场合中使用该方法的话,便非但不能获得如此好处,反而还可能会有性能损失。

下面程序对比了使用intern()方法和未使用intern()方法存储100万个String时的性能,从输出结果可以看出,若是单纯使用intern()方法进行数据存储的话,程序运行时间要远高于未使用intern()方法时:

public class Intern2 {

    public static void main(String[] args) {
        print("noIntern: " + noIntern());
        print("intern: " + intern());
    }

    private static long noIntern(){
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            int j = i % 100;
            String str = String.valueOf(j);
        }
        return System.currentTimeMillis() - start;
    }

    private static long intern(){
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            int j = i % 100;
            String str = String.valueOf(j).intern();
        }
        return System.currentTimeMillis() - start;
    }
}

//Output:
noIntern: 48    // 未使用intern方法时,存储100万个String所需时间
intern: 99      // 使用intern方法时,存储100万个String所需时间

由于intern()操作每次都需要与常量池中的数据进行比较以查看常量池中是否存在等值数据,同时JVM需要确保常量池中的数据的唯一性,这就涉及到加锁机制,这些操作都是有需要占用CPU时间的,所以如果进行intern操作的是大量不会被重复利用的String的话,则有点得不偿失。由此可见,String.intern()主要
适用于只有有限值,并且这些有限值会被重复利用的场景,如:数据库表中的列名、人的姓氏、编码类型等。

栈溢出测试源码:

在 Java 6,7,8 中 JVM 字符串池的实现

字符串池是使用一个拥有固定容量的 HashMap 每个元素包含具有相同 hash
值的字符串列表。一些实现的细节可以从 Java bug
报告中获得 

默认的池大小是 1009 (出现在上面提及的 bug 报告的源码中,在 Java7u40
中增加了)。在 JAVA 6 早期版本中是一个常量,在随后的 java6u30 至 java6u41
中调整为可配置的。而在java
7中一开始就是可以配置的(至少在java7u02中是可以配置的)。你需要指定参数 -XX:StringTableSize=N,
 N 是字符串池 Map 的大小。确保它是为性能调优而预先准备的大小。

在 Java 6
中这个参数没有太多帮助,因为你仍任被限制在固定的 PermGen
内存大小中。后续的讨论将直接忽略 Java 6

总结:

  • String.intern()方法是一种手动将字符串加入常量池中的方法,原理如下:如果在常量池中存在与调用intern()方法的字符串等值的字符串,就直接返回常量池中相应字符串的引用,否则在常量池中复制一份该字符串,并将其引用返回(Java7中会直接在常量池中保存当前字符串的引用);
  • Java 6
    中常量池位于PremGen区,大小受限,不建议使用String.intern()方法,不过Java
    7
    将常量池移到了Java堆区,大小可控,可以重新考虑使用String.intern()方法,但是由对比测试可知,使用该方法的耗时不容忽视,所以需要慎重考虑该方法的使用;
  • String.intern()方法主要适用于程序中需要保存有限个会被反复使用的值的场景,这样可以减少内存消耗,同时在进行比较操作时减少时耗,提高程序性能。

package com.paddx.test.memory;

Java 7 (直至 Java7u40)

在 Java7
中,换句话说,你被限制在一个更大的堆内存中。这意味着你可以预先设置好
String
池的大小(这个值取决于你的应用程序需求)。通常说来,一旦程序开始内存消耗,内存都是成百兆的增长,在这种情况下,给一个拥有
100 万字符串对象的字符串池分配 8-16M
的内存看起来是比较适合的(不要使用1,000,000
作为 -XX:StringTaleSize 的值 – 它不是质数;使用 1,000,003代替)

你可能期待关于 String 在 Map 中的分配 — 可以阅读我之前关于 HashCode
方法调优的经验。

style=”color: #888888;”>你必须设置一个更大的 -XX:StringTalbeSize 值(相比较默认的
1009 ),如果你希望更多的使用 String. style=”color: #888888;”>intern() — 否则这个方法将很快递减到 0
(池大小)。

我没有注意到在 intern 小于 100
字符的字符串时的依赖情况(我认为在一个包含 50
个重复字符的字符串与现实数据并不相似,因此 100
个字符看上去是一个很好的测试限制)

下面是默认池大小的应用程序日志:第一列是已经 intern 的字符串数量,第二列
intern 10,000 个字符串所有的时间(秒)

0; time = 0.0 sec
50000; time = 0.03 sec
100000; time = 0.073 sec
150000; time = 0.13 sec
200000; time = 0.196 sec
250000; time = 0.279 sec
300000; time = 0.376 sec
350000; time = 0.471 sec
400000; time = 0.574 sec
450000; time = 0.666 sec
500000; time = 0.755 sec
550000; time = 0.854 sec
600000; time = 0.916 sec
650000; time = 1.006 sec
700000; time = 1.095 sec
750000; time = 1.273 sec
800000; time = 1.248 sec
850000; time = 1.446 sec
900000; time = 1.585 sec
950000; time = 1.635 sec
1000000; time = 1.913 sec

测试是在 Core i5-3317U@1.7Ghz CPU
设备上进行的。你可以看到,它成线性增长,并且在 JVM
字符串池包含一百万个字符串时,我仍然可以近似每秒 intern 5000
个字符串,这对于在内存中处理大量数据的应用程序来说太慢了。

现在,调整 -XX:StringTableSize=100003 参数来重新运行测试:

50000; time = 0.017 sec
100000; time = 0.009 sec
150000; time = 0.01 sec
200000; time = 0.009 sec
250000; time = 0.007 sec
300000; time = 0.008 sec
350000; time = 0.009 sec
400000; time = 0.009 sec
450000; time = 0.01 sec
500000; time = 0.013 sec
550000; time = 0.011 sec
600000; time = 0.012 sec
650000; time = 0.015 sec
700000; time = 0.015 sec
750000; time = 0.01 sec
800000; time = 0.01 sec
850000; time = 0.011 sec
900000; time = 0.011 sec
950000; time = 0.012 sec
1000000; time = 0.012 sec

可以看到,这时插入字符串的时间近似于常量(在 Map
的字符串列表中平均字符串个数不超过 10
个),下面是相同设置的结果,不过这次我们将向池中插入 1000
万个字符串(这意味着 Map 中的字符串列表平均包含 100 个字符串)

2000000; time = 0.024 sec
3000000; time = 0.028 sec
4000000; time = 0.053 sec
5000000; time = 0.051 sec
6000000; time = 0.034 sec
7000000; time = 0.041 sec
8000000; time = 0.089 sec
9000000; time = 0.111 sec
10000000; time = 0.123 sec

现在让我们将吃的大小增加到 100 万(精确的说是 1,000,003)

1000000; time = 0.005 sec
2000000; time = 0.005 sec
3000000; time = 0.005 sec
4000000; time = 0.004 sec
5000000; time = 0.004 sec
6000000; time = 0.009 sec
7000000; time = 0.01 sec
8000000; time = 0.009 sec
9000000; time = 0.009 sec
10000000; time = 0.009 sec

如你所看到的,时间非常平均,并且与 “0 到 100万”
的表没有太大差别。甚至在池大小足够大的情况下,我的笔记本也能每秒添加1,000,000个字符对象。

public class StackErrorMock {

我们还需要手工管理字符串池吗?

现在我们需要对比 JVM
字符串池和 WeakHashMap<String, WeakReference<String>> 它可以用来模拟
JVM 字符串池。下面的方法用来替换 String.intern

private static final WeakHashMap<String, WeakReference<String>> s_manualCache = 
    new WeakHashMap<String, WeakReference<String>>( 100000 );

private static String manualIntern( final String str )
{
    final WeakReference<String> cached = s_manualCache.get( str );
    if ( cached != null )
    {
        final String value = cached.get();
        if ( value != null )
            return value;
    }
    s_manualCache.put( str, new WeakReference<String>( str ) );
    return str;
}

下面针对手工池的相同测试:

0; manual time = 0.001 sec
50000; manual time = 0.03 sec
100000; manual time = 0.034 sec
150000; manual time = 0.008 sec
200000; manual time = 0.019 sec
250000; manual time = 0.011 sec
300000; manual time = 0.011 sec
350000; manual time = 0.008 sec
400000; manual time = 0.027 sec
450000; manual time = 0.008 sec
500000; manual time = 0.009 sec
550000; manual time = 0.008 sec
600000; manual time = 0.008 sec
650000; manual time = 0.008 sec
700000; manual time = 0.008 sec
750000; manual time = 0.011 sec
800000; manual time = 0.007 sec
850000; manual time = 0.008 sec
900000; manual time = 0.008 sec
950000; manual time = 0.008 sec
1000000; manual time = 0.008 sec

当 JVM
有足够内存时,手工编写的池提供了良好的性能。不过不幸的是,我的测试(保留 String.valueOf(0 < N < 1,000,000,000))保留非常短的字符串,在使用 -Xmx1280M 参数时它允许我保留月为
2.5M 的这类字符串。JVM 字符串池 (size=1,000,003)从另一方面讲在 JVM
内存足够时提供了相同的性能特性,知道 JVM 字符串池包含 12.72M
的字符串并消耗掉所有内存(5倍多)。我认为,这非常值得你在你的应用中去掉所有手工字符串池。

private static int index = 1;

在 Java 7u40+ 以及 Java 8 中的 String.intern()

Java7u40 版本扩展了字符串池的大小(这是组要的性能更新)到
60013.这个值允许你在池中包含大约 30000
个独立的字符串。通常来说,这对于需要保存的数据来说已经足够了,你可以通过 -XX:+PrintFlagsFinal JVM
参数获得这个值。

我尝试在原始发布的 Java 8 中运行相同的测试,Java 8
仍然支持 -XX:StringTableSize 参数来兼容 Java 7 特性。主要的区别在于
Java 8 中默认的池大小增加到 60013:

50000; time = 0.019 sec
100000; time = 0.009 sec
150000; time = 0.009 sec
200000; time = 0.009 sec
250000; time = 0.009 sec
300000; time = 0.009 sec
350000; time = 0.011 sec
400000; time = 0.012 sec
450000; time = 0.01 sec
500000; time = 0.013 sec
550000; time = 0.013 sec
600000; time = 0.014 sec
650000; time = 0.018 sec
700000; time = 0.015 sec
750000; time = 0.029 sec
800000; time = 0.018 sec
850000; time = 0.02 sec
900000; time = 0.017 sec
950000; time = 0.018 sec
1000000; time = 0.021 sec

public void call(){

测试代码

这篇文章的测试代码很简单,一个方法中循环创建并保留新字符串。你可以测量它保留
10000 个字符串所需要的时间。最好配合 -verbose:gc JVM
参数来运行这个测试,这样可以查看垃圾收集是何时以及如何发生的。另外最好使用 -Xmx 参数来执行堆的最大值。

这里有两个测试:testStringPoolGarbageCollection 将显示 JVM
字符串池被垃圾收集 — 检查垃圾收集日志消息。在 Java 6 的默认 PermGen
大小配置上,这个测试会失败,因此最好增加这个值,或者更新测试方法,或者使用
Java 7.

第二个测试显示内存中保留了多少字符串。在 Java 6
中执行需要两个不同的内存配置 比如: -Xmx128M 以及 -Xmx1280M (10
倍以上)。你可能发现这个值不会影响放入池中字符串的数量。另一方面,在
Java 7 中你能够在堆中填满你的字符串。

/**
 - Testing String.intern.
 *
 - Run this class at least with -verbose:gc JVM parameter.
 */
public class InternTest {
    public static void main( String[] args ) {
        testStringPoolGarbageCollection();
        testLongLoop();
    }

    /**
     - Use this method to see where interned strings are stored
     - and how many of them can you fit for the given heap size.
     */
    private static void testLongLoop()
    {
        test( 1000 * 1000 * 1000 );
        //uncomment the following line to see the hand-written cache performance
        //testManual( 1000 * 1000 * 1000 );
    }

    /**
     - Use this method to check that not used interned strings are garbage collected.
     */
    private static void testStringPoolGarbageCollection()
    {
        //first method call - use it as a reference
        test( 1000 * 1000 );
        //we are going to clean the cache here.
        System.gc();
        //check the memory consumption and how long does it take to intern strings
        //in the second method call.
        test( 1000 * 1000 );
    }

    private static void test( final int cnt )
    {
        final List<String> lst = new ArrayList<String>( 100 );
        long start = System.currentTimeMillis();
        for ( int i = 0; i < cnt; ++i )
        {
            final String str = "Very long test string, which tells you about something " +
            "very-very important, definitely deserving to be interned #" + i;
//uncomment the following line to test dependency from string length
//            final String str = Integer.toString( i );
            lst.add( str.intern() );
            if ( i % 10000 == 0 )
            {
                System.out.println( i + "; time = " + ( System.currentTimeMillis() - start ) / 1000.0 + " sec" );
                start = System.currentTimeMillis();
            }
        }
        System.out.println( "Total length = " + lst.size() );
    }

    private static final WeakHashMap<String, WeakReference<String>> s_manualCache =
        new WeakHashMap<String, WeakReference<String>>( 100000 );

    private static String manualIntern( final String str )
    {
        final WeakReference<String> cached = s_manualCache.get( str );
        if ( cached != null )
        {
            final String value = cached.get();
            if ( value != null )
                return value;
        }
        s_manualCache.put( str, new WeakReference<String>( str ) );
        return str;
    }

    private static void testManual( final int cnt )
    {
        final List<String> lst = new ArrayList<String>( 100 );
        long start = System.currentTimeMillis();
        for ( int i = 0; i < cnt; ++i )
        {
            final String str = "Very long test string, which tells you about something " +
                "very-very important, definitely deserving to be interned #" + i;
            lst.add( manualIntern( str ) );
            if ( i % 10000 == 0 )
            {
                System.out.println( i + "; manual time = " + ( System.currentTimeMillis() - start ) / 1000.0 + " sec" );
                start = System.currentTimeMillis();
            }
        }
        System.out.println( "Total length = " + lst.size() );
    }
}

index++;

总结

  • 由于 Java 6
    中使用固定的内存大小(PermGen)因此不要使用 String.intern() 方法
  • Java7 和 8
    在堆内存中实现字符串池。这以为这字符串池的内存限制等于应用程序的内存限制。
  • 在 Java 7 和 8 中使用 -XX:StringTableSize 来设置字符串池 Map
    的大小。它是固定的,因为它使用 HashMap 实现。近似于你应用单独的字符串个数(你希望保留的)并且设置池的大小为最接近的质数并乘以
    2
    (减少碰撞的可能性)。它是的 String.intern 可以使用相同(固定)的时间并且在每次插入时消耗更小的内存(同样的任务,使用java WeakHashMap将消耗4-5倍的内存)。
  • 在 Java 6 和 7(Java7u40以前) 中 -XX:StringTableSize 参数的值是
    1009。Java7u40 以后这个值调整为 60013 (Java 8 中使用相同的值)
  • 如果你不确定字符串池的用量,参考:-XX:+PrintStringTableStatistics JVM
    参数,当你的应用挂掉时它告诉你字符串池的使用量信息。

call();

}

public static void main(String[] args) {

StackErrorMock mock = new StackErrorMock();

try {

mock.call();

}catch (Throwable e){

System.out.println(“Stack deep : “+index);

e.printStackTrace();

}

}

}

代码段 1

运行三次,可以看出每次栈的深度都是不一样的,输出结果如下。

澳门新葡萄京娱乐场 2

至于红色框里的值是怎么出来的,就需要深入到 JVM
的源码中才能探讨,这里不作详细阐述。

虚拟机栈除了上述错误外,还有另一种错误,那就是当申请不到空间时,会抛出
OutOfMemoryError。这里有一个小细节需要注意,catch 捕获的是
Throwable,而不是 Exception。因为 StackOverflowError 和 OutOfMemoryError
都不属于 Exception 的子类。

2、本地方法栈:

这部分主要与虚拟机用到的 Native 方法相关,一般情况下, Java
应用程序员并不需要关心这部分的内容。

3、PC 寄存器:

PC
寄存器,也叫程序计数器。JVM支持多个线程同时运行,每个线程都有自己的程序计数器。倘若当前执行的是
JVM 的方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native
方法,则PC寄存器中为空。

4、堆

堆内存是 JVM
所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过
GC 进行回收。当申请不到空间时会抛出
OutOfMemoryError。下面我们简单的模拟一个堆内存溢出的情况:

package com.paddx.test.memory;

import java.util.ArrayList;

import java.util.List;

public class HeapOomMock {

public static void main(String[] args) {

List<byte[]> list = new ArrayList<byte[]>();

int i = 0;

boolean flag = true;

while {

try {

i++;

list.add(new byte[1024 * 1024]);//每次增加一个1M大小的数组对象

}catch (Throwable e){

e.printStackTrace();

flag = false;

System.out.println(“count=”+i);//记录运行的次数

}

}

}

}

代码段 2

运行上述代码,输出结果如下:

澳门新葡萄京娱乐场 3

注意,这里我指定了堆内存的大小为16M,所以这个地方显示的count=14(这个数字不是固定的)。

5、方法区:

方法区也是所有线程共享。主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
关于方法区内存溢出的问题会在下文中详细探讨。

二、PermGen

绝大部分 Java 程序员应该都见过 “java.lang.OutOfMemoryError: PermGen
space “这个异常。这里的 “PermGen
space”其实指的就是方法区。不过方法区和“PermGen
space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM
规范的一种实现,并且只有 HotSpot 才有 “PermGen
space”,而对于其他类型的虚拟机,如 JRockit、J9 并没有“PermGen
space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在
jsp
页面比较多的情况,容易出现永久代内存溢出。我们现在通过动态生成类来模拟
“PermGen space”的内存溢出:

package com.paddx.test.memory;

public class Test {

}

代码段 3

package com.paddx.test.memory;

import java.io.File;

import java.net.URL;

import java.net.URLClassLoader;

import java.util.ArrayList;

import java.util.List;

public class PermGenOomMock{

public static void main(String[] args) {

URL url = null;

List<ClassLoader> classLoaderList = new
ArrayList<ClassLoader>();

try {

url = new File.toURI;

URL[] urls = {url};

while {

ClassLoader loader = new URLClassLoader;

classLoaderList.add;

loader.loadClass(“com.paddx.test.memory.Test”);

}

} catch (Exception e) {

e.printStackTrace();

}

}

}

代码段 4

运行结果如下:

澳门新葡萄京娱乐场 4

本例中使用的 JDK 版本是 1.7,指定的 PermGen 区的大小为
8M。通过每次生成不同URLClassLoader对象来加载Test类,从而生成不同的类对象,这样就能看到我们熟悉的
“java.lang.OutOfMemoryError: PermGen space ” 异常了。这里之所以采用 JDK
1.7,是因为在 JDK 1.8 中, HotSpot 已经没有 “PermGen
space”这个区间了,取而代之是一个叫做 Metaspace 的东西。下面我们就来看看
Metaspace 与 PermGen space 的区别。

三、Metaspace

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java
Heap或者是 Native
Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用转移到了native
heap;字面量(interned strings)转移到了java heap;类的静态变量(class
statics)转移到了java heap。我们可以通过一段程序来比较 JDK 1.6 与 JDK
1.7及 JDK 1.8 的区别,以字符串常量为例:

package com.paddx.test.memory;

import java.util.ArrayList;

import java.util.List;

public class StringOomMock {

static String base = “string”;

public static void main(String[] args) {

List<String> list = new ArrayList<String>();

for (int i=0;i< Integer.MAX_VALUE;i++){

String str = base + base;

base = str;

list.add(str.intern;

}

}

}

这段程序以2的指数级不断的生成新的字符串,这样可以比较快速的消耗内存。我们通过
JDK 1.6、JDK 1.7 和 JDK 1.8 分别运行:

JDK 1.6 的运行结果:

澳门新葡萄京娱乐场 5

JDK 1.7的运行结果:

澳门新葡萄京娱乐场 6

JDK 1.8的运行结果:

澳门新葡萄京娱乐场 7

从上述结果可以看出,JDK 1.6下,会出现“PermGen Space”的内存溢出,而在 JDK
1.7和 JDK 1.8 中,会出现堆内存溢出,并且 JDK 1.8中 PermSize 和
MaxPermGen 已经无效。因此,可以大致验证 JDK 1.7 和 1.8
将字符串常量由永久代转移到堆中,并且 JDK 1.8
中已经不存在永久代的结论。现在我们看看元空间到底是一个什么东西?

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集

-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

现在我们在 JDK 8下重新运行一下代码段 4,不过这次不再指定 PermSize 和
MaxPermSize。而是指定 MetaSpaceSize 和
MaxMetaSpaceSize的大小。输出结果如下:

澳门新葡萄京娱乐场 8

从输出结果,我们可以看出,这次不再出现永久代溢出,而是出现了元空间的溢出。

四、总结

通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK 8
中永久代向元空间的转换。不过大家应该都有一个疑问,就是为什么要做这个转换?所以,最后给大家总结以下几点原因:

1、字符串存在永久代中,容易出现性能问题和内存溢出。

2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

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

4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 854393687

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用”没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

发表评论

电子邮件地址不会被公开。 必填项已用*标注