Java内存问题的一些见解

在Java中,内存泄露和其他内存相关问题在性能和可扩展性方面表现的最为突出。我们有充分的理由去详细地讨论他们。

电子书下载地址:
http://wiki.jikexueyuan.com/project/java-interview-bible/

Java内存模型——或者更确切的说垃圾回收器——已经解决了许多内存问题。然而同时,也带来了新的问题。特别是在有着大量并行用户的J2EE运行环境下,内存越来越成为一种至关重要的资源。乍看之下,这似乎有些奇怪,因为当前内存已经足够廉价,并且我们也有了64位的JVM和更先进的垃圾回收算法。

1. GC线程是否为守护线程?()

答案:是

解析:线程分为守护线程和非守护线程(即用户线程)。

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着
JVM 一同结束工作。
守护线程最典型的应用就是 GC (垃圾回收器)

接下来,我们将会仔细的讨论一下关于Java内存的问题。这些问题可以分为四组:

2. 解释内存中的栈(stack)、堆(heap)和静态存储区的用法。

答:通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间;而通过new关键字和构造器创建的对象放在堆空间;程序中的字面量(literal)如直接书写的100、“hello”和常量都是放在静态存储区中。栈空间操作最快但是也很小,通常大量的对象都是放在堆空间,整个内存包括硬盘上的虚拟内存都可以被当成堆空间来使用。

String str = new String(“hello”);

上面的语句中 str 放在栈上,用 new
创建出来的字符串对象放在堆上,而“hello”这个字面量放在静态存储区。

  • 澳门新葡亰3522平台游戏,在Java中,内存泄露一般都是由于引用对象不再被使用而造成的。当有多个引用的对象,同时这些对象又不再需要,然而开发者又忘记清理它们,这时极容易导致内存泄露的发生。
  • 执行消耗太多的内存而导致不必要的高内存占用。这在为了用户体验而管理大量状态信息的 Web 应用中很常见。随着活跃用户数量的增加,内存很快到达了上限。未绑定或低效缓存配置是持续高内存占用的另一来源。
  • 当用户负载增加时,低效的对象创建容易导致性能问题。从而垃圾回收器必须不断地清理堆内存。而这导致了垃圾回收器对CPU产生了不必要的高占用。随着CPU因垃圾回收而被阻塞,应用程序响应时间频繁的增加,导致其一直处于中等负载之下。这种行为也成为“GC trashing”。
  • 低效的垃圾回收行为往往是由于垃圾回收器的缺失或者错误的配置。这些垃圾回收器将会时刻追踪对象是否被清理。然而这种行为如何以及何时发生必须由配置或者程序员,或者系统架构师决定的。通常,人们只是简单地“忘记”了正确的配置和优化垃圾回收器。我曾参加过一些关于“性能”的专题讨论会,发现一个简单的参数变化将会导致高达25%的性能提升。

3. Java 中会存在内存泄漏吗,请简单描述。

答:理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是
Java
被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被
GC 回收也会发生内存泄露。一个例子就是 Hibernate
的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象。下面的例子也展示了
Java 中发生内存泄露的情况:

package com.lovo;  

import java.util.Arrays;  
import java.util.EmptyStackException;  

public class MyStack<T> {  
    private T[] elements;  
    private int size = 0;  

    private static final int INIT_CAPACITY = 16;  

    public MyStack() {  
        elements = (T[]) new Object[INIT_CAPACITY];  
    }  

    public void push(T elem) {  
        ensureCapacity();  
        elements[size++] = elem;  
    }  

    public T pop() {  
        if(size == 0)   
            throw new EmptyStackException();  
        return elements[--size];  
    }  

    private void ensureCapacity() {  
        if(elements.length == size) {  
            elements = Arrays.copyOf(elements, 2 * size + 1);  
        }  
    }  
}  

