JVM,就是Java虚拟机,Java的的程序都是运行在JVM当中。
JVM执行流程
程序在执行之前先要把java源代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方(类加载器(ClassLoader)) 把文件加载到内存中的运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口(本地库接口(Native Interface))来实现整个程序的功能。
执行引擎
将Java字节码转换成CPU指令。
本地方法接口
调用不同系统的API实现不同的功能。
运行时数据区
方法区
方法区中存放的是类对象,可以理解为模板。在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)。运行时常量池是方法区的一部分,存放字面量与符号引用。
JDK 1.8 元空间的变化
1.对于现在使用最最广泛的 HotSpot 来说,JDK 8 元空间的内存属于本地内存,这样元空间的大小就不在受 JVM 最大内存的参数影响了,而是与本地内存的大小有关。
2.JDK 8 中将字符串常量池移动到了堆中。
堆
堆中存放的是new出来的具体对象。堆区和方法区之间是内存共享的:多个线程都可以去new对象,那么必须从方法区中拿对象的模板;每个线程创建出来的对象都会放在堆中。
虚拟机栈(线程私有)
栈主要记录的是 方法的调用关系和可能出现的栈溢出错误。 每一个线程都有对应的一个Java虚拟机栈,每调用一个方法都会以一个栈帧的形式加入到线程的栈中,方法执行完成之后栈帧就会被调出栈。此时可能存在一种情况,在递归调用时,调用的深度过深可能会出现栈溢出的错误。
- 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。
- 操作栈:每个方法会生成一个先进后出的操作栈。
- 动态连接:指向运行时常量池的方法引用。
- 方法返回地址:PC 寄存器的地址。
什么是线程私有?
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存
本地方法栈(线程私有)
工作原理和Java虚拟机栈一样,记录的是本地方法的调用关系。
程序计数器(线程私有)
记录了当前线程的方法执行到了那一行(指令)。程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空。
堆溢出问题
Java堆用于存储对象实例,不断的创建对象,就可能会在对象数量达到最大堆容量后就会产生内存溢出。
演示堆溢出现象:
设置JVM参数-Xms:设置堆的最小值、-Xmx:设置堆最大值。
public class HeapDemo {
static class OOMObject {}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
// 不停的为list添加元素
while (true) {
list.add(new OOMObject());
}
}
}
当出现"Java heap space"则很明确的告知我们,OOM发生在堆上,此时堆内存被占满。此时需要优化堆内存的大小(通过调整-Xss参数来)来避免这个错误。
类加载
类加载的过程
对于一个类来说,它的生命周期是这样的:
加载
加载就是读取.class文件。
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
连接
验证
这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信 息被当作代码运行后不会危害虚拟机自身的安全。
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。比如此时有这样一行代码:public static int value = 123;它是初始化 value 的 int 值为 0,而非 123。
解析
解析阶段是Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。
初始化
初始化阶段,Java 虚拟机真正开始执行类中编写的Java 代码,将控制权移交给应用程序。初始化阶段就是执行类构造器方法的过程。
双亲委派机制
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父
类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启
动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)
时,子加载器才会尝试自己去完成加载。
1.BootStrap:启动类加载器:加载 JDK 中 lib 目录中 Java 的核心类库,即$JAVA_HOME/lib目录。 扩展类加载器。加载 lib/ext 目录下的类。
2.ExtClassLoader: 扩展类加载器,加载lib/ext目录下的类;
3.AppClassLoader:应用程序类加载器;
4.自定义加载器:根据自己的需求定制类加载器;
垃圾回收
public class GCDemo {
public static void main(String[] args) {
test();
}
private static void test() {
Student student = new Student();
System.out.println(student);
}
}
对于上述这个实例,当test执行完成之后,就不会再使用了,所以对于这种无效的对象,将会被当作垃圾回收掉。如何标记这个对象是垃圾?
死亡对象的判断算法
引用计数算法
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任
何时刻计数器为0的对象就是不能再被使用的,即对象已"死”。
但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的
循环引用问题
public class GCDemo01 {
public Object instance = null;
private static int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
GCDemo01 test1 = new GCDemo01();
GCDemo01 test2 = new GCDemo01();
test1.instance = test2;
test2.instance = test1;
test1 = null;
test2 = null;
// 强制jvm进行垃圾回收
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
比如上述代码,当test1=null;test=null时,那么test1和test2中的instance再也无法访问到,所以此时堆中对象的引用计数无法归零,导致无法垃圾回收。
可达性分析算法
通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。Java中就采用了"可达性分析"来判断对象是否存活。
在Java语言中,可作为GC Roots的对象包含下面几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象;
2.方法区中类静态属性引用的对象;
3.方法区中常量引用的对象;
4.本地方法栈中 JNI(Native方法)引用的对象。
从上面我们可以看出“引用”的功能,除了最早我们使用它(引用)来查找对象,现在我们还可以使用“引用”来判断死亡对象了。所以在 JDK1.2 时,Java 对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。
1.强引用:类似于Student student = new Student()这种引用,会经历正常的GC,判定为死亡时会被回收;
2.软引用:软引用是用来描述一些还有用但是不是必须的对象,当系统内存不够或者触发阈值时会被回收;
3.弱引用:弱引用也是用来描述非必需对象的。在每次新生代GC时都会回收弱引用;
4.虚引用:只是在对象被回收时,收到一个通知。
垃圾回收的过程
通过上面的学习可以将死亡对象在堆中标记出来,标记出来之后就可以进行垃圾回收操作。先来看一下堆的结构:
HotSpot默认新生代与老年代的比例是1:2,新生代中Eden区与Survivor区的大小比例是8:1,也就是说Eden:Survivor From(S0):Survivor To(S1)=8:1:1。所有新new出来的对象全都在Eden区。每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。
回收过程如下:
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参数MaxTen),最终如果还存活就将其放到老年代中。
新生代:一般创建的对象都会进入新生代;
老年代:大对象和经历了N次(默认值是15)垃圾回收依然存活下来的对象会从新生代移动到老年代。
新生代的GC称为Minor GC ,老年代的GC称为Full GC或Major GC。
每次进行垃圾回收的时候,程序都会进入暂停状态(STW),STOP THE WORLD。 为了高效的扫描内存区域,缩短程序暂停的时间,有一系列的垃圾回收算法。
标记-清除算法
"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
"标记-清除"算法的不足主要有两个 :
1.效率问题 : 标记和清除这两个过程的效率都不高
2.空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中
需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
复制算法
"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。HotSpot在S0和S1区使用的就是这种算法。
标记-整理算法
"标记-整理算法"主要应用于老年代。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
缺点是在回收之后多了一步整理内存的工作;优点是可以有大量连续的内存空间。
在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-整理"算法。
垃圾收集器
垃圾收集算法是内存回收的方法论,而垃圾收集器就是内存回收的具体实现。垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。垃圾收集器的不断更新就是为了减少STW。
Serial
Serial 收集器是最基本、发展历史最悠久的串行GC收集器。它是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
ParNew
ParNew是对Serial优化了的并行GC。用多线程的方式扫描内存,提高垃圾回收的效率,减少STW的时间。
Parallel Scavenge
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
与前面的区别在于,它采用了GC自适应的调节策略:
Parallel Scavenge收集器有一个参数- XX:+UseAdaptiveSizePolicy 。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了, 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
Serial Old
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS
CMS是一种老年代的并发GC。 与之前的方法不同的是,它使用了三色标记算法。它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS remark)、并发清除(CMS concurrent sweep)。
G1
G1(Garbage First)垃圾回收器是用在heap memory很大的情况下,在内存区域的划分上不再像之前的新生代和老年代一样,而是把heap划分为很多很多的region块,然后并行的对其进行垃圾回收,从而提高效率。
图中一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区
域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。
G1垃圾收集器还增加了一种新的内存区域,叫做Humongous(大对象)内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象。
1.年轻代
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域。
2.老年代
对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样。
一个对象的一生
我是一个普通的 Java 对象,我出生在 Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了 Survivor 区的 “From” 区(S0 区),自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的 “From” 区,有时候在 Survivor 的 “To” 区(S1 区),居无定所。直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在老年代里,我生活了很多年(每次GC加一岁)然后被回收了。文章来源:https://www.toymoban.com/news/detail-562851.html
继续加油~文章来源地址https://www.toymoban.com/news/detail-562851.html
到了这里,关于【Java】JVM执行流程、类加载过程和垃圾回收机制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!