java内存JVM

JVM内存

JAVA内存结构一般是指JVM运行代码时会将自己管理的内存分成几个运行时数据区,这些运行时数据区包括方法区、虚拟机栈、本地方法栈、堆和程序计数器

  1. 本地方法栈:

在本地方法栈中存放的是native关键字修饰的方法,用native修饰的方法说明java的作用范围达不到了,会去调用底层c语言的库。本地方法栈中方法在调用时会通过本地方法接口(JNI)加载本地方法库中的方法。

2.PC程序计数器

用于存放下一条指令所在单元的地址的地方,每个线程都有一个程序计数器,是线程私有的,就是一个指针,,指向方法区中的方法字节码(用来存储指向一条指令的地址,即下一条要执行的指令代码)

3.方法区/永久代(线程共享)

方法区是所有的线程共享的,所有字段和方法字节码,以及一些特殊方法,如构造函数、接口代码也在此定义,所有定义的方法的信息都保存在改区域,次区域属于共享区域,

静态变量,常量,类信息(构造方法、接口定义)运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。

4.虚拟机栈

栈是运行时的单位,java虚拟机栈,线程私有,生命周期和和线程一致。描述的java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量表(形参也是局部变量)、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行结束,都对应一个栈帧从虚拟机栈中入栈到出栈的过程。

局部变量变量表:

存放了编译期可知的各种基本数据类型(byte、shot 、int、 long、 floa、double、char、boolean)、对象引用(reference类型)和returnAddress类型。

动态链接: (指向运行时常量池方法的引用)

每个栈内部都会包含一个指向运行时常量池中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)

在java源码被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池(常量池在方法区中)里,比如 描述一个方法调用了另外的其他方法时,就是通过常量池中指向其他方法的符号引用来表示的,动态链接的作用就是为了将这些符号转为调用方法的实际直接引用。

《深入了解JVM虚拟机》中写

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另一部分将在每一次运行期间都转化为直接引用,这部分就被称为动态链接。

静态链接:如果被调用的目标方法再编译期可知,且运行期间保持不变,则将这个转换过程称为静态链接

静态链接绑定的机制为早期绑定,是由于在编译时期便可知道目标方法的类型,就可以进行绑定

动态链接:如果被调用的目标方法无法在编译器可知,只能够在运行的时候将符号引用转换为直接引用,则称之为动态链接

操作数栈

在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈、出栈。某些字节码指令将值压入操作数栈,其余的字节码指令将操作数指令取出栈,使用他们后再把结果压入栈。比如:执行复制、交换、求和等操作。

操作数栈,主要使用与保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。

方法出口(方法返回地址):

方法出口指的是 执行完改行代码后,下一行要运行的代码。

方法返回地址

1.存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:

  • 正常执行完成

  • 出现未处理的异常,非正常退出

2、无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

3、本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

4、正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

方法退出的两种方式

当一个方法开始执行后,只有两种方式可以退出这个方法,

正常退出:

1、执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口

2、一个方法在正常调用完成之后,究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定。

3、在字节码指令中,返回指令包含:

  • ireturn:当返回值是boolean,byte,char,short和int类型时使用

  • lreturn:Long类型

  • freturn:Float类型

  • dreturn:Double类型

  • areturn:引用类型

  • return:返回值类型为void的方法、实例初始化方法、类和接口的初始化方法

异常退出:

1、在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口

2.方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码

5.堆(Heap)

是被线程共享的一块区域,创建的对象和数据都保存在java堆内存中,也是垃圾收集器进行垃圾收集的最重要的区域,由于现在JVM采用分代收集算法,因此java堆从GC的角度还可以细分为:新生区(Eded区,From Survivor区和 To Survivor区)和老年代。

5.1:新生代

是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发

MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

  • 新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。

  • 当年轻代空间不足时,就会触发Minor GC,这里的年轻代空间不足指的是Eden区满,Survivor区满不会触发GC(每次Minor GC 会清理年轻代的内存)

5.1.1:Eden 区

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老

年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行

一次垃圾回收。

5.1.2:ServivorFrom 区

上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

5.1.3:ServivorTo

保留了一次 MinorGC 过程中的幸存者。

5.1.4: MinorGC的过程(复制->清空->互换)

MinorGC 采用复制算法。

1edenservicorFrom 复制到 ServicorTo,年龄+1

首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年

龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不

够位置了就放到老年区);

2:清空 edenservicorFrom

然后,清空 Eden 和 ServicorFrom 中的对象;

3ServicorTo ServicorFrom 互换

最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom

5.2:老年代

主要存放应用程序中生命周期长的内存对象。

老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行

了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足

够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没

有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减

少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的

时候,就会抛出 OOM(Out of Memory)异常。

5.2.1Major GC(老年代垃圾回收)

Major GC指发生在老年代的GC。

Major GC触发条件

老年代空间不足时,会先尝试触发Minor GC。Minor GC之后空间还不足,则会触发Major GC。

