虚拟机部分(JVM)

 

第一部分:JVM的内存区域划分(堆是用来存放对象而栈是用来执行程序的)java

 

一、 线程独有的内存区域程序员

1PROGRAM COUNTER REGISTER,程序计数器算法

这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器经过改变这个计数器的值来选取下一条须要执行的字节码指令。因为程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,所以,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。数据库

2JAVA STACK,虚拟机栈数组

Java栈也称做虚拟机栈,Java栈是Java方法执行的内存模型tomcat

Java栈中存放的是一个个的栈帧,每一个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操做数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。安全

下图表示了一个Java栈的模型:网络

 栈帧中的各部分:局部变量表,顾名思义,就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就能够肯定其大小了,所以在程序执行期间局部变量表的大小是不会改变的。操做数栈,栈最典型的一个应用就是用来对表达式求值。程序中的全部计算过程都是在借助于操做数栈来完成的指向运行时常量池的引用,由于在方法执行的过程当中有可能须要用到类中的常量,因此必须要有一个引用指向运行时常量方法返回地址,当一个方法执行完毕以后,要返回以前调用它的地方,所以在栈帧中必须保存一个方法返回地址数据结构

栈的执行:多线程

当线程执行一个方法时,就会随之建立一个对应的栈帧,并将创建的栈帧压栈。当方法执行完毕以后,便会将栈帧出栈。所以可知,线程当前执行的方法所对应的栈帧一定位于Java栈的顶部。栈区的空间不用程序员去管理,由于Java有本身的垃圾回收机制,这部分空间的分配和释放都是由系统自动实施的。

因为每一个线程正在执行的方法可能不一样,所以每一个线程都会有一个本身的Java栈,互不干扰;生命周期和线程相同。每一个方法执行的同时都会建立一个栈帧,用于存储局部变量表、操做数栈、动态连接、方法出口等信息,每个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

 

3NATIVE METHOD STACK,本地方法栈

本地方法栈与Java栈的做用和原理很是类似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。

 

2、线程间共享的内存区域

1HEAP,堆

堆是Java虚拟机所管理的内存中最大的一块,它在虚拟机启动时建立,此内存惟一的目的就是存放对象实例。因为如今垃圾收集器采用的基本都是分代收集算法,因此堆还能够细分为新生代和老年代,再细致一点还有Eden区、From Survivior区、To Survivor区。

2METHOD AREA,方法区

这块区域用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据.

 

2、对象的建立过程

1   NEW指令:定位类(初始化)

2       类加载 ,虚拟机从堆内存中分配内存   垃圾收集器选择的是Serial、ParNew

           内存规整:指针碰撞    内存不规整:空闲列表   垃圾收集器选择的是CMS

           线程安全性:CAS +  TLAB

3       分配到的内存初始化为零(不包括对象头)

4       对象头设置

5       初始化

 

对象的访问定位

Java程序须要经过栈上的reference(引用)数据来操做堆上的具体对象

 

 

 

 

第二部分:JVM分代垃圾回收策略

1、为何要分代

不一样的对象的生命周期是不同的。所以,不一样生命周期的对象能够采起不一样的收集方式,以便提升回收效率。

2、如何分代

虚拟机中的共划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

年轻代:

