JVM基础(入门)

一、什么是JVM

jvm全称是JVM(Java Virtual Machine,Java虚拟机),JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
JVM的作用:为java程序提供一个可以运行的环境;Java程序的跨平台特性主要就是因为JVM实现的。在编译java程序时会将写好的源程序通过编译器编译生成.class文件(又称为字节码文件),之后就是通过JVM内部的解释器将字节码文件解释成为具体平台上的机器指令执行,所以就可以实现java程序的跨平台特性。

二、JVM的内部体系结构

JVM内部体系结构大致分为三部分:类装载器(ClassLoader)子系统,运行时数据区和执行引擎。
如下图所示:
在这里插入图片描述

1、类加载器

在这里插入图片描述

作用:加载 .class文件;类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。
JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:

根类加载器(bootstrap class loader)
它用来加载 Java 的核心类,是用**原生代码**来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由 **C++** 实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
扩展类加载器(extensions class loader)
它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包中的类。==由Java语言实现,父类加载器为null==。
系统类加载器(system class loader)
	被称为系统(也称为应用APP)类加载器,由Java语言实现,**父类加载器为ExtClassLoader。** 它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。==如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器==。
	![在这里插入图片描述](https://img-blog.csdnimg.cn/529b272956f8434b88f7083976bad9a5.png)

2、双亲委派机制

双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
(1)双亲委派机制作用:

  1. 保证安全,java核心api中定义类型不会被随意替换;
  2. 通过层级关系避免类的重复加载

(2)执行顺序为:应用程序加载器=>扩展类加载器=>根加载器(最终执行);根加载器检查是否能够加载当前类,如果可以加载,就结束,不能就会抛出异常,通知子加载器进行加载。如果到了应用程序加载器还没找到,就会报错(Class Not Found);

3、沙箱安全机制

什么是沙箱?

Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

java中的安全模型

在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。如下图所示 JDK1.0安全模型:
在这里插入图片描述
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示 JDK1.1安全模型:
在这里插入图片描述
在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示 JDK1.2安全模型:
在这里插入图片描述
当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示 最新的安全模型(jdk 1.6)
在这里插入图片描述

组成沙箱的基本组件
  • 字节码校验器
    确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
  • 类装载器
    虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
    类装载器在三个方面对沙箱起作用:
  • 它防止恶意代码去干涉善意的代码;
  • 它守护了被信任的类库边界;
  • 它将代码归入保护域,确定了代码可以进行哪些操作。

4、native关键字

在这里插入图片描述

  • 凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!
  • 如图所示,会进入本地方法栈,然后调用本地方法本地接口JNI
  • JNI的作用:扩展java的使用,融合不同的编程语言为java所用!最初想融合:c,c++
  • Java在内存中专门开辟了一块标记区域:本地方法栈(Native Method Stack)登记native方法
  • 在最终执行过程中加载本地方法库中的方法通过JNI

5、程序计数器

内存空间小,线程私有。指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令;字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
注意:如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

6、方法区

属于所有线程所共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。简单来说,所有定义的方法的信息都保存在该区域。比如构造函数,接口代码
注意:运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关

7、Java 虚拟机栈

  • 线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
  • 线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题
  • 一旦线程结束,栈也就结束了
  • 栈帧:八大类型((boolean、byte、char、short、int、float、long、double))+对象引用+实例的方法
    注意:
    StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
    OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

8、堆

Heap:一个JVM只有一个堆内存,堆内存的大小是可以调节的;线程共享,主要是存放对象实例和数组。类加载器读取了类文件后,一般会把类的常量、方法、变量放到堆中,保存我们所有引用类型的真实对象。
堆内存中还要细分为以下三个区域:

  • 新生区:对象诞生和成长的地方,甚至死亡;
    新生区可细分为:
    1)伊甸园区.所有对象都是在伊甸园区new出来的;
    2)幸存者区(0,1)
  • 老年区:老年区:新生区进行轻gc(垃圾回收机制)活下来的对象
  • 永久区:这个区域是常驻内存的,存放JDK自身携带的Class对象.interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭虚拟机就会释放这个区域的内存。
    注意:
    1)jdk 1.6 之前:永久代,常量池是在方法区
    2)jdk 1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中
    3)jdk 1.8之后:无永久代,常量池在元空间(逻辑上存在,物理上不存在,因为元空间直接在内存,不受堆空间的限制)
    在这里插入图片描述
    在这里插入图片描述
    通过上图可知为什么元空间逻辑上存在,物理上不存在。
    在这里插入图片描述
    注意:
  • GC垃圾回收,主要是工作在养老区和伊甸区。
  • 内存满了就会出现OOM,堆内存不够!
  • 在JDK8以后,永久存储区改了个名字(元空间).

