java进阶4 -「jvm和gc」

一 Java内存划分

在Java运行时的数据区里,由JVM管理的内存区域分为下图几个模块:

1) 程序计数器(Program Counter Register)

程序计数器是一个比较小的内存区域, 用于指示当前线程所执行的字节码执行到了第几行, 可以理解为是当前线程的行号指示器. 字节码解释器在工作时, 会通过改变计数器的值来取下一条语句指令. 每个程序计数器只用来记录一个线程的行号, 所以它是线程私有的.

如果程序执行的是一个Java方法, 则计数器记录的是正在执行的虚拟机字节码指令地址; 如果正在执行的是一个本地native方法, 则计数器的值为undefined. 由于程序计数器指示记录当前指令地址, 所以不存在内存溢出的情况. 因此, 程序计数器也是所有JVM内存区域中唯一没有定义OutOfMemoryError的区域。

 

2) 虚拟机栈(JVM Stack)

一个线程的每个方法执行时都会创建一个栈帧(Stack Frame), 栈帧中存储的有局部变量表、操作栈、动态链接、方法出口等。当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。

局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit), 其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError;不过多数Java虚拟机都允许动态扩展虚拟机栈的大小,所以线程可以一直申请栈,直到内存不足,此时会抛出 OutOfMemoryError。每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的

 

3) 本地方法栈(Native Method Statck)

本地方法栈在作用和运行机制等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的。

 

4) 堆区(Heap)

堆区是理解Java GC机制最重要的区域,没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存. 根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。

  

5) 方法区(Method Area)

方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、static变量、编译器即时编译的代码等。

方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上 执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一,但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。

在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。

 

方法区和永久代以及元空间是什么关系呢? 方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?

1、整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

当元空间溢出时会得到如下错误:java.lang.OutOfMemoryError: MetaSpace

你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。

-XX:MetaspaceSize 调整标志定义元空间的初始大小。如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

2、元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。

 

6) 直接内存(Direct Memory)

直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存. 比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。由于直接内存受到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

 

 

二 Java内存分配机制

这里说的内存分配主要指的是在堆上的分配,Java内存分配和回收的机制概括来说就是: 分代分配,  分代回收。 对象将根据存活的时间被分为:年轻代、年老代、永久代(也就是方法区) 

1) 年轻代(Young Generation) 

对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉,这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。 

 HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。一般情况下,新创建的对象都会被分配到Eden区, 这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时(默认15岁),就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

 

在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对 象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。

  

2) 年老代(Old Generation)

对象如果在年轻代存活了足够长的时间没被清理,则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。  

如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应该少用,更应避免使用短命的大对象)

可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——"card table",所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法相当低效,故老年代用的算法是标记-整理算法.

 

3) 永久代(方法区)

永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:
1 类的所有实例都已经被回收
2 加载类的ClassLoader已经被回收
类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
 

 4) 元空间

类似于永久代,都是对JVM规范中方法区的实现, 不过它是直接使用物理内存而不占用JVM堆内存。默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大小.

为什么 JDK 8 中永久代向元空间的转换, 总结以下几点原因:

1、字符串存在永久代中,容易出现性能问题和内存溢出。

2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

 

 

三 JVM常见配置参数 

1 堆设置 

-Xms : 初始堆大小     这个值一般和Xmx相同,避免每次垃圾回收完成后jvm重新分配内存
-Xmx : 最大堆大小 
-Xmn : 新生代大小     整个堆大小 = 年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定(64M),所以增大年轻代将会减小年老代大小。
-Xss  : 每个线程的堆栈大小  JDK5.0以后每个线程堆栈大小为1M,在相同物理内存下减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,经验值在3000~5000左右
-XX:NewSize=n : 设置年轻代大小 
-XX:NewRatio=n : 设置年轻代和年老代的比值.    如为3, 表示年轻代与年老代比值为1:3, 年轻代占整个年轻代年老代和的1/4 
-XX:SurvivorRatio=n : 年轻代中Eden区与两个Survivor区的比值.    注意Survivor区有两个. 如为3, 表示Eden:Survivor=3:2, 一个Survivor区占整个年轻代的1/5 
-XX:MaxPermSize=n : 设置持久代大小
-XX:MaxTenuringThreshold=0 : 设置垃圾最大年龄. 如果将此值设置为一个较大值, 则年轻代对象会在Survivor区进行多次复制, 这样可以增加对象在年轻代的存活时间
-XX:+DisableExplicitGC   代码中手动调用System.gc()就不会生效。而有些框架中因为是使用的堆外内存,必须手动调用System.gc()来释放。如果禁用掉就会导致堆外内存使用一直增长,造成内存泄露。
 

