Java核心要点提炼之JVM篇

一、JVM管理的内存

1.线程私有数据区域

线程私有数据区域生命周期与线程相同, 依赖用户线程的启动而创建,结束而销毁。

1)程序计数器。 作用是当前线程所执行字节码的行号指示器,在每次指令执行后自增, 维护下一个将要执行指令的地址。Native方法该计数器值为undefined。

JVM中的并发是通过线程切换并分配时间片执行来实现的. 在任何时刻, 一个处理器内核只会执行一条线程中的指令.为了线程切换后能恢复到正确的执行位置, 每条线程都需要有一个独立的程序计数器。

2)虚拟机栈。作用是执行java方法。每个方法被执行时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息. 每个方法被调用至返回的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

3)本地方法栈。作用是执行Native方法, 如果一个VM实现使用C-linkage模型来支持Native调用, 那么该栈将会是一个C栈, 但HotSpot VM直接就把本地方法栈和虚拟机栈合二为一。

 

2.线程共享数据区域

随虚拟机的启动而创建关闭而销毁.

1)堆。作用是几乎所有对象实例和数组都要在堆上分配, 是VM管理的最大一块内存, 也是垃圾收集器的主要活动区域.

Java堆从GC的角度还可以细分为: 新生代(Eden区、From Survivor区和To Survivor区)和老年代; 从内存分配的角度来看, 还可以划分出多个线程私有的分配缓冲区(TLAB)。

进一步划分的目的是为了更好地回收内存和更快地分配内存。

2)方法区。用于存储被JVM加载的类信息、常量、静态变量等数据。

HotSpot VM GC分代收集使用Java堆的永久代来实现方法区,这样垃圾收集器就可以像管理Java堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。

运行时常量池。用于存放编译期生成的各种字面量和符号引用, 这部分内容会存放到方法区的运行时常量池中。运行期间也可能将新的常量放入池中, 如String的intern()方法。

在1.7的HotSpot已经将原本放在永久代的字符串常量池和类的静态变量(class statics)移出到了java heap;符号引用(Symbols)转移到了native heap;

 在1.8中, 永久区被元数据区Metaspace取代,元数据区和永久代都是对JVM规范中方法区的实现。元数据区并不在虚拟机中,使用的是本地内存。 默认情况下,元数据区的大小仅受本地内存限制。可以通过-XX:MetaspaceSize,指定初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

3)直接内存。在JDK 1.4引入的NIO, 它可以使用Native函数库直接分配堆外内存, 这样就避免了在Java堆和Native堆中来回复制数据, 因此在一些场景中可以显著提高性能。内存的分配受到本机总内存大小及处理器寻址空间的限制。不属于JVM管理的内存。

 

二、HotSpot中的对象

1.对象存储布局

HotSpot VM内, 对象在内存中的存储布局可以分为三块区域:对象头、实例数据和对齐填充。

1)对象头。包括两部分: 一部分是类型指针, 确定该对象属于哪个类实例;

一部分用于存储对象自身的运行时数据: HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等, 这部分数据的长度在32位和64位的VM中分别为32bit和64bit, 官方称之为“Mark Word”。

如果对象是一个数组, 那在对象头中还必须记录数组长度。

2)实例数据。是对象真正存储的有效信息, 是在代码里所定义的各种类型的字段内容(无论是从父类继承下来的, 还是在子类中定义的都需要记录下来)。

这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。 HotSpot默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans, 相同宽度的字段总是被分配到一起, 在满足这个前提条件下, 在父类中定义的变量会出现在子类之前. 如果CompactFields参数值为true(默认), 那子类中较窄的变量也可能会插入到父类变量的空隙中。

3)对齐填充。 仅起到占位符的作用, 因为HotSpot自动内存管理系统要求对象的大小必须是8字节的整数倍。

 

2.对象新建

new一个Java Object(包括数组和Class对象), 在JVM会发生如下步骤:

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

2)为新生对象分配内存。VM将为新生对象分配内存, VM采用指针碰撞(内存规整: Serial、ParNew等有内存压缩整理功能的收集器)或空闲链表(内存不规整: CMS这种基于Mark-Sweep算法的收集器)方式将一块确定大小的内存从Java堆中划分出来。