说明:发生在老年代的GC ,基本上进行一次Major GC 就会伴随进行一次 Minor GC。Major GC 的速度一般会比 Minor GC 慢 10 倍,并且STW的时间更长。

5.3. 永久代

指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被

放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这

也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

方法区、永久代、元空间

方法区

方法区实在虚拟机规范里面被定义的,不同的虚拟机对这个定义的实现不同。

方法区是JVM 所有线程共享。

主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与 进行区分,通常又叫 非堆。

PermGen(永久代)

PermGen , 就是 PermGen space ,全称是 Permanent Generation space ,是指内存的永久保存区域。这块内存主要是被JVM存放Class和Meta信息的, Class 在被 Loader 时就会被放到 PermGen space 中。

绝大部分 Java 程序员应该都见过 java.lang.OutOfMemoryError: PermGen space 这个异常。

这里的 PermGen space 其实指的就是 方法区 。不过 方法区 和 PermGen space又有一定的区别。

  • 1.方法区 是 JVM 的规范,所有虚拟机 必须遵守的。常见的JVM 虚拟机 Hotspot 、 JRockit(Oracle)、J9(IBM)

  • 2.PermGen space 则是 HotSpot 虚拟机 基于 JVM 规范对 方法区 的一个落地实现, 并且只有 HotSpot 才有 PermGen space。

  • 3.而如 JRockit(Oracle)、J9(IBM) 虚拟机有 方法区 ,但是就没有 PermGen space。

  • 4.PermGen space 是 JDK7及之前, HotSpot 虚拟机 对 方法区 的一个落地实现。在JDK8被移除。

  • 5.Metaspace(元空间)是 JDK8及之后,废弃了 PermGen space ,取而代之的是 Metaspace , 这是 HotSpot 虚拟机 对 方法区 的新的落地实现。

JDK6、JDK7 时,方法区 就是 PermGen(永久代)。

JDK8 时,方法区就是 Metaspace(元空间)

Metaspace(元空间)

Metaspace(元空间)和 PermGen(永久代)类似,都是对 JVM规范中方法区的一种落地实现。

不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

Oracle 移除PermGen(永久代)从从JDK7 就开始。例如,字符串常量池,已经在JDK7 中从永久代中移除。直到JDK8 的发布将宣告 PermGen(永久代)的终结。

其实,移除 PermGen 的工作从 JDK7 就开始,永久代的部分数据就已经转移到了 Java Heap 或者是 Native Heap。

但永久代仍存在于JDK7 中,并没完全移除,比如:

  • 字面量 (interned strings)转移到 Java heap;

  • 类的静态变量(class statics)转移到Java heap ;

  • 符号引用(Symbols) 转移到 Native heap ;

必须知道的是 JDK6 、JDK7 依然存在 PermGen space;

运行时常量池

  • 在 JDK6 ,抛出永久代(PermGen space)异常,说明 运行时常量池 存在于 方法区

  • 在 JDK7、JDK8 抛出堆(Java heap space)异常,说明 运行时常量池 此时在 Java堆 中;

元空间与永久代的区别

元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。默认情况下,元空间的大小仅受 本地内存 限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize ,初始空间大小:达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间:默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集;

-XX:MaxMetaspaceFreeRatio ,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集;

永久代为什么会向元空间转换

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

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

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

4)Oracle 可能会将HotSpot 与 JRockit 合二为一。

MinorGC、MajorGC、FullGC垃圾回收

MinorGC/Young GC

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,也叫Young GC。因为Java对象大多具备朝生夕死的特征,所以MinorGC非常频繁,一般回收速度也比较快。一般采用复制算法。

说明:Minor GC可能会引发STW,暂停其他用户的线程,需要等JVM垃圾回收结束后,用户线程才恢复运行。

Minor GC 触发条件:

Eden区空间不足会触发,幸存区空间不足不会触发,但是触发Minor GC就会清理Eden区和幸存

触发结果

经过一次Minor GC后

  • 伊甸园区没有被回收的对象被放到to区,并且年龄计数器设置为1。

  • from区的没有被回收的对象到to区,年龄计数器加一,from和to区交换,此时to为空

Major GC

  • 对老年代的垃圾回收。

  • Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。

  • 如果Major GC后,内存还不足,就报OOM了。

触发条件

出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。 也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发

Full GC

清理整个堆空间,包括年轻代、老年代和元空间(方法区),非常慢。

触发条件

  • 调用System.gc(),系统建议执行Full GC,但不是必然执行。

  • 老年代空间不足。

  • 方法区空间不足。

  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。

  • 有Edan区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把对象转存到老年代,且老年代的可用内存小于该对象大小

总结:垃圾回收频繁发生在新生区,很少发生在养老区,几乎不发生在永久区/元空间,性能调优就会减少MaJor GC 和Full GC

垃圾回收与算法:

  1. 如何确定垃圾

1.1引用计数法