2 收集器设置 

-XX:+UseSerialGC:设置串行收集器   小数据量.默认情况下,JDK5.0以前都是使用串行收集器
-XX:+UseParallelGC:设置并行收集器    并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等
-XX:+UseParalledlOldGC:设置并行年老代收集器    
-XX:+UseConcMarkSweepGC:设置并发收集器. 并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间.适用于应用服务器,电信领域等
 

3 并行收集器设置 

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数.并行收集线程数. 
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
 

4 并发收集器设置 

-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩和整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理.
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩.可能会影响性能,但是可以消除碎片
 

 

四 垃圾回收过程

垃圾回收是JVM中非常重要的部分,也正是由于这一机制的存在,使得Java语言不用像c++一样需要开发者自己去释放内存。而是通过垃圾回收器GC来进行内存的回收释放,下面来看下这个流程是怎么样的:

垃圾回收流程

GC主要处理的是年轻代与老年代的内存清理操作,元空间,永久代一般很少用GC。整个流程图如上图所示总的来说如下流程:

① 当一个新对象产生,需要内存空间,为该对象进行内存空间申请。

② 首先判断伊甸园区是否有有内存空间,有的话直接将新对象保存在伊甸园区。

③ 如果此时伊甸园区内存空间不足,会自动触发MinorGC,将伊甸园区不用的内存空间进行清理,清理之后判断伊甸园区内存空间是否充足,充足的话在伊甸园区分配内存空间。

④ 如果执行MinerGC发现伊甸园区不足,判断存活区,如果存活区有剩余空间,将伊甸园区部分活跃对象保存在存活区,随后继续判断伊甸园区是否充足,如果充足在伊甸园区进行分配。

⑤ 如果此时存活区也没空间了,继续判断老年区,如果老年区空间充足,则将存活区中活跃对象保存到老年代,而后存活区有空余空间,随后伊甸园区将活跃对象保存在存活区之中,在伊甸园区为新对象开辟空间。

⑥ 如果老年代满了,此时将产生MajorGC进行老年代内存清理,进行完全垃圾回收。

⑦ 如果老年代执行MajorGC发现依然无法进行对象保存,此时会进行OOM异常(OutOfMemoryError)。

上面流程就是整个垃圾回收机制流程,总的来说,新创建的对象一般都会在伊甸园区生成,除非这个创建对象太大,那有可能直接在老年区生成。

 

程序中主动调用System.gc()强制执行的GC为full GC

 

 

五 垃圾回收器类型

(1) Serial 收集器
新生代收集器,使用停止-复制算法,使用一个线程进行GC,串行,其它工作线程暂停。
 
(2) ParNew 收集器
新生代收集器,使用停止-复制算法,Serial收集器的多线程版,用多个线程进行GC,并行,其它工作线程暂停,缩短垃圾收集时间。
 
(3) Parallel Scavenge 收集器
新生代收集器,使用停止-复制算法.这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适合用户交互,提高用户体验)
 
(4) Serial Old收集器
老年代收集器,单线程收集器,串行,使用标记-整理算法,使用单线程进行GC,其它工作线程暂停.
 
(5) Parallel Old收集器
老年代收集器,多线程并行,多线程机制与Parallel Scavenge差不多,使用标记-整理算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后,与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果
 
(6) CMS(Concurrent Mark Sweep)收集器
老年代收集器,致力于获取最短回收停顿时间,使用标记-清除算法. 优点是并发收集(用户线程可以和GC线程同时工作),停顿小。由于并发收集器不对内存空间进行压缩整理,所以运行一段时间会产生碎片,使得运行效率降低。-XX:CMSFullGCsBeforeCompaction 这个可以设置运行多少次gc后对内存空间进行压缩整理。建议使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收。  
 
(7) G1收集器
新生代收集器 + 老年代收集器
在JDK1.7中正式发布。不同于其他的分代回收算法,G1将堆空间划分成了互相独立的区块。每块既有可能属于O区,也就可能是Y区,且每类区域空间可以是不连续的(对比CMS的O区和Y区都必须是连续的)。这种将O区划分成多块的理念源于:当并发后台线程寻找可回收的对象时,有些区块可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程,但可以用相对较少的时间优先回收包含垃圾最多区块。这就是为什么G1命名为Garbage First的原因:第一时间处理垃圾最多的区块
 
 

