深入理解Java内存模型(基础)

并发编程模型的分类

在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型(主内存共享),Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

深入理解Finalize

基础

Java技术允许使用finalize()在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
这个方法是由垃圾收集器在确定这个对象没有被引用时,对这个对象调用的。它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()以整理系统资源或者执行其他清理工作(要不然会引起资源泄露,有可能导致程序崩溃)。finalize()是在垃圾收集器删除对象之前被自动调用的。
垃圾收集器只知道释放那些由new分配的内存,所以不知道如何释放对象的“特殊”内存。为解决这个问题,Java提供了一个名为finalize(),它的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存(垃圾回收需要2次)。
所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作(如关闭流等操作)。但JVM(Java虚拟机)不保证此方法总被调用。
finalize()抛出的未捕获异常只会导致该对象的finalize()执行退出。
用户可以自己调用对象的finalize(),但是这种调用是正常的方法调用,和对象的销毁过程无关。

对象销毁和对象重生

一个简单的对象生命周期为,UnfinalizedFinalizableFinalizedReclaimed
在对象的销毁过程中,按照对象的finalize()的执行情况,可以分为以下几种,系统会记录对象的对应状态。

unfinalized

没有执行finalize(),系统也不准备执行。

finalizable

可以执行finalize()了,系统会在随后的某个时间执行finalize

finalized

该对象的finalize()已经被执行了。
GC怎么来保持对finalizable()的对象的追踪呢。GC有一个Queue,叫做F-Queue,所有对象在变为finalizable的时候会加入到该Queue,然后等待GC执行它的finalize()
这时我们引入了对对象的另外一种记录分类,系统可以检查到一个对象属于哪一种:

reachable

从活动的对象引用链可以到达的对象。包括所有线程当前栈的局部变量,所有的静态变量等等。

finalizer-reachable

除了reachable外,从F-Queue可以通过引用到达的对象。

unreachable

其它的对象,不可达对象。

  1. 首先,所有的对象都是从Reachable+Unfinalized(没有执行finalize(),对象可达)走向死亡之路的。
  2. 从当前活动集到对象不可达时,对象可以从Reachable状态变到F-Reachable(进入F-Queue,对象变成finalizable状态)或者Unreachable状态。
  3. 当对象为非Reachable+Unfinalized时,GC会把它移入F-Queue,状态变为F-Reachable+Finalizable(进入F-Queue,可达,finalizable状态)。
  4. 任何时候,GC都可以从F-Queue中拿到一个Finalizable的对象,标记它为Finalized,然后执行它的finalize(),由于该对象在这个线程中又可达了,于是该对象变成Reachable了(并且Finalized)。而finalize()执行时,又有可能把其它的F-Reachable(进入F-Queuefinalizable状态)的对象变为一个Reachable的,这个叫做对象再生。
  5. 当一个对象在Unreachable+Unfinalized时,如果该对象使用的是默认的Objectfinalize,或者虽然重写了,但是新的实现什么也不干(子类覆写一个空方法)。为了性能,GC可以把该对象之间变到Reclaimed状态直接销毁,而不用加入到F-Queue等待GC做进一步处理。(不可达,不执行finalize()
  6. 从状态图看出,不管怎么折腾,任意一个对象的finalize只至多执行一次,一旦对象变为Finalized(执行过finalized()),就怎么也不会在回到F-Queuefinalizable状态)去了。当然没有机会再执行finalize了。
  7. 当对象处于Unreachable+Finalized时,该对象离真正的死亡不远了。GC可以安全的回收该对象的内存了。进入Reclaimed

GC输出日志分析

基于JDK7

verbose命令

Java -verbose:gc中参数-verbose:gc表示输出虚拟机中GC的详细情况。
使用后输出如下:[Full GC 168K->97K(1984K),0.0253873 secs]
解读如下:箭头前后的数据168K和97K分别表示垃圾收集GC前后所有存活对象使用的内存容量,说明有168K-97K=71K的对象容量被回收,括号内的数据1984K为堆内存的总容量,收集所需要的时间是0.0253873秒(这个时间在每次执行的时候会有所不同)。