之前Java中判断垃圾的算法使用引用计数器进行判定的,对象中有一个字段保存着计数器,只要这个对象被引用了那么计数器就加1;释放了这个引用就计数器减1。垃圾回收器回收的时候看这个对象的引用是否为0,如果为0那么代表这个对象是垃圾,该被回收。

但引用计数法有循环依赖的问题:

举例:创建A对象a,创建B对象b。

这个时候A的成员变量引用b,B的成员变量引用a。即a和b的计数器都为1,销毁a的时候发现b在引用a,销毁b的时候发现a在引用b。垃圾回收的时候就不会回收这两个对象,但是除此之外没有其他引用指向这两个对象。

循环引用会导致即使外界已经没有任何指针能够访问他们了,但是他们所占资源仍然无法释放的情况。

优点:不需要STW,找到计数器为0的对象直接进行清除

缺点:维护计数器空间和时间上都有所牺牲,而且无法解决循环引用。

1.2可达性分析(GC Root)

为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”

对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。

要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记

过程。两次标记后仍然是可回收对象,则将面临回收。

当对象被标记为垃圾时,要清除其实还有一次标记过程,也就是说对象要被清除得经过两次标记过程:

当对象经过可达性分析后发现没有与GCRoots相关的引用链,他会被第一次标记,接着会进行判断是否要进行调用对象的finalize()方法,【判断条件:对象重写该方法并且该对象没有执行过该方法】。finalize()方法只会被执行一次。

如果有必要执行finalize()方法的话,会将该对象添加到一个F-Quene的队列里面,稍后会有一个虚拟机自己建立并且调度优先级很低的Finalizer线程去执行对象的finalize()方法,由于该方法可能会陷入死循环或者执行缓慢导致队列中的其他对象永远不被调用finalizy方法,所以该方法并不一定保证能够执行完。接着稍后会对该队列中的对象进行第二次标记,如果还是没有对象引用它,那么将会被回收;如果有对象引用了它那么会进行将该对象移除队列中。(比如:把对象自己 (this) 赋值给某个类变量或者对象的成员变量,则在第二次标记时将会被移除 “即将回收” 的集合)

GCRoot通常一般是以下对象

1.虚拟机栈中引用的对象、栈帧中本地变量表中引用的对象(比如:各个线程被调用的方法堆栈中使到的参数、局部变量、临时变量等)
2方法区中类的静态属性引用的对象 (比如:Java类的引用类型静态变量)
3.方法区中常量引用的对象 (比如:字符串常量池中的引用)
4.本地方法栈中 JNI (Native方法)引用的对象
5.Java虚拟机内部的引用 (比如:基本数据类型对应的Class对象、NullPonitException等常驻的异常对象、系统类加载器)
6.所有被 Synchronized 持有的对象
7.反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等

2.垃圾回收算法

2.1标记清除算法(Mark-Sweep)

最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清

除阶段回收被标记的对象所占用的空间。如图

从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可

利用空间的问题。

2.2复制算法(copying)

为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小

的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用

的内存清掉,如图:

这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原

本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。

2.3. 标记压缩算法(Mark-Compact)

结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清

理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:

2.4:分代收集算法

分代收集算法是目前大部分JVM所采用的方法,其核心思想是根据对象的不同声明周期将内存划分为不同的域,一般情况将GC堆划分为老年代和新生代。老年代的特点是每次垃圾回收只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要回收,因此可以根据不同区域选择不同算法。

2.4.1:新生代复制算法

目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要

回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代

划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用

Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另

一块 Survivor 空间中。

2.4.2老年代(标记压缩/标记清理)

因为对象存活率高,老年代每次回收少量对象,而且没有额外空间对它进行分配担保,,区域比较大可以使用标记压缩或标记清理算法进行回收,不必进行内存复制,且直接腾出空闲内存。

  1. java虚拟机中处于方法区的永久代(Permanet Generation),它用来存储class类,常量,方法描述等。对永久代的回收主要包括废弃常量和无用类。

2.对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目

前存放对象的那一块),少数情况会直接分配到老生代。

3. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden

Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From

Space 进行清理。

4. 如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。

5. 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。

6. 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被

移到老生代中。

GC分代收集算法VS分区收集算法

分代收集算法

当前主流VM垃圾收集都采用“分代收集”(Generation Collection)算法,这种算法会根据对象存活周期的不同将内存划分为几块,如JVM中的新生代、老年代、永久代,这样就可以根据各年代特点分别采用最适当的GC算法。

分区收集算法

分区算法则将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收。这样做的好处是可以控制一次回收多少个小区间,根据目标停顿时间,每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿。

垃圾回收器

垃圾收集器是垃圾回收算法(标记-清除算法,标记-压缩算法,复制算法)的具体实现,不同的商家在提供不同的JVM中的垃圾收集器可能会有很大的差别,下图为JDK1.8中HotSpot虚拟机中的垃圾收集器。java虚拟机中针对新生代和老年代分别提供了多种不同的垃圾收集器,连接表示可以配合使用的组合,G1是对整个堆进行收集。