平时工作中大多数系统都使用CMS、即使静默升级到JDK7默认仍然采用CMS、那么G1相对于CMS的区别在:

1. CMS是老年代的收集器需要配合新生代收集器一起使用;  G1收集器收集范围为老年代+新生代,不需要结合其他收集器

2. CMS收集器以小的停顿时间为目标的收集器;G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象

3. CMS收集器是使用"标记-清除"算法进行的垃圾回收,容易产生内存碎片; G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片

4. G1在压缩空间方面有优势

5. G1的Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活

 

就目前而言、CMS还是默认首选的GC策略、可能在以下场景下G1更适合:

  1. 服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
  2. 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
  3. 想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象
 
总结: 
新生代:Serial收集器、ParNew收集器、Parallel Scavenge 收集器
老年代:Serial Old收集器、Parallel Old收集器、CMS收集器、 G1 收集器(跨新生代和老年代)
 
 
 

六 聊一下CMS垃圾收集器

CMS全称Concurrent Mark Sweep, 是一种并发标记清除算法。它并发执行与用户程序,减少垃圾收集时程序暂停的时间。(并发收集、减少停顿)

1. 初始标记:只标记GC Roots能直接关联的对象,速度快,与用户线程共享运行,不需要Stop The World。

2. 并发标记:从GC Roots开始递归的标记对象图,与用户线程一起工作,需要部分STW阶段。

3. 最终标记:修正在并发标记期间并发修改导致的错误标记,需要STW。

4. 并发清除:与用户线程一起工作,清除被标记的对象,不需要STW。

5. 并发重置:与用户线程一起工作,为下一次GC做准备,不需要STW

 

面试官: 说说并发标记和最终标记的区别?


并发标记阶段与用户线程一起运行,在标记过程中对象图可能被修改,会产生”脏标记“。需要最终标记修正。最终标记阶段需要Stop The World,去修正并发标记期间的”脏标记“,确保正确的标记对象。

如果没有最终标记阶段,并发标记的”脏标记“会导致非垃圾对象被清除,或者垃圾对象未被清除。所以,最终标记阶段是CMS算法关键,它通过STW去修正并发阶段的错误”脏标记“,使得CMS并发标记清除算法成为可能。

 

面试官: CMS 收集器的优缺点分别是什么?

CMS 收集器的主要优点是并发收集、低停顿,适用于对响应时间有要求的场景。 但是CMS也有一些缺点:

  1. 会产生大量空间碎片,空间利用率低。
  2. 标记和清除过程需要占用CPU资源,并发时会对程序性能产生一定影响。
  3. 并发执行时,需要处理一致性问题,会加大开发难度。
  4. 只适用于老年代回收,新生代仍需其他收集器配合。
  5. 浮动垃圾可能带来更长的GC停顿时间,不适用于需要极低停顿的场景。

所以,CMS 是一款比较经典的低停顿收集器,但也存在一定的缺陷, 后面G1成为了更好的选择。但CMS算法本身的思想仍然值得我们学习。

 

面试官: 标记清除的执行过程?

标记:收集器(Collector )从引用根节点开始遍历,标记所有被引用的对象。
一般是在对象的 Header 中记录为可达对象。标记的是引用的对象,不是垃圾!!

清除:收集器(Collector )对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header中 没有标记为可达对象,则将其回收。
注意:这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的位置。

 

 

七 聊一下g1垃圾收集器

G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。

G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。

为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作。

检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

初始标记(Initial Marking) 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

 
 

 

八 死亡对象的判断方法

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

引用计数法

给对象中添加一个引用计数器:

  • 每当有一个地方引用它,计数器就加 1;
  • 当引用失效,计数器就减 1;
  • 任何时候计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。

所谓对象之间的相互引用问题,如下面代码所示:除了对象 objAobjB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。

public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
    }
}

可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。

哪些对象可以作为 GC Roots 呢?

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象
  • JNI(Java Native Interface)引用的对象  

 

九 JVM调优总结(如何规避频繁gc)

1 程序内不要调用System.gc()或Runtime.gc()
2 中间件调用了自己的GC方法, 需要设置参数禁止这些GC
3 堆内存设置不要太小
4 不要频繁实例化 & 释放对象. 尽量保存并重用, 如使用StringBuffer()和String()等
5 最好将-Xms和-Xmx设为相同值. 为了优化GC, 最好让-Xmn值约等于-Xmx的1/3
 