全部新生成的对象首先都是放在年轻代的。年轻代的目标就是尽量快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(通常而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的而且此时还存活的对象,将被复制“年老区(Tenured)”。

年老代:

在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。所以,能够认为年老代中存放的都是一些生命周期较长的对象。GC过程当中,当某些对象通过屡次GC都没有被回收,可能会进入到年老代。或者,当新生代没有足够的空间来为对象分配内存时,可能会直接在年老代进行分配。

持久代:

用于存放静态文件,现在Java类、方法等。持久代对垃圾回收没有显著影响,大部分对象在运行都不要进行GC,可是有些应用可能动态生成或者调用一些class,持久代大小经过-XX:MaxPermSize=<N>进行设置。永久代实际上对应着虚拟机运行时数据区的“方法区”,这里主要存放类信息、静态变量、常量等数据。 

 

3、什么状况下触发垃圾回收

GC有两种类型:Scavenge GCFull GC

通常状况下,当新对象生成,而且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,而且把尚且存活的对象移动到Survivor区。而后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。由于大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,因此Eden区的GC会频繁进行。于是,通常在这里须要使用速度快、效率高的算法,使Eden去能尽快空闲出来。对整个堆进行整理,包括Young、Tenured和Perm。

 

Full GC由于须要对整个块进行回收,因此比Scavenge GC要慢,所以应该尽量减小Full GC的次数。在对JVM调优的过程当中,很大一部分工做就是对于FullGC的调节。有以下缘由可能致使Full GC:

· 年老代(Tenured)被写满

· 持久代(Perm)被写满

· System.gc()被显示调用

·上一次GC以后Heap的各域分配策略动态变化

 

 

 

 第三部分:JVM以内存分配与回收策略

TLAB,内存分配的动做,能够按照线程划分在不一样的空间之中进行,即每一个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲.

1、 对象优先在Eden区分配

对象一般在新生代的Eden区进行分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC

Minor GC:指发生在新生代的垃圾收集动做,很是频繁,速度较快。

Major GC:指发生在老年代的GC,出现Major GC,常常会伴随一次Minor GC,同时Minor GC也会引发Major GC,通常在GC日志中统称为GC,不频繁。

Full GC:指发生在老年代和新生代的GC,速度很慢,须要Stop The World。

2、大对象直接进入老年代

须要大量连续内存空间的Java对象称为大对象,大对象的出现会致使提早触发垃圾收集以获取更大的连续的空间来进行大对象的分配。虚拟机提供了-XX:PretenureSizeThreadshold参数来设置大对象的阈值,超过阈值的对象直接分配到老年代。

3、长期存活的对象进入老年代

每一个对象有一个对象年龄计数器,与前面的对象的存储布局中的GC分代年龄对应。对象出生在Eden区、通过一次Minor GC后仍然存活,并可以被Survivor容纳,设置年龄为1,对象在Survivor区,每次通过一次Minor GC,年龄就加1,当年龄达到必定程度(默认15),就晋升到老年代,虚拟机提供了-XX:MaxTenuringThreshold来进行设置。

4、动态对象年龄判断

对象的年龄到达了MaxTenuringThreshold能够进入老年代,同时,若是在survivor区中相同年龄全部对象大小的总和大于survivor区的一半,年龄大于等于该年龄的对象就能够直接进入老年代。无需等到MaxTenuringThreshold中要求的年龄。

5、空间分配担保在发生Minor GC

在发生Minor GC时,虚拟机会检查老年代连续的空闲区域是否大于新生代全部对象的总和,若成

立,则说明Minor GC是安全的.

 

 

 

 

第四部分 Java垃圾回收(GC)机制详解

 

1、为何须要垃圾回收

若是不进行垃圾回收,内存早晚都会被消耗空

2、哪些内存须要回收?

所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象。那么如何找到这些对象?

1引用计数法 XXXXXX

引用计数法的实现是,给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。任什么时候刻计数值为0的对象就是不可能再被使用的。XXXXXX

2可达性分析法 111111

可达性分析法的基本思想是经过一系列称为“GC Roots”的对象做为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证实此对象是不可用的。111111

 

3、四种引用状态 对象的回收---堆

Java中引用的定义:若是引用类型的数据中存储的数值表明的是另外一块内存的起始地址,就称这块内存表明着一个引用。

 

4、方法区的垃圾回收

方法区的垃圾回收主要回收两部份内容:1. 废弃常量。2. 无用的类。

废弃常量

若是一个字符串“abc”已经进入常量池,可是当前系统没有任何一个String对象引用了叫作abc的字面量,那么,若是发生垃圾回收而且有必要时,“abc”就会被系统移出常量池。常量池中的其余类(接口)、方法、字段的符号引用也与此相似。

无用的类:

1. 该类的全部实例都已经被回收,即Java堆中不存在该类的任何实例。

2. 加载该类的ClassLoader已经被回收

3. 该类对应的java.lang.Class对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。

 

 

第五部分:垃圾收集算法

1、标记-清除(Mark-Sweep)算法

分为“标记”和“清除”两个阶段:首先标记出全部须要回收的对象,标记完成后统一回收全部被标记的对象

这种算法的不足主要体如今效率和空间,从效率的角度讲,标记和清除两个过程的效率都不高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片, 内存碎片太多可能会致使之后程序运行过程当中在须要分配较大对象时,没法找到足够的连续内存而不得不提早触发一次垃圾收集动做。

标记-清除算法执行过程如图:

2、复制(Copying)算法

复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,而后再把已经使用过的内存空间一次性清理掉。这样每次只须要对整个半区进行内存回收,内存分配时也不须要考虑内存碎片等复杂状况,只须要移动指针,按照顺序分配便可。

复制算法的执行过程如图:

这种算法有个缺点,内存缩小为了原来的一半,这样代价过高了如今的商用虚拟机都采用这种算法来回收新生代,不过研究代表1:1的比例很是不科学,所以新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor

每次回收时,EdenSurvivor中还存活着的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。固然,咱们没有办法保证每次回收都只有很少于10%的对象存活,当Survivor空间不够用时,须要依赖老年代进行分配担保。

 

3、标记-整理(Mark-Compact)算法

复制算法在对象存活率较高的场景下要进行大量的复制操做,效率很低。万一对象100%存活,那么须要有额外的空间进行分配担保。老年代都是不易被回收的对象,对象存活率高,所以通常不能直接选用复制算法。根据老年代的特色,有人提出了另一种标记-整理算法,过程与标记-清除算法同样,不过不是直接对可回收对象进行清理,而是让全部存活对象都向一端移动,而后直接清理掉边界之外的内存

标记-整理算法的工做过程如图:

4、分代收集算法

现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。上面内容的结合罢了,根据对象的生命周期的不一样将内存划分为几块,而后根据各块的特色采用最适当的收集算法。大批对象死去、少许对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法

 

 

第六部分、垃圾收集器

不一样虚拟机所提供的垃圾收集器可能会有很大差异,咱们使用的是HotSpot。

HotSpot这个虚拟机所包含的全部收集器如图:

 

 

 

上图展现了7种做用于不一样分代的收集器,若是两个收集器之间存在连线,那说明它们能够搭配使用。

一、 Serial收集器

  这个收集器是一个采用复制算法的单线程的收集器,单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工做,另外一方面也意味着它进行垃圾收集时必须暂停其余线程的全部工做,直到它收集结束为止。不过实际上到目前为止,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,由于它简单而高效。

说明:1. 须要STW(Stop The World),停顿时间长。2. 简单高效,对于单个CPU环境而言,Serial收集器因为没有线程交互开销,能够获取最高的单线程收集效率。

2Serial Old收集器

Serial收集器的老年代版本,一样是一个单线程收集器,使用标记-整理算法,这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

3ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其他行为和Serial收集器彻底同样,包括使用的也是复制算法。ParNew收集器除了多线程之外和Serial收集器并无太多创新的地方,可是它倒是Server模式下的虚拟机首选的新生代收集器,其中有一个很重要的和性能无关的缘由是,除了Serial收集器外,目前只有它能与CMS收集器配合工做(看图)。

CMS收集器是一款几乎能够认为有划时代意义的垃圾收集器,由于它第一次实现了让垃圾收集线程与用户线程基本上同时工做。ParNew收集器运行过程以下图所示:

4Parallel Scavenge收集器

一个新生代收集器,也是用复制算法的收集器,也是并行的多线程收集器,可是它的特色是它的关注点和其余收集器不一样。CMS等收集器的关注点是尽量缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量 < 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间) >另外,Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器