划分可用空间和内存分配的并发问题解决。除了考虑如何划分可用空间外, 由于在VM上创建对象的行为非常频繁, 因此需要考虑内存分配的并发问题。

解决方案有两个: 1.对分配内存空间的动作进行同步 -采用 CAS配上失败重试方式保证更新操作的原子性;2.把内存分配的动作按照线程划分在不同的空间之中进行 -每个线程在Java堆中预先分配一小块内存, 称为本地线程分配缓冲TLAB, 各线程首先在TLAB上分配, 只有TLAB用完, 分配新的TLAB时才需要同步锁定。

3)内存空间初始化。将分配到的内存空间初始化为零值(不包括对象头, 且如果使用TLAB这一个工作也可以提前至TLAB分配时进行)。 这一步保证了对象的实例字段可以不赋初始值就直接使用。

4)对对象进行必要的设置。如该对象所属的类实例、如何能访问到类的元数据信息、对象的哈希码、对象的GC分代年龄等, 这部分息放在对象头中。

5)执行对象的<init>方法。在虚拟机角度一个新对象已经产生, 但在Java视角对象的创建才刚刚开始。 所以new指令之后一般会接着执行<init>方法, 把对象按照程序进行初始化, 这样一个对象才算完全产生出来。

 

3.对象定位

Java程序通过栈上的reference来操作堆上的具体对象. 主流的有句柄和直接指针两种方式去定位和访问堆上的对象.

1)句柄。 Java堆中将会划分出一块内存来作为句柄池, reference中存储对象的句柄地址, 而句柄中包含了对象实例数据与类型数据的具体各自的地址信息。

2)直接指针(HotSpot使用)。reference中存储的直接就是Java堆对象地址, 对象中必须放置访问类型数据的相关信息,。

使用句柄来访问的好处是reference中存储的是稳定句柄地址, 在对象被移动(垃圾收集时移动)时只会改变句柄中的实例数据指针,而reference本身不变。

使用直接指针的好处就是速度更快, 它节省了一次指针定位的时间开销。

 

三、垃圾回收机制

stop-the-world 。它会在任何一种GC算法中发生。当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。GC优化很多时候就是减少stop-the-world 的发生。JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉。

1.垃圾回收算法

垃圾回收一般要做2件基本的事情:1.发现无用信息对象; 2.回收被无用对象占用的内存空间。

发现无用对象算法

1)引用计数法。堆中每个对象实例都有一个引用计数。当一个对象被创建时,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1,但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

优点:引用计数收集器可以很快地执行,穿插在程序运行的过程当中,对程序需要不被长时间打断的情形下十分实用。

缺点:无法检测出循环引用,如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。

2)根搜索算法。程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被是无用的节点。java中可作为GC Root的对象有:1.虚拟机栈和本地方法栈中引用的对象2.方法区中静态属性和常量引用的对象。

回收无用对象算法

1)标记-清除算法。从根集合进行扫描,对存活的对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,在存活对象比较多的情况下极为高效,但会造成内存碎片。

2)标记-整理算法。采用标记-清除算法一样的方式进行对象的标记,在回收后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。成本更高,但解决了内存碎片的问题。在基于此算法的收集器的实现中,一般使用句柄进行对象定位。

3)复制算法。为了克服句柄的开销和解决堆内存碎片的问题。把堆分成一个对象面和多个空闲面, 当对象面满了,将每个活动对象复制到空闲面,空闲面变成对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

 

2.按代的垃圾回收机制

不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

1)年轻代:(Young Generation)。1.新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。2.年轻代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,如此往复。3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,对新生代、老年代都进行回收4.新生代发生的GC也叫做Minor GC,发生频率比较高(不一定等Eden区满了才触发)。

2)老年代(Old Generation)。1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。老年代中存放的都是一些生命周期较长的对象。2.内存比新生代也大很多(默认所占空间比例是1:2),当老年代内存满时触发Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

老年代的对象需要引用新生代的对象?

老年代中存在一个 card table ,所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,只需要查询 card table 来决定是否可以被回收,而不用查询整个老年代。

3)持久代(Permanent generation)也称之为方法区(Method area)。用于存储被JVM加载的类信息、常量、静态变量等数据。在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:1、所有实例被回收2、加载该类的ClassLoader 被回收3、Class 对象无法通过任何途径访问(包括反射)。

 

