JVM整理(内存结构+垃圾回收+类加载)

JVM的了解

JVM是什么

JVM是一个软件,不同的平台有不同的版本。我们编写的java源码,编译后会生成class文件,称之为字节码文件。jvm就是负责将字节码文件翻译成特定平台下的机器码执行的软件。也就是说,只要在不同的平台上安装对应的jvm,那么就可以运行相应的字节码文件,从而运行我们编写的java程序。java所谓的跨平台也是通过JVM实现的,因为在这个过程中,我们没有对java代码进行任何调整。

JVM的内存结构

jvm在执行java程序的过程中,会把它所管理的内存划分为若干个不同的数据区域。在运行时主要包含如下数据区域:

1、程序计数器

是当前线程所执行的字节码的行号指示器。该区域是线程私有的。

如果线程正在执行的是java方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址。

如果正在执行的是native方法,则这个计数器的值为空(undefined)

在该区域不会发生OutOfMemoryError。

2、方法区

线程共享,用于存储类信息、常量、静态变量等数据。会抛出OutOfMemoryError异常。在HotSpot中,其就是永久代。其中一部分内存称为运行时常量池,用于存放编译期生成的字面量和符号引用,在类加载后进入该区域。

3、虚拟机栈

线程私有,虚拟机栈即为常说的栈内存,里面用于存放局部变量、基本类型、对象引用信息。其中64位长的long和double会占用2个局部变量空间,其余的占用1个。局部变量表所需的内存空间在编译期间完成分配。运行期间不会改变局部变量表的大小。

会产生StackOverflowError(申请的栈深度大于虚拟机所允许的深度)和OutofMemoryError(无法通过扩展申请到足够的内存)异常。

4、本地方法栈

线程私有,用于执行native方法。也会产生上述两种异常。

5、堆

所有线程共享,虚拟机启动时创建。通过-Xms -Xmx设定堆大小,但注意不要超过物理内存。目的就是用于存放对象实例 。

是负责垃圾收集管理的主要区域,因收集器基本都是采用分代收集算法,因此还可以细分为新生代和老年代。新生代又可以细分为Eden空间,From Survivor和To Survivor空间。

在内存中可以是物理上不连续的,只要逻辑上连续即可。如果堆中内存无法完成实例分配并且也无法扩展时,会抛出OutOfMemoryError异常。

JVM垃圾回收

垃圾回收就是要解决如下几个问题:

1、在哪里回收

2、回收哪些内容

3、什么时候回收

4、如何回收

JVM主要回收堆中内存。

回收的是那些已经无用的对象,那么就要判断什么样的对象是无效的,即已死的对象。主要通过两种方法

1、引用计数法

每个对象创建时都会创建一个引用计数器,当这个对象被引用的时候计数器就加1,当不被引用或者引用失效的时候计数器就会减1。任何时候,对象的引用计数器值为0就说明这个对象不被使用了,就认为是“垃圾”,可以被GC处理掉。

优点:算法实现简单

缺点,当循环引用时有些垃圾对象无法准确识别。

2、可达性分析算法

通过一系列“GC Roots"的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,则证明该对象是不可用的。

在Java中,可以作为GC Roots的对象包括如下几种:

  • 虚拟机栈中引用的对象

  • 方法区中类静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中JNI引用的对象。

优点:可以找到所有的垃圾对象。

缺点:需要遍历所有的对象,搜索效率不高。

上述两种算法均与引用相关,何为引用?引用分为4种:

  • 强引用:Object obj=new Object()类似的。强引用存在则一定不会被垃圾回收掉该对象。

  • 软引用:通过SoftReference类来实现软引用,在系统将要发生溢出前,会将这些对象再次进行回收,如果回收后内存仍然不够,则抛出内存溢出异常。

  • 弱引用:通过WeakReference类来实现弱引用,只能生成到下一次GC之前。当发生GC时,无法内存是否足够,都会被回收。

  • 虚引用:通过PhantomReference类无法通过虚引用来获得一个对象,设置虚引用的唯一目的是为了能在GC时收到一个系统通知。