虚拟机提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小。

前者小点好, 后者是一个开关参数,这个参数打开以后,就不须要手动指定新生代大小、Eden区和Survivor参数等细节参数了,虚拟机会根据当前系统的运行状况手机性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

5Parallel Old收集器

Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。 “吞吐量优先收集器”终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,均可以优先考虑Parallel Scavenge收集器+Parallel Old收集器的组合。

运行过程以下图所示:

 

 

 

6CMS收集器

CMSConrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。使用标记- 清除算法,收集过程分为以下四步:

(1). 初始标记,标记GCRoots能直接关联到的对象,时间很短。

(2). 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。

(3). 从新标记,修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,时间较长。

(4). 并发清除,回收内存空间,时间很长。

其中,并发标记与并发清除两个阶段耗时最长,可是能够与用户线程并发执行。运行过程以下图所示:

7G1收集器

HotSpot开发团队赋予它的使命是将来能够替换掉JDK1.5中发布的CMS收集器。与其余GC收集器相比,G1收集器有如下特色:

(1). 并行和并发。使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行。

(2). 分代收集。独立管理整个堆,可是可以采用不一样的方式去处理新建立对象和已经存活了一段时间、熬过屡次GC的旧对象,以获取更好的收集效果。

(3). 空间整合。基于标记 - 整理算法,无内存碎片产生。

