jvm

1、基本概念

jvm是运行java代码的虚拟计算机,可以说是java能够跨平台运行的重要原因。

java为什么牛逼?答:它能跨平台运行

为什么他能跨平台运行?答:就是因为这个神秘的jvm

Java源程序(.java)要先编译成与平台无关的字节码文件(.class),然后字节码文件再解释成机器码运行。

解释是通过 Java虚拟机(jvm)来执行的

换成人话来说,jvm就像是一个万能的接口,对内,它百无禁忌,你完全不需要考虑不同平台不同架构之间不同的机器码规则,只要你按照java语法来写程序,然后一切交给jvm就ojbk,它来搞定。

而对外,jvm就变得八面玲珑(面对不同操作系统有不同的jvm),它将你的代码完美的翻译给操作系统。

操作系统(日本):搜得死内!

操作系统(美国): I see!

操作系统(东北):啊这么回事啊!

.class文件是不管你是哪个操作系统的,他只面对jvm来进行解释,只要对应平台有jvm,那么java就能够运行

jvm的生命周期

jvm:我既想知道我是怎么来的,又想知道我是怎么没的

当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。

程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。

既然我们说到了java文件的编译和解释,那么接下来就正好来谈谈jvm的运行过程吧

2、运行过程

我们说java文件如果想执行,那么需要经过以下过程:

java源文件——>>通过编译器的编译——>>class字节码文件——>>通过jvm 的解释——>>变成对应平台可以执行的机器码文件


那么,jvm对class究竟进行了怎样的解释呢?

jvm的解释过程

在此推荐

类的加载

众所周知,在java中,万物皆是对象,其中类的加载是指jvm将class文件放入内存,也相当于把class中的类,一个个放在内存里(摆立整的!)

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆区生成一个代表这个类的java.lang.Class对象.(JVM规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在方法区中

注意:在此时会扫描到我们的代码中是否有静态变量或者是静态方法等等这些静态数据结构,还未分配内存。

类的元数据才是存在方法区的。

方法区生成全部静态数据,并在堆中生成一个Class类的对象,代表这是一个类

注意:每一个类都是java.lang.class对象!
写好的类的数据结构被存放在方法区,然后生成一个Class对象存放在堆,之后new出来的新对象都是通过这个堆里的Class对象来的

类的链接

将Java类的二进制代码合并到JVM的运行状态之中的过程。

  • 验证:确保加载的类信息符合JVM规范,没有安全方面的问题
  • 准备:正式为类变量(static) 分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。准备阶段主要为类变量分配内存并设置默认的初始值。这些内存都在方法区分配。注意此时就会为我们的类变量也就是静态变量分配内存,但是*普通成员*变量还没。但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。
  • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

实际上Java代码编译成字节码之后,最开始是没有构造方法的概念的,只有类初始化方法 对象初始化方法 。

类初始化

  • 执行类构造器< clinit> ()方法的过程【初始化方法】方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
  • 虚拟机会保证一 个类的 ()方法在多线程环境中被正确加锁和同步。
  • 初始化时候才会为我们的普通成员变量赋值。

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。
Java程序对类的使用方式可分为主动使用与被动使用。只有当对类的首次主动使用的时候才会导致类的初始化,所以主动使用又叫做类加载过程中“初始化”开始的时机。
类的主动使用包括以下六种:

类主动使用的六种情况
创建类的实例,也就是new的方式
访问某个类或接口的静态变量,或者对该静态变量赋值(凡是被final修饰不不不其实更准确的说是在编译器把结果放入常量池的静态字段除外)
调用类的静态方法
反射(如 Class.forName(“com.gx.yichun”))
初始化某个类的子类,则其父类也会被初始化
Java虚拟机启动时被标明为启动类的类( JavaTest ),还有就是Main方法的类会首先被初始化
类初始化方法

编译器会按照其出现顺序,收集:类变量(static变量)的赋值语句静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。

对象初始化方法(构造方法)

编译器会按照其出现顺序,收集:成员变量的赋值语句普通代码块,最后收集构造函数的代码,最终组成对象初始化方法,值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。

大致的介绍一下java内存

首先,我们需要理解java内存的一些基础知识,在java内存中,分为堆,栈,方法区

方法区

又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。运行时常量池都分配在 Java 虚拟机的方法区之中,全局变量和静态变量放在一块,初始化的全局变量和静态变量放在一块,未初始化的和未初始化的静态变量放在相邻的一块。

用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池(Runtime Constant Pool)

是方法区的一部分

存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令) jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