JVM启动参数启用verbose GC

通过JVM启动参数设置来启用verbose gc,并指定了名字和gc日志文件存储路径。

JVM内存泄漏分析

问题

Java线程是JVM基础的一部分。你的Java堆空间内存占用不仅仅是由于静态对象和长生命的对象导致,还有可能因为短生命对象。
OutOfMemoryError问题经常被误认为是内存泄露引起。我们经常忽略错误的线程执行模型和它们持有的JVM里的短生命对象,直到它们的执行完成我们才发现。
在这种问题情形下:

  • 程序中短生命/无状态对象(XML,JSON数据负载等)被线程持有的时间会变得很长(线程锁争用,大量数据负载,远程系统的慢响应时间等)。
  • 这种短生命对象会因为垃圾收集而晋升到长生命空间,比如老年代空间。
  • 副作用是会导致老年代空间很快被占满,增加了Full GC(major收集)的频率。
  • 由于这种严重的情况,它将导致更多的GC垃圾收集,增加JVM暂停时间和最终的OutOfMemoryError:Java堆空间。

你的应用此时被停掉,你很疑惑到底怎么回事。最后,你考虑增加Java堆空间或者寻找哪里有内存泄露,你真的找对路了么?
避免在线程栈大小(虚拟机栈)和Java堆内存占用之间产生混淆是非常重要的。线程栈(虚拟机栈)大小是一种特殊的内存空间,它被JVM用于存储每个方法调用。当一个线程调用方法A,它将这个调用入栈。如果方法A调用方法B,同样也会入栈。一旦方法执行完毕,这个调用便从栈里出栈。
这种线程方法调用会导致Java对象产生,并分配在Java堆里。增加线程栈的大小是没有任何效果的(对象最终在堆得年轻代(Eden)产生)。而调整线程栈大小通常是要处理java.lang.stackoverflowerror错误或者OutOfMemoryError: unable to create new native thread错误的时候才会需要。

JVM调优案例分析-Eclipse启动

为什么要进行JVM调优

运行在JVM上的程序都会进行内存分配以及垃圾回收,在这个过程中设置合理的内存大小及垃圾回收算法能显著提高应用的响应速度及运行效率,相反不合理的JVM参数设置会造成应用程序响应不稳定并导致整个应用程序挂掉。

如何JVM调优

不同类型应用程序的JVM参数设置都不一样,如何设置最优的JVM参数不仅需要对GC机制有一定的了解,而且还要反复的试验才能得出最合适的JVM参数值。
例如:某些系统要求运行稳定、并且响应速度高,这类应用就需要通过调整其JVM参数来减少内存大小调整及垃圾回收所占用的时间,以尽量的提高响应速度。报表类及容易产生大对象对响应速度要求不是很高的系统,可以把堆空间设置较大些。
除了了解GC的机制外等一些基本调优方法外,往往还需要借助一些监控工具来进行JVM参数调优,例:Jprofiler 、VisualVM、Jconsole等,通过这些工具可以监控JVM运行时内存分配情况,线程状态、数量,堆空间类、对象数量类型信息等,此外还可以借助分析垃圾回收日志对JVM进行优化。

JVM垃圾回收算法-CMS(Concurrent Low Pause Collector)

介绍