(4). 可预测的停顿。能简历可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片断内,消耗在垃圾收集上的时间不得超过N毫秒。

在G1以前的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1再也不是这样。使用G1收集器时,Java堆的内存布局与其余收集器有很大差异,它将整个Java堆划分为多个大小相等的独立区域,虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分(能够不连续)Region的集合。

 

 

 

第七部分:理解GC日志

 

[GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs]

[Times: user=0.00 sys=0.00, real=0.03 secs]

[Full GC (System) [Tenured: 2241K->193K(5312K), 0.0056517 secs] 4289K->193K(7680K),

[Perm : 2950K->2950K(21248K)], 0.0057094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

Heap

def new generation total 2432K, used 43K [0x00000000052a0000, 0x0000000005540000,

0x0000000006ea0000)

eden space 2176K, 2% used [0x00000000052a0000, 0x00000000052aaeb8,

0x00000000054c0000)

from space 256K, 0% used [0x00000000054c0000, 0x00000000054c0000,

0x0000000005500000)

to space 256K, 0% used [0x0000000005500000, 0x0000000005500000,

0x0000000005540000)

tenured generation total 5312K, used 193K [0x0000000006ea0000, 0x00000000073d0000,

0x000000000a6a0000)

the space 5312K, 3% used [0x0000000006ea0000, 0x0000000006ed0730,

0x0000000006ed0800, 0x00000000073d0000)

compacting perm gen total 21248K, used 2982K [0x000000000a6a0000,

0x000000000bb60000, 0x000000000faa0000)

the space 21248K, 14% used [0x000000000a6a0000, 0x000000000a989980,

0x000000000a989a00, 0x000000000bb60000)

No shared spaces configured.

一、日志的开头GC”、“Full GC”表示此次垃圾收集的停顿类型,若是有Full,则说明本次GC中止了其余全部工做线程这说明是调用System.gc()方法所触发的GC。

二、GC”中接下来的“DefNew”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,因此显示的是DefNew。老年代和永久代同理,名称也是由收集器决定的。

3、后面方括号内部的310K->194K(2368K)”,指的是该区域已使用的容量->GC后该内存区域已使用的容量(该内存区总容量)方括号外面的310K->194K(7680K)””则指的是GCJava堆已使用的容量->GCJava堆已使用的容量(Java堆总容量)

4、再日后“0.0269163 secs”表示该内存区域GC所占用的时间,单位是秒。最后的“[Times:user=0.00 sys=0.00 real=0.03 secs]”则更具体了,user表示用户态消耗的CPU时间、内核态消耗的CPU时间、操做从开始到结束通过的墙钟时间。后面两个的区别是,墙钟时间包括各类非运算的等待消耗,好比等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操做会叠加这些CPU时间,因此若是看到user或sys时间超过real时间是彻底正常的。

五、Heap”后面就列举出堆内存目前各个年代的区域的内存状况。

 

 

 

第八部分:JVM调优-eclipse开始

 

eclipse 默认配置:eclipse.ini

在配置的末尾处添加以下配置文件:

-XX:+PrintGCDetails // 输出GC的详细日志

-XX:+PrintGCDateStamps // 输出GC的时间戳(以日期的形式)

-Xloggc:gc.log // 输出GC的详细日志

 

至此优化结束,附最终的eclipse.ini文件:

-startup

plugins/org.eclipse.equinox.launcher_1.3.100.v20150511-1540.jar

--launcher.library

plugins/org.eclipse.equinox.launcher.win32.win32.x86_1.1.300.v20150602-1417

-clean

-product

org.eclipse.epp.package.jee.product

--launcher.defaultAction

openFile

--launcher.XXMaxPermSize

256M

-showsplash

org.eclipse.platform

--launcher.XXMaxPermSize

256m

--launcher.defaultAction

openFile

--launcher.appendVmargs

-vmargs

-Dosgi.requiredJavaVersion=1.7

-Xms1024m

-Xmx1024m

-Xverify:none

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-Xloggc:gc.log

 

 

 

 

第九部分:内存溢出与内存泄漏

 

1、基本概念

内存溢出:简单地说内存溢出就是指程序运行过程当中申请的内存大于系统可以提供的内存,致使没法申请到足够的内存,因而就发生了内存溢出。out of memory

内存泄漏:内存泄漏指程序运行过程当中分配内存给临时变量,用完以后却没有被GC回收,始终占用着内存,既不能被使用也不能分配给其余程序,因而就发生了内存泄漏 memory leak

memory leak会最终会致使out of memory!

 

2、内存溢出的常见状况(内存区域的划分中只有程序计数器不会发生泄漏或者溢出)

1java.lang.OutOfMemoryError: PermGen space (持久带溢出) “方法区

jvm经过持久带实现了java虚拟机规范中的方法区,而运行时常量池就是保存在方法区中的,所以发生这种溢出多是运行时常量池溢出或是因为程序中使用了大量的jar或class,使得方法区中保存的class对象没有被及时回收或者class信息占用的内存超过了配置的大小。

2java.lang.OutOfMemoryError: Java heap space (堆溢出) 

发生这种溢出的缘由通常是建立的对象太多,在进行垃圾回收以前对象数量达到了最大堆的容量限制。

内存泄漏,可进一步经过工具查看泄漏对象到GC Roots的引用链,定位出泄漏代码的位置,修改程序或算法

内存溢出 ,就是说内存中的对象确实都还必须存活,那就应该检查虚拟机的堆参数-Xmx(最大堆大小)-Xms(初始堆大小),与机器物理内存对比看是否能够调大。

3、虚拟机栈和本地方法栈溢出

若是线程请求的栈深度大于虚拟机所容许的最大深度,将抛出StackOverflowError。

若是虚拟机在扩展栈时没法申请到足够的内存空间,则抛出OutOfMemoryError。

 

3、内存泄漏的常见状况

内存泄漏的根本缘由是长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象已经再也不须要,但因为长生命周期对象持有它的引用而致使不能被回收

内存泄漏能够分为4类:常发性内存泄漏/偶发性内存泄漏/一次性内存泄漏/隐式内存泄漏

从用户使用程序的角度来看,内存泄漏自己不会产生什么危害,做为通常的用户,根本感受不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统全部的内存。从这个角度来讲,一次性内存泄漏并无什么危害,由于它不会堆积,而隐式内存泄漏危害性则很是大,由于较之于常发性和偶发性内存泄漏它更难被检测到。

下面总结几种常见的内存泄漏:

1静态集合类引发的内存泄漏

HashMapVector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的全部的对象Object也不能被释放,从而形成内存泄漏,由于他们也将一直被Vector等引用着。

Vector<Object> v=new Vector<Object>(100);

for (int i = 1; i<100; i++)

{

Object o = new Object();

v.add(o);

o = null;

}

在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,若是仅仅释放引用自己(o=null),那么Vector 仍然引用该对象,因此这个对象对GC 来讲是不可回收的。所以,若是对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null

2、修改HashSet中对象的参数值,且参数是计算哈希值的字段

当一个对象被存储到HashSet集合中之后,修改了这个对象中那些参与计算哈希值的字段后,这个对象的哈希值与最初存储在集合中的就不一样了,这种状况下,用contains方法在集合中检索对象是找不到的,这将会致使没法从HashSet中删除当前对象,形成内存泄漏。 

三、监听器

一般一个应用当中会用到不少监听器,咱们会调用一个控件的诸如addXXXListener()等方法来增长监听器,但每每在释放对象的时候却没有记住去删除这些监听器,从而增长了内存泄漏的机会。

4各类链接

好比数据库链接,网络链接和io链接,除非其显式的调用了其close() 方法将其链接关闭,不然是不会自动被GC 回收的。对于Resultset 和Statement 对象能够不进行显式回收,但Connection 必定要显式回收,由于Connection 在任什么时候候都没法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会当即为NULL。

可是若是使用链接池,状况就不同了,除了要显式地关闭链接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另一个也会关闭),不然就会形成大量的Statement 对象没法释放,从而引发内存泄漏。这种状况下通常都会在try里面去链接,在finally里面释放链接。

