解析Java的JNI编程中的对象引用与内存泄漏问题

JNI,Java Native Interface,是 native code 的编程接口。JNI 使 Java 代码程序可以与 native code 交互——在 Java 程序中调用 native code;在 native code 中嵌入 Java 虚拟机调用 Java 的代码。
JNI 编程在软件开发中运用广泛,其优势可以归结为以下几点:
利用 native code 的平台相关性,在平台相关的编程中彰显优势。
对 native code 的代码重用。
native code 底层操作,更加高效。
然而任何事物都具有两面性,JNI 编程也同样如此。程序员在使用 JNI 时应当认识到 JNI 编程中如下的几点弊端,扬长避短,才可以写出更加完善、高性能的代码:
从 Java 环境到 native code 的上下文切换耗时、低效。
JNI 编程,如果操作不当,可能引起 Java 虚拟机的崩溃。
JNI 编程,如果操作不当,可能引起内存泄漏。
JAVA 中的内存泄漏
JAVA 编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:JVM 中 Java Heap 的内存泄漏;JVM 内存中 native memory 的内存泄漏。

局部和全局引用

JNI将实例、数组类型暴露为不透明的引用。native代码从不会直接检查一个不透明的引用指针的上下文,而是通过使用JNI函数来访问由不透明的引用所指向的数据结构。因为只处理不透明的引用,这样就不需要担心不同的java VM实现而导致的不同的内部对象的布局。然而,还是有必要了解一下JNI中不同种类的引用:
1)JNI 支持3中不透明的引用:局部引用、全局引用和弱全局引用。
2)局部和全局引用,有着各自不同的生命周期。局部引用能够被自动释放,而全局引用和若全局引用在被程序员释放之前,是一直有效的。
3)一个局部或者全局引用,使所提及的对象不能被垃圾回收。而弱全局引用,则允许提及的对象进行垃圾回收。
4)不是所有的引用都可以在所有上下文中使用的。例如:在一个创建返回引用native方法之后,使用一个局部引用,这是非法的。

那么到底什么是局部引用,什么事全局引用,它们有什么不同?

局部引用

多数JNI函数都创建局部引用。例如JNI函数NewObject创建一个实例,并且返回一个指向该实例的局部引用。

局部引用只在创建它的native方法的动态上下文中有效,并且只在native方法的一次调用中有效。所有局部引用只在一个native方法的执行期间有效,在该方法返回时,它就被回收。

在native方法中使用一个静态变量来保存一个局部引用,以便在随后的调用中使用该局部引用,这种方式是行不通的。例如以下例子,误用了局部引用:
/* This code is illegal */ 
jstring

MyNewString(JNIEnv *env, jchar *chars, jint len)
{
  static jclass stringClass = NULL;
  jmethodID cid;
  jcharArray elemArr;
  jstring result;
  if (stringClass == NULL) {
    stringClass = (*env)->FindClass(env, "java/lang/String");
    if (stringClass == NULL) {
      return NULL; /* exception thrown */
    }
  }
  /* It is wrong to use the cached stringClass here,
    because it may be invalid. */
  cid = (*env)->GetMethodID(env, stringClass, "<init>", "([C)V");
  ...
  elemArr = (*env)->NewCharArray(env, len);
  ...
  result = (*env)->NewObject(env, stringClass, cid, elemArr);
  (*env)->DeleteLocalRef(env, elemArr);
  return result;
}

这种保存局部引用的方式是不正确的,因为FindClass()返回的是对java.lang.String的局部引用。这是因为,在native代码从MyNewString返回退出时,VM 会释放所有局部引用,包括存储在stringClass变量中的指向类对象的引用。这样当再次后继调用MyNewString时,可能会访问非法地址,导致内存被破坏,或者系统崩溃。

局部引用失效,有两种方式:‘
1)系统会自动释放局部变量。
2)程序员可以显示地管理局部引用的生命周期,例如调用DeleteLocalRef。

一个局部引用可能在被摧毁之前,被传给多个native方法。例如,MyNewString中,返回一个由NewObject创建的字符串引用,它将由NewObject的调用者来决定是否释放该引用。而在以下代码中:

JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env, jobject this) {
   char *c_str = ...<pre name="code" class="cpp">   ... <pre name="code" class="cpp">return MyNewString(c_str);<pre name="code" class="cpp">}

在VM接收到来自Java_C_f的局部引用以后,将基础字符串对象传递给ava_C_f的调用者,然后摧毁原本由MyNewString中调用的JNI函数NewObject所创建的局部引用。

局部对象只属于创建它们的线程,只在该线程中有效。一个线程想要调用另一个线程创建的局部引用是不被允许的。将一个局部引用保存到全局变量中,然后在其它线程中使用它,这是一种错误的编程。

全局引用

在一个native方法被多次调用之间,可以使用一个全局引用跨越它们。一个全局引用可以跨越多个线程,并且在被程序员释放之前,一致有效。和局部引用一样,全局引用保证了所引用的对象不会被垃圾回收。

和局部引用不一样(局部变量可以由多数JNI函数创建),全局引用只能由一个JNI函数创建(NewGlobalRef)。下面是一个使用全局引用版本的MyNewString:
/* This code is OK */ 
jstring

MyNewString(JNIEnv *env, jchar *chars, jint len)
{
  static jclass stringClass = NULL;
  ...
  if (stringClass == NULL) {
    jclass localRefCls =
      (*env)->FindClass(env, "java/lang/String");
    if (localRefCls == NULL) {
      return NULL; /* exception thrown */
    }
    /* Create a global reference */
    stringClass = (*env)->NewGlobalRef(env, localRefCls);
    /* The local reference is no longer useful */
    (*env)->DeleteLocalRef(env, localRefCls);
    /* Is the global reference created successfully? */
    if (stringClass == NULL) {
      return NULL; /* out of memory exception thrown */
    }
  }
  ...
}

弱全局引用

弱全局引用是在java 2 SDK1.2才出现的。它由NewGolableWeakRef函数创建,并且被DeleteGloablWeakRef函数摧毁。和全局引用一样,它可以跨native方法调用,也可以跨越不同线程。但是和全局引用不同的是,它不阻止对基础对象的垃圾回收。下面是弱全局引用版的MyNewString:

JNIEXPORT void JNICALL

Java_mypkg_MyCls_f(JNIEnv *env, jobject self)
{
  static jclass myCls2 = NULL;
  if (myCls2 == NULL) {
    jclass myCls2Local =
      (*env)->FindClass(env, "mypkg/MyCls2");
    if (myCls2Local == NULL) {
      return; /* can't find class */
    }
    myCls2 = NewWeakGlobalRef(env, myCls2Local);
    if (myCls2 == NULL) {
      return; /* out of memory */
    }
  }
  ... /* use myCls2 */
}

弱全局引用在一个被native代码缓存着的引用不想阻止基础对象被垃圾回收时,非常有用。如以上例子,mypkg.MyCls.f需要缓存mypkg.MyCls2的引用。而通过将mypkg.MyCls2缓存到弱引用中,能够实现MyCls2类依旧可以被卸载。

上面代码中,我们假设了MyCls类和MyCls2类的生命周期是相同的(例如,在同一个类中被加载、卸载)。所以没有考虑MyCls2被卸载了,然后在类MyCls和native方法的实现Java_mypkg_MyCls_f还要被继续使用时,再被重新加载起来的情况。针对于这个MyCls2类可能被卸载再加载的情况,在使用时,需要检查该弱全局引用是否还有效。如何检查,这将在下面提到。

比较引用

可以用JNI函数IsSameObject来检查给定的两个局部引用、全局引用或者弱全局引用,是否指向同一个对象。
(*env)->IsSameObject(env, obj1, obj2) 
返回值为:
JNI_TRUE,表示两个对象一致,是同一个对象。
JNI_FALSE,表示两个对象不一致,不是同一个对象。

在java VM中NULL是null的引用。
如果一个对象obj是局部引用或者全局引用,则可以这样来检查它是否指向null对象:

(*env)->IsSameObject(env, obj, NULL)

或者:

NULL == obj

而对于弱全局引用,以上规则需要改变一下:
我们可以用这个函数来判断一个非0弱全局引用wobj所指向的对象是否仍旧存活着(依旧有效)。

(*env)->IsSameObject(env, wobj, NULL)

返回值:
JNI_TRUE,表示对象已经被回收了。
JNI_FALSE,表示wobj指向的对象,依旧有效。

 释放引用
除了引用的对象要占用内存,每个JNI引用本身也会消耗一定内存。作为一个JNI程序员,应该对在一段给定的时间里,程序会用到的引用的个数,做到心中有数。特别是,尽管程序所创建的局部引用最终会被VM会被自动地释放,仍旧需要知道在程序在执行期间的任何时刻,创建的局部引用的上限个数。创建过多的引用,即便他们是瞬间、短暂的,也会导致内存耗尽。

释放局部引用
多数情况下,在执行一个native方法时,你不需要担心局部引用的释放,java VM会在native方法返回调用者的时候释放。然而有时候需要JNI程序员显示的释放局部引用,来避免过高的内存使用。那么什么时候需要显示的释放呢,且看一下情景:
1)在单个native方法调用中,创建了大量的局部引用。这可能会导致JNI局部引用表溢出。此时有必要及时地删除那些不再被使用的局部引用。例如以下代码,在该循环中,每次都有可能创建一个巨大的字符串数组。在每个迭代之后,native代码需要显示地释放指向字符串元素的局部引用:

for (i = 0; i < len; i++) {
  jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
  ... /* process jstr */
  (*env)->DeleteLocalRef(env, jstr);
}

2)你可能要创建一个工具函数,它会被未知的上下文调用。例如之前提到到MyNewString这个例子,它在每次返回调用者欠,都及时地将局部引用释放。

3)native方法,可能不会返回(例如,一个可能进入无限事件分发的循环中的方法)。此时在循环中释放局部引用,是至关重要的,这样才能不会无限期地累积,进而导致内存泄露。

4)native方法可能访问一个巨大的对象,因此,创建了一个指向该对象的局部引用。native方法在返回调用者之前,除访问对象之外,还执行了额外的计算。指向这个大对象的局部引用,将会包含该对象,以防被垃圾回收。这个现象会持续到native方法返回到调用者时,即便这个对象不会再被使用,也依旧会受保护。在以下例子中,由于在lengthyComputation()前,显示地调用了DeleteLocalRef,所以垃圾回收器有机会可以释放lref所指向的对象。

/* A native method implementation */
JNIEXPORT void JNICALL
Java_pkg_Cls_func(JNIEnv *env, jobject this)
{
  lref = ...       /* a large Java object */
  ...           /* last use of lref */
  (*env)->DeleteLocalRef(env, lref);
  lengthyComputation();  /* may take some time */
  return;         /* all local refs are freed */
}

这个情形的实质,就是允许程序在native方法执行期间,java的垃圾回收机制有机会回收native代码不在访问的对象。

管理局部引用
不知道java 7怎么样了,应该更强大吧,有时间,去看看,这里且按照java2的特性来吧。
SDK1.2中提供了一组额外的函数来管理局部引用的生命周期。他们是EnsureLocalCapacity、NewLocalRef、PushLocalFram以及PopLocalFram。
JNI的规范要求VM可以自动确保每个native方法可以创建至少16个局部引用。经验显示,如果native方法中未包含和java VM的对象进行复杂的互相操作,这个容量对大多数native方法而言,已经足够了。如果,出现这还不够的情况,需要创建更多的局部引用,那么native方法可以调用EnsureLocalCapacity来保证这些局部引用有足够的空间。

/* The number of local references to be created is equal to
  the length of the array. */
if ((*env)->EnsureLocalCapacity(env, len)) < 0) {   ... /* out of memory */
} for (i = 0; i < len; i++) {
  jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
  ... /* process jstr */   /* DeleteLocalRef is no longer necessary */ }

这样做,所消耗的内存,自然就有可能比之前的版本来的多。

