《深入理解Java虚拟机》读书笔记

时间:2022-12-27 17:00:05

《深入理解Java虚拟机》读书笔记

堆分配参数:

-XX:+PrintGC 使用该参数,虚拟机启动后,只要遇到GC就会打印日志;

-XX:+UseSerialGC 配置串行回收器;

-XX:+PrintGCDeltails 可以查看详细信息,包括各个区的情况

-Xms:设置Java程序启动时初始堆的大小(主要参数)

-Xmx:设置Java程序能获得的最大堆大小(主要参数)

新生代的配置:

-Xmn: 可以设置新生代的大小,设置一个比较大的新生代会减少老年代的大小,这个设置对系统性能以及GC行为有很大的影响,新生代大小一般会设置整个堆空间的1/3到1/4左右。

备注:在实际工作中,可以直接将初始的堆大小与最大堆大小设置相等,这样的好处是可以减少程序运行时的垃圾回收次数,从而提高性能。

常见异常:

         java.lang.OutOfMemoryError.Java heap spacess---heap

                   JVM中如果98%的时间是用于GC且可用的Heap size不足2%的时候将抛出此异常信息。

                   -Xms -Xmx

         java.lang.OutOfMemoryError.PermGen space ---no heap

                   -XX:PermSize

                   -XX:MaxPermSize

         StrackOverflowError

                   Java虚拟机在运行时,调用方法时,都需要创建栈帧,当栈的空间不够时就会产生StrackOverflowError

                            -Xss

 

JVM中的参数:

         以-X开头的都是非标准的(这些参数并不保证在所有的JVM上都被实现)。

                   -Xmx

                   -Xmn

                   -Xms

         以-XX开头的都是不稳定的并且不推荐在生产环境中使用,这些参数的改动也不会发布通知。

                   -XX:Permsize

                   -XX:MaxPermsize

                  

JVM选项的说明:

         布尔型参数选项:-XX+表示打开,-XX-表示关闭。(-XX.+PrintGCDetails)

         数字型参数选项通过-XX=设定。

         字符型参数选项通过-XX=设定,通常用来指定一个文件、路径或者一个命令列表

         -Xms:初始堆大小

         -Xmx:最大堆大小

         -XX:NewSize=n 设置年轻代的大小

         -XX:NewRatio=n 设置年轻代和老年代的比值。如3则表示年轻代和老年代的比值为1:3

         -XX:SurvivorRatio=n 年轻代中Eden与两个Survivor区的比值。如为3,则表示Eden:Survivor=3:2。一个Survivor占整个年轻代的1/5.

         -XX:MaxPermSize=n 设置永久代的大小

 

JVM GC

         串行回收器(Serial Collector)

         并行回收器(Parallel Collector)

         并行合并回收器(Parallel Compacting Collection)

         并发标记清除回收器(Concurrent Mark-Sweep Collector)应用广泛

         G1垃圾收集器(Jdk7+)未来的主流

         并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不停止应用执行。所以,并发算法适用于交互性高的一些程序。经过观察,并发算法会减少年轻代的大小,其实就是使用了一个大的年老代,这反过来跟并行算法相比吞吐量相对较低。

        

JVM垃圾回收时间:

         当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的是,年轻代满是指Eden代满,Survivor满不会引发GC

         当老年代满时会引发Full GC,Full GC将会同时回收年轻代、年老代

         当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载

 

常见问题:

          Q:为什么崩溃前垃圾回收的时间越来越长?

   A:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据

   Q:为什么Full GC的次数越来越多?

   A:因为内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收

   Q:为什么年老代占用的内存越来越大?

   A:因为年轻代的内存无法被回收,越来越多地被Copy到年老代

   Q:什么是串行回收和并行回收?

   A:串行回收是指在同一时间段内只允许一件事情发生,即当多个CPU可用时,也只能有一个CPU用于执行垃圾回收操作,并且在执行垃圾回收时,程序中的工作线程将会被暂停。当垃圾回收工作完成后才会恢复之前被暂停的工作线程,这就是串行回收。

   并行回收是指可以运用多个CPU同时执行垃圾回收,因此提升了应用程序的吞吐量,不过并行垃圾回收仍然使用了“Stop-the-World”机制和复制算法。

   Q:什么是并发和“Stop-the-World”机制?

   当通过“Stop-the-World”机制回收垃圾时,垃圾收集器会在内存回收的过程中暂停程序中的所有工作线程,直至完成内存回收工作后才会恢复之前被暂停的工作线程。

   并发回收是指在同一时间段内,应用程序的工作线程和垃圾回收线程将会同时运行或者交叉运行。

