Java学习笔记(二)JVM运行原理

来源:互联网 发布:刷会员的软件 编辑:程序博客网 时间:2024/05/01 13:06

继上篇java基础总结篇后,来总结一下Java的编译,JVM启动流程和运行原理。首先简要介绍一下JDK和JRE,JDK是针对Java开发人员的产品,它包括了Java运行时环境JRE、Java工具(编译工具javac、生成文档工具javadoc等)和Java基础类库。JRE是运行JAVA程序所必须的环境集合,包含JVM标准实现及Java核心类库。JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心部分,所有的java程序会首先被编译为.class文件,这种文件可以在虚拟机上执行,也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与系统交互,有虚拟机将程序解释给本地系统执行。只有JVM还不能执行class,因为在解释class的时候JVM需要调用解释所需要jre中的类库lib。JVM屏蔽了与具体操作系统平台相关的信息,使得程序只需生成在java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java中一切皆对象,它的运行需要核心类库的支持,jdk中还提供一些java工具和基础类库,并且形成了三个版本的jdk,J2SE、J2EE和J2ME,它们所提供的类库不同,集成的框架不同,定义的标准不同。J2EE只是Java的企业应用,有一系列的规范集,包括JDBC、JNDI、EJBs、RMI、JSP、Servlets、XML、JMS、Java IDL、JTS、JTA、JavaMail和JAF等规范集。J2EE的新名称为JavaEE,J2EE只是一个企业应用,而我们需要一个跨J2SE/WEB/EJB容器的微容器,保护我们的业务核心组件,以延续它的生命力,而不是依赖J2SE/J2EE。

一、编译

Java源文件需要被编译成class文件才能被JVM解释执行,下面看一下.class字节码文件都有哪些写内容呢?

java源码如下:

package test;class A {String name = "A";public void print(){int a = 6;System.out.println(name);};}public class B extends A{String name = "B";public void print(){int a = 8;System.out.println(name);}}

编译生成的B.class字节码文件16进制显示部分如下:


其中包含了:魔数及主次版本信息、常量池、访问标识access_flags、(类索引、父类索引、接口索引集合)、字段表集合、方法表集合、属性表集合。我们通过javap工具来看一下class文件,运行javap -verbose B.class:




结果主要显示了常量池和方法的字节码信息,我们这里不对class中其它的信息做讨论,只查看方法和常量池,因为它涉及到JVM解释执行时的符号引用与直接引用。

常量池constant pool里面主要存放两大类数据,字面量和符号引用。字面量如文本字符串,声明为final的常量值。符号引用包括类和接口的全限定名,字段的名称和描述符,以及方法的名称和限定符。

由于java代码在进行javac编译时,并不像C和C++那样有连接这一步,而是在虚拟机加载class文件的时候进行动态链接。也就是说class文件中,并不会保存各个方法字段的最终内存布局信息。当虚拟机运行的时候,需要从常量池获得对应的符号引用,再在类创建时或者运行时解析,翻译到具体的内存地址中。从上图中可以看到,常量类型有Class、Utf8、Fieldref、Methodref、String、NameAndType等,我们来考察print()方法里的一条字节码指令:

invokevirtual #27 //Method java/io/PrintStream.println:(Ljava/lang/String;)V

它的操作码是invokevirtual,操作数时#27,这个操作数是指常量池的下标,那么去找下标为27的常量池项,是:

#27 = Methodref #28.#30 // java/io/PrintStream.println:(Ljava/lang/String;)V

后面的#28#30也是常量池的两项,顺着这条线索把能传递引用到的都找出来,会看到(按深度优先顺序):

#27 = Methodref #28.#30 // java/io/PrintStream.println:(Ljava/lang/String;)V

#28 = Class #29 // java/io/PrintStream

#29 = Utf8 java.io.PrintStream

#30 = NameAndType #31.#32 // println:(Ljava/lang/String;)V

#31 = Utf8 println

#32 = Utf8 (Ljava/lang/String;)V

由此可以看出,Class文件中invokevirtual指令的操作数经过几层间接之后,最后都是有字符串来表示的。这就是Class文件里的“符号引用”,由字符串去找类的class对象的方法列表里的这个匹配的方法,最终将找到的内存地址写入常量池#27里,这就是直接引用了,下次在调用此命令时就不需要再一层一层解析,当然这个过程可能不同的虚拟机会有不同的实现,不同的方法或属性它的实现方法也是有所不同的,这里暂不讨论。

