JVM的快速入门

JVM体系的结构概述

  1. JVM是运行在操作系统之上的,它并不没有直接和硬件进行交互。
    在这里插入图片描述
  2. JVM体系结构图:
    在这里插入图片描述
  3. 类加载
    (1) 类加载机制:虚拟机把描述类的数据从.class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
    (2) 类加载的过程:
    在这里插入图片描述
  • 加载:将class文件字节码内容加载到内存中;并将这些内容转换成方法区中
    的运行时数据结构;在内存中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口
  • 验证:确保class 文件中的字节内容符合jvm规范,并且不会危害jvm自身的安
    全。
  • 准备:正式为类变量(静态变量)分配内存空间,并为静态变量初始化(赋默认
    值),静态变量的内存在方法区中分配。
  • 解析:虚拟机常量池内的符号引用替换为直接引用的过程。
    比如String s =“aaa”,转化为 s的地址指向“aaa”的地址
  • 初始化:根据程序员通过程序制定的主观计划完成静态变量等资源的初始化。在这个过程会完成静态变量的赋值和静态代码块中的语句。
  1. 类加载器(ClassLoader)
    (1) 类加载器:用于实现类加载过程中加载阶段,负责将class文件字节码内容加载到内存中;并将这些内容转换成方法区中的运行时数据结构,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。
    在这里插入图片描述
    (2) 类加载的分类:
  • 虚拟机自带的类加载
    启动类加载器:C++语言实现,负责加载%Java_home%/jre/lib/rt.jar 中的内容
    扩展类加载器:Java语言实现,负责加载%Java_home%/jre/lib/ext/*.jar中的内容
    应用程序类加载器:也可以称为系统类加载器,它负责加载用户类路径classPath的所有类。如果应用程序中没有定义过自己的类加载器,一般情况默认使用应用程序类加载。
  • 用户自定义的类加载器,继承java.lang.ClassLoader
    在这里插入图片描述
    (3) 类加载器的双亲委派模型(Parents Delegation Model):
  • 工作过程:如果一个类加载器收到了加载请求,它首先不会尝试使用自己加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载中,只有父类反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要的类)的时候,子类才会尝试自己加载。
  • 好处:采用双亲委派模型组织累加器之间的关系,有一个显而易见的好处是
    Java随着它的类加载一起具备了一种带有优先级的层次关系,例如加载位于
    rt.jar中的java.lang.Object类,无论哪一个类加载加载这个类,最终都委托给
    最顶级的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
    (4) 代码示例:
public class Test{
public static void main(String[] args) throws IOException {
	Object obj = new Object();
	System.out.println(obj.getClass().getClassLoader());
	MyClass mc = new MyClass();
	System.out.println(mc.getClass().getClassLoader().
	getParent().getParent());
	System.out.println(mc.getClass().getClassLoader().getParent());
	System.out.println(mc.getClass().getClassLoader());
	}
}
class MyClass{}

双亲委派模型对于java正常运作很重要,但是底层实现非常简单,实现双亲委派的大代码集中在java.lang.ClassLoader的loadClass()方法中:先检查是否已经被加载过,若没有加载则调用父类的loadClass方法,直到找到启动类加载,启动类加载器在在自己的范围内进行搜素,如果父类加载器加载失败,则子类处理异常,调用自身的findClass方法进行加载。

protected synchronized Class<?> loadClass(String name, boolean
resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
private Class findBootstrapClass0(String name)throws ClassNotFoundException{
	check();
	if (!checkName(name))throw new ClassNotFoundException(name);
	return findBootstrapClass(name);
}
private native Class findBootstrapClass(String name)throws ClassNotFoundException;
  1. JVM的内存结构详细介绍
    在这里插入图片描述
    注意:橘色代表所有线程共享的区域 灰色代表线程隔离的数据区域
  • 程序计数器:是一块较小的内存空间,它可以看作是当前线程执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程活肤等基础功能都是依赖这个计数器来完成。同时为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为”线程私有”的内存
  • Java虚拟机栈:与程序计数器一样,也是线程私有的,并且生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
    在这里插入图片描述
  • 本地方法栈:为虚拟机使用到的Native方法服务,也属于线程私有的数据区域。一般情况下,我们无需关注此区域。
  • 方法区:又被称为非堆,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,也是各个线程共享的内存区域。
    注意:方法区中存在一个叫运行时常量池(Runtime Constant Pool)的区域,它主要用于存放编译生成的各种字面值和符号引用,这部分内容将在类加载后存放到运行时常量池中。
  • Java堆:是Java虚拟机所管理的内存中最大的一块,被所有线程共享的一块内存区域,在虚拟机启动时创建,主要目的是存放对象实例。同时此区域是垃圾收集器管理的最主要区域,因此很多时候也被称为”GC堆”,根据垃圾收集器采用的分代收集算法。
    堆内存逻辑分为:
    在这里插入图片描述

垃圾回收

  1. 判断对象是否已死
    (1) 引用计数算法:给每一个对象中添加一个引用计数器,即每当一个地方引用此对象时,则计数器就加1,当引用失效时,则计数器就减1;任何时刻计数器为0的对象是不可能再被使用。
    优点:引用计数算符实现比较简单,判断效率高
    缺点:很难解决对象之间循环引用的问题。
    注意:目前主流的Java虚拟机并没有采用引用计数算法来管理内存。
    (2)可达性分析算法:通过一系列的称为"GC Roots"的对象作为起始点从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链,则证明此对象时不可用的。
    在这里插入图片描述
    在Java语言中,可以作为GC Roots的对象包括以下几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中(Native方法)引用的对象
  2. 引用的分类
    (1) 背景:传统中理解一个对象只有被引用或是没有被引用两种状态,但是对于如何描述"食之无味,弃之可惜"的对象无能力。我们希望描述这样一类对象:当内存空间还足够时,则能保留在内存中,如果内存空间在进行垃圾回收之后还是非常紧张,则可以抛弃这些对象(很多系统的缓冲功能都符合这样的应用场景)。
    (2) 在JDK1.2之后,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期:

    • 强引用(Strong Reference)
    • 软引用(Soft Reference)
    • 弱引用(Weak Reference)
    • 虚引用(Phantom Reference)

    (3) 引用的级别由高到低:强引用 > 软引用 > 弱引用 > 虚引用

    • 强引用:是程序中使用最普遍的引用,类似"Object obj = new Object();"这类引用,只要强引用还存在,垃圾回收器永远不会回收掉强引用中的对象,即使当内存空间不足时,JVM抛出 OutOfMemoryError,也不回收。
    • 软引用:用来描述一些还有用但并非必需的对象。如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。引用可用来实现内存敏感的高速缓存。在JDK1.2之后,提供了SoftReference类实现软引用。
    • 弱引用:也是用来描述一些并非必需的对象。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。在JDK1.2之后,提供了WeakReference类实现弱引用。
    • 虚拟引用:顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,也无法通过虚拟引用来获取一个实例对象。为一个对象设置虚拟引用关联的唯一目的是让当前这个对象被垃圾回收器回收时收到一个通知。在JDK1.2之后,提供了PhantomReference类实现虚拟引用。
  3. 不同区域的垃圾回收
    在这里插入图片描述
    方法区:即HotSpot虚拟机中的永久代,永久代的垃圾收集主要回收两部分:废弃常量和无用的类对象

  • 废弃常量:假设字符串常量"abc"已经进入常量池中,但是当前系统没有任何一个String类型引用指向"abc"常量,也没有其他地方使用"abc"字面值常量,如果发生内存回收,而且又有必要的情况下,会对"abc"常量进行清除。
  • 无用的类对象:
    (1)该类所有的实例对象都已经被回收,即Java堆中不存在任何该类的实

    (2)加载该类的ClassLoader已经被回收
    (3)该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地
    方通过反射访问该类的方法
    注意:此处无用对象满足3个条件可以回收,但是不是必须。是否需要被回收
    可以通过 -Xnoclassgc 参数进行控制;
    同时还可以使XX:+TraceClassLoading查看类加载的信息。
  • 堆区:尤其是在新生代的垃圾回收中,常规应用进行一次垃圾回收一般可以回收70%~95%空间。
    堆内存分配图:
    在这里插入图片描述
    简述:新生区是对象的创建、应用、消亡的区域,一个对象在这里产生、应
    用、最终被垃圾回收器收集,消亡。新生区又分为两部分:伊甸区和幸存者
    区。所有新创建的对象(new) 都是在伊甸区; 幸存者分为两个:幸存者0区
    和1区,当伊甸区的空间用完时,程序 需要创建新的对象 ,JVM对象伊甸区开始进行垃圾回收,应用的是YGC,将伊甸区不再使用的对象进行销毁,然后将伊甸区剩余的对象移到幸存者0区,0区 满了,对0区进行垃圾销毁,存活的对象移到幸存者1区,如果1区也满了,则再将1区的移动到养老区;如果养老区也 满了,此时将JVM将开启 FullGC(简称:FGC),进行 养老区的内存清理。但是 如果执行Full GC之后 依然无法保存 新的对象,则产生OOM异常:堆内存溢出
  1. 垃圾回收的算法
    (1) 标记-清除算法(Mark-Sweep):它是最基础的垃圾回收算法,其他算法都是基于这种思想而改进的。标记-清除算法分为 “标记” 和 “清除” 两个阶段:首先标记出所需要回收的对象,在标记完后统一回收所有被标记的对象。
    在这里插入图片描述
    缺点:
  • 标记和清除两个过程效率都不高
  • 标记清除后会产生大量的不连续的内存碎片,后续会发生大对象找不
    到可利用空间的问题
    (2) 复制算法(Copying):它将可用内存分为两块,每次只用其中的一块;当这块内存用完以后,将还存活的对象复制到另一块上面,然后再把已经使用的内存空间一次清理掉。
    在这里插入图片描述
  • 分析:这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。
  • 应用:目前商业版的虚拟机都采用复制算法回收新生代,新生代中的98%的对象是"朝生夕死",所以将堆内存分为一块较大的Eden空间和两块较小的Servivor(幸存者)空间,HotSpot虚拟机默认Eden和Servivor大小比例为8:1。每一次使用Eden和其中一块Servivor,当回收时,将Eden和Servivor中还存活着的对象一次性的复制到另外一块Servivor,最后清理掉Eden和使用过的Servivor空间
    (3) 标记-整理算法(Mark-Compact):标记操作和”标记-清除“算法一样,后续操作变成不直接清理对象,而是在清理无用对象的时候完成让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
    在这里插入图片描述
  • 分析:不会产生内存碎片,在标记的基础上需要移动对象,还是会降低效率
    的。
  • 应用:老年代的对象存活率较高,一般采用此收集算法进行回收。
    (4) 分代收集算法(Generational Collection):目前商业虚拟机的垃圾回收都是采用的"分代收集",它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
  • 大部分JVM的GC对于新生代都采取Copying算法
  • 老生代因为每次只回收少量对象,因而采用Mark-Compact算法
发布了24 篇原创文章 · 获赞 1 · 访问量 507

猜你喜欢

转载自blog.csdn.net/Mr_YXX/article/details/104936116