Q:什么是快速分配策略?

A:基于线程安全的考虑,如果一个类在分配内存之前已经成功完成装载步骤之后,JVM就会优先选择在TLAB(Thread Local Allocation本地线程分配缓冲区)中为对象实例分配内存空间,TLAB在Java堆区中是一块线程私有的区域,包含在Eden空间内,除了可以避免一系列的非线程安全问题外,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。

Q:什么是逃逸分析和栈上分配?

A:Java堆区已经不再是对象分配内存的唯一选择,如果希望降低GC的回收平率和提升GC的回收效率,那么则可以使用堆外存储技术,目前最常见的堆外存储技术就是利用逃逸分析技术筛选出未发生逃逸的对象,然后避开堆区而直接选择在栈帧中分配内存空间。逃逸分析是JVM在执行性能优化之前的一种分析技术,它的具体目标就是分析出对象的作用域。即当一个对象被定义在方法体内部之后,它的受访权限仅限于方法体内,一旦其引用被外部成员引用后,这个对象就发生了逃逸。反之如果定义在方法体内部的对象并没有被任何外部成员引用时,JVM就会为其在栈帧中分配内存空间。

HeapOutOfMemory
当堆上分配的对象大于指定堆的最大值时,抛出该错。
可以使用-XX:+HeapDumpOnOutOfMemoryError 查看内存快照进行分析

MethodAreaOutOfMemory
方法区内存不足,存放类信息,常量,静态变量,即时编译后的代码,检查这几个信息是否有异常大多的原因是因为动态产生过多的类。

ConstantPoolOutOfMemory
常量池溢出,查看是否intern使用不当

DirectMemoryOutOfMemory
本机直接内存溢出,容量可通过-XX:MaxDirectMemorySize指定,如果不指定,默认和堆最大值相同。这个溢出发生在系统进行直接内存分配。例如:unsafe.allocateMemory()
特征为:OOM后发现Dump问价你很小,程序中直接或间接使用了NIO

 

StackOutOfMemory
扩展栈时无法获取足够的内存空间,在创建线程时
解决方法之一:减少最大堆

*
栈深度大于虚拟机所允许的深度,经常是由于死循环的递归调用

第三章:垃圾收集器和内存分配策略

1、JVM垃圾回收区域:

程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈操作。每一个栈帧中分配多少内存基本是在类结构确定下来时就已知的,因此这三个区域的内存分配和回收都具有确定性,在这三个区域内不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而java堆和方法区不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也不一样,我们只有在程序运行时才能知道会创建哪些对象,这部分内存的分配和回收是动态的,垃圾收集器所关注的是这部分内存。

2、引用计数算法:

         给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。Java语言并没有选用引用计数器算法来管理内存,其中最主要的原因就是它很难解决对象之间相互循环引用的问题。

3、根搜索算法:

         Java使用根搜索算法(GCRoot Tracing)判断对象是否存活。该算法的基本思路是:通过一系列的名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可引用的,所以它们将会被判定为可回收对象。在Java语言中可作为GC Roots的对象包括下面几种:

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

         方法区中的类静态属性引用的对象;

         运行时常量池中的对象引用;

         方法区中的常量引用的对象;

         本地方法栈中JNI(即一般所说的native方法)引用的对象。

         对象引用:无论是通过引用计数器算法判断对象的引用数量,还是通过根搜索算法判断对象的引用链是否可达,判断对象是否存活都与“引用”有关。JDK1.2之后,Java将引用分为四种:强引用,软引用,弱引用,虚引用。这四种引用强度依次逐渐减弱。