Java栈与数据结构上的栈有着类似的含义,它是一块先进后出的数据结构,只支持出栈和入栈的两种操作。在Java栈中保存的主要内容为栈帧。每次调用一个函数,都会有一个对应的栈帧被压入Java栈。每一个函数调用结束,都会有一个栈帧被弹出Java栈。

每次调用一个函数都会被当做栈帧压入到栈中。其中每一个栈帧对应一个函数。由于每次调用函数都会生成一个栈帧,从而占用一定的栈空间。如果线程中存在大量的递归操作,会频繁的压栈,导致栈的深入过于深入,当栈的空间被消耗殆尽的时候,会抛出StackOverflowError栈溢出错误。

每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中

  • 栈区描述的是方法执行的内存模型。每个方法在执行时都会创建一个栈帧(存储局部变量、操作数栈、动态链接、方法出口等)
  • JVM为每个线程创建一个栈,栈属于线程私有,不能实现线程间的共享,用于存放该线程执行方法的信息(实际参数、局部变量等)
  • 基本类型变量去,执行环境上下文,操作指令区(存放操作指令)

3、jvm内存区域


除了在上面介绍的三种内存区域,方法区,堆,栈外,还有两种:程序计数器,和本地方法区

其中方法区,堆是程序共享的(大家都有大家都有)

而堆,栈,都是程序私有的(私有财产神圣不可侵犯!梦回初中历史课)

线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束而创建/销毁

线程共享区域虚拟机的启动/关闭而创建/销毁。

程序计数器

一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,是监工老大爷

正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。

本地方法区

本地方法区和java栈作用类似,区别是虚拟机栈为执行Java方法服务, 而本地方法栈则为 Native方法服务。

4、从GC角度来看java堆


java堆可以细致的分为:

新生代

  • Eden 区 : Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老 年代)。
  • From Survivor 区 : 上一次GC的幸存者,作为这一次GC 的被扫描者。
  • To Survivor 区 : 保留了一次MinorGC过程中的幸存者。

  • 新生代中使用MinorGC——复制算法。

老年代

  • 主要存放应用程序中生命周期长的内存对象。 老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
  • 老年代使用MajorGC——标记整理算法

5、垃圾回收与算法

Q:如何确定垃圾?

    1. 在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单 的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关 联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收 对象。 
    2. 为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots” 对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记 过程。两次标记后仍然是可回收对象,则将面临回收

1.分代算法

所谓分代算法其实就是把内存划分为了不怎么经常死的老年代和动不动就GG的新生代。

对两者使用不同的GC方法就是所谓的分代算法啦

对新生代使用:新生代中使用MinorGC——复制算法。

对老年代使用:老年代使用MajorGC——标记整理算法

2. 复制算法

正常的复制算法就是将空间分为1:1。

但是这样做会很浪费空间啊,所以程序员们就发明了上面的分代算法。新生代们经常死,所以只需要付出少量存活对象的复制成本就可以完成收集。

其实在分代算法中就是使用了8:1:1的复制算法啦

  1. Eden区和FromSurvivor区在上一轮GC后还存活的对象苟延残喘到TO Survivor区中(如果有对象的年龄达到了老年的标准,则赋值到老年代区)同时把这些对象的年龄+1(如果 ServicorTo 不 够位置了就放到老年区)
  2. 清空Eden,From Survivor区
  3. 将From和To区互换(经受新的一轮制裁吧哈哈哈哈哈哈)

3.标记-清理

  1. 标记阶段:首先通过根节点,标记所有从根节点开始的可达对象。未被标记的对象就是未被引用的垃圾对象
  2. 清除阶段:清除所有未被标记的对象。

想法挺丰满,现实很骨感。

效率问题:标记和清除两个过程的效率都不高。

空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要 分配较大对象时,无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。

空间都碎成渣渣了,还GC个屁

4.标记—整理算法

老年代使用MajorGC——标记整理算法(标记清除算法超进化!)

  1. 标记阶段:先通过根节点,标记所有从根节点开始的可达对象,未被标记的为垃圾对象
  2. 整理阶段:将所有的存活对象压缩到内存的一段,之后清理边界外所有的空间

    最起码咱整理了不是

在老年代中很少出现经常死的(毕竟是新生代练过来的,谁还不是个大佬了),因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