用于新生代的收集器有:SerIal、ParNew、Paraller Scavenge

用于老年代的收集器有:CMS(Concurrent Mark Sweep)Serial Old 、Parallel Old

G1垃圾收集器相对比其他收集器而言,最大的区别在与取消了年轻代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的年轻代、老年代区域。

收集器分类:

  • 串行垃圾收集器:Serial Serial old

  • 并行垃圾收集器:ParNew、Parallel Old、Paraller Scavenge

  • 并发垃圾收集器:CMS(Concurrent Mark Sweep)

如何查看当前JAVA默认使用的垃圾收集器

java -XX:+PrintCommandLineFlags -version

UseParallelGC代表新生代默认使用的是Paraller Scavenge进行垃圾收集的,而老年代则使用是Parallel Old进行垃圾收集的

java8默认使用的 Paraller Scavenge 和Parallel Old

Serial 垃圾收集器(单线程、复制算法)

Serial是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1之前新生代唯一的垃圾收集器。Serial是一个单线程的收集器,它不但只会使用一个CPU或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,知道垃圾收集结束,这种现象称之为SWT(Stop-The-world),

Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限

定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率。对于交互性较强的应用而言,这种垃圾收集器是不能够接受的。一般在Javaweb应用中是不会采用该收集器的。

设置为串行垃圾收集器命令

-XX:+UseSerialGC -XX:+PrintGCDetails -Xms16m -Xmx16m

控制台打印信息

可以看到垃圾收集已经变为DefNew和Tenured的的组合进行了,也就是Serial和Serial Old的组合

ParNew垃圾收集器(Serial+多线程)

ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用的是复制算法,除了使用多线程进行垃圾收集之外,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程同样也要暂停所有的工作线程,ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数

设置为ParNew进行新生代的垃圾回收

-XX:+UseParNewGC -XX:+PrintGCDetails -Xms16m -Xmx16m

通过-XX:+UseParNewGC参数设置年轻代使用ParNew回收器,老年代使用的依然是串行收集器

Parallel Scavenge 收集器(多线程复制算法、高效)

Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃

圾收集器,在垃圾收集过程中需要暂停所有的工作线程,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码

的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),

高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而

不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个

重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情兄收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量。)

ParallelGC收集器工作机制和ParNewGC收集器一样,只是在此基础之上,新增了两个和系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。

相关参数如下:

  -XX:+UseParallelGC

    年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器。

  -XX:+UseParallelOldGC

    年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。

  -XX:MaxGCPauseMillis

    设置最大的垃圾收集时的停顿时间,单位为毫秒需要注意的时,ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他的参数,如果堆的大小设置的较小,就会导致GC工作变得很频繁,反而可能会影响到性能。该参数使用需谨慎。

  -XX:GCTimeRatio

    设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)。它的值为0~100之间的数字,默认值为99,也就是垃圾回收时间不能超过1%

  -XX:UseAdaptiveSizePolicy

    自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡。一般用于,手动调整参数比较困难的场景,让收集器自动进行调整。

Serial Old 收集器(单线程标记整理算法 )

Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,

这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。

在 Server 模式下,主要有两个用途:

1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。

2. 作为年老代中使用 CMS 收集器的后备垃圾收集方案。

新生代 Serial 与年老代 Serial Old 搭配垃圾收集过程图:

新生代 Parallel Scavenge 收集器与 ParNew 收集器工作原理类似,都是多线程的收集器,都使

用的是复制算法,在垃圾收集过程中都需要暂停所有的工作线程。新生代 Parallel

Scavenge/ParNew 与年老代 Serial Old 搭配垃圾收集过程图:

Parallel Old 收集器(多线程标记整理算法)

Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6

才开始提供。

在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只

能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞

吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge

和年老代 Parallel Old 收集器的搭配策略。

新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配运行过程图:

CMS 收集器(多线程标记清除算法)

Concurrent mark sweep(CMS)收集器是一种老年代垃圾收集器,其最主要目标是获取最短垃圾

回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。

最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。通过参数-XX:+UseConcMarkSweepGC进行设置。CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:

初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。

重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记

记录,仍然需要暂停所有的工作线程。

并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看

CMS 收集器的内存回收和用户线程是一起并发地执行。

CMS 收集器工作过程:

设置以CMS方式进行垃圾收集:

-XX:+UseConcMarkSweepGC

G1垃圾收集器(Garbage first)

Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收

集器两个最突出的改进是:

1. 基于标记-整理算法,不产生内存碎片。

2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域

的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾

最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收

集效率。

原理:

 G1垃圾收集器相对比其他收集器而言,最大的区别在于它取消了年轻代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的年轻代、老年代区域。

 在G1划分的区域中,年轻代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。

这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。在G1中,有一种特殊的区域,叫Humongous区域。

  • 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。

  • 这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。

  • 为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

Young GC

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。

  • Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。

  • Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

Rembered Set

