> 文章列表 > JVM、JVM中的垃圾回收、类加载、IoC和DI

JVM、JVM中的垃圾回收、类加载、IoC和DI

JVM、JVM中的垃圾回收、类加载、IoC和DI

一、JVM 

1、概念

JVM:Java Virtual Machine 的简称,意为 Java虚拟机,可以运行Java代码,是整个Java实现跨平台的最核心的部分;所有的Java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行,也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。支持多种语言,理论上无论哪种编程语言,只要能将源代码编译为*.class代码,都能在JVM上解释运行。

 PS:JDK=JRE+开发工具集;JRE=JVM+JavaSE标准类库

虚拟机:是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。常见的虚拟机:JVM、VMwave、Virtual Box
PS(有关JVM、编译与解释阶段):http://t.csdn.cn/yApO5

2、JVM 运行时数据区,也叫内存布局

PS:可参考:

JVM内存划分_jvm内存换分_走在小路的博客-CSDN博客

JVM中的五大内存区域划分详解_jvm内存划分_Archie_java的博客-CSDN博客

 2.1、堆:主要存的是程序里新建的所有对象

堆内存里还分为新生代和老生代。新生代放新建的对象,当经过一定 GC 次数之后还存活的对象
会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)。垃圾回收的时候会将 Endn 中存活的对象放到一个未使用的 Survivor 中,并把当前的 Endn 和正在使用的 Survivor 清楚掉。

HotSpot实现的复制算法流程如下:
1. 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当
Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过
这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
2. 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到
From区域,并将Eden和To区域清空。
3. 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数
MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

2.2、程序计数器:存的是CPU下一条要执行的指令的地址,用来记录当前线程执行的行号的。

如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空。
程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM(OutOfMemoryError)情况的区域。

2.3、栈:主要存局部变量、方法调用相关的信息。

2.3.1、Java虚拟机栈(给JVM使用)

Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的
内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。JVM栈只对栈帧进行存储,压栈和出栈操作。Java栈是Java方法执行的内存模型。

JVM栈包含有四个部分:

1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。
2. 操作栈:每个方法会生成一个先进后出的操作栈。
3. 动态链接:指向运行时常量池的方法引用。
4. 方法返回地址:PC 寄存器的地址。

2.3.2、本地方法栈(给本地方法使用)

和虚拟机栈类似。

2.4、方法区:类对象(.class 加载到内存里就是类对象)、静态成员等。

用来存储被虚拟机加载的类信息、敞亮、静态变量、即时编译器编译后的代码等的数据。

在《Java虚拟机规范中》把此区域称之为方法区,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代JDK 8 中叫做元空间
PS:JDK8 中将字符串常量池移动到了堆中。
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量 : 字符串 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。

二、JVM 中的垃圾回收

1、垃圾回收

1.1、概念:

Java垃圾回收是Java程序执行自动内存管理的过程。Java程序编译为字节码,可以在Java虚拟机(简称JVM)上运行。当Java程序在JVM上运行时,将在堆上创建对象,这是专用于该程序的内存的一部分。最终,将不再需要某些对象。垃圾收集器找到这些未使用的对象并将其删除以释放内存。

有一些对象在使用之后就不再使用了,就称之为“垃圾”。

1.2、死亡对象的判断算法

1.2.1、引用计数法

给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。

优点:简单。

缺点:会浪费一部分内存空间;不能回收循环引用的对象。

1.2.2、可达性分析算法

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

Java语言中,可作为 GC Roots 的对象:

1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
2. 方法区中类静态属性引用的对象;
3. 方法区中常量引用的对象;
4. 本地方法栈中 JNI(Native方法)引用的对象。

PS:以下图为例:(5~7之间虽然还有关联,但是它们到GC Roots是不可达的,因此会被判定为可回收对象)

优点:避免了空间浪费;解决了循环引用的问题。

缺点:系统开销大,遍历一次可能比较慢。

1.3、垃圾回收算法

1.3.1、标记-清除:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

 缺点:效率都不高;会产生大量不连续的内存碎片。

1.3.2、复制:为了解决标记-清除法效率不高的问题。

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。

 优点:解决了内存碎片问题;算法简单、高效。

缺点:内存空间利用率低;若保留的对象很多,要释放的对象较少,此时复制的开销就会较大。

1.3.3、标记-整理

标记过程仍与"标记-清除"过程一致,但后续步
骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的 内存。流程图如下:

 

1.3.4、分代 

分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收。
是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代(一般创建的对象都会进入新生代)和老年代(大对象和经历了15次(默认情况下)垃圾回收仍存活下来的对象会从新生代移动到老年代)。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