3.垃圾回收器

对于年轻代和老年代,JVM可使用多种不同代不同垃圾回收器,其中两个回收器之间有连线表示这两个回收器可以同时使用。

而这些垃圾回收器又分为串行回收方式(Stop The World,一个垃圾收集线程)、并行回收方式(Stop The World,多个线程执行垃圾回收,适合于吞吐量优先的系统)和并发回收方式执行(部分Stop The World,系统和垃圾回收一起执行,适合于响应优先的系统),分别运用于不同的场景。如下图所示

年经代垃圾收集器

1)Serial收集器。属于串行收集器。其运行示意图如下

串行收集器是当JVM需要进行垃圾回收的时候,需要中断所有的用户线程,知道它回收结束为止。适合低端机器,是Client模式下的默认收集器,对CPU和内存的消耗不高,适合用户交互比较少,后台任务较多的系统。Serial收集器默认新旧生代的回收器搭配为Serial+ SerialOld。

2)ParNew收集器。多线程版本的Serial收集器,其运行示意图如下

同样有Stop The World的问题,他是多CPU模式下的首选回收器,也是Server模式下的默认收集器。

3)ParallelScavenge。ParallelScavenge又被称为是吞吐量优先的收集器,新生代收集器,复制算法,并行收集,吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用1分钟,那么吞吐量就是99%。 

老年代收集器

1)SerialOld。SerialOld是旧生代Client模式下的默认收集器,单线程执行;在JDK1.6之前也是ParallelScvenge回收新生代模式下旧生代的默认收集器,同时也是并发收集器CMS回收失败后的备用收集器。其运行示意图如下

2)CMS。CMS又称响应时间优先的回收器,使用并发回收垃圾,使用标记-清除算法,他的运行示意图如下

CMS模式主要分为5个过程:初始标记、并发标记、准备清理、重新标记、并发清除。

CMS工作的基本阶段分为: 

1.初始化标记:第一次暂停,初始化标记,标记Old代所有的GC root可达对象及被Young代中引用的对象。作为并发标记阶段的GC Root。

2.并发标记:和应用程序线程同时运行,这个阶段会遍历整个老年代从“初始化标记”阶段找到的GC Root开始标记所有存活的对象,并发标记的同时应用程序会改变一些对象的引用,并不能对老年代所有存活对象进行标记。

在进行并发标记时minor GC 也可能会同时进行,这个时候很容易造成旧生代对象引用关系改变,CMS 为了应对这样的并发现象,使用一个Mod Union Table 来进行记录每次minor GC 后修改了的Card 的信息。这也是ParallelScavenge不能和CMS一起使用的原因。

3.准备清理:并发的标记并发标记阶段被修改的对象为脏页。

4.重新标记:第二次暂停,检查脏页的对象,重新标记并发标记阶段遗漏的对象如并发标记阶段被修改的对象 。重新标记阶段主要耗时在年轻代的扫描,标记被年轻代中存活对象引用的对象,故保证重新标记阶段之前进行一次年轻代的垃圾回收,使年轻代是足够干净。

5.并发清理:运行过程中清理,遍历老年代,释放无用对象占用的空间。 

6.并发重置:此次CMS结束后,重设CMS状态等待下次CMS的触发 。

由于CMS使用标记-清除算法会产生碎片,可能会导致回收过后的连续空间仍然不能容纳新生代移动过来或者新创建的大资源,因此会导致CMS回收失败,进而触发另外一次FULL GC,采用SerialOld进行二次回收。

3)ParallelOld。ParallelOld是老生代并行收集器的一种,使用标记整理算法、吞吐量优先的收集器。这个收集器是JDK1.6之后引入的收集器,早期没有ParallelOld之前,吞吐量优先的收集器老年代只能使用串行回收收集器,大大的拖累了吞吐量优先的性能,自从JDK1.6之后,才能真正做到较高效率的吞吐量优先。其运行示意图如下

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)。

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)。

jdk1.9 默认垃圾收集器G1。

分代垃圾收集器特点