5单例模式

若是单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,致使内存泄露。不正确使用单例模式是引发内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),若是单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,致使内存泄露。

避免内存泄漏的几点建议:

一、尽早释放无用对象的引用。

二、避免在循环中建立对象。

三、使用字符串处理时避免使用String,应使用StringBuffer。

四、尽可能少使用静态变量,由于静态变量存放在永久代,基本不参与垃圾回收。

 

 

 

第十部分:内存溢出及解决方案   OutOfMemoryError

 

1.JVM Heap(堆)溢出:java.lang.OutOfMemoryError: Java heap space        

JVM在启动的时候会自动设置JVM Heap的值, 能够利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置,决方法:手动设置JVM Heap(堆)的大小。eclipse中tomcat中 VM arguments :   -Xmx512m  -Xms512M

 

2.PermGen space溢出: java.lang.OutOfMemoryError: PermGen space         方法区

PermGen space是指内存的永久保存区域。因为这块内存主要是被JVM存放Class和Meta信息的,Class在被Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不一样,sun的 GC不会在主程序运行期对PermGen space进行清理,因此若是你的APP会载入不少CLASS的话,就极可能出现PermGenspace溢出。通常发生在程序的启动阶段。

解决方法: 经过-XX:PermSize-XX:MaxPermSize设置永久代大小便可 -XX:PermSize=128M -XX:MaxPermSize=512m

 