PS:Minor GC和Full GC,这两种GC的区别:
1. Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝
生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
2. Full GC 又称为 老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,
经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行
Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。 

1.4、垃圾收集器:垃圾回收的具体体现

作用:是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。

 PS:存在连线表示两者之间可以搭配使用。

  • 串行:所有垃圾回收事件都在一个线程中串行进行。在每个垃圾回收之后执行压缩。
  • 并行:多个线程用于次要垃圾回收。单线程用于大型垃圾回收和旧式压缩。另外,Parallel Old变量使用多个线程进行主要垃圾收集和Old Generation压缩。

1.5、一个对象的一生

我是一个普通的 Java 对象,我出生在 Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了 Survivor 区的 “From” 区(S0 区),自从去了 Survivor 区,我就开始漂了,有时候在 Survivor “From” 区, 有时候在 Survivor “To” 区(S1 区),居无定所。直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多 人。在老年代里,我生活了很多年(每次GC加一岁)然后被回收了。

2、分代回收的工作过程

PS:可参考:

(1条消息) java -- 分代回收_java 分代回收_404QAQ的博客-CSDN博客

分代垃圾回收过程_JuHootin的博客-CSDN博客

三、JVM 中的类加载的过程以及双亲委派模型

1、类加载

类的生命周期

1.1、加载:主要是把 .class 文件加载到内存中。

在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。==》根据雷鸣找到 class文件
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。==》把 class文件加载到内存中
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。==》创建一个类对象

1.2、连接

1.2.1、验证:验证 class文件是否符合JVM标准、是否会危害虚拟机。

1.2.2、准备:给静态变量赋初始值(初始化值为0)。

1.2.3、解析:JVM 将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。

1.3、初始化:执行构造方法的过程。

PS:如果在自己的代码中创建一个java.lang.String的类,是不能被类加载器加载的。因为虚拟机会抛出 ‘java.lang.SecurityException:Prohibited package name:java.lang’ 异常。

PS:一个类加载例子:

public class Test extends B{public static void main(String[] args) {new Test();//362154new Test();//2154
//        结果:
//        A 的静态代码块
//        B 的静态代码块
//        A 的构造代码块
//        A 的构造方法
//        B 的构造代码块
//        B 的构造方法
//        A 的构造代码块
//        A 的构造方法
//        B 的构造代码块
//        B 的构造方法}
}
//创建类实例时,先进行类加载
//类加载时,先执行静态方法
//在执行构造代码块,再执行构造方法
//先执行父类,再执行子类
//类加载只需要执行一次,所以静态方法也只执行一次
class A{public A(){System.out.println("A 的构造方法");//1}{System.out.println("A 的构造代码块");//2}static {System.out.println("A 的静态代码块");//3}
}
class B extends A{public B(){System.out.println("B 的构造方法");//4}{System.out.println("B 的构造代码块");//5}static {System.out.println("B 的静态代码块");//6}
}

2、双亲委派模型

2.1、概念

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

2.2、优点

1. 避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了。
2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模
型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 
类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户
自己提供的因此安全性就不能得到保证了。

2.3、缺点

双亲委派模型虽然有其优点,但在某些情况下也存在一定的问题,比如 Java 中 SPI(Service Provider Interface,服务提供接口,是 Java 提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI 的作用就是为这些被扩展的 API 寻找服务实现。)机制中的 JDBC 实现。

四、IoC 和 DI 

1、概念

IoC: Inversion of Control,控制反转。通过将对象交给 Spring 中 IoC 容器管理,在其他类中不直接 new 对象,而是通过将对象传递到当前类的方式来实现解耦的(耦合是指:两个或两个以上对象存在依赖,当一方修改之后会影响另一方,那么就说这些对象间存在耦合。而解耦就是解除两个或两个以上对象,修改之后影响另一方的问题。)。这样,控制权由应用代码转移带了容器,控制权发生了反转,这就是控制反转,它是spring框架的核心思想之一。

DI:Dependency Injection,依赖注入。由 IoC 容器在运⾏期间,动态地将某种依赖关系注⼊到对象之中。

PS:可参考:

http://t.csdn.cn/4vRKA

面试突击73:IoC 和 DI 有什么区别?-阿里云开发者社区 (aliyun.com)

2、区别

IoC:是一个思想,也就是一种指导原则,最终还是要有可行的落地方案。强调的是将对象实例的创建控制权由spring容器来统一管理,需要的时候从容器中取出,而不是由调用者自身去创建,从而达到降低代码耦合性与硬代码的目的。

DI:是一种具体的实现。强调的是当调用者需要使用对象实例时,spring容器为调用者提供对象实例这个过程。