二、JVM原理

了解了编译之后,就来看看Java程序运行的过程是什么原理呢?Java程序的运行是在JVM中解释执行的,那么JVM是如何工作的呢?JVM是java程序运行的环境,同时是一个操作系统的一个应用程序进程,因此它有它自己的生命周期,也有自己的代码和数据空间。

(一)JVM环境

JVM通过java.exe来完成,通过下面4步来完成JVM环境。1.创建JVM装载环境和配置2.装载JVM.dll3.初始化JVM获得JNIEnv接口4.找到main()方法并运行过程如下图:


(二)JVM实例

以上完成一个JVM的环境的装载与创建,那么JVM实例是怎么做事的呢?JVM体系主要分为三个子系统和两大组件,分别是:类装载器子系统,执行引擎子系统和GC子系统,组件是内存运行时数据区域和本地接口。

1.JVM体系结构


2.JVM运行时数据区

(1)程序计数器
JVM中运行的程序是支持多线程的,这里的多线程并不是指操作系统的多线程,是属于JVM内部自己的多线程,所以线程的创建调度执行都必须JVM自己实现。程序计数器就是为多线程而生,是当前线程执行字节码的信号指示器,是线程私有的,它的生命周期和线程相同分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
(2)虚拟机栈
Java虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行时都会同时创建一个栈帧用于存储局部变量表、操作栈、动作链接、方法出口等信息。线程私有,生命周期和线程相同,都有各个独立的计数器,各不影响。每个方法被调用直至执行完成的过程,就对应这一个栈帧在虚拟机栈中从入栈到出栈的过程。
(3)本地方法栈
和虚拟机栈差不多,但是本地方法栈是服务于虚拟机所使用的Native方法服务,在HotSpot虚拟机中,本地方法栈和Java栈合二为一了。
(4)堆
堆是JVM中最大的,应用的对象和数据都是存在在这个区域。这块区域是线程共享的,也是GC主要的回收区。
(5)方法区
方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息等)、静态变量、常量以及编译后的代码等。在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或者接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来了。

3.类装载子系统

Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由JVM中的类装载器完成的,实质是将Class文件从硬盘读取到内存。类的加载是动态的,并不是一次性将所有的类都加载进内存,而是需要时才加载。
类的装载方式有两种:
(1)隐式装载,程序在运行过程中当碰到通过new等方式生成对象时,隐式调用类装载器加载对应的类型到JVM中。
(2)显式装载,通过class.forname()等方法,显式加载需要的类。
与类相关的父类和实现的接口也都会被加载。
Java中类大致分为三种:
(1)系统类
(2)扩展类
(3)由程序员自定义的类
根据类的不同种类,类加载器也不只是一种,类加载器分为以下几种:
(1)启动类加载器(Bootstrap class loader):这个类装载器是在JVM启动的时候创建的,他负责装载Java API,包含Object对象。和其它的类装载器不同的是这个装载器是通过native code来实现的,而不是Java代码。
(2)扩展类加载器(Extension class loader):它装载除了基本的Java API以外的扩展类。它也负责装载其他的安全扩展功能。
(3)系统类加载器(System class loader):System class loader加载的是应用程序类,他负责加载用户在$CLASSPATH里指定的类。
(4)用户自定义加载类(User-defined class loader):这是应用程序开发者直接用代码实现的类装载器。
每一个类装载器都有一个自己的搜索类的路径,而且都有一个自己的命名空间来保存已经装载的类。当一个类装载器装载类时,它会通过保存在命名空间里的类全局限定名进行搜索来检测这个类是否已经被装载了。所以即使全限定名一样的类,命名空间不一样,他们任然是不同的类。
那么这些类是如何相互合作的呢?系统会调用哪个类去执行装载呢?
其实这些类存在着父子关系,他们之间实现了一种委托模型,在装载类时,会先请示父类使用其搜索路径载入,如果父类找不到,那么自己才依照自己的搜索路径搜索。
那么类的装载过程是什么呢?
类装载器就是寻找类或借口字节码文件进行解析并构造JVM内部对象表示的组件,装载经过以下步骤:
(1)装载:查找和导入class文件
(2)链接:其中解析步骤可省略 
         a.检查:检查载入的class文件数据的正确性
         b.准备:给类的静态变量分配存储空间 
         c.解析:将符号引用转成直接引用