上述引用从上至下越来越弱。

垃圾回收算法

标记-清除算法

该方法分为两个阶段:

标记出所有需要回收的对象。

标记完成后统一回收所有被标记的对象。

优点:没有产生额外的内存空间消耗,内存利用率高。

缺点:效率不高,需要遍历所有对象;空间问题,清除后会产生大量不连续的内存碎片,未来可能需要分配大对象时,因没有足够的连续空间而不得不再次进行GC操作。

复制算法

把内存按容量划分为大小相等的两块,每次只使用其中的一块,当前使用的叫活动区间,另一半叫空闲区间。当这一块内存使用完了,就将还存活的对象复制到另一块内存中,然后再把已使用过的内存空间一次全部清理掉。复制到空闲区间时,严格按照内存地址依次排列。如此循环实现GC过程。

优点:GC后的内存整齐,不易产生内存碎片,只需移动堆顶指针,按顺序分配即可,实现简单,运行高效。

缺点:GC要使用两倍的内存,或者说导致堆只能使用被分配到的内存的一半,这个算法对空间要求太高!如果存活的对象较多,则意味着要复制很多对象并且要维护大量对象的内存地址,所以存活的对象数量不能太多,否则效率也会很低。

标记-整理算法

是上述两种的综合,也分为两个阶段

1)与标记-清除算法的第一步一样,通过遍历GC-Roots标记所有存活的对象

2)将所有存活的对象向内存一端(具体哪端由GC实现)移动,然后将最后一个存活对象后面的内存空间全部回收。

优点:内存空间利用率高,消除了复制算法内存减半的情况;GC后不会产生内存碎片。

缺点:需要遍历标记活着的对象,效率较低;复制移动对象后,还要维护这些活着对象的引用地址列表。

分代回收算法

根据对象存活周期的不同,将内存划分为几块。不同年龄的对象存放到不同的内存空间中,根据各个年代的特点采用最适当的收集算法进行GC。一般将堆划分为新生代和老年代。

详细说明:

1)内存结构

Java堆=年老代+年轻代。默认比例为3:1

年轻代=Eden区+From Survivor区+To Survivor区。空间大小一般比例为8:1:1。通过-XX:SurvivorRation=8配置表示比例为8:1

2)对象分类

根据对象存活时间长短,分为三类

短命对象:存活时间较短的对象,如中间变量对象、临时对象、循环体创建的对象等。这也是产生最多数量的对象,GC回收的关注重点。

长命对象:存活时间较长的对象,如单例模式产生的单例对象、数据库连接对象、缓存对象等。

长生对象:一旦创建则一直存活,几乎不死的对象。

3)对象分配区域

短命对象存在于年轻代,长命对象存在于年老代,而长生对象则存在于方法区中。因此根据上面方法区中存放的对象也可以知道如类信息,静态变量、常量等数据。

4)GC类别

新生代GC:Minor GC 指发生在新生代的垃圾收集动作,因Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,回收速度也较快。

老年代GC:Full GC指发生在老年代的GC,出现Full GC,经常会至少伴随一次Minor GC.老年代GC一般比较新生代GC慢10倍以上。

5)新生代GC过程

新生代中的对象98%是朝生夕死的,当回收时,将Eden区和Survivor中存活着的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。因空间比例为8:1,因此每次可以使用内存容量的90%。周而复始。始终会有一个10%空闲的survivor区间,作为下一次Minor GC存放对象的准备空间。

当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保,如果另一块Survivor空间没有足够的空间存放上一次新生代GC下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

6)老年代GC过程

什么样的对象可以进入老年代?

在年轻代中每进行一次GC,则其年龄加1,当年龄达到一个阈值时,就会被移动到老年代。可以通过-XX: MaxTenuringThreshold配置阈值。

