在上篇文章中我们稍微提了一下JVM,那在这篇文章中我们就来详细聊聊。在这里我推荐大家去阅读《Java Web技术内幕》这本书籍,这本书的内容还是不错的,有框架部分也有较为底层的部分,尤其是Java I/O、JVM以及中文编码这几大模块讲的很详细,大家有空可以去看一看。本篇文章也多处借用了书籍的内容。下面我们开始吧。

         JVM的全称是Java Virtual Machine (Java虚拟机),它通过模拟一个计算机来达到计算机所具有的计算功能。我们先来看看一个真实的计算机如何才能具备计算的功能。

以计算为中心来看计算机的体系结构可以分为如下几个部分:

接下来我们再来看看JVM的体系结构:

JVM体系结构(类加载机制、内存结构、垃圾回收)-LMLPHP 

 那么,JVM和实体机到底有何不同呢?大体有如下几点:

接下来我们对JVM体系中的重点部分进行详细讲解。

JVM内存区域

PC寄存器:是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器,属于独占区域。由于Java程序是多线程执行的,所以当多个线程交叉执行时,被中断线程的程序当前执行到那条的内存地址必然要保存下来,以便于它被恢复时再按照被中断时的指令地址继续执行下去。如果线程执行的是java方法,那么,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果是native方法,那么值为undefined,这个区域也是唯一一个没有规定内存溢出情况的区域。

java栈:描述的是java方法执行的动态内存模型,每一个方法执行都会创建一个栈帧,用于存储局部变量表、操作数栈、方法的返回地址,指向当前方法所属的类的运行时常量池的引用。一个方法的开始和结束对应入栈和出栈。

本地方法栈:为虚拟机执行native方法服务

堆:存放对象实例、垃圾收集器管理的主要区域、新生代,老年代。java堆中一些设置都是为了垃圾回收器。当然也会抛出内存溢出outofmemoryError

方法区(可以理解为class文件在内存存放的位置):存储虚拟机加载的类信息(类的版本、字段、方法、接口),常量、静态变量、即时编译器编译后的的代码等数据。

垃圾回收

1、如何判定对象为垃圾对象(标记算法)

引用计数法
        在对象中添加一个引用计数器,当有地方引用它时,计数器加一,引用失效时,计数器为0,但无法处理循环引用的问题。(-verbose:gc和-xx:PrintGCDetail打印垃圾回收器的回收日志  两个分别为简单和详细)

可达性分析法
       通过一系列的被称为 GC root的对象作为起点,依次往下寻找和GC root有关或间接有关的一些对象,这就会形成一条引用链,这条引用链上的对象都是可用的。当一个对象到 GC root 没有任何引用 链相连,则证明此对象是不可用。可被作为GC root对象的有:虚拟机栈(局部变量表)引用的对象、方法区中的类属性引用的对象、方法区中的常量引用的对象,本地方法栈中引用的对象 

引用
强引用:引用只要还在,就不会被GC回收;
软引用:在内存不足发生OOM之期,就回收掉该引用;
弱引用:在下一次GC前,就回收掉该引用;
虚引用:在任何时候可能被回收;

finalize
       若对象在进行可达性分析后发现没有与 GC roots 相连接的引用链,那么他将会被第一次标 记并进行一次筛选,筛选的条件是该对象是否有必要执行finalize()方法,当对象没有重写 finalize()方法或者 finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没必要执 行。 若该对象被判定为有必要执行 finalize 方法,则这个对象会被放在一个 F-Queue 队列, finalize 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-queue 中的对象进行第二 次小规模的标记,若对象要在 finalize 中成功拯救自己—只要重新与引用链上的任何一个对 象建立关联即可,那么在第二次标记时他们将会被移出“即 将回收”集合。

2、如何回收

回收策略

1)、标记-清除算法:经过可达性分析法后,对需要清除的对象进行标记然后清除。缺点,产生大量不连续的内存碎片、标记和清除效率都不高 (效率问题和空间问题)

2)、停止-复制算法:在堆中分为两块区域新生代和老年代,老年代并不是特别关注,主要是新生代。新生代(Young Gen)又可以划分为Eden(只要是新创建的对象都会被扔到这里,也是垃圾回收器最常光顾),Survivor存活区(当 Eden 区中没有足够的 内存空间进行分配时,虚拟机将发    起一次minorGC{minor gc:发生在新生代的垃圾收集动作,非常频繁,一般回收速度比较快 full gc:发生在老年代的gc},若对象在 eden 出生并经过第一次 minor gc 后仍然存活,并且能被 survivor 容纳的话,将被移到 survivor 空间中,并且对象年龄设为 1.)。老年代(TenuredGen):对象在 survivor 中每熬过一次 minor gc,年龄就+1,当他年龄达到一定程度(默认为 15), 就会晋升到老年代,垃圾回收也相对没有那么频繁。