其他

  1. JAVA虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储class类, 常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
  2. 对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目 前存放对象的那一块),少数情况会直接分配到老生代。
  3. 当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。
  4. 如果To Space无法足够存储某个对象,则将这个对象存储到老生代。
  5. 在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。
  6. 当对象在Survivor区躲过一次GC 后,其年龄就会+1。默认情况下年龄到达15 的对象会被 移到老生代中

6、四种引用

咋把他放jvm里了......

因为java的垃圾回收机制需要他!

Java 中的垃圾回收机制在判断是否回收某个对象的时候,都需要依据“引用”这个概念。

JDK1.2 之前,一个对象只有“已被引用”和"未被引用"两种状态,远远不够灵活无法描述特殊情况下的对象,比如,当内存充足时需要保留,而内存紧张时才需要被抛弃的一类对象。

在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。

1、强引用

java默认的引用就是强引用

Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null;  //手动置null

强引用较为强硬,只要他还存在,回收期就绝对回收被它罩着的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收(太狠了)

当然除非你手动给他断开了(从今以后不罩着你了)

2、软引用

你听这名字他就不硬。

它用来描述一些不是很必需但是仍然有用的东西(曹操:鸡肋!)。内存如果足够,它是不会被清除的,但是如果内存不足了,系统就会扛着镰刀过来了。

在jdk1.2之后,用java.lang.ref.SoftReference类来表示软引用。

接下来我们来看个百度到的例子来仔细区分一下强弱引用:(链接:https://www.cnblogs.com/liyutian/p/9690974.html)‘’

在运行下面的Java代码之前,需要先配置参数 -Xms2M -Xmx3M,将 JVM 的初始内存设为2M,最大可用内存为 3M。

首先先来测试一下强引用,在限制了 JVM 内存的前提下,下面的代码运行正常

public class TestOOM {

    public static void main(String[] args) {
         testStrongReference();
    }
    private static void testStrongReference() {
        // 当 new byte为 1M 时,程序运行正常
        byte[] buff = new byte[1024 * 1024 * 1];
    }
}

但是如果我们将

byte[] buff = new byte[1024 * 1024 * 1];

替换为创建一个大小为 2M 的字节数组

byte[] buff = new byte[1024 * 1024 * 2];

则内存不够使用,程序直接报错,强引用并不会被回收

接着来看一下软引用会有什么不一样,在下面的示例中连续创建了 10 个大小为 1M 的字节数组,并赋值给了软引用,然后循环遍历将这些对象打印出来。

public class TestOOM {
    private static List<Object> list = new ArrayList<>();
    public static void main(String[] args) {
         testSoftReference();
    }
    private static void testSoftReference() {
        for (int i = 0; i < 10; i++) {
            byte[] buff = new byte[1024 * 1024];
            SoftReference<byte[]> sr = new SoftReference<>(buff);
            list.add(sr);
        }

        System.gc(); //主动通知垃圾回收

        for(int i=0; i < list.size(); i++){
            Object obj = ((SoftReference) list.get(i)).get();
            System.out.println(obj);
        }

    }

}

我们发现无论循环创建多少个软引用对象,打印结果总是只有最后一个对象被保留,其他的obj全都被置空回收了。这里就说明了在内存不足的情况下,软引用将会被自动回收。
值得注意的一点 , 即使有 byte[] buff 引用指向对象, 且 buff 是一个strong reference, 但是 SoftReference sr 指向的对象仍然被回收了,这是因为Java的编译器发现了在之后的代码中, buff 已经没有被使用了, 所以自动进行了优化。
如果我们将上面示例稍微修改一下:

    private static void testSoftReference() {
        byte[] buff = null;

        for (int i = 0; i < 10; i++) {
            buff = new byte[1024 * 1024];
            SoftReference<byte[]> sr = new SoftReference<>(buff);
            list.add(sr);
        }

        System.gc(); //主动通知垃圾回收

        for(int i=0; i < list.size(); i++){
            Object obj = ((SoftReference) list.get(i)).get();
            System.out.println(obj);
        }

        System.out.println("buff: " + buff.toString());
    }

则 buff 会因为强引用的存在,而无法被垃圾回收,从而抛出OOM的错误。

如果一个对象惟一剩下的引用是软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正 “需要” 内存时才收集软可及的对象。

3、弱应用

轮到菜逼的世界了

无论内存足够不足够,只要开始GC,他就得上

在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。

4、虚引用

有的人活着,他已经死了。

哦蚂蚁哇心得一路!(纳尼!)。

一个对象有一个虚引用,和一个对象没有应用基本没区别。

在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象

7、GC分代收集算法VSGC分区收集算法

7.1分代收集算法

主流 VM 垃圾收集都采用”分代收集”,比较大众化,他是根据对象活的时间长短,将内存划分为几块:

新生代

老年代

永久代(元空间)

我们上面所讲的8:1:1的复制算法,其实就是复制算法在分代收集算法中的一种体现

在新生代-复制算法:

每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量 存活对象的复制成本就可以完成收集

在老年代-标记整理算法:

因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标 记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存.

7.2分区收集算法

我查了半天百度,结果都是说的一样的,,,,算了,等以后我对分区收集算法有了新的理解的时候再来吧,有人看见了记得提醒我更新啊

分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收。

这样可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次GC 所产生的停顿。

8、GC垃圾收集器

因为新生代与老年代的垃圾回收算法都不同,所以Jvm为两者分别提供了多种不同的垃圾回收器


这是在JDK1.6中的垃圾收集器。

8.1serial垃圾收集器(单线程、复制算法)

这个单词的英文意思是“连续”,最基本最普通的垃圾收集器,使用复制算法。

在JDK1.3之前是java新生代唯一的垃圾收集器,它使用一个CPu或一条线程去完成垃圾收集工作,而且他在干活的时候,必须暂停其他所有线程,直到垃圾回收结束(比较霸道)

虽然说他要暂停其他线程,但是它简单高效,对于限定单个CPU环境来说,因为没有线程交互,所以可以获得最好的单线程垃圾收集效率。

因此他是Client模式下默认的新生代垃圾收集器。

8.2ParNew收集器

serial升级了

ParNew收集器其实是Serial收集器的多线程版本,

它是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为除了Serial收集器外,目前只有它能与CMS收集器配合工作。

8.3Parallel Scavenge收集器(多线程复制算法、高效)

使用复制算法,也是并行多线程,这些他跟上面的ParNew收集器都是一样一样的

那区别在哪呢?区别在于这个收集器跟别人的关注点都不一样。

别的收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的则是达到一个可控制的吞吐量(Throughput),即CPU用于运行用户代码的时间与CPU总消耗时间的比值

吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)

虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,吞吐量就是99%。

停顿时间越短对于需要与用户交互的程序来说越好,良好的响应速度能提升用户的体验;

高吞吐量可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不太需要太多交互的任务。

8.4Serial Old收集器(单线程,标记整理,老年代)

Serial家族的老年代版本,真是人丁兴旺啊

对应Seial在新生代的版本,他是jvmclient模式下老年代的默认垃圾收集器。

当然,在server模式下它也是有些用处的:

  1. 在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用;
  2. 作为CMS 收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。


新生代Parallel Scavenge收集器与ParNew收集器工作原理类似,都是多线程的收集器,都使用的是复制算法,在垃圾收集过程中都需要暂停所有的工作线程。

所在这里我们合并一下两者:新生代Parallel Scavenge/ParNew与年老代Serial Old搭配垃圾收集过程图

8.5 Parallel Old(多线程,标记整理,老年代)

Parallel Scavenge的老年代版本,JDK1.6才出现。

在那之前,Parallel Scavenge只能搭配Serial Old,无法保证吞吐量,这个收集器就是为了这个出现的,在老年代一样保持吞吐量!

8.6CMS收集器(多线程,标记清除,老年代)

相比于parallel家族为了高吞吐,CMS是一种为了低停顿时间诞生的收集器。

它是一种**以获取最短回收停顿时间为目标的收集器。

优点:并发收集,低停顿

基于“标记-清除”算法。目前很大一部分Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS收集器就非常符合这类应用的需求。

它的工作机制比较复杂,分为以下四个阶段:

  1. 初始标记(CMS initial mark):

    标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

  2. 并发标记(CMS concurrent mark)

    进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。

  3. 重新标记(CMS remark)

    为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记 记录,仍然需要暂停所有的工作线程。

  4. 并发清除(CMS concurrent sweep)

    清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看 CMS收集器的内存回收和用户线程是一起并发地执行