在GC年轻代的对象时,我们如何找到年轻代中对象的根对象呢?

  根对象可能是在年轻代中,也可以在老年代中,那么老年代中的所有对象都是根么?如果全量扫描老年代,那么这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,其作用是跟踪指向某个堆内的对象引用。

  无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描

每个Region初始化时,会初始化一个RSet,该集合用来记录并跟踪其它Region指向该Region中对象的引用,每个Region默认按照512Kb划分成多个Card,所以RSet需要记录的东西应该是 xx Region的 xx Card。

Mixed GC

  当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个YoungRegion,还会回收一部分的Old Region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC 并不是 Full GC。

  

  MixedGC什么时候触发?

由参数 -XX:InitiatingHeapOccupancyPercent=n 决定。默认:45%,该参数的意思是:当老年代大小占整个堆大小百分比达到该阀值时触发。

  它的GC步骤分2步

    1. 全局并发标记(global concurrent marking)

    2. 拷贝存活对象(evacuation)

全局并发标记

1.初始标记(initial mark,STW)

  标记从根节点直接可达的对象,这个阶段会执行一次年轻代GC,会产生全局停顿。

2.根区域扫描(root region scan)

  G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。

3.并发标记(Concurrent Marking)

  G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断。

4.重新标记(Remark,STW)

  该阶段是 STW 回收,因为程序在运行,针对上一次的标记进行修正。

5.清除垃圾(Cleanup,STW)

  清点和重置标记状态,该阶段会STW,这个阶段并不会实际上去做垃圾的收集,等待evacuation阶段来回收。

拷贝存活对象

Evacuation阶段是全暂停的。该阶段把一部分Region里的活对象拷贝到另一部分Region中,从而实现垃圾的回收清理。

设置以G1方式进行垃圾回收

-XX:+UseG1GC -XX:+PrintGCDetails -Xms16m -Xmx16m

相比于CMS,有两个优点:

1.基于标记-整理算法,不产生内存碎片。

2.G1的Stop The World(STW)更可控,在停顿时间上添加了预测机制,可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

G1的特点

  1. G1 收集器是一种面向服务器的垃圾收集器,应用在多处理器和大容量内存环境中。能充分利用多CPU、多核环境硬件优势,尽量缩短STW。

  1. G1 整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。

  1. G1 抛弃了之前的分代收集的方式,面向整个堆内存进行回收,把内存划分为多个大小相等的独立区域Region,JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M。可以用参数“-XX:G1HeapRegionSize”手动指定Region大小,但是推荐默认的计算方式。

  1. G1最大的优势就在于可预测的停顿时间模型,我们可以自己通过参数-XX:MaxGCPauseMillis来设置允许的停顿时间(默认200ms),G1会收集每个Region回收之后的空间大小、回收需要的时间,根据评估得到的价值,在后台维护一个优先级列表,然后基于我们设置的停顿时间优先回收价值收益最大的Region。

  1. G1 保留了新生代和老年代的概念,但不再是物理隔离了,是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域。

  1. 在G1中,新增了两个参数G1MaxNewSizePercent、G1NewSizePercent,用来控制新生代的大小,默认的情况下G1NewSizePercent为5,也就是堆空间的5%,G1MaxNewSizePercent默认为60,也就是堆空间的60%。如果堆大小为4096M,那么新生代占据200MB左右的内存,对应大概是100个Region。在系统运行中,JVM会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,新生代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1。

  1. 一个Region可能之前是新生代,进行了垃圾回收之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。

  1. G1 垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Humongous区,对G1来说,超过一个Region一半大小的对象都被认为大对象,将会被放入Humongous Region,而对于超过整个Region的大对象,则用几个连续的Humongous来存储。

  1. Full GC 的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。

初始化新生代的空间大小逻辑

首先,通过原有参数-Xms设置初始堆的大小,-Xmx设置最大堆的大小还是生效的,可以设置堆的大小。

  1. 可以通过原有参数-Xmn或者新的参数G1NewSizePercent、G1MaxNewSizePercent来设置年轻代的大小,如果设置了-Xmn相当于设置G1NewSizePercent=G1MaxNewSizePercent。

  1. 接着看是不是设置了-XX:NewRatio(表示年轻代与老年代比值,默认值为2,代表年轻代老年代大小为1:2),如果第一点的参数都设置了,那么忽略NewRatio,反之则代表G1NewSizePercent=G1MaxNewSizePercent,并且分配规则还是按照NewRatio的规则。

  1. 如果只是设置了G1NewSizePercent、G1MaxNewSizePercent中的一个,那么就按照这两个参数的默认值5%和60%来设置。

  1. 如果设置了-XX:SurvivorRatio,默认为8,那么Eden和Survivor还是按照这个比例来分配。

  1. 按照这个规则,我们新生代和老年代的空间分配基本就完成,如果说新生代走默认的规则,每次动态扩展空间大小怎么办?

  1. 有一个参数叫做-XX:GCTimeRatio表示GC时间与应用耗费时间比,默认为9,就是说GC时间和应用时间占比超过10%才进行扩展,扩展比例为20%,最小不能小于1M。