CMS,全称Concurrent Low Pause Collector,是jdk1.4后期版本开始引入的新gc算法,在jdk5和jdk6中得到了进一步改进,它的主要适合场景是对响应时间的重要性需求 大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用。CMS是用于对Tenured Generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少Full GC发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。
在我们的应用中,因为有缓存的存在,并且对于响应时间也有比较高的要求,因此希望能尝试使用CMS来替代默认的server型JVM使用的并行收集器,以便获得更短的垃圾回收的暂停时间,提高程序的响应性。
CMS并非没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停,它的收集周期是这样。

  1. 初始标记(CMS-initial-mark)
  2. 并发标记(CMS-concurrent-mark)
  3. 重新标记(CMS-remark)
  4. 并发清除(CMS-concurrent-sweep)
  5. 并发重设状态等待下次CMS的触发(CMS-concurrent-reset)。

其中的1,3两个步骤需要暂停所有的应用程序线程的。第一次暂停从root对象开始标记存活的对象,这个阶段称为初始标记;第二次暂停是在并发标记之后, 暂停所有应用程序线程,重新标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致)。第一次暂停会比较短,第二次暂停通常会比较长,并且remark这个阶段可以并行标记。
而并发标记、并发清除、并发重设阶段的所谓并发,是指一个或者多个垃圾回收线程和应用程序线程并发地运行,垃圾回收线程不会暂停应用程序的执行,如果你有多于一个处理器,那么并发收集线程将与应用线程在不同的处理器上运行,显然,这样的开销就是会降低应用的吞吐量。remark阶段的并行,是指暂停了所有应用程序后,启动一定数目的垃圾回收进程进行并行标记,此时的应用线程是暂停的。
CMS的Young Generation的回收采用的仍然是并行复制收集器,这个跟Paralle GC算法是一致的。

JVM内存调整(五)

JVM运行时数据区的内存大小可以通过参数来设置,通常能设置的两块区域为堆空间和持久代(方法区),设置方法是以参数的形式来指定,Sun的HotSpot需要在JVM启动前设置这些参数,启动JVM后不能动态改变其大小。
JVM参数说明:

JVM垃圾回收(四)

垃圾回收概念

什么是垃圾回收

JVM中自动检测并移除不再使用的数据对象的这种机制称为:垃圾回收(Garbage Collection),简称GC。

GC的基本原理

JVM通过使用垃圾收集器及使用相应的垃圾回收算法将内存中不再被使用的对象进行回收。

为什么要垃圾回收

由于不同Java对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行垃圾回收,整个程序会因内存耗尽导致整个程序崩溃。垃圾回收还会整理那些零散的内存碎片,碎片过多最直接的问题就是会导致无法分配大块的内存空间以及降低程序的运行效率。

哪些区域会被GC

VM栈、本地方法栈以及程序计数器会随方法或线程的结束而自然被回收,所以这些区域不需要考虑回收问题。堆空间和持久代(方法区)是GC回收的重点区域,不同区域对象的收集叫法不一样。

  • 对年轻代的对象的收集称为minor GC(eden:minor gc,整个新生代:major gc)。
  • 对老年代的对象的收集称为Full GC。程序中主动调用System.gc()强制执行的GC为Full GC。Full GC基本都是整个堆空间及持久代发生了垃圾回收,通常优化的目标之一是尽量减少GC和Full GC的频率。
  • 持久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收(Full GC)。

JVM运行数据区(三)

Runtime Data Area主要包括五个部分:

  • Heap(堆)
  • Method Area(方法区域)
  • VM Stack(虚拟机栈)
  • Native Method Stack(本地方法栈)( 在Sun的HotSpot虚拟机中VM Stack和Native method stack是合并到一起的)
  • Program Counter(程序计数器)

Heap和Method Area是被所有线程的共享使用的,而Vm Stack, Program Counter和Native Method Stack是以线程为粒度的,每个线程独自拥有。

JVM类加载器(二)

类加载器负责加载Java类的字节代码到Java虚拟机中,可以根据指定的类名(如java.lang.Object)来装载class文件的内容到Runtime Data Area中的Method Area(方法区域)。Java程序员可以extends java.lang.ClassLoader类来写自己的Class loader

类加载器的模型

类加载器双亲委派模型

如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

双亲委派模型的优点

Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在tools.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×