缺点:

  • 对CPU资源非常敏感,面向并发设计的程序都会对CPU资源较敏感。
  • 无法处理浮动垃圾,并发清理阶段用户程序运行产生的垃圾过了标记阶段所以无法在本次收集中清理掉,称为浮动垃圾。CMS收集器默认在老年代使用了68%的空间后被激活。
  • 基于“标记-清除”算法会产生大量空间碎片。

8.7G1收集器(Garbage First)

它 是当前收集器技术发展的最前沿成果。与CMS相比有两个显著改进:

  1. 使用标记整理算法,不再产生空间碎片
  2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

这是由于它能够极力避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的由来)。区域划分、有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。

G1将新生代,老年代的物理空间划分取消了。


取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。


在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

总结一下

G1可以说是放弃了分代收集算法的物理隔绝,转而变成了分区收集算法,将堆按照一块一块分离开来,G1将整个堆分成相同大小的region,每个分区都可能是新生代或者是老年代,但是在同一时刻只能属于某个代。新生代、幸存区、老年代这些概念还存在,成为逻辑上的概念,方便复用之前分代框架的逻辑,在物理上不需要连续

因此带来了额外的好处——有的region内垃圾对象特别多,G1会优先回收这些region,这样可以花费较少的时间来回收这些分区的垃圾,这也是G1名字的由来,即首先收集垃圾最多的region。

依然是在新生代满了的时候对整个新生代进行回收——整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩

9、JVM类加载机制

前面提到过的JVM类加载机制在这里重新提一次

JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化

  • 加载:
    • 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对 象,作为方法区这个类的各种数据的入口。
    • 注意这里不一定非得要从一个Class文件获取,这里既 可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理), 也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。
    • java方法区生成全部静态数据,并在堆中生成一个Class类的对象,代表这是一个类
    • 具体解释可看中 第二点【运行过程】
  • 验证:
    • 这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  • 准备:
    • 准备阶段是正式为类变量(static)分配内存并设置类变量的默认初始值阶段(也就是0,而不是我们给的值,为其赋值是在类初始化【client】方法中),即在方法区中分配这些变量所使用的内存空间
  • 解析:
    • 解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中 的: 1. CONSTANT_Class_info 2. CONSTANT_Field_info 3. CONSTANT_Method_info 等类型的常量。
    • 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟 机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引 用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
    • 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有 了直接引用,那引用的目标必定已经在内存中存在。
  • 初始化:
    • 初始化阶段是类加载后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载 器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码
    • 初始化阶段是执行类构造器方法的过程
    • 方法是由编译器自动收集类中的类变 量的赋值操作和静态语句块中的语句合并而成的
    • 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译 器可以不为这个类生成()方法

以下几种情况不会执行类初始化:

类被动使用的六种情况(不会执行类初始化)
通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
定义对象数组,不会触发该类的初始化。
常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触 发定义常量所在的类。
通过类名获取Class对象,不会触发类的初始化。
通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初 始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

顺便提一下类主动使用的六种情况(会执行类初始化)

类主动使用的六种情况
创建类的实例,也就是new的方式
访问某个类或接口的静态变量,或者对该静态变量赋值(凡是被final修饰不不不其实更准确的说是在编译器把结果放入常量池的静态字段除外)
调用类的静态方法
反射(如 Class.forName(“com.gx.yichun”))
初始化某个类的子类,则其父类也会被初始化
Java虚拟机启动时被标明为启动类的类( JavaTest ),还有就是Main方法的类会首先被初始化

10、类加载器

虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提 供了3种类加载器:

10.1启动类加载器(Bootstrap ClassLoader)

负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被 虚拟机认可(按文件名识别,如rt.jar)的类。

10.2扩展类加载器(Extension ClassLoader)

负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类 库

10.3应用程序类加载器(Application ClassLoader)

负责加载用户路径(classpath)上的类库。

JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader 实现自定义的类加载器。

10.4双亲委派

  • 当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父 类去完成
  • 每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中
  • 只有当父类加载器反馈自己无法完成这个请求的时候,子类加载器才会尝试自己去加载

10.5 OSGI动态模型系统

OSGi 技术是面向Java的动态模型系统,或者通俗点说JAVA动态模块系统

OSGI提供以下优势:

  1. 你可以动态地安装、卸载、启动、停止不同的应用模块,而不需要重启容器。
  2. 你的应用可以在同一时刻跑多个同一个模块的实例。
  3. OSGI在SOA领域提供成熟的解决方案,包括嵌入式,移动设备和富客户端应用等。

醉后不知天在水,满船清梦压星河