第五章 JVM、垃圾回收(GC)

一、JVM

1、JVM定义

JVM(虚拟机):指以软件的方式模拟具有完整硬件系统功能、运行在一个完全隔离环境中的计算机系统,是物理机的软件实现。常见虚拟机有VMWare、VirtualBox、Java Virtual Machine…
Java虚拟机阵营:Sun HotSpot VM、BEA JRockit VM(JDK1.8合并)…
Java虚拟机是采用虚拟化技术,隔离出一块独立的子操作系统,使Java软件不受任何影响在虚拟机内进行执行。
JVM由三个主要的子系统构成:

  • 类加载子系统:装载具有适合名称的类或接口
  • 运行时数据区(内存结构):包含方法区、Java堆、Java栈、本地方法栈、指令计数器及其他隐含寄存器
  • 执行引擎:负责执行包含在已装载的类或接口中的指令

2、JVM实现了Java平台的无关性

在这里插入图片描述
Java是一种技术,由四个方面组成:Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)
开发人员编写Java代码,并将Java源代码文件(.java文件)通过Java编译器进行编译后形成java字节码文件(.class文件),通过类加载子系统加载到运行时数据区(内存空间),再通过JVM执行引擎进行执行。
运行期环境称为Java平台。Java平台由Java虚拟机和Java应用程序接口搭建,Java语言是进入这个平台的通道,用Java语言编写的Java源文件可以运行在这个平台上。这个平台的结构如下:
在这里插入图片描述
在Java平台的结构中, 可以看出,Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现;在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的平台无关性。
JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。

二、类加载子系统

(1)什么是类的加载

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

(2)类的生命周期

类的生命周期包括加载、连接、初始化、使用和卸载:

  • 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
  • 连接,连接又包含三块内容:验证、准备、初始化。1)验证,文件格式、元数据、字节码、符号引用验证;2)准备,为类的静态变量分配内存,并将其初始化为默认值;3)解析,把类中的符号引用转换为直接引用
  • 初始化,为类的静态变量赋予正确的初始值
  • 使用,new出对象程序中使用
  • 卸载,执行垃圾回收

(3)类加载器

在这里插入图片描述

  • 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
  • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器

(4)类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

三、JVM内存管理

(1)JVM内存结构(运行时数据区)

在这里插入图片描述
在这里插入图片描述
方法区和堆是所有线程共享的内存区域;而java栈、本地方法栈和程序计数器是运行时线程私有的内存区域。
一个.class文件装载到内存后会划分为以下五部分:
方法区(method),栈内存(stack),堆内存(heap),本地方法栈(java中的jni调用),程序计数器
在这里插入图片描述

(1.1)Java堆(Heap)

堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的通过new创建的对象实例都在这里分配内存。当对象无法在该空间申请到内存时抛出OutOfMemoryEroor异常。同时也是垃圾收集器管理的主要区域。

public Math{
	public static void main(String[] args){
		Math math = new Math();
		//在Java堆中存储一个Math对象。主线程的Java栈中main的栈帧中局部变量表中存储着一个matn的引用,这个引用指向堆中的Math对象,堆中的Math对象利用了方法区的类信息。
		System.out.println(math.math());
	}
}
}

在这里插入图片描述
Java的堆内存基于Generation算法(Generational Collector)划分为新生代、年老代和持久代。新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。
分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收(GC),以便提高回收效率。
三区域及对象的迁移过程:
(1)新生代Young Generation(1/3堆空间)
几乎所有新生成的对象首先都是放在年轻代的。新生代内存按照8:1:1的比例分为一个Eden区和两个Survivor(Survivor0,Survivor1)区。大部分对象在Eden区中生成。当新对象生成,Eden Space申请失败(因为空间不足等),则会发起一次GC(Scavenge GC)。回收时先将Eden区存活对象复制到一个Survivor0区,然后清空Eden区,当这个Survivor0区也存放满了时,则将Eden区和Survivor0区存活对象复制到另一个Survivor1区,然后清空Eden和这个Survivor0区,此时Survivor0区是空的,然后将Survivor0区和Survivor1区交换,即保持Survivor1区为空, 如此往复。当Survivor1区不足以存放 Eden和Survivor0的存活对象时,就将存活对象直接存放到老年代。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例。
(2)老年代Old Generation(2/3堆空间)
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象。
(3)元数据MetaData Space(直接内存JDK1.8后)
不属于堆内存,属于内存空间。真正与堆隔离。方法区是类逻辑上的一个抽象模板,而元空间是方法区的实现,是真实存在的内存。