3.栈溢出: java.lang.StackOverflowError : Thread Stack space              

栈溢出了,JVM依然是采用栈式的虚拟机。函数的调用过程都体如今堆栈和退栈上了。调用构造函数的 “层”太多了,以至于把栈区溢出了。

解决方法:1:修改程序。2:经过 -Xss: 来设置每一个线程的Stack大小便可。在Java虚拟机规范中,对这个区域规定了两种异常情况:StackOverflowError和OutOfMemoryError异常。

1StackOverflowError异常

每当java程序代码启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。当线程调用java方法时,虚拟机压入一个新的栈帧到该线程的java栈中。只要这个方法尚未返回,它就一直存在。若是线程的方法嵌套调用层次太多(如递归调用),随着java栈中帧的逐渐增多,最终会因为该线程java栈中全部栈帧大小总和大于-Xss设置的值,而产生StackOverflowError内存溢出异常

2OutOfMemoryError异常

java程序代码启动一个新线程时,没有足够的内存空间为该线程分配java(一个线程java栈的大小由-Xss参数肯定),jvm则抛出OutOfMemoryError异常。

 

4. Server容器启动的时候咱们常常关心和设置JVM的几个参数以下:

-Xmsjava Heap初始大小, 默认是物理内存的1/64

-Xmxjava Heap最大值,不可超过物理内存。

-Xmn:young generation的heap大小,通常设置为Xmx的三、4分之一 。增大年轻代后,将会减少

年老代大小,能够根据监控合理设置。

-Xss:每一个线程的Stack大小,而最佳值应该是128K,默认值好像是512k

-XX:PermSize:设定内存的永久保存区初始大小,缺省值为64M

-XX:MaxPermSize:设定内存的永久保存区最大大小,缺省值为64M

-XX:SurvivorRatio:Eden区与Survivor区的大小比值,设置为8,则两个Survivor区与一个Eden区

的比值为2:8,一个Survivor区占整个年轻代的1/10。

-XX:+UseParallelGC:F年轻代使用并发收集,而年老代仍旧使用串行收集。

-XX:+UseParNewGC:设置年轻代为并行收集,JDK5.0以上,JVM会根据系统配置自行设置,所无需再设置此值。

-XX:ParallelGCThreads:并行收集器的线程数,值最好配置与处理器数目相等 一样适用于CMS。

-XX:+UseParallelOldGC:年老代垃圾收集方式为并行收集(Parallel Compacting)。

