《实战java虚拟机》07-分析Java堆

第七章 分析Java

内存一直是应用系统中最重要的组成部分,在Java应用中,系统内存通常会被分为几块,了解不同内存区域的作用有助于更好地编写Java应用,构建更加稳定的系统。

堆空间更是Java内存中最重要的区域,几乎所有的应用程序对象都在堆中分配,当系统出现故障时,具备Java堆的内存分析能力,也可以更加方便地进行诊断。

对症才能下药:找到内存溢出的原因

内存溢出(OutOfMemory,简称OOM)是一个令人头疼的问题,通常出现在某一块内存空间耗尽的时候

堆溢出

堆是Java程序中最重要的内存空间,由于大量的对象都被直接分配在堆上,所以它也是最有可能发生溢出的区间。一般来说,绝大部分Java内存溢出都属于这种情况。其原因是大量对象占据了堆空间,而这些对象都持有强引用,无法回收,当对象大小之和大于Xmx参数指定的对空间大小时,溢出错误就自然而然发生了。

下面这段代码就是堆溢出的典型案例,一个ArrayList对象总是持有byte数组的强引用,导致byte数组无法回收。

public class Test {

    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 2048; i++) {
            list.add(new byte[1024 * 1024]);
        }
    }
}

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
 at test.Test.main(Test.java:12)

可以看到,在错误信息中注明了“Java heap space”,表示这是一次堆溢出。

为了解决这个问题可以使用-Xmx参数指定更大的堆空间,但是堆空间也不是无限大的,通过MAT或者Visual VM等工具,找到大量占用堆空间的对象并在应用程序上作出合理的优化,也是十分必要的。

直接内存溢出

在Java的NIO中,支持直接内存使用,也就是通过Java代码获得一块堆外内存空间,这块空间是直接向操作系统申请的。直接内存的申请速度一般要比堆内存慢,但是其访问速度要快于堆内存,因此,对于那些可复用的,并且经常会被访问的空间,使用直接内存可以提高系统性能。但由于直接内存没有被Java虚拟机完全托管,若使用不当,也容易触发直接内存溢出,导致宕机。

过多线程导致OOM

由于每一个线程的开启都要占用系统内存,因此当线程数量太多时,也有可能导致OOM。由于线程的栈空间也是在堆外分配的,因此和直接内存非常相似,如果想让系统支持更多的线程,那么应该使用一个较小的堆空间。或者,减少每一个线程所占的内存空间。

注意:如果减少了线程的栈空间大小,栈溢出的风险就会相应的提升。

永久区溢出

永久区(Perm)是存放类元数据的区域。如果一个系统中有太多的类型,那么永久区是有可能溢出的。在JDK1.8中,永久区被一块称为元数据的区域替代,但是它们的功能是类似的,都是为了保存类的元信息。

public class Test {