(3)初始化:对静态变量,静态代码块执行初始化工作
类文件被装载解析后,在JVM中都有一个对应的java.lang.Class对象,提供了类结构的信息描述,程序员可以通过对象或者是类来查看相应的Class信息,Class类没有public的构造方法,Class对象是在装载时由JVM通过调用类装载器中的defineClass()方法自动构造。
前面讲到过JVM的方法区,类的信息就是被装载到这里去的,包括:类型基本信息、常量池、域信息、方法信息、异常表、类变量、常量、对类加载器的引用、对Class对象的引用等,这些信息是从编译后的Class字节码文件中提取的。方法区是被所有线程共享的,所以还要考虑线程安全。方法区大小不固定,JVM可以根据需要调整,可以在堆中分配,同时也存在着垃圾的收集。
这里再补充一下常量池的知识,我们知道Class文件中就有关于类的常量池,用来进行符号连接操作的,当加载进方法区后就生成类的运行时常量池,但是是每个类所私有的,所以在JVM中还实现了一种共享的全局字符串池,它里面的内容是在类加载完成后,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中,这个string pool是在方法区的,在程序中用new生成String对象时,都会在堆中分配存储空间,可以通过intern来实现对常量池中String的引用,这样会节省内存空间。除了共享字符串常量池,好像还可以通过Integer等包装类实现相应类型的共享池。

4.执行引擎子系统