参数配置

  1. -XX:+UseG1GC

  1. -XX:+G1HeapRegionSize=n:设置的G1区域的大小,值是2的幂,范围是1MB到32MB,目标是根据最小的Java堆大小划分出约2048个区域;

  1. -XX:+MaxGCPauseMillis=n:设定G1的目标停顿时间,它会尽量的去达成这个目标;

  1. -XX:+InitiatingHeapOccupancyPercent=n:触发GC时,堆内存的占用百分比,基于整个堆的使用率,而不只是某一代内存的使用比例,默认为45;

  1. -XX:+ConGCThreads=n:并发GC使用的线程数

  1. -XX:+G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%

  1. -XX:G1MaxNewSizePercent设置新生代初始占比

  1. 开发人员仅仅需要声明以下参数即可:开始G1+设置最大内存+设置最大停顿时间。 -XX:+UseG1GC -Xmx32g -XX:+MaxGCPauseMillis=100

优化建议

假设参数-XX:MaxGCPauseMillis设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代GC。那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。或者年轻代GC过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。

所以这里核心还是在于调节-XX:MaxGCPauseMillis这个参数的值,在保证年轻代gc别太频繁的同时,还得考虑每次GC过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed GC。

JVM参数配置:

调优参数:

参数

描述

-Xms

设置堆的最小空间大小,建议小于物理内存的1/4,默认值为物理内存的1/64

-Xmx

设置堆的最大空间大小,建议与-Xms保持一致,默认值为物理内存的1/4

-Xmn

新生代内存大小,建议不超过内存的1/2,

至于这个参数则是对 -XX:newSize、-XX:MaxnewSize两个参数的同时配置,也就是说如果通过-Xmn来配置新生代的内存大小,

那么-XX:newSize = -XX:MaxnewSize = -Xmn,虽然会很方便,但需要注意的是这个参数是在JDK1.4版本以后才使用的

-XX:newSize

表示新生代初始内存大小,应该小于-Xms

-XX:MaxnewSize

表示新生代可分配内存大小的最大上限,值应该小于-Xmx的值

-Xss

-Xss 默认是 512k~1024k 等价于 -XX:ThreadStackSize=512k,该值等于零表示使用的是默认值。一般情况下无需设置

-XX:PermSize

永久代初始值,默认值为物理内存的1/64

-XX:MaxPermSize

永久代最大值,默认值为物理内存的1/4

-XX:MetaspaceSize

JDK1.8 元空间初始空间大小

-XX:MaxMetaspaceSize

JDK1.8 元空间最大空间大小

-XX:NewRatio=2

新生代内存容量与老生代内存容量的比例,等于2表示;,新生代:老年代=1:2

-XX:SurvivorRatio=8

新生代中Eden与Survivor比值,默认值时8,表示8:1:1

-XX:MaxTenuringThreshold=15

对象在新生代存活区切换的次数(坚持过MinorGC的次数,每坚持过一次,该值就增加1),大于该值会进入老年代(年龄阈值)

-XX:MaxHeapFreeRatio=70

GC后java堆中空闲量占的最大比例,大于该值,则堆内存会减少

-XX:MinHeapFreeRatio=40

GC后java堆中空闲量占的最小比例,小于该值,则堆内存会增加

-XX:LargePageSizeInBytes=4m

设置用于Java堆的大页面尺寸

-XX:PretenureSizeThreshold= size

大于该值的对象直接晋升入老年代(这种对象少用为好)

行为参数

行为参数主要用来选择使用什么样的垃圾收集器组合,以及控制运行过程中的GC策略等

-XX:+UseSerialGC

启用串行GC,即采用Serial+Serial Old模式

-XX:+UseParallelGC

启用并行GC,即采用Parallel Scavenge+Serial Old收集器组合(-Server模式下的默认组合)

-XX:GCTimeRatio=99

设置用户执行时间占总时间的比例(默认值99,即1%的时间用于GC)

-XX:MaxGCPauseMillis=time

设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效)

-XX:+UseParNewGC

使用ParNew+Serial Old收集器组合

-XX:ParallelGCThreads

设置执行内存回收的线程数,在+UseParNewGC的情况下使用

-XX:+UseParallelOldGC

使用Parallel Scavenge +Parallel Old组合收集器

-XX:+UseConcMarkSweepGC

使用ParNew+CMS+Serial Old组合并发收集,优先使用ParNew+CMS,当用户线程内存不足时,采用备用方案Serial Old收集。

-XX:-DisableExplicitGC

禁止调用System.gc();但jvm的gc仍然有效

-XX:+ScavengeBeforeFullGC

新生代GC优先于Full GC执行

调试参数

调试参数,主要用于监控和打印GC的信息

-XX:-CITime

打印消耗在JIT编译的时间

-XX:ErrorFile=./hs_err_pid<pid>.log

保存错误日志或者数据到文件中