-XX:MaxGCPauseMillis:每次年轻代垃圾回收的最长时间(最大暂停时间),若是没法知足此时间,

JVM会自动调全年轻代大小,以知足此值。

-XX:+ScavengeBeforeFullGC:Full GC前调用YGC,默认是true。

 

 

第十一部分 :线上环境上查看JVM的参数并进行调优

1、什么是类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,而后在堆区建立一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,而且向Java程序员提供了访问方法区内的数据结构的接口。

类加载器并不须要等到某个类被“首次主动使用”时再加载它,JVM规范容许类加载器在预料某个类将要被使用时就预先加载它,若是在预先加载的过程当中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)若是这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

 

2、类的生命周期

类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是肯定的(按顺序开始,而不是按顺序进行或完成),而解析阶段则不必定,它在某些状况下能够在初始化阶段以后开始,这是为了支持Java语言的运行时绑定。

1、加载:查找并加载类的二进制数据

加载是类加载过程的第一个阶段,在加载阶段,虚拟机须要完成如下三件事情:

(1)经过一个类的全限定名来获取其定义的二进制字节流。

(2)将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。

(3)在Java堆中生成一个表明这个类的java.lang.Class对象,做为对方法区中这些数据的访问入口。

加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,并且在Java堆中也建立一个java.lang.Class类的对象,这样即可以经过该对象访问方法区中的这些数据。

 

二、 链接包括验证/准备/解析

1)验证:确保被加载的类的正确性

验证是链接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。验证阶段大体会完成4个阶段的检验动做:

文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围以内、常量池中的常量是否有不被支持的类型。

元数据验证对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object以外。

字节码验证经过数据流和控制流分析,肯定程序语义是合法的、符合逻辑的。符号引用验证:确保解析动做能正确执行。

验证阶段是很是重要的,但不是必须的,它对程序运行期没有影响,若是所引用的类通过反复验证,那么能够考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

2)准备:为类的静态变量分配内存,并将其初始化为默认值。

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该

阶段有如下几点须要注意:

① 这时候进行内存分配的仅包括类变量(static,而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

② 这里所设置的初始值一般状况下是数据类型默认的零值(如00Lnullfalse等),而不是被在Java代码中被显式地赋予的值,把value赋值为3的动做将在初始化阶段才会执行

这里还须要注意以下几点:

基本数据类型来讲,对于类变量(static)和全局变量,若是不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来讲,在使用前必须显式地为其赋值,不然编译时不经过。

对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,不然编译时不经过;而只被final修饰的常量则既能够在声明时显式地为其赋值,也能够在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。

对于引用数据类型reference来讲,如数组引用、对象引用等,若是没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。若是在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

③ 若是类字段的字段属性表中存在ConstantValue属性,即同时被finalstatic修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值

假设上面的类变量value被定义为: public static final int value = 3;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。咱们能够理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。

3)解析:把类中的符号引用转换为直接引用

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,能够是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

 

3、初始化

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。

在Java中对类变量进行初始值设定有两种方式:

② 明类变量时指定初始值

②使用静态代码块为类变量指定初始值

类初始化时机:只有当对类主动使用的时候才会致使类的初始化,类的主动使用包括如下四种:

– 使用new关键字实例化对象、读取或者设置一个类的静态字段(被final修饰的静态字段除外)、调用一个类的静态方法的时候。

– 使用java.lang.reflect包中的方法对类进行反射调用的时候。

– 初始化一个类,发现其父类尚未初始化过的时候。

– 虚拟机启动的时候,虚拟机会先初始化用户指定的包含main()方法的那个类。以上四种状况称为主动使用,其余的状况均称为被动使用,被动使用不会致使初始化。

 

4、结束生命周期

在以下几种状况下,Java虚拟机将结束生命周期

– 执行了System.exit()方法

– 程序正常执行结束

– 程序在执行过程当中遇到了异常或错误而异常终止

– 因为操做系统出现错误而致使Java虚拟机进程终止

 

4、类的加载

类加载有三种方式:

一、命令行启动应用时候由JVM初始化加载

二、经过Class.forName()方法动态加载

三、经过ClassLoader.loadClass()方法动态加载

 

5、双亲委派模型