另外,PushLocalFram\PopLocalFram函数允许程序员创建嵌套作用域的局部引用。如下代码:

#define N_REFS ... /* the maximum number of local references
 used in each iteration */
for (i = 0; i < len; i++) {
  if ((*env)->PushLocalFrame(env, N_REFS) < 0) {
    ... /* out of memory */
  }
  jstr = (*env)->GetObjectArrayElement(env, arr, i);
  ... /* process jstr */
  (*env)->PopLocalFrame(env, NULL);
}

PushLocalFram为指定数目的局部引用,创建一个新的作用域,PopLocalFram摧毁最上层的作用域,并且释放该域中的所有局部引用。

使用这两个函数的好处是它们可以管理局部引用的生命周期,而不需关系在执行过程中可能被创建的每个单独局部引用。例子中,如果处理jstr的过程,创建了额外的局部引用,它们也会在PopLocalFram之后被立即释放。

NewLocalRef函数,在你写一个工具函数时,非常有用。这个会在下面章节——管理引用的规则,具体分析。

native代码可能会创建超出16个局部引用的范围,也可能将他们保存在PushLocalFram或者EnsureLocalCapacity调用,VM会为局部引用分配所需要的内存。然而,这些内存是否足够,是没有保证的。如果内存分配失败,虚拟机将会退出。

 释放全局引用

在native代码不再需要访问一个全局引用的时候,应该调用DeleteGlobalRef来释放它。如果调用这个函数失败,Java VM将不会回收对应的对象。

在native代码不在需要访问一个弱全局引用的时候,应该调用DeleteWeakGlobalRef来释放它。如果调用这个函数失败了,java VM 仍旧将会回收对应的底层对象,但是,不会回收这个弱引用本身所消耗掉的内存。

 管理引用的规则
管理引用的目的是为了清除不需要的内存占用和对象保留。

总体来说,只有两种类型的native代码:直接实现native方法的函数,在二进制上下文中被使用的工具函数。

在写native方法的实现的时候,需要当心在循环中过度创建局部引用,以及在native方法中被创建的,却不返回给调用者的局部引用。在native方法方法返回后还留有16个局部引用在使用中,将它们交给java VM来释放,这是可以接受的。但是native方法的调用,不应该引起全局引用和弱全局引用的累积。应为这些引用不会在native方法返后被自动地释放。

在写工具函数的时候,必须要注意不能泄露任何局部引用或者超出该函数之外的执行。因为一个工具函数,可能在意料之外的上下文中,被不停的重复调用。任何不需要的引用创建都有可能导致内存泄露。
1)当一个返回一个基础类型的工具函数被调用,它必须应该没有局部引用、若全局引用的累积。
2)当一个返回一个引用类型的工具函数被调用,它必须应该没有局部、全局或若全局引用的累积,除了要被作为返回值的引用。

一个工具函数以捕获为目的创建一些全局或者弱全局引用,这是可接受的,因为只有在最开始的时候,才会创建这些引用。

如果一个工具函数返回一个引用,你应该使返回的引用的类型(例如局部引用、全局引用)作为函数规范的一部分。它应该始终如一,而不是有时候返回一个局部引用,有时候却返回一个全局引用。调用者需要知道工具函数返回的引用的类型,以便正确地管理自己的JNI引用。以下代码重复地调用一个工具工具函数(GetInfoString)。我们需要知道GetInfoString返回的引用的类型,以便释放该引用:

while (JNI_TRUE) {
  jstring infoString = GetInfoString(info);
  ... /* process infoString */
  ??? /* we need to call DeleteLocalRef, DeleteGlobalRef,
 or DeleteWeakGlobalRef depending on the type of
 reference returned by GetInfoString. */
}

在java2 SDK1.2中,NewLocalRef函数可以用来保证一个工具函数一直返回一个局部引用。为了说明这个问题,我们对MyNewString做一些改动,它缓存了一个被频繁请求的字符串(“CommonString”)到全局引用:

jstring