上面的代码实现了一个栈(先进后出(FILO))结构,乍看之下似乎没有什么明显的问题,它甚至可以通过你编写的各种单元测试。然而其中的
pop 却存在内存泄露的问题,当我们用 pop
方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过期引用(obsolete
reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的,这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,从而对性能造成重大影响,极端情况下会引发
Disk Paging(物理内存与硬盘的虚拟内存交换数据),甚至造成
OutOfMemoryError。

在大多数情况下,内存问题不仅影响性能,还会影响可扩展性。每次请求消耗的内存数量越高,用户或Session可以执行的并行事务就越少。在某些情况下内存问题也影响可用性。当JVM耗尽了内存或者即将接近内存极限,这个时候它将退出并报OutOfMemory错误。这时经理会来到你的办公室,你就知道自己摊上大事了。

4. GC 是什么?为什么要有 GC?

答:GC
是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java
提供的 GC
功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java
语言没有提供释放已分配内存的显示操作方法。Java
程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc()
Runtime.getRuntime().gc() ,但 JVM 可以屏蔽掉显示的垃圾回收调用。

垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在
Java 诞生初期,垃圾回收是 Java
最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今
Java 的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得 iOS
的系统比 Android 系统有更好的用户体验,其中一个深层次的原因就在于
Android 系统中垃圾回收的不可预知性。

内存问题很难被解决通常有两个原因: 第一,某些情况下分析很复杂,也很困难,特别是如果你缺少正确的方法来解决他们;其次,他们通常是应用程序的架构基础。简单的代码更改不会帮助解决他们。

5. 第 3 行中生成的 object在第几行执行后成为 garbage collection 的对象?

1.public class MyClass {

2. public StringBuffer aMethod() {

3. StringBuffer sf = new StringBuffer(“Hello”);

4. StringBuffer[] sf_arr = new StringBuffer[1];

5. sf_arr[0] = sf;

6. sf = null;

7. sf_arr[0] = null;

8. return sf;

9. }

10.}

答:第 7 行

为了使开发过程更容易,我会展示一些实际应用中常被使用的反模式。这些模式已经能够在开发过程中避免内存问题。

6. 描述一下 JVM 加载 class 文件的原理机制?

答:JVM 中类的装载是由类加载器(ClassLoader) 和它的子类来实现的,Java
中的类加载器是一个重要的 Java
运行时系统组件,它负责在运行时查找和装入类文件中的类。

  1. 由于 Java 的跨平台性,经过编译的 Java
    源程序并不是一个可执行程序,而是一个或多个类文件。当 Java
    程序需要使用某个类时,JVM
    会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的
    .class 文件中的数据读入到内存中,通常是创建一个字节数组读入 .class
    文件,然后产生与所加载类对应的 Class 对象。加载完成后,Class
    对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后
    JVM 对类进行初始化,包括:1.
    如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2.
    如果类中存在初始化语句,就依次执行这些初始化语句。

  2. 类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从JDK
    1.2开始,类加载过程采取了父亲委托机制(PDM)。PDM 更好的保证了 Java
    平台的安全性,在该机制中,JVM 自带的 Bootstrap
    是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM
    不会向 Java 程序提供对 Bootstrap
    的引用。下面是关于几个类加载器的说明:

a)Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);

b)Extension:从 java.ext.dirs
系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap;

c)System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量
classpath 或者系统属性 java.class.path
所指定的目录中记载类,是用户自定义加载器的默认父加载器。

HTTPSession作为缓存

此反模式是指滥用HTTPSession对象作为数据缓存。session对象的存在是为了存储信息,这个信息里面存在着一个HTTP请求。这也称为一个Session状态。这意味着,数据将被保存直至它们被处理。这些方法通常存在于一些重要的web应用程序中。web应用程序除了在服务器上存储这些信息外,没有别的方法。然而,一些信息是能够存储在cookie中,但是这将会带来一些其他的影响。

在cookie中,尽可能地保持少而短的数据,这是非常重要的。有时候很容易发生这种现象,session里存储着成兆字节的数据对象。这将会立即导致堆栈高占用和内存短缺。同时并行用户的数量非常有限,JVM将应对越来越多出现OutOfMemoryError错误的用户。多数用户Session也有其他性能损失。集群场景的session复制中,这将会增加序列化和沟通工作将导致额外的性能和可伸缩性问题。

在某些项目中这些问题的解决方案是增加数量的内存和切换到64位jvm。他们无法抵抗住仅仅增加几个G大小的堆栈内存的诱惑。然而,与其提供一个对真正问题的解决方案,不如隐藏这个现象。这个“解决方案”只是暂时的,同时还会引入了一个新的问题。越来越大的堆内存使它更难以找到“真正的”内存问题。对这种非常大的堆(大约6G)来说,大部分可用的分析工具是无法处理这些内存垃圾。我们在dynaTrace投入了大量的研发工作希望能够有效地分析大量的内存垃圾。随着这个问题变得越来越重要,一种新的JSR规范也提到了它。

由于应用程序架构尚未明确,导致Session缓存问题经常出现,。在开发过程中,数据被轻松而又简单的放入session当中。这是经常发生的,类似于一种“add and forget”方式,即没有人能够确保当这种数据不再需要时是被移除的。通常,当session超时时不需要的session数据应该被处理。在企业中,一些应用程序常常大量使用Session超时,这将会导致无法正常工作。此外经常使用非常高的Session超时- 24小时为用户提供额外的“体验”,使他们不必再次登录。

举一个实际的例子,从session里的数据库列表中选择所需要的数据。其目的是为了避免不必要的数据库查询。(是不是觉得有点过早优化呢?)。这将导致在session对象中为每个单独的用户放入几千个字节。虽然,缓存这些信息它是合理的,但用户session可以肯定是一个错误的地方。

另外一个例子是,为了管理Session状态而滥用Hibernate session。Hibernatesession对象只是为了快速访问数据库而放入HTTPsession对象中。然而,这将导致更多必要的数据被存储。同时每个用户的内存占用也将显著提高。

现如今,AJAX应用程序Session状态也可以在客户端进行管理。这使服务端程序变成无状态的,或接近无状态的,同时也显然有着更好的可扩展性。