-XX:-ExtendedDTraceProbes

开启solaris特有的dtrace探针

-XX:HeapDumpPath=./java_pid<pid>.hprof

指定导出堆信息时的路径或文件名

-XX:-HeapDumpOnOutOfMemoryError

当首次遭遇OOM时导出此时堆中相关信息

-XX:OnError="<cmd args>;<cmd args>"

出现致命ERROR之后运行自定义命令

-XX:OnOutOfMemoryError="<cmd args>;<cmd args>"

当首次遭遇OOM时执行自定义命令

-XX:-PrintClassHistogram

遇到Ctrl-Break后打印类实例的柱状信息,与jmap -histo功能相同

-XX:-PrintConcurrentLocks

遇到Ctrl-Break后打印并发锁的相关信息,与jstack -l功能相同

-XX:-PrintCommandLineFlags

打印在命令行中出现过的标记

-XX:-PrintCompilation

当一个方法被编译时打印相关信息

-XX:-PrintGC

每次GC时打印相关信息

-XX:-PrintGCDetails

每次GC时打印详细信息

-XX:-PrintGCTimeStamps

打印每次GC的时间戳

-XX:-TraceClassLoading

跟踪类的加载信息

-XX:-TraceClassLoadingPreorder

跟踪被引用到的所有类的加载信息

-XX:-TraceClassResolution

跟踪常量池

-XX:-TraceClassUnloading

跟踪类的卸载信息

-XX:-TraceLoaderConstraints

跟踪类加载器约束的相关信息

调节内存大小的命令

JAVA类的加载过程

  1. 加载:

将class文件字节码内容加载到内存中,并将这些静态数据转换方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象

  1. 链接:

将java类的二进制代码合并到jvm的运行状态之中的过程。

2.1:验证

确保加载的类信息符合JVM规范,没有安全方面的问题。

2.2:准备

正式为类变量(static)分配内存并设置默认初始值得阶段,这些内存都将在方法区中分配

2.3:解析

虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

  1. 初始化

  1. 执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)

  1. 当初始化一个类时,如果发现其父类没有初始化,则需要先触发其父类的初始化

  1. 虚拟机会保证一个类的<clinit>()方法在多线程中被正确加锁和同步

什么时候会发生类的初始化:

1.类的主动引用(一定会发生类的初始化)

1.1:当虚拟机启动,先初始化main方法所在的类

1.2:new一个类的对象

package com.rds.bip.personal.classtest;
//测试类什么时候被初始化
public class TestInit {    
    static {
        System.out.println("Main类被初始化");        
    }
    
    public static void main(String[] args) {
        //主动引用
        Son son=new Son();                
    }
}
//父类
class   Father{    
    static {        
        System.out.println("父类被初始化");
    }        
}
//子类 继承父类
class Son extends Father{
    static {        
        System.out.println("子类被初始化");
    }    
}

运行结果
Main类被初始化
父类被初始化
子类被初始化

1.3:调用类的静态成员(除了final常量)和静态方法

package com.rds.bip.personal.classtest;
//测试类什么时候被初始化
public class TestInit {    
    static {
        System.out.println("Main类被初始化");        
    }
    
    public static void main(String[] args) throws ClassNotFoundException {
        //主动引用
        //Son son=new Son();            
        //通过反射
        //Class.forName("com.rds.bip.personal.classtest.Son");
        //调用静态变量
        int a=Son.a;
        
        
    }
}
//父类
class   Father{    
    static {        
        System.out.println("父类被初始化");
    }        
}
//子类 继承父类
class Son extends Father{
    static {        
        System.out.println("子类被初始化");
    }    
    
    static int a=1;
}

运行结果
Main类被初始化
父类被初始化
子类被初始化

1.4:使用java.lang.reflect包的方法对类进行反射调用

package com.rds.bip.personal.classtest;
//测试类什么时候被初始化
public class TestInit {    
    static {
        System.out.println("Main类被初始化");        
    }
    
    public static void main(String[] args) throws ClassNotFoundException {
        //主动引用
        //Son son=new Son();            
        //通过反射
        Class.forName("com.rds.bip.personal.classtest.Son");
        
    }
}
//父类
class   Father{    
    static {        
        System.out.println("父类被初始化");
    }        
}
//子类 继承父类
class Son extends Father{
    static {        
        System.out.println("子类被初始化");
    }    
}

运行结果
Main类被初始化
父类被初始化
子类被初始化

1.5:当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类。

2.类的被动引用(不会发生类的初始化)

2.1:当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量不会导致子类初始化。

package com.rds.bip.personal.classtest;
//测试类什么时候被初始化
public class TestInit {    
    static {
        System.out.println("Main类被初始化");        
    }    
    public static void main(String[] args) throws ClassNotFoundException {
        //主动引用
        //Son son=new Son();            
        //通过反射
        //Class.forName("com.rds.bip.personal.classtest.Son");
        //调用静态变量
        //int a=Son.a;        
         //不会发生类的引用(累的初始化)
         //子类引用父类的静态变量不会导致子类初始化
        System.out.println(Son.b);            
    }
}
//父类
class   Father{    
    static {        
        System.out.println("父类被初始化");
    }
    