MyNewString(JNIEnv *env, jchar *chars, jint len)
{
  static jstring result;
  /* wstrncmp compares two Unicode strings */
  if (wstrncmp("CommonString", chars, len) == 0) {
    /* refers to the global ref caching "CommonString" */
    static jstring cachedString = NULL;
    if (cachedString == NULL) {
      /* create cachedString for the first time */
      jstring cachedStringLocal = ... ;
      /* cache the result in a global reference */
      cachedString =
        (*env)->NewGlobalRef(env, cachedStringLocal);
    }
    return (*env)->NewLocalRef(env, cachedString);
  }
  ... /* create the string as a local reference and store in
 result as a local reference */
  return result;
}

正常的流程返回的时候局部引用。就像之前解释的那样,我们必须将缓存字符保存到一个全局引用中,这样就可以在多个线程中调用native方法时,都能访问它。

return (*env)->NewLocalRef(env, cachedString);

这条语句,创建了一个局部引用,它指向了缓存在全局引用的指向的统一对象。作为和调用者的约定的一部分,MyNewString总是返回一个局部引用。

PushLocalFram、PopLocalFram函数用来管理局部引用的生命周期特别得方便。只需要在native函数的入口调用PushLocalFram,在函数退出时调用PopLocalFram,局部变量就会被释放。

jobject f(JNIEnv *env, ...)
{
  jobject result;
  if ((*env)->PushLocalFrame(env, 10) < 0) {
    /* frame not pushed, no PopLocalFrame needed */
    return NULL;
  }
  ...
  result = ...;
  if (...) {
    /* remember to pop local frame before return */
    result = (*env)->PopLocalFrame(env, result);
    return result;
  }
  ...
  result = (*env)->PopLocalFrame(env, result);
  /* normal return */
  return result;
}

PopLocalFram函数调用失败时,可能会导致未定义的行为,例如VM崩溃。

内存泄漏问题
Java Heap 的内存泄漏
Java 对象存储在 JVM 进程空间中的 Java Heap 中,Java Heap 可以在 JVM 运行过程中动态变化。如果 Java 对象越来越多,占据 Java Heap 的空间也越来越大,JVM 会在运行时扩充 Java Heap 的容量。如果 Java Heap 容量扩充到上限,并且在 GC 后仍然没有足够空间分配新的 Java 对象,便会抛出 out of memory 异常,导致 JVM 进程崩溃。
Java Heap 中 out of memory 异常的出现有两种原因——①程序过于庞大,致使过多 Java 对象的同时存在;②程序编写的错误导致 Java Heap 内存泄漏。
多种原因可能导致 Java Heap 内存泄漏。JNI 编程错误也可能导致 Java Heap 的内存泄漏。
JVM 中 native memory 的内存泄漏
从操作系统角度看,JVM 在运行时和其它进程没有本质区别。在系统级别上,它们具有同样的调度机制,同样的内存分配方式,同样的内存格局。
JVM 进程空间中,Java Heap 以外的内存空间称为 JVM 的 native memory。进程的很多资源都是存储在 JVM 的 native memory 中,例如载入的代码映像,线程的堆栈,线程的管理控制块,JVM 的静态数据、全局数据等等。也包括 JNI 程序中 native code 分配到的资源。
在 JVM 运行中,多数进程资源从 native memory 中动态分配。当越来越多的资源在 native memory 中分配,占据越来越多 native memory 空间并且达到 native memory 上限时,JVM 会抛出异常,使 JVM 进程异常退出。而此时 Java Heap 往往还没有达到上限。
多种原因可能导致 JVM 的 native memory 内存泄漏。例如 JVM 在运行中过多的线程被创建,并且在同时运行。JVM 为线程分配的资源就可能耗尽 native memory 的容量。
JNI 编程错误也可能导致 native memory 的内存泄漏。对这个话题的讨论是本文的重点。