双亲委派模型的工做流程是:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,所以,全部的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即没法完成该加载,子加载器才会尝试本身去加载该类。

一、当AppClassLoader加载一个class时,它首先不会本身去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

二、当ExtClassLoader加载一个class时,它首先也不会本身去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

三、若是BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

四、ExtClassLoader也加载失败,则会使用AppClassLoader来加载,若是AppClassLoader也加载失败,则会报出异常ClassNotFoundException

双亲委派模型意义:-系统类防止内存中出现多份一样的字节码     -保证Java程序安全稳定运行

 

 

 

 

 

常见知识问答

jvm内存机制

JAVA虚拟机运行时的区域区域划分为(线程独有的内存区域)堆,方法区和(线程间共享的内存区域)程序计数器,java栈和本地服务栈。

 

介绍下垃圾收集机制,垃圾收集有哪些算法,各自的特色

垃圾收集算法包括:

标记-清楚算法,首先标记出全部须要回收的对象,标记完成后统一回收全部被标记的对象。从效率的角度讲,标记和清除两个过程的效率都不高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片,须要分配较大对象时,没法找到足够的连续内存而不得不提早触发一次垃圾收集动做。

复制法,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,而后再把已经使用过的内存空间一次性清理掉。所以新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。

标记-整理算法,过程与标记-清除算法同样,不过不是直接对可回收对象进行清理,而是让全部存活对象都向一端移动,而后直接清理掉边界之外的内存。

分代收集算法:结合以上算法,大批对象死去、少许对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法。

 

 

3,聊聊GC,谈谈Major GC ,FullGc的区别,垃圾收集器有哪些,他们的区别?

Minor GC:指发生在新生代的垃圾收集动做,很是频繁,速度较快。

Major GC:指发生在老年代的GC,出现Major GC,常常会伴随一次Minor GC,同时Minor GC也会引发Major GC,通常在GC日志中统称为GC,不频繁。

Full GC:指发生在老年代和新生代的GC,速度很慢,须要Stop The World。

垃圾收集器包括:

Serial收集器是采用复制算法的单线程的收集器,须要STW(Stop The World),停顿时间长; 简单高效,对于单个CPU环境而言。

ParNew收集器其实就是Serial收集器的多线程版本;

Serial Old收集器是Serial收集器的老年代版本,一样是一个单线程收集器,使用“标记-整理算法”;

 

Parallel Scavenge收集器 是一个新生代收集器,也是用复制算法的收集器,也是并行的多线程收集器,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量  

Parallel Old收集器Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,均可以优先考虑Parallel Scavenge收集器+Parallel Old收集器的组合

 

CMS收集器是以获取最短回收停顿时间为目标的收集器。使用标记- 清除算法

G1,使用G1收集器时,将整个Java堆划分为多个大小相等的独立区域,虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分的集合。

 

4,OutOfMemoryError这个错误你遇到过吗?你是怎么解决处理的?

遇到过

若是是JVM Heap(堆)溢出:JVM在启动的时候会自动设置JVM Heap的值, 能够利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置;若是是PermGen space(方法区)溢出:经过-XX:PermSize和-XX:MaxPermSize设置永久代大小便可;若是是栈溢出 1:修改程序。2:经过 -Xss: 来设置每一个线程的Stack大小便可。

 

5,JVM调优有哪些参数,介绍下,线上环境上,你是怎么查看JVM的参数并进行调优的?

-Xmsjava Heap初始大小, 默认是物理内存的1/64

-Xmxjava Heap最大值,不可超过物理内存。

-Xmn:young generation的heap大小,通常设置为Xmx的三、4分之一 。增大年轻代后,将会减少

年老代大小,能够根据监控合理设置。

-Xss:每一个线程的Stack大小,而最佳值应该是128K,默认值好像是512k

-XX:PermSize:设定内存的永久保存区初始大小,缺省值为64M

-XX:MaxPermSize:设定内存的永久保存区最大大小,缺省值为64M

 

6,能不能本身写一个类叫java.lang.String(类加载的过程,双亲委派模型)

双亲委派模型的工做流程是:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,所以,全部的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即没法完成该加载,子加载器才会尝试本身去加载该类。

相关文章
相关标签/搜索