Survivor中相同年龄的对象大小总和超过survivor空间的一半,则不小于这个年龄的对象都会直接进入年老代。创建的对象的大小超过设定阈值,这个对象会被直接存进年老代。通过-XX:PetenureSizeThreshold设置阈值。年轻代中大于survivor空间的对象,Minor GC时会被移进年老代。

在年老代中的对象存活时间较长,且没有备用的空闲空间,因此不适合复制算法,使用标记-清除或者整理比较合适。

什么时候触发Major GC呢?在Minor GC时,先检测JVM的统计数据,查看历史上进入老年代的对象平均大小是否大于目前年老代中的剩余空间,如果大于则触发Full GC。

7)调优

JVM内存调优,主要是减少GC的频率和减少Full GC的次数,Full GC的时候会极大地影响系统的性能。所以在此基础上,更加要关注会导致Full GC的情况。

容易导致Full GC的情况

  • 年老代空间不足

    1)分配足够大空间给年老代。2)避免直接创建过大对象或者数组,否则会绕过年轻代直接进入年老代。3)应该使对象尽量在年轻代就被回收,或待得时间尽量久,避免过早的把对象移进年老代。

  • 方法区的永久代空间不足

    1)分配足够大空间给方法区。2)避免创建过多的静态对象。

  • 显示调用System.gc()

JVM堆内存分配问题:

  • 年轻代过小(年老代过大)

    导致频繁发生GC,增大系统消耗。容易让普通大文件直接进入年老代,从而更容易诱发Full GC

  • 年轻代过大(年老代过小)

    导致年老代过小,从而更容易诱发Full GC,GC耗时增加,降低GC的效率。

  • Eden区过大(Survivor过小)

    Minor GC时容易让普通大文件直接张过Survivor进入年老代,从而更容易诱发FULL GC。

  • Eden区过小(Survivor过大)

    导致GC频率升高,影响系统性能。

JVM类加载

类加载的时机

类从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期包含7个阶段,如下:

其中验证、准备、解析统称为连接。

类加载的过程

加载

通过类的全限定名来获取定义此类的二进制字节流,主要是查找和导入class文件。

验证

检查载入class文件数据的正确性。目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

准备

为类的静态变量分配存储空间,并设置该变量的初始值。如static int i=5;在此处则为其分配空间,并将i=0。这里不包含用final修饰的static,因为final在编译的时候就会分配了,同时这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

解析

将符号引用转换为直接引用。

符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析

初始化

类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)

类的装载通过ClassLoader类及其子类来负责。JVM在运行时会产生3个ClassLoader:根装载器、ExtClassLader(扩展类装载器)、AppClassLoader(应用装载器)。

其中根装载器不是ClassLoader的子类,它是用C++编写的,因此在Java中看不到它。根装载器主要用于装载核心类库,如rt.jar,charset.jar等。

ExtClassLoader、AppClassLoader都是ClassLoader的子类。ExtClassLoader负责装载扩展目录ext下的jar包。AppClassLoader负责装载类路径下的jar包。

这三个类是父子关系(不是extend关系),即

根装载器-->ExtClassloader-->AppClassLoader。默认情况下,使用AppClassLoader加载类。可以通过继承ClassLoader并重写findClass()方法来实现自己的类加载器。

Jvm装载时采用全盘负责委托机制,即当加载一个类时,除非显示地指定使用哪个ClassLoader,否则优先由父装载器进行装载,只有父类找不到时才由自己查找。可以通过ClassLoader类中的loadClass()方法也得到该结论。

这样做的目的是为了保证安全,试想,若有人编写了一个java.lang.String类并加载到JVM中就可怕了。采用父类委托,则可以保证类似于String类,绝对由根加载器加载。

猜你喜欢

转载自www.cnblogs.com/yehhuang/p/9555728.html