(参考:http://www.cnblogs.com/muffe/p/3541645.html)
JVM通过执行引擎来完成字节码的执行,在执行过程中JVM采用自己的一套指令系统,每个线程在创建后,都会产生一个程序计数器(pc)和栈(stack)。
pc:存放了下一条将要执行的指令;
stack:存放Stack Frame(栈帧),最上面的表示为当前正在执行的方法。每个方法的执行都会产生Stack Frame,Stack Frame中存放了传递给方法的参数、方法内部的局部变量表、操作数栈、动态链接和方法返回地址等信息。每一个方法的调用的过程,就对应着一个栈帧在虚拟机栈中入栈道出栈的过程;
        局部变量表:局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。它被组织为一个以字长(32位)为单位、从0开始计数的数组,其中第0位索引默认是用于传递所属对象实例的引用,在方法中可以通过关键字“this”来访问隐含的参数。局部变量表是可以重用的,当当前字节码PC计数器值超出了某个变量作用域,那么这个变量对应的SLOT就可以交给其他变量使用。
        操作数栈:存放指令运算的中间结果。
主要的执行技术有:解释,即时编译,自适应优化,芯片级直接执行
(1)解释:属于第一代JVM;
(2)即时编译:JIT属于第二代编译;
(3)自适应优化:(目前Sun的HotSpotJVM采用这种技术)吸取第一代JVM和第二代JVM经验,采用两者结合的方式。开始对所有的代码都采取解释执行的方式,并监视代码运行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行仔细优化。若方法不频繁使用,则取消编译过的代码,仍对其进行解释执行;
(4)芯片直接执行:内嵌在芯片上,用本地方法执行Java字节码。

5.对象创建

讲解完Class文件格式、JVM运行时数据区、JVM加载类原理和JVM执行原理,我们接下来就看看JVM是如何实例化一个对象的呢?
参考:http://www.cnblogs.com/chenyangyao/p/5296807.html
对象变量是在栈中,而对象实例的存储是在堆中分配内存的。对象的创建过程一般是从new指令开始的,JVM首先对符号引用进行解析,如果找不到对应的符号引用,那么这个类还没有被加载,因此JVM便会进行类加载过程。符号引用解析完成之后,JVM会为对象在在堆中分配内存,HotSpot虚拟机实现的JAVA对象包括三个部分:对象头、实例字段和对齐填充字段。
对象头:主要包括两部分。1.用于存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等。2.类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。然而,并不是查找对象的元数据一定要通过对象本身,比如数组对象的对象头中必须保存记录数组长度的数据,因为从数组元数据中无法确定数组大小,但是从普通对象的元数据信息可以确定java对象的大小。
实例字段:存储从父类继承下来的实例字段。字段的存储顺序会受到虚拟机的分配策略和字段在java源码中定义的顺序的影响。
对齐填充:不是必须。
为对象分配玩内存后,JVM会将该内存(除了对象头区域)进行零值初始化,这也就解释了为什么JAVA的属性字段无需显式初始化就可以被使用,而方法的局部变量必须显式初始化后才能访问。然后调用类中{}内定义的初始化代码块,再调用构造函数,调用顺序会一直上溯到Object类。

6.GC子系统

在程序运行过程中会生成很多的对象实例,当这个实例没有引用变量引用时,就不能够被访问,那么这个实例就成为垃圾数据,内存垃圾过多就会引发内存耗尽,如果采用程序中手动释放就可能会发生释放不合理造成非法引用。所以对于没有变量引用的实例,Java会利用GC子系统自动进行内存回收。接下来看一下GC实现原理:
首先来看一些垃圾回收有关的算法:参考:http://blog.csdn.net/yuguiyang1990/article/details/13303003

1.引用计数法(Reference Counting Collector)

引用计数法是垃圾收集器中的早期策略。在这种方法中,堆中每个对象都有一个引用计数。当一个对象被创建时,且将该对象分配给一个变量,该变量计数设置为1,。当任何其他变量被赋值为这个对象的引用时,计数加1,但当一个对象的某个引用超过了生命周期或者被设置为一个新值时,对象引用计数减1.任何引用计数为0的对象可以被当成垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1.
优点:引用计数收集器可以很快地执行,交织在运行过程中。对程序不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。
2.Tracing算法(Tracing Collector)
tracing算法是为了解决引用计数的问题而提出,它使用了根基概念。基于tracing算法的垃圾收集器从根开始扫描,识别出哪些对象可达,那些对象不可达,并用某种方式标记科大对象。基于此法的垃圾收集器也称标记和清除(mark-and-sweep)垃圾收集器。对象引用遍历从一组对象开始,沿着整个对象图上的每条链,递归确定可到达的对象。如果某对象不能从这些根对象的一个到达,则将它作为垃圾收集。在对象遍历阶段,GC必须记住哪些对象可以到达,以便删除不可到达的对象,这称为标记(marking)对象。下一步,GC要删除不可到达的对象。删除时,有些GC只是简单的扫描堆栈,删除未标记的对象,并释放他们的内存以生成新的对象,这叫清除(sweeping)。这种方法问题在于内存会分成好多小段,而他们不足以用于新的对象,但是组合起来却很大。因此,许多GC可以重新组织内存中的对象,并进行压缩(compact)形成可利用空间。为此,GC需要停止其他活动。这种方式意味着所有与应用程序相关的工作停止,只有GC运行。结果,在响应期间增减了许多混杂请求。另外,更复杂的GC不断增加或同时运行以减少或者清除应用程序的中断。有的GC使用单线程完成这种工作,有的则采用多线程以增加效率。
3.compacting算法(Compacting Collector)
为了解决堆碎片问题,基于tracing的垃圾回收吸收了Compacting算法的思想,在清除过程中,算法将所有的对象移到堆的一端,堆的另一端就变成了一个相邻的空闲内存区,收集器会对他移动的所有对象的所有引用进行更新,使得这些引用在新的位置能识别原来的对象。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。
4.coping算法(Coping Collector)
该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满了,基于coping算法的垃圾收集就从根集中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。
5.generation算法(Generation Collector)
stop-and-copy垃圾收集器的一个缺陷是收集器必须是复制所有活动对象,这增加了程序等待时间,这是coping算法低效的原因。在程序设计中有这样的规律:多数对象存在的时间比较短,少数存在时间比较长。因此,generation算法将堆分成两个或多个,每个子堆作为对象的一代(generation)。由于多数对象存在时间较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行存活下来的对象移到下一最高贷的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。
6.adaptive算法(Adaptive Collector)
在特定情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择合适的算法的垃圾收集器。

那么Java中的GC是怎样实现的呢?自从1999年的JDK1.2开始,JDK中默认的虚拟机是HotSpot,因此这边讨论的也是HotSpot的GC实现。其实GC主要完成3件事:确定哪些内存需要回收,确定什么时候需要执行GC,如何执行GC。
首先讲一下JavaGC的标记,HotSpot采用的是Tracing标记算法,找到所有的GC的根节点(GC Root),将他们放到队列里,然后依次递归遍历所有的根节点以及引用的所有子节点和子子节点,将所有被遍历到的节点标记成live。Java中,可以当做GC Root的对象有以下几种:
1.虚拟机(JVM)栈中的引用对象
2.方法区中的类静态属性引用的对象
3.方法区中的常量引用的对象
4.本地方法栈中JNI属性引用的对象
Java内存分配与回收的机制概括的说,就是:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。
年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(约80%很快消亡),这个GC机制被称为Minor GC。年轻代分为3个区域:Eden区和两个存活区(Survivor0、Survivor1)。内存分配过程为:
1.绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象会很快消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
2.当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);(对象经复制后,地址也会改变,在一篇博客中看到先计算出新地址,然后遍历每个实例将新旧地址对应修改,然后进行迁移);
3.此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0;(在一篇博客中有看到,GC过程为标记,计划和清理,引用更新和压缩。但这里没有清理,压缩也不需要判断是否压缩,只要满了就会执行压缩过程,到底是这样的呢?)
4.当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivors1,以后Eden区执行Minor GC后,将剩余的对象添加到Survivor1(此时,Survivor0是空白的)。
5.当两个存活区切换了多次之后(默认15次,可设置),仍然存活的对象复制到老年代。
从上面的过程可以看出,Eden区是连续的控件,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着的还活着的对象,而Eden区和另一个Survivor区的内容都不需要了,可以直接清空。下次GC,两个Survivor角色互换。这种垃圾回收方式就是先前提到的“停止-复制(stop-and-copy)”清理法。由于大部分对象都是短命的,甚至存活不到Survivor中,所以Eden和Survivor的比例较大,默认为8:1,但是启动虚拟机时可以设置的。HotSpot虚拟机采用了两种技术加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),这两种做法分别是:由于Eden是连续的,因此前一种技术核心是跟踪创建的最后一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可。后一种技术是对于多线程而言的,将Eden分区为若干段,每个线程独立使用,避免相互影响。两种技术可以结合使用。
年老代:对象在年轻代存活足够长时间还没被清理会被复制到年老代,空间一般比年轻代大,发生的GC次数也少。当年老代内存不足时,将执行Major GC,也叫Full GC。
如果对象比较大,Young空间不足,则大对象会直接分配到年老代上(避免使用短命大对象),可以手动设置直接升入年老代的对象大小。
可能存在老年代对象引用新生对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然低效。解决方法是,年老代中维护一个512byte的块——“card table”,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查询这里即可。
年老代的内存清理如果使用停止复制算法,则相当低效。一般使用标记-整理算法,即:标记出仍然存活的对象,将所有存活的对象向一端移动,以保证内存连续。在发生Minor GC时,虚拟机会检查每次晋升进入年老代的大小是否大于年老代剩余空间大小,如果大于,直接出发Full GC,否则查看是否设置了允许担保失败,如果允许,则只会进行MinorGC,如果不允许,则进行FullGC。
方法区(永久代):永久代存放的是一些常量,类等信息。对于无用类的回收,必须保证:
1.类的所有实例都已经被回收
2.加载类的ClassLoader已经被回收
3.类对象的Class对象没有被引用
永久代的回收不是必须的,在Java SE8特性中已经被移除。
什么时候发生GC呢?在上面已经描述过了,当Eden区和年老代区空间不足时都会发生GC的,另外也可以在程序中代码显式调用,还有其他一下特殊情况,Windows内存不足等等。