6 年轻代的大小选择
响应时间优先的应用: 尽可能大,直到接近系统的最低响应时间限制。此时,年轻代收集发生的频率达到最低,同时减少了到达老年代的对象
吞吐量优先的应用: 尽可能大,可能到达Gbit的程度。因为对响应时间没有要求,gc可以并行进行,适用于8cpu以上的服务器
 
7 年老代大小选择 
响应时间优先的应用: 年老代使用并发收集器, 所以其大小需要小心设置.如果堆设置小了, 可以会造成内存碎片, 高回收频率以及应用暂停而使用传统的标记清除方式; 如果堆大了,则需要较长的收集时间.
吞吐量优先的应用: 一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代. 原因是,这样可以尽可能回收掉大部分短期对象, 减少中期的对象, 而年老代尽存放长期存活对象.
 
8 较小堆引起的碎片问题
因为年老代的并发收集器使用标记-清除算法,所以不会对堆进行压缩.当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象.但是,当堆空间较小时,运行一段时间以后,就会出现"碎片",如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记,清除方式进行回收.如果 出现"碎片",可能需要进行如下配置: 
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩. 
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

  

十 类加载机制

java虚拟机把描述类的数据从class文件加载到内存,并对数据进行验证,准备,解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的加载机制。

Class文件由类加载器装载后,在JVM中将形成一份描述class结构的元信息对象,通过该元信息对象可以获知class的结构信息: 如构造函数,属性和方法等,java允许用户借由这个class相关的元信息对象间接调用class对象的功能. 

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了: 加载、验证、准备、解析、初始化、使用、和卸载 七个阶段。其中验证、准备和解析三个部分统称为连接,这七个阶段的发生顺序如下图所示:

(1) 装载:查找和导入Class文件;
(2) 链接:把类的二进制数据合并到JRE中;
    (a)校验:检查载入Class文件数据的正确性;
    (b)准备:给类的静态变量分配存储空间;
    (c)解析:将符号引用转成直接引用;
(3) 初始化:对类的静态变量,静态代码块执行初始化操作

 

10.1 各个过程的详解

1 加载

加载时整合类加载的第一个阶段,虚拟机需要完成下面三件事情:

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

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

3 在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

整个类加载过程中,除了在加载节点用户程序可以自定义类加载器参与之外,其余所有的动作完全由虚拟机主导和控制。

 

那么方法区是什么呢?

方法区是系统分配的一个内存逻辑区域,是用来存储类型信息的(可以理解为类的描述信息),包括:

(1) 常量池

(2) 类的全限定名,即全路径名,如java/lang/Object

(3) 字段,方法信息,类变量信息

 

2 链接

(1) 验证

验证是为了确保class文件中的字节流符合虚拟机的要求,并且不会危害虚拟机的安全

(2) 准备 

jvm会在准备阶段给类变量分配内存并设置类变量的初始值。注意这里只是赋初始值,比如pubic static int value = 123 这句话中,在执行准备阶段的时候,会给value分配内存并设置初始值为0,而不是123

(3)解析 

jvm将常量池中的引用转换为直接引用的过程,对于一些还没有被用到的引用类型,其解析可能会推迟到使用的时候

 

3 初始化

类初始化是类加载的最后阶段(这里没有算上之后的使用和卸载)。在这个阶段,jvm才开始真正执行类定义中的java程序代码

(1) 在遇到new, getstatic, putstatic, invokestatic 这四条字节码指令时,如果类还没有进行过初始化,则需要先触发其初始化。常见的触发这些指令的java场景有:

    a) new关键字实例化对象

    b) 读取或设置一个类变量(static)时,final类变量除外,因为编译器已经把值放入常量池

    c) 调用一个类的静态方法时

(2) 使用java.lang.reflect包的方法对类进行调用时,如果类还没有进行过初始化,则需要先触发其初始化

(3) 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化

(4) 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类

 

 

10.2 类加载器的开放性和唯一性

1 开放性

虚拟机规范并没有指明二进制字节流要从一个Class文件获取,或者说根本没有指明从哪里获取、怎样获取。这种开放使得Java在很多领域得到充分运用,例如:

  • 从ZIP包中读取,这很常见,成为JAR,EAR,WAR格式的基础
  • 从网络中获取,最典型的应用就是Applet
  • 运行时计算生成,最典型的是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
  • 有其他文件生成,最典型的JSP应用,由JSP文件生成对应的Class类 