    public static void main(String[] args) {
        try {
            for (int i = 0; i < 100000; i++) {
                CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap<>());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里使用jdk1.6,并使用参数-XX:MaxPermSize=5m

程序运行一段时间后,抛出如下异常:

Caused by: java.lang.OutOfMemoryError: PermGen space

解决永久代溢出问题,可以从以下几个方面考虑:

  • 增大MaxPermSize的值。
  • 减少系统需要的类的数量。
  • 使用CLassLoader合理地装载各个类,并定期进行回收。

GC效率低下引起的OOM

GC是内存回收的关键,如果GC效率低下,那么系统的性能会受到严重的影响。如果系统的堆空间太小,那么GC所花的时间就会比较多,并且回收所释放的内存会较少。

根据GC占用的系统时间,以及释放内存的大小,虚拟机会评估GC的的效率,一旦虚拟机认为GC的效率过低,会有可能直接抛出OOM。但是,虚拟机不会太随意判定,因为即使GC效率不高,强制终止程序还是显得有些野蛮。

一般情况下,虚拟机会检查如下几种情况:

  • 化在GC上的时间是否超过了98%。
  • 老年代释放的内存是否小于2%。
  • eden区释放的内存是否小于2%。
  • 是否连续5次GC都同时出现了上述几种情况。

只有满足所有条件,虚拟机才有可能抛出OOM:

java.lang.OutOfMemoryError:GC overhead limit exceeded

尽管虚拟机限制的条件如此严格,但是绝大部分场合还是会抛出堆溢出错误。这个OOM只起到辅助作用,帮助提示系统分配的堆可能大小,因此虚拟机并不强制一定开启这个错误提示,可以通过-XX:-UseGCOverheadLimit来禁止这种OOM产生。

无处不在的字符串:String在虚拟机中的实现

String字符串一直都是各种编程语言的核心。字符串应用很广泛,每一种计算机语言都必须对其做特殊的优化和实现。在Java中,String虽然不是基本数据类型,但是也享有了和基本数据类型一样的待遇。

String对象的特点

在Java语言中,String的三个基本特点:

  • 不变性。
  • 针对常量池的优化。
  • 类的final定义。

1.不变性

不变性是指String对象一旦生成,则不能再对它进行改变。String的这个特性可以泛化成不变(immutable)模式,即一个对象的状态在对象被创建之后就不再发生变化。不变模式的主要作用在于,当一个对象需要被多线程共享并且访问频繁时。可以省略同步和锁等待的时间,从而大幅提高性能。

注意,不变性可以提高多线程访问的性能。因为对象不可变,对于所有线程都是只读的,多线程访问时,即使不加同步也不会导致数据不一致,减少了系统开销。

由于不变性,一些看起来像修改的操作,实际上都是依靠产生新的字符串实现的。比如String.substring()String.concat()方法,它们都没有修改原始字符串,而是产生了一个新的字符串,这一点需要注意。

如果是一个可以修改的字符串,那么需要使用Stringbuffer或者Stringbuilder对象。

2.针对常量池的优化

针对常量池的优化指当两个String对象拥有相同的值时,它们只引用常量池中的同一个副本。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。

public static void main(String[] args) {
        String str1 = new String("abc");
        String str2 = new String("abc");
        System.out.println(str1 == str2); //false
        System.out.println(str1 == str2.intern());  //false
        System.out.println("abc" == str2.intern()); //true
}

虽然str1和str2的内容相同,但是在堆中的引用是不同的。String.intern()返回字符串在常量池的引用,显然和str1也是不同的,而String.intern()始终和常量字符串相等。

3.类的定义

除上面两点外,final类型定义也是String对象的重要特点。作为final类的String对象在系统中不能有任何子类

《实战java虚拟机》07-分析Java堆
String的内存分配方式

有关String的内存泄漏

内存泄漏简单来说是指由于疏忽或程序未能正确释放已经不再使用的内存,并不是物理内存消失,而是指不再使用的对象占据内存不被释放,导致可用内存不断减小,最终可能导致内存溢出。

这里以String.substring()方法为例。

在JDK1.6中,String主要由三部分组成:value数组、offset偏移、count长度。

《实战java虚拟机》07-分析Java堆
String对象内部结构

这个结构为内存泄漏埋下了伏笔,字符串的实际内容由value、offset、count一起决定,而非value一项。

如果value数组包含一个字符,而count长度只有1个字节,那么String的实际上只有1个字节,却占用了至少100个字节,剩余99个就属于内存泄漏的部分,它们不会被使用,不会被释放,却长期占用内存,直到字符串被回收。

在JDK1.7后String实质性内容都由value来决定,去掉了offset、count两项

有关String常量池的位置

在虚拟机中,有一块被称为常量池的区域专门存放字符串常量。在JDK1.6之前,这块区域属于永久代的一部分,在JDK1.7之后,它就被移到了堆中进行管理

示例:

-Xmx5m -XX:MaxPermSize=5m
public class StringInternOOM {
    
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

上述代码在JDK1.6中会抛出PermGen space,在JDK1.7之后会抛出Java heap space

溢出的区域已经不同,JDK1.6中发生在永久区,而JDK1.7之后则发生在堆中,则间接表明了常量池的位置变化。


原文始发于微信公众号(Java菜鸟程序员):《实战java虚拟机》07-分析Java堆

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/21065.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!