用户您好!请先登录!

Java虚拟机原理+垃圾回收

Java虚拟机原理+垃圾回收

文章目录

代码的大体执行过程

代码在JVM里面的详细执行过程

类加载器详解

运行时数据区详解

先来看下面这一段代码:

public class APP {
public int add() {
int a = 1;
int b = 2;
int c = (a+b)*3;
return c;
}
public static void main(String[] args) {
APP app1 = new APP();
System.out.println(app1.add());
APP app2 = new APP();
System.out.println(app2.add());
}
}

代码的大体执行过程

JDK、JRE、JVM的区别和联系;

代码的大体执行过程如下:

Java虚拟机-------虚拟机原理+垃圾回收

从.java源文件编译生成.class字节码文件的过程如下:

Java虚拟机-------虚拟机原理+垃圾回收

代码在JVM里面的详细执行过程

在JVM内部就这个样子的:

Java虚拟机-------虚拟机原理+垃圾回收

然后,先说说类装载子系统

Java虚拟机-------虚拟机原理+垃圾回收

然后是运行时数据区(内存模型)的:

Java虚拟机-------虚拟机原理+垃圾回收

最后是执行引擎:

Java虚拟机-------虚拟机原理+垃圾回收

类加载器详解

class文件的加载过程详细的可以看我的另一篇博客,类加载器;

补充的一点是:

虚拟机规范中明确了在5中情况下会对类进行加载:

创建对象实例:new 对象的时候,会对类进行初始化(前提是这个类没有被初始化);

通过class文件反射创建对象;

调用类的静态属性或静态属性赋值;

调用类中的静态方法;

初始化一个类的子类的时候,在使用子类的时候,先初始化父类;

Java虚拟机启动时被标记为启动类的的类,比如main所在的类;

不会被加载的情况:

在同一个虚拟机中,一个类只能被加载一次,如果已经被初始化的一个类不会再被加载;

在编译时,能确定下来的静态变量,不会对类进行初始化;

运行时数据区详解

从上面的运行时数据区(内存模型)的模型图我们可以看到,堆和方法区是线程之间共享的(会发生并发安全的地方),而虚拟机栈、本地方法栈、程序计数器是线程私有的(也就是每个线程都已自己的虚拟机栈、本地方法栈和程序计数器),下面是关于内存模型中各个部分的介绍:

程序计数器(线程私有):就是一个指针,指向方法区中的方法字节码(用来存储下一条指令的地址,也就是马上要执行的指令的地址),有执行引擎读取下一条指令,是一个非常小的空间,几乎可以忽略不计;

方法区(线程共享):类的所有字段和方法字节码,以及一些特殊方法如:构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中;

虚拟机栈(线程私有):Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息),不存在垃圾回收等问题,只要线程一结束就释放,生命周期和线程一致;

本地方法栈(线程私有):就是存放哪些native方法的;

堆(线程共享):很大的一块空间,用于存放对象实例,垃圾回收主要发生的地方;

栈帧中的组成部分介绍:

局部变量表:可以这么理解,局部变量表里面存放的是一个一个的小容器,用来存放数据的,比如我们开头那段代码中的a = 1, b = 2,在虚拟机里面不可能真给你弄出一个a、b来存放1和2,于是就用局部变量表中的这些小容器来存放,比如容器1存放1,容器2存放2,容器3存放他们的计算结果300……,当然,除了能存放基本的数据类型以外,还可以存放引用类型的对象指针;

操作数栈:每次要进行操作时(相加、相减、乘除等等),先把相关的数都放到操作数栈里面,让后继续相关的操作,并将操作的结果放到局部变量表中去;

动态链接:比如上面那个main方法运行到第二行就要进入到add()方法中去了,就是在运行的时候将符号引用转化为直接引用;

方法出口:比如上面的代码,add()方法运行完之后,还要将结果返回给main方法的,这个主要指的是return一类的;

Java虚拟机-------虚拟机原理+垃圾回收

垃圾回收

文章目录

  • JVM垃圾回收简介
  • 如何将对象识别为垃圾:
  • 垃圾回收算法
  • 标记—清除
  • 复制算法
  • 标记—整理
  • 分代回收算法
  • 垃圾回收器
  • 内存分配与回收策略
  • 方法区的回收

JVM垃圾回收简介

在JVM提供垃圾回收,将内存空间不在使用的对象进行回收,垃圾回收主要针对堆空间,垃圾回收操作需要消耗一定的资源和时间;

JVM对堆空间进行分区:年轻代、年老代、永久代;

对年轻代的垃圾回收:minor GC

对年轻代和老年代同时作用的GC称为:Full GC

Java中的四种引用

Java 的堆内存和 GC 线程

当我们在 Java 上 new 一个对象的时候,都要在 java heap(堆)上为该对象开辟一块内存,当该对象的引用出作用域失效的时候,那么该对象就会作为 GC 垃圾回收器回收的目标之一。

GC 垃圾回收器是由 JVM 专门的一个线程,叫做垃圾回收线程来实现的,该线程的优先级比较低,因此,如果有的对象不被引用了,GC 是不会及时的回收该对象使用的内存的,但是我们有一种方法,当 GC线程执行的时候,可以让它更快的知道该对象是否能被立即回收,那就是当该对象不再使用的时候,把该对象的引用及时置 null,这样就可以帮助 GC 及时判断该对象不再被使用了,可以立即回收了。