详解finalize函数:
finalize是位于Object类的一个方法,该方法的访问修饰符为protected,由于所有类为Object的子类,因此用户类很容易访问到这个方法。由于,finalize函数没有自动实现链式调用,我们必须手动的实现,因此finalize函数的最后一个语句通常是super.finalize()。通过这种方式,我们可以实现从下到上实现finalize调用,即先释放自己的资源,然后再释放父类资源。JVM保证调用finalize函数之前,这个对象是不可达的,但是JVM不保证这个函数一定会被调用。其实这个函数用来释放很多对象、资源不是一种好方式,一是因为GC为了能够支持finalize函数,要对覆盖这个函数的对象做很多附加工作,Finalizer由另外一个线程执行finalize对象的回收,GC把每一个需要执行Finalizer的对象放到一个队列中去,然后启动另一个线程来执行这些Finalizer,GC线程继续去删除其他待回收的对象,在下一个GC周期,这些执行完Finalizer的对象内存才会被回收。二就是执行完这些对象可能变为可达的,要第二次执行GC才能确定是否回收。三是由于GC调用finalize的时间不确定,因此这种方式释放资源也是不确定的。对于finalize到底有什么用处?用到的时候再总结。

了解了GC原理后,在编程时,就可以通过一些方式和技巧,让GC运行的更加有效率。
关于Java的三个引用类:
SoftReference:具有较强的引用功能。只有当内存不足时,才进行回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象保证Java抛出OutOfMemory异常之前,被设置为null,它可以用于实现一些常用图片的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。
WeakReference:Weak引用对象与Soft引用对象的最大不同之处就在于:GC在进行回收时,需要通过算法检查是否回收soft引用对象,而对于Weak引用对象,GC总是进行回收。