对象会首先会进入年轻代的Eden中.在GC之前对象是存在Eden和from中的,进行GC的时候Eden中的对象被拷贝到To这样一个survive空间中,From中的对象到一定次数会被复制到老年代。如果没到次数From中的对象会被复制到To中,复制完成后To中保存的是有效的对象,Eden和From中剩下的都是无效的对象,这个时候就把Eden和From中所有的对象清空。在复制的时候Eden中的对象进入To中,To可能已经满了,这个时候Eden和From中的对象就会被直接复制到Old Generation中.复制完成后,To和From的名字会对调一下,因为Eden和From都是空的,对调后Eden和To都是空的,下次分配就会分配到Eden。一直循环这个流程。好处:使用对象最多和效率最高的就是在Young Generation中,通过From to就避免过于频繁的产生FullGC(Old Generation满了一般都会产生FullGC)

  • 虚拟机在进行MinorGC(新生代的GC)的时候,会判断要进入OldGeneration区域对象的大小,是否大于Old Generation剩余空间大小,如果大于就会发生Full GC。
  • 刚分配对象在Eden中,如果空间不足尝试进行GC,回收空间,如果进行了MinorGC空间依旧不够就放入Old Generation,如果OldGeneration空间还不够就OOM了。
  • 比较大的对象,数组等,大于某值(可配置)就直接分配到老年代,(避免频繁内存拷贝)
  • 年轻代和年老代属于Heap空间的,Permanent Generation(永久代)可以理解成方法区,(它属于方法区)也有可能发生GC,例如类的实例对象全部被GC了,同时它的类加载器也被GC掉了,这个时候就会触发永久代中对象的GC。
  • 如果OldGeneration满了就会产生FullGC。老年代满原因:1、from survive中对象的生命周期到一定阈值2、分配的对象直接是大对象3、由于To 空间不够,进行GC直接把对象拷贝到年老代(年老代GC时候采用不同的算法)
    如果Young Generation大小分配不合理或空间比较小,这个时候导致对象很容易进入Old Generation中,而Old Generation中回收具体对象的时候速度是远远低于Young Generation回收速度。
    因此实际分配要考虑年老代和新生代的比例,考虑Eden和survives的比例,提升系统性能。
    Permanent Generation中发生GC的时候也对性能影响非常大,也是Full GC。

