本文分为20多个问题,通过问题的方式,来逐渐理解jvm,由浅及深。希望帮助到大家。
正确的顺序如下:
1父类静态代码块
2父类静态变量
3子类静态代码块
3子类静态变量
4父类成员变量赋值
5父类构造方式开始执行
6子类成员变量赋值
7子类构造方式开始执行
需要注意的地方是静态变量和静态代码块谁在前面谁先执行。
看一段代码示例:
package com.jdk.learn;
/** * Created by ricky on 2017/8/25. * * 类加载器加载顺序考究 *
* */ public class ClassLoaderTest {
public static void main(String[] args) { son sons=new son(); } }
class parent{ private static int a=1; private static int b; private int c=initc(); static { b=1; System.out.println("1.父类静态代码块:赋值b成功"); System.out.println("1.父类静态代码块:a的值"+a); } int initc(){ System.out.println("3.父类成员变量赋值:---> c的值"+c); this.c=12; System.out.println("3.父类成员变量赋值:---> c的值"+c); return c; } public parent(){ System.out.println("4.父类构造方式开始执行---> a:"+a+",b:"+b); System.out.println("4.父类构造方式开始执行---> c:"+c); } }
class son extends parent{ private static int sa=1; private static int sb; private int sc=initc2(); static { sb=1; System.out.println("2.子类静态代码块:赋值sb成功"); System.out.println("2.子类静态代码块:sa的值"+sa); } int initc2(){ System.out.println("5.子类成员变量赋值--->:sc的值"+sc); this.sc=12; return sc; } public son(){ System.out.println("6.子类构造方式开始执行---> sa:"+sa+",sb:"+sb); System.out.println("6.子类构造方式开始执行---> sc:"+sc); } } |
执行结果如下:
1.父类静态代码块:赋值b成功
1.父类静态代码块:a的值1
2.子类静态代码块:赋值sb成功
2.子类静态代码块:sa的值1
3.父类成员变量赋值:---> c的值0
3.父类成员变量赋值:---> c的值12
4.父类构造方式开始执行---> a:1,b:1
4.父类构造方式开始执行---> c:12
5.子类成员变量赋值--->:sc的值0
6.子类构造方式开始执行---> sa:1,sb:1
6.子类构造方式开始执行---> sc:12
对变量的赋值初始值为0,对于对象来说为null。
执行了System.exit()方法;
程序正常执行结束;
程序在执行过程中遇到了异常或错误而异常终止;
由于操作系统出现错误而导致Java虚拟机进程;
类的加载机制分为如下三个阶段:加载,连接,初始化。其中连接又分为三个小阶段:验证,准备,解析。
加载阶段:
将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后再堆内创建一个class对象,用来封装类在方法区内的数据结构。
加载class文件的方式:
从本地系统中直接加载
通过网络下载.class文件
从zip,jar等归档文件中加载.class文件
从专有数据库中提取.class文件
将Java源文件动态编译为.class文件
类的加载最终产品是位于堆中的class对象。Class对象封装了类在方法区内的数据结构,并且向java程序员提供了访问方法区内的数据结构和接口。
类加载并不需要等到某个类被主动使用的时候才加载,jvm规范允许类加载器在预料到某个类要被使用的时候就预先加载。如果预先加载过程中报错,类加载器必须在首次主动使用的时候才会报错。如果类一直没有被使用,就不会报错。
验证阶段:
此阶段验证的内容如下
类文件的结构检查:
确保类文件遵从java类文件的固定头格式,就像平时做文件上传验证文件头一样。还会验证文件的主次版本号,确保当前class文件的版本号被当前的jvm兼容。验证类的字节流是否完整,根据md5码进行验证。
语义检查:
检查这个类是否存在父类,父类是否合法,是否存在。
检查该类是不是final的,是否被继承了。被final修饰的类是不允许被继承的。
检查该类的方法重载是否合法。
检查类方法翻译后的字节码流是否合法。
引用验证,验证当前类使用的其他类和方法是否能够被顺利找到。
准备阶段:
通过验证阶段之后,开始给类的静态变量分配内存,设置默认的初始值。类变量的内存会被分配到方法区中,实例变量会被分配到堆内存中。准备阶段的变量会赋予初始值,但是final类型的会被赋予它的值,可以理解为编译的时候,直接编译成常量赋给。如果是一个int类型的变量会分配给他4个字节的内存空间,并赋予值为0。如果是long会赋予给8个字节,并赋予0。
解析阶段:
解析阶段会把类中的符号引用替换成直接引用。比如Worker类的gotoWork方法会引用car类的run方法。
在work类的二进制数据,包含了一个Car类的run的符号引用,由方法的全名和相关描述符组成。解析阶段,java虚拟机会把这个符号引用替换成一个指针,该指针指向car类的run方法在方法区中的内存位置,这个指针就是直接引用。
初始化阶段:
类的初始化阶段就是对垒中所有变量赋予正确的值,静态变量的赋值和成员变量的赋值都在此完成。初始化的顺序参考上方的整理。
初始化有几点需要注意
如果类还没有被加载和连接,就先进行加载和连接。如果存在直接的父类,父类没有被初始化,则先初始化父类。
类分为主动使用和被动使用。主动使用使类进行初始化,被动使用不会初始化。
主动使用有以下六种情形:
1创建类的实例
2访问某个类或接口的静态变量,或者对静态变量进行赋值
3调用类的静态方法
4反射
5初始化一个类的子类
6具有main方法的java启动类
需要注意的是:
初始化一个类的时候,要求他的父类都已经被初始化,此条规则不适用于接口。初始化一个类的时候,不会初始化它所实现的接口,在初始化一个接口的时候,并不会初始化他的父接口。
只有到程序访问的静态变量或者静态方法确实在当前类或当前接口中定义的时候,才可以认为是对类或接口的主动使用。
调用classloader类的loadclass方法加载一个类,不是对类的主动使用,因为loadclass调用的一个子方法具有两个参数,name和resolve,由于resolve是false。在代码中并不会调用resolveClass,所以不会对类进行解析。
被动使用的几种情况:
(1)通过子类引用父类的静态字段,为子类的被动使用,不会导致子类初始化。
class Dfather{ static int count = 1; static{ System.out.println("Initialize class Dfather"); } }
class Dson extends Dfather{ static{ System.out.println("Initialize class Dson"); } }
public class Test4 { public static void main(String[] args) { int x = Dson.count; } } |
上面这个例子中,虽然是以Dson.count 形式调用的,但是因为count是Dfather的静态成员变量,所以只初始化Dfather类,而不初始化Dson类。
(2)通过数组定义类引用类,为类的被动使用,不会触发此类的初始化。
其实数组已经不是E类型了,E的数组jvm在运行期,会动态生成一个新的类型,新类型为:如果是一维数组,则为:[L+元素的类全名;二维数组,则为[[L+元素的类全名
如果是基础类型(int/float等),则为[I(int类型)、[F(float类型)等。
class E{ static{ System.out.println("Initialize class E"); } }
public class Test5 { public static void main(String[] args) { E[] e = new E[10]; } } |
(3)常量在编译阶段会存入调用方法所在类的常量池中。再引用就是直接用常量池中的值了。
class F{ static final int count = 1; static{ System.out.println("Initialize class F"); } }
public class Test6 { public static void main(String[] args) { int x = F.count; } } |
一种特殊的情况如下:
class F{ static final String s = UUID.randomUUID().toString(); static{ System.out.println("Initialize class F"); } }
public class Test6 { public static void main(String[] args) { String x = F.s; } } |
则语句 "Initialize class F" 会打印出来,因为UUID.randomUUID().toString()这个方法,是运行期确认的,所以,这不是被动使用。
类加载器就是用来把类加载到java虚拟机中的一种东西。对于任意的一个类,由他的类加载器和他的类本身确立其在jvm中的唯一性。
jvm内置三大类加载器:
根类加载器又叫bootstrap加载器,该类加载器是最顶层的加载器。负责核心类库的加载。比如java.lang.*等。加载路径可以通过sun.boot.class.path指定目录加载。可以通过参数-Xbootclasspath来指定根加载器的路径。根类加载器实现依赖于底层系统。正常的路径在于jre/lib下面。
扩展类加载器又叫ext classloader。用于加载javahome的jre/lib/ext子目录的jar包。或者从java.ext.dirs的指定路径加载。如果把用户的jar放在这个目录下也会加载。扩展类是纯java 的,是classloader的子类。
系统类加载器又叫system classloader。也叫应用类加载器。从环境变量的classpath下面加载类。是classloader的子类。可通过系统属性的java.class.path进行指定,可通过-classpath指定。平时项目都是通过它加载。
用户自定义类加载器,用户可以继承ClassLoader类,实现其中的findClass方法,来实现自定义的类加载器。
自定义类加载器必须继承classloader。需要实现里面的findClass方法。我们可以传入路径,通过二进制输出流,将路径内容读取为二进制数组。通过调用defineClass方法定义class。
当一个类加载器调用loadClass之后,并不会直接加载。先去类加载器的命名空间中查找这个类是否被加载,如果已经被加载,直接返回。如果没有被加载。先请求父类加载器加载,父类加载器也没法加载,就再请求父类,直到根节点,如果找到了就代为加载,放到自己的缓存中,没找到就由自己进行加载,加载不了就报错。
双亲委派机制的优点是能够提高软件系统的安全性,在此机制下,用户自定义的类加载器不可能加载应该由父类加载器加载的可靠类,从而防止恶意代码替代父加载器。
可以通过重写类加载器的loadClass 的方式里面的逻辑来进行破坏,传统的是先一层层找。但是破坏的话,改变逻辑,先从自己上面找。
参考java高并发书的 168页。
每一个类加载器实例都有各自的命名空间,命名空间是由该类加载器及其所有的父类加载器构成的。在同一个命名空间中,不会出现类的完整名字相同的两个类,在不同命名空间中,可能出现类的完整名字相同的两个类。
使用同一个类加载器,加载相同类,二者的引用是一直的,class对象相等。
使用不同类加载器或者同一个类加载器的不同实例,去加载一个class,则会产生多个class对象。
参考java高并发书的170页。
由同一类加载器加载的属于相同包的类组成了运行时包。运行时包是由类加载器的命名空间和类的全限定名称共同组成的。这样做的好处是变用户自定义的类冒充核心类库的类,比如java.lang.string类的方法getChar只是包访问权限。用于此时伪造了一个java.lang.hackString,用自己的类加载器加载,然后尝试访问,这样是不行的。类加载器不同。
每一个类在经过加载之后,在虚拟机中就会有对应的class实例。类C被类加载器CL加载,CL就是C的初始化类加载器。JVM为每一个类加载器维护了一个类列表。该列表记录了将类加载器作为初始化类加载器的所有class。在加载一个类的时候,类加载器先在这里面寻找。在类的加载过程中,只要是参与过类的加载的,再起类加载器的列表中都会有这个类。因此,在自定义的类中是可以访问String类型的。
类的最后的生命周期就是卸载。满足以下三个条件类才会被卸载,从方法区中卸载。
1该类的所有实例已经被gc。
2加载该类的classloader实例被回收。
3该类的class实例没有在其他地方引用。
是解释型的。虽然java代码需要编译成.class文件。但是编译后的.class文件不是二进制的机器可以直接运行的程序,需要通过java虚拟机,进行解释才能正常运行。解释一句,执行一句。编译性的定义是编译过后,机器可以直接执行。也正是因为.class文件,是的jvm实现跨平台,一次编译,处处运行。
jvm的内存区域主要分为方法区,堆,虚拟机栈,本地方法栈,程序计数器。
程序计数器:
一块较小的内存区域,是当前线程执行字节码的行号指示器。每个线程都有一个独立的程序计数器。是线程私有的。正是因为程序计数器的存在,多个线程来回切换的时候,原来的线程才能找到上次执行到哪里。执行java方法的时候,计数器记录的是虚拟机字节码指令的地址,如果是native方法,则为空。这个内存区域不会产生out of memorry的情况。
虚拟机栈:
是描述java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法出口等。每一个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机中从入栈到出栈的过程。
栈帧用来存储数据和部分过程结果的数据结构。也被用来处理动态连接,方法返回值,和异常分配。栈帧随着方法调用而创建,随着方法结束而销毁。
本地方法栈:
本地方法栈和虚拟机栈本质一样,不过是只存储本地方法,为本地方法服务。
堆内存:
创建的对象和数组保存在堆内存中,是被线程共享的一块内存区域。是垃圾回收的重要区域,是内存中最大的一块区域。存了类的静态变量和字符常量。
方法区:
又名永久代,用于存储被jvm加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。hotsoptvm用永久代的方式来实现方法区,这样垃圾回收器就可以像java堆一样管理这部分内存。
运行时常量池是方法区的一部分。class文件中除了有类的版本,字段,方法和接口描述等信息外,还有就是常量池,用于存放编译期生成的各种字面量和符号引用。
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制
直接内存也可以由 -XX:MaxDirectMemorySize 指定
直接内存申请空间耗费更高的性能
直接内存IO读写的性能要优于普通的堆内存
当我们的需要频繁访问大的内存而不是申请和释放空间时,通过使用直接内存可以提高性能。
jvm的堆从gc角度可以分为新生代和老年代。
新生代用来存放新生对象,一般占据堆的三分之一空间。由于频繁创建对象。所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。
eden区是java新对象的出生地,如果新对象太大,则直接进入老年代。eden区内存不够的时候,触发一次minorGc,对新生代进行垃圾回收。
servivorfrom区是上一次gc的幸存者,作为这次gc的被扫描者。
servivorto区保留了一次gc的幸存者。
minorgc采用复制算法。
minorgc触发过程:
1:eden、servicorFrom 复制到 ServicorTo,年龄+1
首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);
2:清空 eden、servicorFrom
然后,清空 Eden 和 ServicorFrom 中的对象;
3:ServicorTo 和 ServicorFrom 互换
最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom
区。
老年代
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行
了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没
有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。
永久代
指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
元数据区
在Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间
的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native
memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由
MaxPermSize 控制, 而由系统的实际可用空间来控制。
MetaSpace 大小默认没有限制,一般根据系统内存的大小。JVM 会动态改变此值。
可以通过 JVM 参数配置
-XX:MetaspaceSize : 分配给类元数据空间(以字节计)的初始大小(Oracle 逻辑存储上的初始高水位,the initial high-water-mark)。此值为估计值,MetaspaceSize 的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。
-XX:MaxMetaspaceSize :分配给类元数据空间的最大值,超过此值就会触发Full GC 。此值默认没有限制,但应取决于系统内存的大小,JVM 会动态地改变此值。
pergenman space,常发生在jsp中。
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。jrockit中没有永久代概念。
System.gc()方法的调用
此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
老年代空间不足
老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
永生区空间不足
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。
对措施为:增大survivor space、老年代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
堆中分配很大的对象
所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。
为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。
引用计数法
在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
引用计数法,判断不出循环引用的情况。所以没有采用这种方式。例如
objecta.name = objectb objectb.name = objecta
可达性分析
为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
可作为gc root的对象有
1.Java虚拟机栈(栈帧的本地变量表)中引用的对象
2.本地方法栈 中 JNI引用对象
3.方法区 中常量、类静态属性引用的对象。
标记清除算法
标记-清除(Mark-Sweep)算法,是现代垃圾回收算法的思想基础。
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象(好多资料说标记出要回收的对象,其实明白大概意思就可以了)。然后,在清除阶段,清除所有未被标记的对象。
缺点:
1、效率问题,标记和清除两个过程的效率都不高。
2、空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-整理算法
标记整理算法,类似与标记清除算法,不过它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
优点:
1、相对标记清除算法,解决了内存碎片问题。
2、没有内存碎片后,对象创建内存分配也更快速了(可以使用TLAB进行分配)。
缺点:
1、效率问题,(同标记清除算法)标记和整理两个过程的效率都不高。
复制算法:
复制算法,可以解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可(还可使用TLAB进行高效分配内存)。
图的上半部分是未回收前的内存区域,图的下半部分是回收后的内存区域。通过图,我们发现不管回收前还是回收后都有一半的空间未被利用。
优点:
1、效率高,没有内存碎片。
缺点:
1、浪费一半的内存空间。
2、复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。
分代收集算法:
当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,就选用复制算法。
而老年代中,因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记清理”或者“标记整理”算法来进行回收。
图的左半部分是未回收前的内存区域,右半部分是回收后的内存区域。
对象分配策略:
对象优先在 Eden 区域分配,如果对象过大直接分配到 Old 区域。
长时间存活的对象进入到 Old 区域。
改进自复制算法
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM 公司的专门研究表明,新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
HotSpot 虚拟机默认 Eden 和 2 块 Survivor 的大小比例是 8:1:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的内存会被“浪费”。当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
强引用
平时使用的new就是强引用,把一个对象赋给一个引用变量。它处于可达状态的时候,是不会被垃圾回收的。强引用是造成内存泄漏的主要原因。
软引用
软引用配合softreference使用,当系统中有足够的内存的时候,它不会被回收,当系统中内存空间不足的时候会被回收,软引用存在于对内存敏感的程序中。
弱引用
弱引用配合weakreference类来实现。比软引用的生存期更短,对于弱引用对象来说,只要垃圾回收机制一回收,不管内存空间是否充足就直接回收掉了。
虚引用
虚引用需要phantomreference来实现,不能单独使用,必须配合引用队列。虚引用主要作用是跟踪对象的被垃圾回收的状态。
引用队列
使用软引用,弱引用和虚引用的时候都可以关联这个引用队列。程序通过判断引用队列里面是不是有这个对象来判断,对象是否已经被回收了。
软引用,弱引用和虚引用用来解决oom问题,用来保存图片的路径。主要用于缓存。
Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;
年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种不
同的垃圾收集器,JDK1.6 中 Sun HotSpot 虚拟机的垃圾收集器如下:
serial垃圾收集器(单线程、复制算法)
Serial(英文连续)是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。
Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
ParNew 垃圾收集器(Serial+多线程)
ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。【Parallel:平行的】
ParNew虽然是除了多线程外和Serial 收集器几乎完全一样,但是ParNew垃圾收集器是很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
Parallel Scavenge 收集器(多线程复制算法、高效)
Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
下面为年老代的收集器
Serial Old 收集器(单线程标记整理算法 )
Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。 在 Server 模式下,主要有两个用途:
1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
2. 作为年老代中使用 CMS 收集器的后备垃圾收集方案。
Parallel Old 收集器(多线程标记整理算法)
Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。
在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
CMS收集器
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
初始标记
只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
并发标记
进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记
记录,仍然需要暂停所有的工作线程。
并发清除
清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看
CMS 收集器的内存回收和用户线程是一起并发地执行。
G1收集器
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收集器两个最突出的改进是:
1. 基于标记-整理算法,不产生内存碎片。
2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
-XX:+PrintCommandLineFlagsjvm参数可查看默认设置收集器类型
-XX:+PrintGCDetails亦可通过打印的GC日志的新生代、老年代名称判断
jhat :虚拟机堆转储快照分析工具
jstack :Java 堆栈跟踪工具
JConsole :Java 监视与管理控制台
OSGi(Open Service Gateway Initiative),是面向 Java 的动态模型系统,是 Java 动态化模块化系统的一系列规范。
动态改变构造
OSGi 服务平台提供在多种网络设备上无需重启的动态改变构造的功能。为了最小化耦合度和促使这些耦合度可管理,OSGi 技术提供一种面向服务的架构,它能使这些组件动态地发现对方。
模块化编程与热插拔
OSGi 旨在为实现 Java 程序的模块化编程提供基础条件,基于 OSGi 的程序很可能可以实现模块级的热插拔功能,当程序升级更新时,可以只停用、重新安装然后启动程序的其中一部分,这对企业级程序开发来说是非常具有诱惑力的特性。
OSGi 描绘了一个很美好的模块化开发目标,而且定义了实现这个目标的所需要服务与架构,同时也有成熟的框架进行实现支持。但并非所有的应用都适合采用 OSGi 作为基础架构,它在提供强大功能同时,也引入了额外的复杂度,因为它不遵守了类加载的双亲委托模型。
热部署就是典型的osgi。