1.Java内存区域与内存溢出异常
运行时数据区:
主要分为线程私有和线程共享两大块。
线程私有(生命周期与线程相同,共生死):
- Java虚拟机栈(NativeStack)
- 本地方法栈(VM Stack)
- 程序计数器(PC)
线程共享(生命周期与JVM相同):
- 堆(heap)
- 方法区(Method area,逻辑分开,物理属于堆)
程序计数器:
是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器.
记录正在执行的虚拟机字节码指令地址(如果正在执行的本地方法则为空)
Java虚拟机栈(-Xss: 设置上限):
虚拟机栈描述的是Java方法执行的内存模式:每个方法在执行的同时都会创建一个栈帧(Stack Frame),用来存储局部变量表,操作数栈,动态链接,方法出口的等信息.每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程.
局部变量表存放了编译期可知的各种基本数据类型(8个基本类型),对象引用(reference类型)和returnAddress类型
这个区域规定了两种异常:StackOverflowError(线程请求栈深入大于虚拟机栈的深度)异常,如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛OutOfMemoryError
本地方法栈:
本地方法栈与虚拟机栈几乎一样,只是本地方法栈执行的是native方法,虚拟机栈执行的是Java方法
堆(-Xmx:设置上限,-Xms:设置下限):
Java堆是Java虚拟机管理的内存最大一块,此内存区域存储对象实例以及数组.Java堆是垃圾收集器管理的主要区域,又叫GC堆.
Java堆分为: 年轻代和老年代
年轻代分为三个区Eden,Form Survivor,To Survivor(默认比例为8:1),
从内存角度来看,Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer ,TLAB),又叫线程工作区,
堆可以在物理上不连续,但逻辑上要连续,当堆中无法分配内存给实例对象,将抛出OutOfMemoryError
方法区(-XX:MaxPermSize设置上限,-XX:PermSize设置下限):
它存放虚拟机加载的Class信息,常量,静态变量,即时编译器编译后的代码等数据;方法区又放在永久代(jdk8开始,永久代移除,方法区存放在本地内存,Native memory),当方法区,无法分配将抛出OutOfMemoryError
运行时常量池:
Runtime Constant Pool是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中,注意运行期也可能将新的常量放入池中,如String类的intern()方法.当常量池无法申请内存时抛出OutOfMemoryError
直接内存(-XX:MaxDirectMemorySize上限,默认与-Xmx一样):
不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域.但这部分内存也频繁地使用,也可能导致OutOfMemoryError异常.
JDK1.4新加入NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存(Native堆),然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作.这样能在一些场景提高性能,避免Java堆和Native堆中来回复制数据,Native堆不会受到Java堆大小控制,但受本机内存大小限制,
2.垃圾收集器与内存分配策略
线程私有内存:线程结束就自动回收,和线程生命周期一致,
所以垃圾回收仅指回收线程共享内存中的堆。
判断对象是否已死的算法:
- 引用计数算法:给对象添加一个计数器,每当有一个地方引用它,计数器就1;当引用失效时,计数器值就减1;计数器为0说明该对象即将被回收.好处:简单。缺点:不能解决对象之间相互循环引用问题。
- 可达性分析算法:通过一系列的称为GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
可作为GC Roots的对象包括以下几种:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(既一般说的Native方法)引用的对象
引用分类
- 强引用(StrongReference):当虚拟机GC的时候不会回收这类对象。
Object obj = new Object();
- 软引用(SoftReference):当虚拟机GC的时候,只有虚拟机内存不够才会回收此类对象。
Object obj = new Object(); SoftReference<Object> srf = new SoftReference<Object>(obj);
- 弱引用(WeakReference):当虚拟机GC的时候,不管虚拟机内存够不够都会回收。
Object obj = new Object();WeakReference<Object> wrf = new WeakReference<Object>(obj);
- 虚引用(PhantomReference):又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
Object obj = new Object();PhantomReference<Object> pf = new PhantomReference<Object>(obj);
方法区回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
- 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
finalize()(此方法只调用一次,回收前调用)
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。
垃圾回收算法
主要分为三种:标记清除算法,复制算法,标记整理算法
标记清除(Maker Sweep):
标记出要回收的对象,然后清除掉.
缺点:会空间产生内存碎片,效率不高
复制算法(Copying):
标记出存活的对象,然后把存活的对象复制到另一块内存块,再清理回收对象.目前常用的虚拟机把此算法应用在堆的新生代,并把堆的新生代分为Eden,Survivor,Survivor区,因为新生代存活对象很少,复制成本低,空间浪费较少,所以使用此算法。
HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
标记整理算法(Maker-Compact):
标记出存活对象,然后清除未存活的对象,最后把剩下的存活对象往一端移动.好处解决了内存碎片问题,并且不会像复制算法浪费空间,常把该算法应用在老年代.因为老年代不能使用复制算法,老年代存活对象比例高,剩下的10%Survivor可能装不下。
垃圾收集器
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程。
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。
1.Serial收集器(新生代,串行)
Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是 Client 模式下的默认新生代收集器,因为在该应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
2.ParNew收集器(新生代,并行)
它是 Serial 收集器的多线程版本。
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
3.Parallel Scavenge收集器(新生代,并行)
与 ParNew 一样是多线程收集器。
其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
4.SerialOld收集器(老年代,串行)
是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
5.Parallel Old收集器(老年代,并行)
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
6.CMS收集器(老年代,并行)
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下四个流程:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:不需要停顿。
具有以下缺点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
7.G1收集器(并行)
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备如下特点:
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
3.内存分配与回收策略
- 对象优先分配在 Eden 区,如果 Eden 区没有足够的空间时,虚拟机执行一次 Minor GC。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是 避免在 Eden 区和两个 Survivor 区之间发生大量的内存拷贝(新生代采用复制算法收集 内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过 了 1 次 Minor GC 那么对象会进入 Survivor 区,之后每经过一次 Minor GC 那么对象 的年龄加 1,直到达到阀值,对象进入老年区。
- 动态判断对象的年龄。如果 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行 Minor GC 时,JVM 会计算 Survivor 区移至老年区的对象的 平均大小,如果这个值大于老年区的剩余值大小则进行一次 Full GC,如果小于检查 HandlePromotionFailure 设置,如果 true 则只进行 Minor GC,如果 false 则进 行 Full GC。
4.JDK的命令行工具
jps:查看虚拟机进程的状况, 如进程 ID。
jmap: 用于生成堆转储快照文件( 某一时刻的) 。
jhat: 对生成的堆转储快照文件进行分析。
jstack:用来生成线程快照( 某一时刻的) 。 生成线程快照的主要
目的是定位线程长时停顿的原因( 如死锁, 死循环, 等待 I/O 等) ,
通过查看各个线程的调用堆栈, 就可以知道没有响应的线程在后台
做了什么或者等待什么资源。
jstat:虚拟机统计信息监视工具。 如显示垃圾收集的情况, 内存使
用的情况。
Jconsole:主要是内存监控和线程监控。 内存监控: 可以显示内存
的使用情况。 线程监控: 遇到线程停顿时, 可以使用这个功能。
5.JVM 常见的启动参数
-Xms: 设置堆的最小值。
-Xmx: 设置堆的最大值。
-Xmn: 设置新生代的大小。
-Xss: 设置每个线程的栈大小。
-XX:NewSize: 设置新生代的初始值。
-XX:MaxNewSize : 设置新生代的最大值。
-XX:PermSize: 设置永久代的初始值。
-XX:MaxPermSize: 设置永久代的最大值。
-XX:SurvivorRatio: 年轻代中 Eden 区与 Survivor 区的大小比值。
-XX:PretenureSizeThreshold: 令大于这个设置值的对象直接在老年代分配
问题总结:
1.内存泄露的解决方案?
- 避免在循环中创建对象。
- 尽早释放无用对象的引用。 ( 最基本的建议)
- 尽量少用静态变量, 因为静态变量存放在永久代( 方法区) , 永久代基本不参与垃圾回收。
- 使用字符串处理, 避免使用 String, 应大量使用 StringBuffer, 每一个 String
2.在实际场景中,你怎么查找内存泄露?
可以使用 Jconsole。
…
参考书籍<<深入理解Java虚拟机>>