以上几组垃圾收集器组合共同点:1.年轻代、老年代是独立且连续的内存块;2.年轻代收集使用单eden、双survivor进行复制算法;3.老年代收集必须扫描整个老年代区域;4.都是以尽可能少而快地执行GC为设计原则。

 

4.GarbageFirst(G1 )

既可以回收新生代也可以回收旧生代,通过重新划分内存区域,同时注重吞吐量和响应时间。开启选项:-XX:+UseG1GC。

1)G1收集与以上几组分代收集器不同点。1.G1的设计原则是”首先收集尽可能多的垃圾(Garbage First)“。G1并不会等内存耗尽或者快耗尽的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;(启发式算法是相对于最优化算法提出的。定义:一个基于直观或经验构造的算法,在可接受的花费(指计算时间和空间)下给出待解决组合优化问题每一个实例的一个可行解计。现阶段,启发式算法以仿自然体算法为主,主要有蚁群算法、模拟退火法、神经网络等。)2.G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。每个分区大小相等,因此G1天然就是一种局部压缩方案;3.G1也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别。G1只有逻辑上的分代概念,每个分区都可能随G1的运行在不同代之间切换;4.G1的收集都是STW的,年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能同时包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

G1的收集过程:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制。

2) G1的内存模型。

1.分区 Region。G1采用分区(Region)的思想,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存;在堆的使用上,要求逻辑上连续,并不要求对象的存储一定是物理上连续的;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。2.卡片 Card。在每个分区内部又被分成了若干个大小为512字节(Byte)卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象。每次对内存的回收,都是对指定分区的卡片进行处理。3.堆 Heap。G1可以通过-Xms/-Xmx来指定堆空间大小。当发生年轻代收集或混合收集时,通过计算GC与应用的耗费时间比,自动调整堆空间大小。如果GC频率太高,则通过增加堆空间,来减少GC频率,相应地GC占用的时间也随之降低;目标参数-XX:GCTimeRatio即为GC与应用的耗费时间比,G1默认为9,而CMS默认为99,因为CMS的设计原则是耗费在GC上的时间尽可能的少。另外,当空间不足,如对象空间分配或转移失败时,G1会首先尝试增加堆空间,如果扩容失败,则发起担保的Full GC。Full GC后,也会调整堆空间。

3)G1的分代模型。

1.分代 Generation。分代垃圾收集可以根据不同对象生命周期不同,更合理的进行垃圾回收。将关注点集中在最近被分配的对象上,而无需整堆扫描,避免长命对象的拷贝,同时独立收集有助于降低响应时间。分区使得内存分配不再要求紧凑的内存空间,但G1依然使用了分代的思想。将内存在逻辑上划分为年轻代和老年代,年轻代又划分为Eden空间和Survivor空间。但年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲分区加入到年轻代空间。整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认整堆60%)之间动态变化,且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以及分区的已记忆集合(RSet)计算得到。G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义。2.本地分配缓冲区 Local allocation buffer (Lab)。由于分区的思想,每个线程都可以”认领”某个分区用于线程本地的内存分配。每个应用线程和GC线程独立的使用分区,减少同步时间,提升GC效率,这个分区称为本地分配缓冲区(Lab)。应用线程可以独占一个本地缓冲区(TLAB)来创建的对象,而大部分都会落入Eden区域(巨型对象或分配失败除外),因此TLAB的分区属于Eden空间;每次垃圾收集时,每个GC线程同样可以独占一个本地缓冲区(GCLAB)用来转移对象,每次回收会将对象复制到Suvivor空间或老年代空间;对于从Eden/Survivor空间晋升到Survivor/老年代空间的对象,同样有GC独占的本地缓冲区进行操作,该部分称为晋升本地缓冲区(PLAB)。

4)分区模型。

G1对内存的使用以分区(Region)为单位,而对对象的分配则以卡片(Card)为单位。这些分区又有部分可以细分为:1.巨型对象分区 Humongous Region。一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)。G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型,相邻连续分区被标记为连续巨型。由于无法享受Lab带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,应用程序应避免生成巨型对象。2.已记忆集合 Remember Set (RSet)。在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,引用源自本分区的对象,无需RSet也可以无遗漏的得到引用关系;同时,G1 GC每次都会对年轻代进行整体收集,引用源自年轻代的对象,也不需要在RSet中记录。只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区。

 