2 唯一性

类加载器虽然只用于实现类的加载动作,但是对于任意一个类,都需要由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性。通俗的说,JVM中两个类是否“相等”,首先就必须是同一个类加载器加载的,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要类加载器不同,那么这两个类必定是不相等的。

这里的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

关于类初始化有几个demo可以看下这篇 http://blog.csdn.net/ns_code/article/details/17845821 

 

10.3 类加载器加载规则

JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

 

10.4 双亲委派模型

1 类加载器

JVM 中内置了三个重要的 ClassLoader

  1. BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jarresources.jarcharsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
  2. ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
  3. AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。就比如说,我们可以对 Java 类的字节码( .class 文件)进行加密,加载时再利用自定义的类加载器对其解密。

除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。

每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoadernull的话,那么该类是通过 BootstrapClassLoader 加载的。

为什么 获取到 ClassLoadernull就是 BootstrapClassLoader 加载的呢? 这是因为BootstrapClassLoader 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。

3 双亲委派模型过程

类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。

ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
虚拟机中被称为 "bootstrap class loader"的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader 实例的父类加载器。

注意 ⚠️:双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些特殊需求想要打破双亲委派模型,也是可以的,后文会介绍具体的方法。

其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 MotherClassLoader 和一个FatherClassLoader 。个人觉得翻译成单亲委派模型更好一些,不过,国内既然翻译成了双亲委派模型并流传了,按照这个来也没问题,不要被误解了就好。

另外,类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。

public abstract class ClassLoader {
  ...
  // 组合
  private final ClassLoader parent;
  protected ClassLoader(ClassLoader parent) {
       this(checkCreateClassLoader(), parent);
  }
  ...
}

在面向对象编程中,有一条非常经典的设计原则:组合优于继承,多用组合少用继承。

 
 
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader 的 loadClass() 中,相关代码如下所示。
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }

            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);

                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}

每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

结合上面的源码,简单总结一下双亲委派模型的执行流程:

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
  • 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。

🌈 拓展一下:

JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同

 

4 双亲委派模型的好处

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。

如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoaderBootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。

 

5 打破双亲委派模型方法

自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

为什么是重写 loadClass() 方法打破双亲委派模型呢?双亲委派模型的执行流程已经解释了:

类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。

重写 loadClass()方法之后,我们就可以改变传统双亲委派模型的执行流程。例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。

我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。

 

6 双亲委派模型的系统实现

在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。某一个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,则成功返回;如果父类加载器无法完成加载任务,将抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载,依次类推。

注意,双亲委派模型是Java设计者推荐给开发者的类加载器的实现方式,并不是强制规定的。以Tomcat为例,每个web应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它首先尝试去加载某个类,如果找不到再代理给父类加载器,这与一般类加载器的顺序是相反的。这是java servlet规范中的推荐做法,其目的是使得web应用自己的类的优先级高于web容器提供的类。

 

 

十一 对象的创建过程

Step1 - 类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

Step2 - 分配内存

类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

内存分配的两种方式 (补充内容,需要掌握):

  • 指针碰撞:
    • 适用场合:堆内存规整(即没有内存碎片)的情况下。
    • 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
    • 使用该分配方式的 GC 收集器:Serial, ParNew
  • 空闲列表:
    • 适用场合:堆内存不规整的情况下。
    • 原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
    • 使用该分配方式的 GC 收集器:CMS

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。

内存分配并发问题(补充内容,需要掌握)

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

Step3 - 初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

Step4 - 设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

Step5 - 执行init方法

 

十二 逃逸分析

我们都知道,Java 创建的对象都是被分配到堆内存上,但不绝对。两个例外: 逃逸分析和TLAB(Thread Local Allocation Buffer)线程私有的缓存区。

 

逃逸分析,是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。

Java默认开启了逃逸分析的选项。Java的 HotSpot JIT编译器,能够在方法重载或者动态加载代码的时候对代码进行逃逸分析。