4、回收方法区:

         永久代(方法区)的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常相似。以常量池中的字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象叫做“abc”的,也没有其他地方引用了这个字面量,如果这个时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。判断一个类是否是“无用的类”需要满足下面三个条件:

         该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;

         加载该类的ClassLoader已经被回收;

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

5、垃圾回收算法之标记-清除算法:

         这是最基础的收集算法。分为两个阶段,标记和清除。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

         缺点:效率低下;

         空间问题,标记清除之后会产生大量不连续的内存碎片。

6、垃圾回收算法之复制算法:

         该算法将内存按容量划分为大小相等的两块区域,每次只使用其中的一块。当一块内存用完了,就将其中还存活的对象复制到另一块区域上,然后再将已经使用过的内存区域一次性清理掉。解决了内存碎片的问题。

         说明:现在的商业虚拟机都是采用这种收集算法来回收新生代,IBM的专门研究表明,新生代中的对象98%是朝生夕死的,所有并不需要按照1:1的比例来划分内存空间,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时候,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚刚用过的Survivor的空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1:1。

                   缺点:在对象存活率较高时需要执行较多的复制操作,效率将会变低,老年代不能使用这种算法。

7、垃圾回收算法之标记-整理算法:

         根据老年代的特点,有人提出了“标记-整理”算法。其中标记过程和“标记-清除”算法一样,但是后续步骤不是直接对可回收的对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

8、垃圾回收算法之分代收集算法:

         当前商业虚拟机的垃圾回收都是采用“分代收集”算法,根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量对象存活,那就选择复制算法;而老年代中因为对象存活率较高,没有额外空间对它进行分配担保,就必须采用“标记-清除”或者“标记-整理”算法来进行回收。

9、垃圾收集器之Serial收集器:

         Serial收集器是最基本、历史最悠久的收集器。该收集器是一个单线程的收集器,即在进行垃圾收集时候,必须暂停其他所有的工作线程,直到它收集结束。到目前为止,它依然是虚拟机运行在Client模式下的默认新生代收集器。优点是简单而高效。目前停顿时间可以控制在几十毫秒最多一百多毫秒以内。

10、垃圾收集器之G1收集器:

         G1(Garbagefirst)收集器是当前收集器技术发展的最前沿成果。G1收集器是垃圾收集器理论进一步发展的产物,它与CMS收集器相比有两个显著的改进:一是G1收集器是基于“标记-整理”算法实现的收集器;二是它可以非常精准地控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

         G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力的避免全区域的垃圾收集,G1收集器将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是G1名称的由来)。

垃圾收集器比较

垃圾收集器

算法

方式

堆区域

机制

Serial收集器

复制算法

串行

新生代

Stop-the-World

Serial Old收集器

标记-压缩算法

串行

老年代

Stop-the-World

ParNew收集器

复制算法

并行

新生代

Stop-the-World

Parallel收集器

复制算法

并行

新生代

Stop-the-World

Parallel Old收集

标记-压缩算法

并行

老年代

Stop-the-World

CMS收集器

标记-清除算法

并行

老年代

Stop-the-World/并发

G1收集器

 

 

整个堆区

 

 

内存选项配置

选项

描述

备注

-Xms

设置Java堆区的初始内存

当可用的Java堆区内存小于40%时,JVM就会将内存调整到选项-Xmx所允许的最大值

-Xmx

设置Java堆区的最大内存

当可用的Java堆区内存大于70%时,JVM就会将内存调整到选项-Xms所指定的初始值

-Xmn

设置新生代(YoungGen)的内存

-Xmn的内存大小为Eden+2个Surivivor空间的值,官方建议配置为整个堆的3/8

-XX:NewSize

设置新生代(YoungGen)的初始内存

和选项-Xmn等价,但是推荐使用-Xmn,相当于一次性设定了NewSize/Max-NewSize的内存大小

-XX:MaxNewSize

设置新生代(YoungGen)的最大内存

 

-XX:NewRatio

新生代(Eden+2个Surivivor空间)与老年代的比值,不包括永久代

选项-XX:NewRatio=4时,表示新生代与老年代所占的比值为1:4。如果已经设置了选项-Xmn,则无需设置该选项

-XX:PermSize

设置方法区的初始内存

 