PhantomReference:用得比较少,主要辅助finalize函数的使用。Phantom对象指一些对象,它们执行完了finalize函数,并为不可达对象,但是他们还没有被GC回收。这种对象可以辅助finalize进行一些后期的回收工作,通过覆盖Reference的clear方法,增强资源回收机制的灵活性。

编码建议:

1.最基本的建议就是尽早释放掉无用的对象(设为null)。
2.注意集合数据类型,包括数组、树、图、链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外注意一些全局变量,以及一些静态变量。这些变量往往容易引起悬挂对象,造成内存浪费。
3.当程序有一定的等待时间,程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。使用增量式GC可以缩短Java程序的暂停时间。
4.如果需要使用经常使用的图片,可以使用soft应用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory。
5.尽量少使用finalize函数。finalize函数是Java提供给程序员一个释放资源的机会。但是它会加大GC的工作量。

垃圾收集器:
在新生代采用的停止复制算法中,“停止”的意义在回收时,需要暂停其他所有线程的执行。这个是很低效的,现在各种新生代收集器越来越优化这点,但仍然只是将停止的时间变短,并未彻底取消停止。下面是HotSPot中支持的集中收集器:
  • Serial收集器:新生代收集器,使用停止复制算法,使用一个线程进行GC,其它工作线程暂停。
  • ParNew收集器:新生代收集器,使用停止复制算法,Serial收集器多线程版本,用多个线程进行GC,其它工作线程暂停,关注缩短垃圾收集时间。
  • Parallel Scavenge收集器:新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,这个值可以设置。这种收集器高效利用CPU,适合后台运算。(但是没懂,怎么去实现,占用比,不是发生满了就调用GC吗?GC工作不是固定的吗?)
  • Serial Old收集器:老年代收集器,单线程收集器,使用标记整理(整理的方法是清理和压缩,清理是将废弃的干掉,压缩是移动对象,紧凑)。单线程,其他暂停。
  • Parallel Old收集器:老年代收集器,多线程,使用标记整理(整理是汇总和压缩,汇总是将幸存的复制到预先准备区),其他暂停。
  • CMS收集器:老年代收集器,致力于获取最短回收停顿时间,使用标记清除算法(会产生碎片,可以设置是否并压缩或几次不压缩的GC后进行压缩),多线程,并发收集(用户线程和GC同时工作),它采用3次标记(初始标记、重新标记,并发标记),1次清除(并发清除),只有初始标记和重新标记需要短暂停顿。(它进行压缩时也要停顿吧,那为什么标记时也要停顿?前面的停顿难道不是因为要复制或压缩才进行的停顿吗?标记不用吧)
  • G1收集器:未知。
另外还有一个增量式GC,它是一种通过一定回收算法,把一个长时间的中断划分为很多个小的中断,来减少GC对用户程序的影响,HotSpot增量式GC的实现是采用Train GC算法。基本想法是,将堆中所有的对象按照创建和使用情况进行分组,将使用频繁高的和具有相关性的对象放在一队,随着程序的运行,不断对组进行调整。当GC运行时,他总是先回收最老的对象(最近最少访问),这样,每次GC运行只回收一定比例的不可达对象,保证程序的顺畅运行。

关于收集器还是要去实践一下,并不能看懂,以后有机会再进行整理。

以上如果有不足之处,欢迎指出。







 
原创粉丝点击