package a.b.c;
public class EscapeAnalysis {
  public static B b;
  /**
   * <p>全局变量赋值发生指针逃逸</p>
   */
  public void globalVariablePointerEscape() {
    b = new B();
  }
  /**
   * <p>方法返回引用,发生指针逃逸</p>
   * @return
   */
  public B methodPointerEscape() {
    return new B();
  }
  /**
   * <p>实例引用发生指针逃逸</p>
   */
  public void instancePassPointerEscape() {
    methodPointerEscape().printClassName(this);
  }
  class B {
    public void printClassName(EscapeAnalysis clazz) {
      System.out.println(clazz.getClass().getName());
    }
  }
}

逃逸分析研究对于 java 编译器有什么好处呢?我们知道 java 对象总是在堆中被分配的,因此 java对象的创建和回收对系统的开销是很大的。java 语言被批评的一个地方,也是认为 java 性能慢的一个原因就是 java不支持栈上分配对象。JDK6里的 Swing内存和性能消耗的瓶颈就是由于 GC 来遍历引用树并回收内存的,如果对象的数目比较多,将给 GC 带来较大的压力,也间接得影响了性能。减少临时对象在堆内分配的数量,无疑是最有效的优化方法。java应用一般是在方法体内声明了一个局部变量,按照 JVM内存分配机制,首先会在堆内存上创建类的实例(对象),然后将此对象的引用压入调用栈,继续执行,这是JVM优化前的方式。当然,我们可以采用逃逸分析对 JVM 进行优化。首先我们需要分析并且找到未逃逸的变量,将该变量类的实例化内存直接在栈里分配,无需进入堆,分配完成之后,继续调用栈内执行,最后线程执行结束,栈空间被回收,局部变量对象也被回收. 通过这种方式的优化,与优化前的方案主要区别在于对象的存储介质,优化前是在堆中,而优化后的是在栈中,从而减少了堆中临时对象的分配,从而优化性能。

 

使用逃逸分析进行性能优化(-XX:+DoEscapeAnalysis开启逃逸分析)

public void method() {
  Test test = new Test();
  //处理逻辑
  ......
  test = null;
}

这段代码,之所以可以在栈上进行内存分配,是因为没有发生指针逃逸,即是引用没有暴露出这个方法体。

 

十三 TLAB

TLAB (Thread Local Allocation Buffer,线程本地分配缓冲区)是 Java 中内存分配的一个概念,它是在 Java 堆中划分出来的针对每个线程的内存区域,专门在该区域为该线程创建的对象分配内存。它的主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。TLAB 本质上还是在 Java 堆中的,因此在 TLAB 区域的对象,也可以被其他线程访问。

如果没有启用 TLAB,多个并发执行的线程需要创建对象、申请分配内存的时候,有可能在 Java 堆的同一个位置申请,这时就需要对拟分配的内存区域进行加锁或者采用 CAS 等操作,保证这个区域只能分配给一个线程。

启用了 TLAB 之后(-XX:+UseTLAB, 默认是开启的),JVM 会针对每一个线程在 Java 堆中预留一个内存区域,在预留这个动作发生的时候,需要进行加锁或者采用 CAS 等操作进行保护,避免多个线程预留同一个区域。一旦某个区域确定划分给某个线程,之后该线程需要分配内存的时候,会优先在这片区域中申请。这个区域针对分配内存这个动作而言是该线程私有的,因此在分配的时候不用进行加锁等保护性的操作。

 

十四 kvm GC根节点的选择

java通过可达性分析来判断对象是否存活,基本思想是通过一系列称为"GC roots"的对象作为起始点,可以作为根节点的是:

(1) 虚拟机栈(栈帧中的本地变量表)中引用的对象

(2) 本地方法栈中JNI(即native方法)引用的对象

(3) 方法区中静态属性引用的对象

(4) 方法区中常量引用的对象

作为GC roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中。

我个人理解: 想下java内存模型的5块,gc主要是收集堆和持久代,恰好对应栈分配的堆对象和静态&常量所申请的持久代

 

 

 

十五 JDK监控及故障处理工具

常见jdk命令工具在 JDK 安装目录下的 bin 目录下:

  • jps (JVM Process Status): 类似 UNIX 的 ps 命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;
  • jstat(JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据;
  • jinfo (Configuration Info for Java) : 显示虚拟机配置信息;
  • jmap (Memory Map for Java) : 生成堆转储快照;
  • jstack (Stack Trace for Java) : 生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。

 

JDK 可视化分析工具
JConsole:Java 监视与管理控制台
Visual VM:多合一故障处理工具

posted @ 2015-09-23 15:35  balfish  阅读(329)  评论(1编辑  收藏  举报