复制算法就是将可用内存按照容量划分为大小相等的两块,每次只使用其中一块。 当这一块的内存用完了,则就将还存活的对象复制到另一块上面,然后再把已经使用过的内 存空间一次清理掉,那么下一次的垃圾回收就发生在另一块内存空间上。但这样每次只使用一块内存会造成内存浪费,HotSpot解决:将内存分为一块较大的 eden 空间和两块较小的 survivor 空间,默认比例是 8:1:1,即每次新生代中可用内存空间为整个新生代容量的 90%,每次使 用 eden 和其中一个 survivour。当回收时,将 eden 和 survivor 中还存活的对象一次性复制到 另外一块 survivor 上,最后清理掉 eden 和刚才用过的 survivor,若另外一块 survivor 空间没 有足够内存空间存放上次新生代收集下来 的存活对象时,这些对象将直接通过分配担保机制 进入老年代。

3)、标记-清理算法:此方法主要是针对老年代,复制算法不适合老年代,效率很低。标记过程和“标记-清除”算法一样,但后续步骤不是直接对可回收对 象进行清除,而是让存活对象都向一端移动,然后直接清理掉另一端的内存

4)、分代收集算法:根据内存的分代,选择不同的垃圾回收算法。新生代:停止-复制算法     老年代:标记-清理算法

垃圾回收器           

第一阶段,Serial(串行)收集器
这个收集器是一个单线程收集器,只会使用一个CPU或者一条收集线程进行垃圾收集工作。其余的工作线程必须暂停,直到收集结束,回收慢。PS:开启Serial收集器的方式:-XX:+UseSerialGC

第二阶段,Parallel(并行)收集器
Serial收集器的多线程版本。垃圾收集器线程和工作线程同时工作。效率高,但是当Heap过大时,应用程序暂停时间较长。PS:开启Parallel收集器的方式:-XX:+UseParallelGC -XX:+UseParallelOldGC

第三阶段,CMS(并发)收集器
CMS收集器在Minor GC时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。在Full GC时不再暂停应用线程,而是使用若干个后台线程定期的对老年代空间进行扫描,及时回收其中不再使用的对象。老年代回收时暂停时间短。产生内存碎片    PS:开启CMS收集器的方式:-XX:+UseParNewGC -  XX:+UseConcMarkSweepGC

第四阶段,G1(并发)收集器
G1收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆(大于4GB)时产生的停顿。相对于CMS的优势而言是内存碎片的产生率大大降低。    PS:开启G1收集器的方式:-XX:+UseG1GC    

类加载机制

类加载过程

类从加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括: 加载-验证-准备-解析-初始化-使用-卸载,其中验证-准备-解析称为链接。

在加载阶段,虚拟机需要完成下面 3 件事: 
1)通过一个类的全限定名获取定义此类的二进制字节流; (可从文件{class、jsp、jar文件}、网络、计算生成一个二进制流{$Proxy}、数据库)
2)将这个字节流所表示的静态存储结构转化为方法区运行时数据结构 
3)在方法区生成一个java.lang.Class对象,作为方法区数据的访问入口。

验证的目的是为了确保 clsss 文件的字节流中包含的信息符合当前虚拟机的要求,且 不会危害虚拟机自身的安全。验证阶段大致会完成下面 4 个阶段的检验动作:
1)文件格式验证 
2)元数据验证 
3)字节码验证 
4)符号引用验证{字节码验证将对类的方法进行校验分析,保证被校验的方法在运行时不会做出危害虚拟机的事,一个类方法体的字节码没有通过 字节码验证,那一定有问题,但若一个方法通过了验证,也不能说明它一定安全}。 

准备阶段是正式为类变量分配内存并设置变量的初始化值得阶段,这些变量所使用的 内存都将在方法区中进行分配。(不是实例变量,且是初始值,若 public static int a=123;准 备阶段后 a 的值为 0,而不是 123,要在初始化之后才变为 123,但若被 final 修饰,public static final int a=123;在准备阶段后就变为了 123) 

解析阶段是虚拟机将常量池中的符号引用变为直接引用的过程。

初始化阶段则是开始为变量附值

初始化和实例化不同:直接类名.a 类不会实例化,只会初始化

类加载器

从 Java 虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器,这 个类加载器使用 c++实现,是虚拟机自身的一部分。另一种就是所有其他的类加载器,这些 类加载器都由 Java 实现,且全部继承自 java.lang.ClassLoader。

从 JAVA 开发人员角度,类加载器分为: 
1)启动类加载器,这个加载器负责把\lib 目录中或者 –Xbootclasspath 下的类库加载到虚拟机内存中,启动类加载器无法被 Java 程序直接引用。 
2)扩展类加载器:负责加载\lib\ext 下或者 java.ext.dirs 系统变量指定 路径下 all 类库,开发者可以直接使用扩展类加载器。
3)应用程序类加载器,负责加载用户路径 classpath 上指定的类库,开发者可以直接 使用这个类加载器,若应用程序中没有定义过自己的类加载器,一般情况下,这个就是程序 中默认的类加载器。
4) 自定义加载器    要实现自定义类加载器:只需要继承 java.lang.classLoader。 重写loadClass方法

以上就是本次的内容。当然,整个JVM体系内容非常多,我只是将一些重点内容给提取出来而已,如果想详细了解的话,还是推荐大家去看《深入理解Java虚拟机》这本书籍。另外,本人才疏学浅,部分内容并没有理解的非常透彻,如有错误部分请及时指出,本人感激不尽。

 

 

 

10-07 18:45