-XX:MaxPermSize

设置方法区的最大内存

 

-XX:SurivivorRatio

Eden空间与2个Surivivor空间的比值大小

Eden空间和另外2个Surivivor空间缺省所占的比值为8:1

-XX:TLABWasteTargetPercent

设置TLAB空间所占用Eden空间的百分比大小

 

 

GC组合配置

GC组合

Minor GC

Full GC

描述

-XX:+UserSerialGC

Serial收集器串行回收

Serial Old收集器串行回收

选项-XX:UseSerialGC

可以手动指定使用Serial收集器+Serial Old收集器组合执行内存回收

-XX:+UseParNewGC

ParNew收集器并行回收

Serial Old收集器串行回收

选项-XX:UseParNewGC可以手动指定使用ParNew收集器+Seral Old收集器组合执行内存回收

-XX:+UseParallelGC

Parallel收集器并行回收

Serial Old收集器串行回收

通过-XX:+UseParallelGC可以手动指定使用Parallel收集器和Serial Old收集器组合执行内存回收

-XX:+UseParallelOldGC

Parallel收集器并行回收

Parallel Old收集器并行回收

通过-XX:+UseParallelOldGC可以手动指定使用Parallel收集器和Parallel Old收集器组合执行内存回收

-XX:+UseConcMarkSweepGC

ParNew收集器并行回收

缺省使用CMS收集器并发执行回收,备用采用Serial Old收集器串行回收

使用-XX:+UseConcMarkSweepGC可以手动指定使用ParNew收集器+CMS收集器+Serial Old收集器组合执行内存回收。优先使用ParNew+CMS组合,当出现ConcurrentNode Failure或者Promotion Failed时,则采用ParNew+Serial Old组合。

-XX:+UseConcMarkSweepGC

-XX:+UseParNewGC

Serial 收集器串行回收

-XX:+UseG1GC

G1收集器并发、并行的内存回收

 

 

第七章:虚拟机类加载机制

《深入理解Java虚拟机》读书笔记

1、虚拟机的类加载机制:

虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

2、类被加载到虚拟机经过的7个阶段:

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

3、何时进行类的初始化?

虚拟机规范严格规定了有且只有四种情况必须立即对类进行初始化(而加载、校验、准备和解析自然需要在初始化之前开始):

1)遇到new、getstatic、putstatic、或者这4条字节码指令时,invokestatic如果类没有进行初始化,则需要先触发其初始化,生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或者设置一个类的静态字段(被final修饰、已经在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候;

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

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

4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发其父类的初始化而不会触发子类的初始化。

4、加载:

在加载阶段,虚拟机需要完成以下三件事情:

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

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

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

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。然后在java堆中实例化一个java.lang.Class对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口,加载阶段和连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但是这些夹在加载阶段之中的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

5、验证:

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

虚拟机验证过程的四个阶段:

文件格式验证;

元数据验证;

字节码验证;

符号引用验证。

6、准备:

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念,首先是这时候进行内存分配的仅包含类变量(被static修饰的变量),而不包含实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:public static int value=123;那么变量value在准备阶段过后的初始值为0,而不是123。

7、解析:

解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关。引用的目标并不一定已经加载到内存中。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。

7、类与类加载器:

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所修要的类。实现这个动作的代码模块被称为“类加载器”。

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性。

8、双亲委派模型:

绝大部分Java程序都会使用到以下三种系统提供的类加载器:

1)启动类加载器(BootStrap ClassLoader):在HotSpot虚拟机中这个类加载器由C++语言实现,是虚拟机自身的一部分。它负责将存放在<JAVA_HOME>\lib目录中的,并且是虚拟机标识的(仅按照文件名识别,如rt.jar,名称不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。

2)扩展类加载器(Extention ClassLoader):这个加载器有sun.misc.Launcher$ExtClassLoader实现。它负责加载<JAVA_HOME>/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3)应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。如果应用程序没有指定自定义的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这三个类加载器互相配合进行加载的。

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需加载的类)时,子加载器才会自己尝试去加载。

        

当一个Java程序响应很慢时如何查找问题

《深入理解Java虚拟机》读书笔记