(1.2)方法区(Method Area)

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,类的所有字段和方法的字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中

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

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。他是线程私有的。可看做一个指针,指向方法区中的方法字节码(用来存储指向下一跳指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
每个方法在运行时都存储着一个独立的程序计数器,程序计数器是指定程序运行的行数指针。

(1.4)JVM栈(JVM Stacks)

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。Java栈描述的是Java方法执行的内存模型:一个线程对应一个栈,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。不存在垃圾回收问题,只要线程已结束栈就出栈,生命周期与线程一致。
方法出口指向下次执行的栈帧(方法)

内存说明:
基础数据类型直接在栈空间分配
方法的形式参数直接在栈空间分配,方法调用完成后从栈空间回收
引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量。栈中的地址空间指向堆空间的对象区。
方法的引用参数,在栈空间分配一个地址空间,指向堆空间的对象区,方法调用完成后从栈空间回收。
创建new的局部变量,在栈中和堆中分配空间,当局部变量生命周期结束后,栈空间立刻回收,堆空间区域等待GC回收。
字符串常量,static静态变量在方法区分配空间。
在这里插入图片描述

(1.5)本地方法栈(Native Method Stacks)

线程私有,可理解为java中jni调用。用于支持native方法执行,存储了每个native方法调用的状态。本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。执行引擎通过本地方法接口,利用本地方法库(C语言库)执行。

(2)对象分配规则

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

四、GC算法 垃圾回收

垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。 注意:垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身。换言之,垃圾回收只会负责释放那些对象占有的内存。对象是个抽象的词,包括引用和其占据的内存空间。当对象没有任何引用时其占据的内存空间随即被收回备用,此时对象也就被销毁。但不能说是回收对象,可以理解为一种文字游戏。
引用:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
垃圾:无任何对象引用的对象。
回收:清理“垃圾”占用的内存空间而非对象本身。
发生地点:一般发生在堆内存中,因为大部分的对象都储存在堆内存中。
发生时间:程序空闲时间不定时回收。

(1)对象的生命周期

  1. 创建阶段(Created)
    在创建阶段系统通过下面的几个步骤来完成对象的创建过程:
    (1)为对象分配存储空间
    (2)开始构造对象
    (3)从超类到子类对static成员进行初始化
    (4)超累成员变量按顺序初始化,递归调用超累的构造方法
    (5)子类成员变量按顺序初始化,子类构造方法调用
    一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到应用状态
  2. 应用阶段(In Use)
    对象至少被一个强引用持有。
  3. 不可见阶段(Invisible)
    当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。
    简单说就是程序的执行已经超出了该对象的作用域了。
  4. 不可达阶段(Unreachable)
    对象处于不可达阶段是指该对象不再被任何强引用所持有。
    与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为”GC root”。存在着这些GC root会导致对象的内存泄露情况,无法被回收。
  5. 收集阶段(Collected)
    当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了finalize()方法,则会去执行该方法的终端操作。

不要重载finazlie()方法!原因有两点:
(1)会影响JVM的对象分配与回收速度
在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗CPU时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC。
(2)可能造成该对象的再次“复活”
在finalize()方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又重新变为“应用阶段”。这个已经破坏了Java对象的生命周期进程,且“复活”的对象不利用后续的代码管理。

  1. 终结阶段(Finalized)
    当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
  2. 对象空间重分配阶段(De-allocated)
    垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。

(2)判断对象是否是垃圾算法

(2.1)引用计数算法

堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1(a = b, b被引用,则b引用的对象计数+1)。当引用失效时(一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时),计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。

(2.2)根搜索算法

首先了解一个概念:根集(Root Set)
所谓根集(Root Set)就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。 这种算法的基本思路:
(1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
(2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
(3)重复(2)。
(4)搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
Java和C#中都是采用根搜索算法来判定对象是否存活的。

首先,垃圾回收器将某些特殊的对象定义为GC根对象。所谓的GC根对象包括:
(1)虚拟机栈中引用的对象(栈帧中的本地变量表);
(2)方法区中的常量引用的对象;
(3)方法区中的类静态属性引用的对象;
(4)本地方法栈中JNI(Native方法)的引用对象。
(5)活跃线程。

接下来,垃圾回收器会对内存中的整个对象图进行遍历,它先从GC根对象开始,然后是根对象引用的其它对象,比如实例变量。回收器将访问到的所有对象都标记为存活。 存活对象在上图中被标记为蓝色。当标记阶段完成了之后,所有的存活对象都已经被标记完了。其它的那些(上图中灰色的那些)也就是GC根对象不可达的对象,也就是说你的应用不会再用到它们了。这些就是垃圾对象,回收器将会在接下来的阶段中清除它们。

(3)GC算法

GC最基础的算法有三种:标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。

(3.1)标记 -清除算法

“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
内存中的对象构成一棵树,当有效的内存被耗尽的时候,程序就会停止,做两件事,第一:标记,标记从树根可达的对象(途中水红色),第二:清除(清楚不可达的对象)。标记清除的时候有停止程序运行,如果不停止,此时如果存在新产生的对象,这个对象是树根可达的,但是没有被标记(标记已经完成了),会清除掉。

缺点:递归效率低性能低;释放空间不连续容易导致内存碎片;会停止整个程序运行;
在这里插入图片描述

(3.2)复制算法

“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
把内存分成两块区域:空闲区域和活动区域,第一还是标记(标记谁是可达的对象),标记之后把可达的对象复制到空闲区,将空闲区变成活动区,同时把以前活动区对象1,4清除掉,变成空闲区。

速度快但耗费空间,假定活动区域全部是活动对象,这个时候进行交换的时候就相当于多占用了一倍空间,但是没啥用。
在这里插入图片描述

(3.3)标记-压缩算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在这里插入图片描述
JVM垃圾回收分代收集算法:
综合了上述算法优略
1, 分代GC在新生代的算法:采用了GC的复制算法,速度快,因为新生代一般是新对象,都是瞬态的用了可能很快被释放的对象。
2, 分代GC在年老代的算法 标记/整理算法,GC后会执行压缩,整理到一个连续的空间,这样就维护着下一次分配对象的指针,下一次对象分配就可以采用碰撞指针技术,将新对象分配在第一个空闲的区域。

(4)垃圾回收器

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

(4.1)串行垃圾回收器

串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序(单CPU、新生代空间较小及对暂停时间要求不是非常高的应用)。是client级别默认的GC方式。
在这里插入图片描述

(4.2)并行垃圾回收器

并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,当执行垃圾回收的时候它也会冻结所有的应用程序线程。
适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式。
在这里插入图片描述

(4.3)并发标记扫描垃圾回收器

并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
(1)当标记的引用对象在Tenured区域;
(2)在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。

五、GC分析 命令调优

JVM调优,调的是什么?
每一次Full GC都会使JVM停止运行–>使Full GC不执行,使Minor GC尽可能少地执行

(1)GC日志分析

摘录GC日志一部分(前部分为年轻代gc回收;后部分为full gc回收):

2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs]
2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]

通过上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen属于Parallel收集器。其中PSYoungGen表示gc回收前后年轻代的内存变化;ParOldGen表示gc回收前后老年代的内存变化;PSPermGen表示gc回收前后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁,耗时短;full gc 会对整个堆内存进行回城,耗时长,因此一般尽量减少full gc的次数
Young GC日志:
在这里插入图片描述
Full GC日志:
在这里插入图片描述

(2)调优命令

Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo

  • jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
  • jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jmap,JVM Memory Map命令用于生成heap dump文件
  • jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
  • jstack,用于生成java虚拟机当前时刻的线程快照。
  • jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

(3)调优工具

常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。

  • jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控
  • jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
  • MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
  • GChisto,一款专业分析gc日志的工具

(4)减少GC开销的措施

根据上述GC的机制,程序的运行会直接影响系统环境的变化,从而影响GC的触发。若不针对GC的特点进行设计和编码,就会出现内存驻留等一系列负面影响。为了避免这些影响,基本的原则就是尽可能地减少垃圾和减少GC过程中的开销。具体措施包括以下几个方面:
(1)不要显式调用System.gc()
此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。
(2)尽量减少临时对象的使用
临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。
(3)对象不用时最好显式置为Null
一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。
(4)尽量使用StringBuffer,而不用String来累加字符串
由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。
(5)能用基本类型如Int,Long,就不用Integer,Long对象
基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。
(6)尽量少用静态对象变量
静态变量属于全局变量,不会被GC回收,它们会一直占用内存。
(7)分散对象创建或删除的时间
集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

六、执行引擎

分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。
解释器能快速的解释字节码,但执行却很慢。 解释器的缺点就是,当一个方法被调用多次,每次都需要重新解释。

七、Java代码编译和执行整个过程

开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被类加载器装入内存,一旦字节码进入虚拟机,它就会被解释器(执行引擎)解释执行。
步骤1:Java代码编译是由Java源码编译器来完成,也就是Java代码到JVM字节码(.class文件)的过程。
在这里插入图片描述
步骤2:Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:
在这里插入图片描述
Java代码编译和执行的整个过程包含了三个重要机制:

  • Java源码编译机制
  • 类加载机制
  • 类执行机制

1、Java源码编译机制

Java源码编译由以下三个过程组成:

  • 分析和输入到符号表
  • 注解处理
  • 语义分析和生成class文件
    在这里插入图片描述
    最后生成的class文件由以下部分组成:
    ①结构信息:包括class文件格式版本号及各部分的数量与大小的信息
    ②元数据:对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池
    ③方法信息:对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息
    例:
    Math.java
class Math{
	public static final Integer CONSTANT_1 = 666;
	public static Object obj = new Object();
	public int math(){
		int a = 1;
		int b = 2;
		int c = (a+b)*10;
		return c;	
	}
	public static void main(String[] args){
		Math math = new Math();
		Math math2 = new Math();
		System.out.println(math.math());
	}
}

使用javac Math.java编译生成Math.class文件,再调用javap Math.class > Math.txt进行反编译生成可读的字节码文件
Math.txt

Compiled from "Math.java"
class Math{
	Math();
	Code:
	0:aload_0
	1:invokespecial #1		//Method java/lang/Object."<init>":()V
	4:return

	public int math();
	Code:
	//int a = 1;
	0:iconst_1	//将Int类型常量1压入操作数栈(操作数栈:操作数栈用于对操作数进行中间的承接)
	1:istore_1	//将int类型值存入局部变量1 (将操作数1出栈并放入局部变量栈a)
	//int b =2
	2:iconst_2
	3:istore_2
	4:iload_1	//从局部变量1中装载int类型值(获得a的值1)将1压入操作数栈
	5:iload_2 //将2压入操作数栈
	6:iadd	//执行加法:操作数栈从栈中弹出2个操作数(1,2),执行加法操作数后得到结果3,重新压入栈
	7:bipush	10//将整数10压入栈
	9:imul	//执行乘法运算
	10:istore_3//将int类型存入局部变量栈
	11:iload_3//加载局部变量3并返回
	12:ireturn
	
	public static void main(java.lang.String[]);
	Code:
	0:new #2				//class Math
	3:dup
	4:invokespecial #3	//Method"<init>":()V
	7:astore_1
	8:getstatic #4			//Field java/lang/System.out:L java/io/PrintStream;
	11:aload_1
	12:invokevirtual #5	//Method math:()I
	15:invokevirtual #6	//Method java/io/PrintStream.println:(I)V
	18:return
}

2、类加载机制

JVM类加载通过ClassLoader及其子类完成的,类的层次关系和加载顺序可以由下图描述:
在这里插入图片描述
①Bootstrap ClassLoader
负责加载 J A V A H O M E j r e / l i b / r t . j a r c l a s s C + + C l a s s L o a d e r E x t e n s i o n C l a s s L o a d e r j a v a j a r JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类 ②Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括 JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
③App ClassLoader
负责记载classpath中指定的jar包及目录中class
④Custom ClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

3、类执行机制

JVM是基于堆栈的虚拟机。JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。

JVM执行class字节码,线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。栈帧的结构如下图所示:
在这里插入图片描述
例:
调用java Math.class,即采用执行引擎去执行Java文件。开启一个进程,其中主线程以main方法为入口执行执行Math类文件。线程在运行时,JVM为每一个线程分配一个独立的java栈,java栈里存储着栈帧,每个栈帧存储着每个方法运行时的局部变量、数据。本例Math.java存储着两个栈帧,一个为指向main方法的栈帧,一个为指向math函数的栈帧。

八、Java虚拟机和Dalvik虚拟机区别

  • java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码;传统的Java程序经过编译,生成Java字节码保存在class文件中,java虚拟机通过解码class文件中的内容来运行程序。而Dalvik虚拟机运行的是Dalvik字节码,所有的Dalvik字节码由Java字节码转换而来,并被打包到一个DEX(Dalvik Executable)可执行文件中Dalvik虚拟机通过解释Dex文件来执行这些字节码。
  • Dalvik可执行文件体积更小。SDK中有一个叫dx的工具负责将java字节码转换为Dalvik字节码。
  • java虚拟机与Dalvik虚拟机架构不同。java虚拟机基于栈架构。程序在运行时虚拟机需要频繁的从栈上读取或写入数据。这过程需要更多的指令分派与内存访问次数,会耗费不少CPU时间,对于像手机设备资源有限的设备来说,这是相当大的一笔开销。Dalvik虚拟机基于寄存器架构,数据的访问通过寄存器间直接传递,这样的访问方式比基于栈方式快的多.
发布了74 篇原创文章 · 获赞 15 · 访问量 6256

猜你喜欢

转载自blog.csdn.net/qq_29966203/article/details/90756633
今日推荐