3.Per Region Table (PRT)。RSet在内部使用Per Region Table(PRT)记录分区的引用情况。由于RSet的记录要占用分区的空间,如果一个分区非常”受欢迎”,那么RSet占用的空间会上升,从而降低分区的可用空间。G1应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用:a.稀少:直接记录引用对象的卡片索引b.细粒度:记录引用对象的分区索引c.粗粒度:只记录引用情况,每个分区对应一个比特位。由上可知,粗粒度的PRT只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的。

4.收集集合 CSet

收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。由上述可知,G1的收集都是根据CSet进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的触发条件。

 

5.JVM调优-堆参数总结

与 Java 应用程序堆内存相关的 JVM 参数有:

-Xms:设置 Java 应用程序启动时的初始堆大小;

-Xmn:设置 Java 应用程序能获得的最小堆大小;

-Xmx:设置 Java 应用程序能获得的最大堆大小;

-Xss:设置线程栈的大小;

-XX:MinHeapFreeRatio:设置堆空间最小空闲比例。当堆空间的空闲内存小于这个数值时,JVM 便会扩展堆空间;

-XX:MaxHeapFreeRatio:设置堆空间的最大空闲比例。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆;

-XX:NewSize:设置新生代的大小;

-XX:NewRatio:设置老年代与新生代的比例,它等于老年代大小除以新生代大小;

-XX:SurvivorRatio:新生代中 eden 区与 survivor 区的比例;

-XX:MaxPermSize:设置最大的持久区大小;

-XX:TargetSurvivorRatio: 设置 survivor 区的可使用率。当 survivor 区的空间使用率达到这个数值时,会将对象送入老年代。

 

6.类加载机制

1)什么是类的加载。指的是将类的.class文件中的二进制数据读入到内存中,将其放在方法区内,然后在堆中创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终生成Class对象,封装了类在方法区内的数据结构,并且提供了访问方法区内的数据结构的接口。

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

加载.class文件的方式:从本地系统中直接加载; 通过网络下载.class文件; 从zip,jar等归档文件中加载.class文件;从专有数据库中提取.class文件; 将Java源文件动态编译为.class文件。

 

2)类的生命周期.类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,在某些情况下在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

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

加载阶段主要是获取类的二进制字节流,在加载阶段,虚拟机需要完成以下三件事情:1、通过一个类的全限定名来获取其定义的二进制字节流。 2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中数据的访问入口。加载阶段是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的数据。

2.连接

验证:确保被加载的类的正确性。这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内。

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

字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,可以采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

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

 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。 · 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。 · 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。 · 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

    如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

   假设上面的类变量value被定义为: public static final int value = 3;

   编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。

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

3.初始化。为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式: ①声明类变量是指定初始值 ②使用静态代码块为类变量指定初始值。JVM初始化步骤: 1、假如这个类还没有被加载和连接,则程序先加载并连接该类 2、假如该类的直接父类还没有被初始化,则先初始化其直接父类3、假如类中有初始化语句,则系统依次执行这些初始化语句。类初始化时机:只有当对类的主动使用的时候才会导致类的初始化;1、创建类的实例,也就是new的方式2、 访问某个类或接口的静态变量,或者对该静态变量赋值3、调用类的静态方法4、 反射(如Class.forName(“com.shengsiyuan.Test”))5、Java虚拟机启动时被标明为启动类的类(Java Test)6、直接使用java.exe命令来运行某个主类

4.结束生命周期。在如下几种情况下,Java虚拟机将结束生命周期;1、执行了System.exit()方法2、程序正常执行结束3、程序在执行过程中遇到了异常或错误而异常终止4、 由于操作系统出现错误而导致Java虚拟机进程终止。

 

3)类加载器。类加载器的层次关系如下图所示:

站在Java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机),是虚拟机自身的一部分;其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且都继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。扩展类加载器:它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。应用程序类加载器:它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。

 

4)JVM类加载机制

全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。

 

5)类的加载。类加载有三种方式:1、命令行启动应用时候由JVM初始化加载2、通过Class.forName()方法动态加载3、通过ClassLoader.loadClass()方法动态加载。

Class.forName()和ClassLoader.loadClass()区别

Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

 

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

双亲委派机制:1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

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

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值