    static int b=1;
}
//子类 继承父类
class Son extends Father{
    static {        
        System.out.println("子类被初始化");
    }    
    
    static int a=1;
}
运行结果
Main类被初始化
父类被初始化
1

2.2:通过数组定义类引用,不会触发此类的初始化

package com.rds.bip.personal.classtest;
//测试类什么时候被初始化
public class TestInit {    
    static {
        System.out.println("Main类被初始化");        
    }
    
    public static void main(String[] args) throws ClassNotFoundException {
        //主动引用
        //Son son=new Son();            
        //通过反射
        //Class.forName("com.rds.bip.personal.classtest.Son");
        //调用静态变量
        //int a=Son.a;
         
         //不会发生类的引用(累的初始化)
         //子类引用父类的静态变量不会导致子类初始化
        //System.out.println(Son.b);
        //通过数组定义类引用,不会触发此类的初始化
        Son[] sons=new Son[10];
        
    }
}
//父类
class   Father{    
    static {        
        System.out.println("父类被初始化");
    }
    
    static int b=1;
}
//子类 继承父类
class Son extends Father{
    static {        
        System.out.println("子类被初始化");
    }    
    
    static int a=1;
}

运行结果
Main类被初始化

2.3:引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)

package com.rds.bip.personal.classtest;
//测试类什么时候被初始化
public class TestInit {    
    static {
        System.out.println("Main类被初始化");        
    }
    
    public static void main(String[] args) throws ClassNotFoundException {
        //主动引用
        //Son son=new Son();            
        //通过反射
        //Class.forName("com.rds.bip.personal.classtest.Son");
        //调用静态变量
        //int a=Son.a;
         
         //不会发生类的引用(累的初始化)
         //子类引用父类的静态变量不会导致子类初始化
        //System.out.println(Son.b);
        //通过数组定义类引用,不会触发此类的初始化
        //Son[] sons=new Son[10];
        
        //引用常量不会触发此类的初始化
        
        System.out.println(Son.c);
        
    }
}
//父类
class   Father{    
    static {        
        System.out.println("父类被初始化");
    }
    
    static int b=1;
}
//子类 继承父类
class Son extends Father{
    static {        
        System.out.println("子类被初始化");
    }    
    
    static int a=1;
    
    static final int c=2;
}

运行结果
Main类被初始化
2

类加载器

类加载器的作用:

类加载器分类:

Bootstap ClassLoader :引导类(根)加载器,负责java核型库,jre/lib/rt.jar

Extension ClassLoader:扩展类加载器,负责jre/lib/ext目录下的jar包

System ClassLoader:系统类加载器,负责加载java.class.path所指的类与jar包。

获取类加载器:

package com.rds.bip.personal.classtest;
//获取类加载器
public class ClassLoaderTest {    
    public static void main(String[] args) throws ClassNotFoundException {
        //获取系统类的加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("系统类加载器:"+systemClassLoader);        
        //获取扩展类加载器
        ClassLoader parent = systemClassLoader.getParent();
        System.out.println("扩展类加载器:"+parent);        
        //获取扩展类加载器的父类-跟加载器(跟加载器是有c/c++ 编写 无法直接获取,所以打印为null)
        ClassLoader parent2 = parent.getParent();
        System.out.println("跟加载器:"+parent2);
        
        //测试当前类是哪个类加载器加载的S        
        ClassLoader classLoader = Class.forName("com.rds.bip.personal.classtest.ClassLoaderTest").getClassLoader();
        System.out.println("当前类的加载器:"+classLoader);
        //测试jdk内置的类是哪个加载器加载的(根加载器)
        ClassLoader classLoader2 = Class.forName("java.lang.Object").getClassLoader();
        System.out.println("jdk内置类加载器:"+classLoader2);
        //获取类加载器可以加载哪些路径
        String property = System.getProperty("java.calss.path");                        
    }   
}
打印结果:
系统类加载器:sun.misc.Launcher$AppClassLoader@2a139a55
扩展类加载器:sun.misc.Launcher$ExtClassLoader@7852e922
跟加载器:null
当前类的加载器:sun.misc.Launcher$AppClassLoader@2a139a55
jdk内置类加载器:null

类加载的双亲委派机制:

双亲委派机制的过程:

  1. 类加载器收到类加载的请求,判断这个类是否被加载过,加载过直接返回,没有则尝试加载。

  1. 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类(根)加载器

  1. 启动类加载器检查是否能够加载当前这个类,能加载就结束,使用当前加载的加载器,否则抛出异常,通知子加载进行加载。

  1. 重复步骤3 ,如果找不到这个类则包ClassNotFound异常。

双亲委派机制的优点:

避免类的重复加载,保证程序的稳定运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值