JNI 编程实现了 native code 和 Java 程序的交互,因此 JNI 代码编程既遵循 native code 编程语言的编程规则,同时也遵守 JNI 编程的文档规范。在内存管理方面,native code 编程语言本身的内存管理机制依然要遵循,同时也要考虑 JNI 编程的内存管理。
本章简单概括 JNI 编程中显而易见的内存泄漏。从 native code 编程语言自身的内存管理,和 JNI 规范附加的内存管理两方面进行阐述。
Native Code 本身的内存泄漏
JNI 编程首先是一门具体的编程语言,或者 C 语言,或者 C++,或者汇编,或者其它 native 的编程语言。每门编程语言环境都实现了自身的内存管理机制。因此,JNI 程序开发者要遵循 native 语言本身的内存管理机制,避免造成内存泄漏。以 C 语言为例,当用 malloc() 在进程堆中动态分配内存时,JNI 程序在使用完后,应当调用 free() 将内存释放。总之,所有在 native 语言编程中应当注意的内存泄漏规则,在 JNI 编程中依然适应。
Native 语言本身引入的内存泄漏会造成 native memory 的内存,严重情况下会造成 native memory 的 out of memory。
Global Reference 引入的内存泄漏
JNI 编程还要同时遵循 JNI 的规范标准,JVM 附加了 JNI 编程特有的内存管理机制。
JNI 中的 Local Reference 只在 native method 执行时存在,当 native method 执行完后自动失效。这种自动失效,使得对 Local Reference 的使用相对简单,native method 执行完后,它们所引用的 Java 对象的 reference count 会相应减 1。不会造成 Java Heap 中 Java 对象的内存泄漏。
而 Global Reference 对 Java 对象的引用一直有效,因此它们引用的 Java 对象会一直存在 Java Heap 中。程序员在使用 Global Reference 时,需要仔细维护对 Global Reference 的使用。如果一定要使用 Global Reference,务必确保在不用的时候删除。就像在 C 语言中,调用 malloc() 动态分配一块内存之后,调用 free() 释放一样。否则,Global Reference 引用的 Java 对象将永远停留在 Java Heap 中,造成 Java Heap 的内存泄漏。

(0)