大家都知道,所有的 Java 类都继承自 Object 类,该类里面有一个方法,如下:

Java虚拟机-------虚拟机原理+垃圾回收

在 Java 自定义类里面可以重写 finalize 方法,当对象被 GC 回收之前,会先调用对象的 finalize 方法,释放相关的资源,然后在 GC 的下一个回收周期里面,再把该对象的内存回收掉。

Java 是否存在内存泄露

Java 代码虽然不用我们自己释放对象,而是靠 GC 来回收对象内存,但最起码我们要告诉 GC 这个对象我们不用了,这样 GC 才能在一次垃圾回收周期内回收该对象的资源;

我们应该尽量减少 static 成员对象的定义,由于 static 域的成员在方法区,其生命周期和 Java 进程的生命周期是相等的,而且该 static 成员对象又引用了其他的对象,导致在一个直接引用链或者间接引用链上的所有对象都无法被 GC 回收;

我们自定义一个集合的时候,如果从逻辑上集合的某个对象已经不需要使用了,应该立刻把该对象的引用置为 null,否则会使该对象无法回收,造成内存泄露;内存泄漏以及jmap命令的用法;

还有其他的 I/O 流打开了却未关闭,如 File 打开没有关闭,Socket 打开没有关闭,数据库 Connection,Statement,Resultset 等对象打开,使用完了却没有关闭等等,都会造成内存资源泄露的问题。

如何将对象识别为垃圾:

1、引用记数法

对对象添加引用的标识,每对对象增加一个引用,引用标识进行+1操作,减少一个引用,标识-1,当标识为0的时候,说明对象不存在引用可以被回收;

缺点:无法处理对象间相互引用的问题;

Java虚拟机-------虚拟机原理+垃圾回收

2、可达性分析

判断对象是否存活,将堆中对象想象成一棵树,从树根(GC root)开始遍历所有的对象,能到达的称为可用对象,不能到达的称之为垃圾;

GC root一定是可达的,主要有:

虚拟机栈中引用的对象一定是可达的;

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

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

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

垃圾回收算法

标记—清除

用可达性分析,先标记、再清除;

Java虚拟机-------虚拟机原理+垃圾回收
Java虚拟机-------虚拟机原理+垃圾回收

缺点:

①标记—清除之后会出现断断续续的空闲空间(内存碎片),空间无法高效利用,

②并且先标记后清除需要对堆空间前后两次遍历,效率不高;

复制算法

一开始,将堆空间内存分成两个部分A、B,只使用其中一部分,然后对A进行可达性分析,将可用的对象拷贝到另一个空间B去,再把A中的对象全部擦除,这样就把标记—清除算法的两个问题都解决了;

回收前:

Java虚拟机-------虚拟机原理+垃圾回收

回收后:

Java虚拟机-------虚拟机原理+垃圾回收

缺点:

①只使用一半堆空间,浪费一半的空间,

②如果对应的A那半部分出现极端情况(A全都是或大部分都是生命周期比较长的对象),那就需要全部拷贝);

标记—整理

先标记、再整理,先可达性分析,标记对象是否可用,然后将可用对象向一端移动,这样垃圾回收之后的堆空间的剩余空间是连续的;

回收前:

Java虚拟机-------虚拟机原理+垃圾回收

回收后:

Java虚拟机-------虚拟机原理+垃圾回收

缺点:

效率也不高,不仅要标记存活对象,还要整理所有存活对象的引用地址,在

效率上不如复制算法;

分代回收算法

新生代:朝生夕灭(也没这么久),存活时间短,老年代:经过多次minor GC依旧存在,存活时间比较长;

分代回收是对上面三种算法的通用,在新生代中每次垃圾回收都发现有大量的对象死去,只有少量存活,因此采用复制算法回收新生代,只需要付出少量对象的复制成本就可以完成收集;而老年代中对象的存活率高,不适合采用复制算法,而且如果老年代采用复制算法,它是没有额外的空间进行分配担保的,因此必须使用标记/清理算法或者标记/整理算法来进行回收。

总结一下就是,分代收集算法的原理是采用复制算法来收集新生代,采用标记/清理算法或者标记/整理算法收集老年代。

垃圾回收器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用;

Java虚拟机-------虚拟机原理+垃圾回收

Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;

Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;

ParNew收集器 (复制算法):新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;

Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 =用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;

Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;

CMS(Concurrent Mark Sweep)收集器(标记-清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

内存分配与回收策略

内存分配与回收策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存 以及 回收分配给对象的内存。一般而言,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓存(TLAB),将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中。总的来说,内存分配规则并不是一层不变的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

1) 对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保。

2) 大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

3) 长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。

4) 动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

需要注意的是,Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。也就是说,垃圾收集器回收的是无任何引用的对象占据的内存空间而不是对象本身。

方法区的回收

方法区的内存回收目标主要是针对 常量池的回收 和 对类型的卸载。回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;

加载该类的ClassLoader已经被回收;

该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收(卸载),这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。特别地,在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

乞力马扎罗的鱼
乞力马扎罗的鱼

不积跬步,无以至千里

要发表评论,您必须先登录