9、简单类对象的实例化过程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

10、垃圾回收机制

程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

注意:

  • JVM在运行GC时并不是对新生代(伊甸区),幸存区,老年区统一回收,大部分时候,回收的都是新生代;
  • GC两个种类:轻GC(普通GC;当伊甸园区满了发生),重GC(全局GC;当老年区满了发生);
  • 每次GC,都会将伊甸园的对象转移到幸存区:一旦伊甸区被GC,那么伊甸园区就会是空的;
  • 幸存区from和幸存区to的区别,记住一句话 :谁空谁是to;
  • 当一个对象经历了15次GC还没有死的情况下,就好进入老年区,因为对象头只留了四位设置GC最大次数(-XX:MaxTenuringThreshold),所以只能最大值设为15。
    注意: -XX:MaxTenuringThreshold(垃圾最大年龄):如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率,该参数只有在串行GC时才有效。

GC触发时间

  • Young GC 一般是在新生代的 Eden 区满了之后触发的,之后采用复制算法在 Survivor 的 from 和 to 区之间来回收新生代的垃圾对象。
  • 在每次发生 Young GC 之前会进行检查,当老年代可用内存小于新生代全部对象的大小,而这时候没开启空间担保参数(HandlePromotionFailure=false)会直接触发Full GC
  • 当开启空间担保参数(HandlePromotionFailure=true)时,如果【老年代可用的连续内存空间】 < 【新生代历次 Young GC 后升入老年代的对象总和的平均大小】,则说明本次 Young GC 后升入老年代的对象大小可能会超过老年代当前可用内存空间。此时必须先触发一次 Old GC 让老年代腾出更多的空间,而后再执行 Young GC。
  • 如果老年代内存使用率超过了 92%,也要直接触发 Old GC,当然这个比例是可以通过参数调整的

11、堆内存调优

OutOfMemoryError(OOM)解决措施:

  1. 尝试扩大堆内存看结果
  2. 分析内存,使用专业工具(JPofiler)看一下是否有垃圾代码或者无限循环的代码占用空间;
    一直设置的指令含义:
-Xms 设置初始化内存分配大小  1/64
-Xmx 设置最大分配内存,默认1/4
-XX:+PrintGCDetails   打印GC垃圾回收信息
需要配置命令:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError  //OOM dump

查找OOM错误可以在VM中设置上述最后一条指令,并且通过JPofiler工具来分析OOM发生的原因。

12、垃圾回收算法

1. 标记清除算法
从根集合出发,将能访问到的对象打上标记,然后遍历堆,将没有标记的对象清除。
优点:不会挪动对象在堆中的位置
缺点:容易产生内存碎片
2. 复制算法

把堆划分为两部分,每次分配对象的时候只使用其中一半.当标记过程结束后,把存活的对象全都挪到堆的另一半中.
在这里插入图片描述
优点:不会产生内存碎片
缺点:浪费了内存空间

3. 标记整理算法

在这里插入图片描述
优点:不会产生内存碎片
缺点:比标记清除算法多了一次扫描时间。多了一个移动成本。

4. 分代回收算法

根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。
新生代:每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。
老年代:老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用标记 —— 清除或者标记 —— 整理算法回收。

13、垃圾回收器

垃圾收集器就是内存回收的具体实现。**

  • Serial 收集器
    Serial(串行)是一个单线程收集器,它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
    新生代采用标记-复制算法,老年代采用标记-整理算法。
  • ParNew 收集器
    ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
    新生代采用标记-复制算法,老年代采用标记-整理算法。
  • CMS 收集器
    CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
    CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
    (1)初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
    (2)并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。
    (3)重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。
    (4) 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

三、JVM总结知识点二

1、死亡对象判断方法

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡

  • 引用计数法
    给对象中添加一个引用计数器:每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
    缺点:它很难解决对象之间相互循环引用的问题。
  • 可达性分析算法
    这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

哪些对象可以作为GC Roots呢?

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

对象可以被回收,就代表一定会被回收嘛?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

2、如何判断一个类是无用的类

类需要同时满足下面 3 个条件才能算是 “无用的类” :

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

四、JVM总结知识点三(类)

在这里插入图片描述

1、类加载过程详解

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述


版权声明:本文为m0_53703555原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>