相关推荐

  • Android性能优化之利用Rxlifecycle解决RxJava内存泄漏详解

    前言: 其实RxJava引起的内存泄漏是我无意中发现了,本来是想了解Retrofit与RxJava相结合中是如何通过适配器模式解决的,结果却发现了RxJava是会引起内存泄漏的,所有想着查找一下资料学习一下如何解决RxJava引起的内存泄漏,就查到了利用Rxlifecycle开源框架可以解决,今天周末就来学习一下如何使用Rxlifecycle. 引用泄漏的背景: RxJava作为一种响应式编程框架,是目前编程界网红,可谓是家喻户晓,其简洁的编码风格.易用易读的链式方法调用.强大的异步支持等使得R

  • JAVA程序内存溢出问题原因分析

    本文较为详细的分析了JAVA程序内存溢出问题原因.分享给大家供大家参考.具体如下: 遇到一个线上系统报 java.lang.OutOfMemoryError: PermGen space 错误,需要定位一下问题.很久之前到时弄过这个,现在还真有点不记得了,但这个真的是一个非常有意思的问题,值得好好研究一下.首先第一反应当然是加上-XX:+PrintGCDetails参数来看具体的GC日志,但是由于程序是tomcat启动的,担心里面封装的东西太多不好定位,既然在windows下面,所以还是借助可视

  • Java中关于内存泄漏出现的原因汇总及如何避免内存泄漏(超详细版)

    Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题.内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收.最近自己阅读了大量相关的文档资料,打算做个 总结 沉淀下来跟大家一起分享和学习,也给自己一个警示,以后 coding 时怎么避免这些情况,提高应用的体验和质量. 我会从 java 内存泄漏的基础知识开始,并通过具体例子来说明 Android 引起内存泄漏的各种原因,以

  • Java内存溢出和内存泄露

    虽然jvm可以通过GC自动回收无用的内存,但是代码不好的话仍然存在内存溢出的风险. 一.为什么要了解内存泄露和内存溢出? 1.内存泄露一般是代码设计存在缺陷导致的,通过了解内存泄露的场景,可以避免不必要的内存溢出和提高自己的代码编写水平: 2.通过了解内存溢出的几种常见情况,可以在出现内存溢出的时候快速的定位问题的位置,缩短解决故障的时间.  二.基本概念  理解这两个概念非常重要. 内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存.即被分配的对象可达但已无

  • 完美解决java读取大文件内存溢出的问题

    1. 传统方式:在内存中读取文件内容 读取文件行的标准方式是在内存中读取,Guava 和Apache Commons IO都提供了如下所示快速读取文件行的方法: Files.readLines(new File(path), Charsets.UTF_8); FileUtils.readLines(new File(path)); 实际上是使用BufferedReader或者其子类LineNumberReader来读取的. 传统方式的问题: 是文件的所有行都被存放在内存中,当文件足够大时很快就会

  • 编写Java代码制造一个内存溢出的情况

    这将会是一篇比较邪恶的文章,当你想在某个人的生活中制造悲剧时你可能会去google搜索它.在Java的世界里,内存溢出仅仅只是你在这种情况下可能会引入的一种bug.你的受害者会在办公室里度过几天甚至是几周的不眠之夜. 在这篇文章中我将会介绍两种溢出方式,它们都是比较容易理解和重现的.并且它们都是来源现实项目的案例研究,但是为了让你清晰地掌握,我把它们简化了. 不过放心,在我们遇到和解决了很过溢出bug之后,类似的案例将会比你想象得更加普遍. 先来一个进入状态的,在使用HashSet/HashMa

  • java内存溢出示例(堆溢出、栈溢出)

    堆溢出: 复制代码 代码如下: /** * @author LXA * 堆溢出 */ public class Heap { public static void main(String[] args) { ArrayList list=new ArrayList(); while(true) { list.add(new Heap()); } } } 报错: java.lang.OutOfMemoryError: Java heap space 栈溢出: 复制代码 代码如下: /** * @a

  • 基于Java内存溢出的解决方法详解

    一.内存溢出类型1.java.lang.OutOfMemoryError: PermGen spaceJVM管理两种类型的内存,堆和非堆.堆是给开发人员用的上面说的就是,是在JVM启动时创建:非堆是留给JVM自己用的,用来存放类的信息的.它和堆不同,运行期内GC不会释放空间.如果web app用了大量的第三方jar或者应用有太多的class文件而恰好MaxPermSize设置较小,超出了也会导致这块内存的占用过多造成溢出,或者tomcat热部署时侯不会清理前面加载的环境,只会将context更改

  • 解析Java的JNI编程中的对象引用与内存泄漏问题

    JNI,Java Native Interface,是 native code 的编程接口.JNI 使 Java 代码程序可以与 native code 交互--在 Java 程序中调用 native code:在 native code 中嵌入 Java 虚拟机调用 Java 的代码. JNI 编程在软件开发中运用广泛,其优势可以归结为以下几点: 利用 native code 的平台相关性,在平台相关的编程中彰显优势. 对 native code 的代码重用. native code 底层操作

  • 深入解析Java的设计模式编程中建造者模式的运用

    定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示. 类型:创建类模式 类图: 四个要素 产品类:一般是一个较为复杂的对象,也就是说创建对象的过程比较复杂,一般会有比较多的代码量.在本类图中,产品类是一个具体的类,而非抽象类.实际编程中,产品类可以是由一个抽象类与它的不同实现组成,也可以是由多个抽象类与他们的实现组成. 抽象建造者:引入抽象建造者的目的,是为了将建造的具体过程交与它的子类来实现.这样更容易扩展.一般至少会有两个抽象方法,一个用来建造产品,一个是用来返回

  • 深入解析Java的设计模式编程中的模板方法模式

    定义:  定义一个操作中的算法的框架,而将一些步骤延迟到子类中.使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤. 听起来好高端的样子,我的理解: 1.父类声明了若干个抽象方法(基本方法)和若干个具体方法(模板方法) 2.抽象方法是一个算法(过程)的步骤,在子类中实现 3.模板方法是一个算法(过程)的框架,在父类中已经约定好,实现对基本方法调用,完成固定的逻辑 4.一个算法(过程)的结构在父类中定义,具体的实现细节则在子类中实现 注:为了防止恶意操作,一般模板方法都加上final

  • 举例解析Java的设计模式编程中里氏替换原则的意义

    里氏替换原则,OCP作为OO的高层原则,主张使用"抽象(Abstraction)"和"多态(Polymorphism)"将设计中的静态结构改为动态结构,维持设计的封闭性."抽象"是语言提供的功能."多态"由继承语义实现. 里氏替换原则包含以下4层含义: 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法. 子类中可以增加自己特有的方法. 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更

  • 深入解析Java的设计模式编程中单例模式的使用

    定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例. 类型:创建类模式 类图: 类图知识点: 1.类图分为三部分,依次是类名.属性.方法 2.以<<开头和以>>结尾的为注释信息 3.修饰符+代表public,-代表private,#代表protected,什么都没有代表包可见. 4.带下划线的属性或方法代表是静态的. 5.对类图中对象的关系不熟悉的朋友可以参考文章:设计模式中类的关系. 单例模式应该是23种设计模式中最简单的一种模式了.它有以下几个要素: 私有的构

  • 解析Java图形化编程中的文本框和文本区

    在图形界面中,文本框和文本区是用于信息输入输出的组件. 文本框 文本框(JTextField)是界面中用于输入和输出一行文本的框.JTextField类用来建立文本框.与文本框相关的接口是ActionListener. 文本框处理程序的基本内容有以下几个方面: 声明一个文本框名. 建立一个文本框对象. 将文本框对象加入到某个容器. 对需要控制的文本框对象注册监视器,监听文本框的输入结束(即输入回车键)事件. 一个处理文本框事件的方法,完成对截获事件进行判断和处理. JTextField类的主要构

  • 5个Android开发中比较常见的内存泄漏问题及解决办法

    android中一个对象已经不需要了,但是其他对象还持有他的引用,导致他不能回收,导致这个对象暂存在内存中,这样内存泄漏就出现了. 内存泄漏出现多了,会是应用占用过多的没存,当占用的内存超过了系统分配的内存容量,就会出现内存溢出了导致应用Crash. 了解了内存泄漏的原因及影响后,我们需要做的就是掌握常见的内存泄漏,并在以后的Android程序开发中,尽量避免它.下面搜罗了5个Android开发中比较常见的内存泄漏问题及解决办法,分享给大家,一起来看看吧. 一.单例造成的内存泄漏 android

  • 解析Java的设计模式编程之解释器模式的运用

    定义:给定一种语言,定义他的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中句子. 类型:行为类模式 类图: 解释器模式是一个比较少用的模式,本人之前也没有用过这个模式.下面我们就来一起看一下解释器模式.   解释器模式的结构 抽象解释器:声明一个所有具体表达式都要实现的抽象接口(或者抽象类),接口中主要是一个interpret()方法,称为解释操作.具体解释任务由它的各个实现类来完成,具体的解释器分别由终结符解释器TerminalExpression和非终结符解释器Nonter

  • 解析Java的Hibernate框架中的持久化类和映射文件

    持久化类 Hibernate的整个概念是采取从Java类属性的值,并将持久到数据库表.一个映射文件Hibernate的帮助确定如何从拉动类的值,并将它们映射与表和相关的域. 其对象或实例将存储在数据库表中的Java类在Hibernate中称为持久化类. Hibernate的效果最好,如果这些类遵循一些简单的规则,也称为普通Java对象(POJO)编程模型.有下列持久化类的主要规则,但是,这些规则并不是必需的. 将所有的持久化Java类需要一个默认的构造函数. 所有类应该包含为了让容易识别对象内H

  • 实例讲解Java的设计模式编程中责任链模式的运用

    定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系.将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止. 类型:行为类模式 类图: 首先来看一段代码: public void test(int i, Request request){ if(i==1){ Handler1.response(request); }else if(i == 2){ Handler2.response(request); }else if(i == 3){ Handler3

随机推荐