Java—JVM

Java—JVM (自家学习用,大多摘自链接里的内容)

一、概述
链接: link.
链接: link.

JVM它是Java Virtual Machine 的缩写,JVM 是运行在操作系统之上的,它与硬件没有直接的交互。组成部分包括类装载器(ClassLoader)、堆、方法区、栈、本地方法栈、程序计数器(PC)、本地接口(Native interface)

其中栈、本地方法栈、程序计数器(PC)为线程私有;堆、方法区是多线程共享,也是垃圾回收GC管理的区域。
在这里插入图片描述
:是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储对象的引用、局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

:是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为:新生代
(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

方法区/永久代:即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量(final)、静态变量(static)、即时编译器编译后的代码等数据.。HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。运行时常量池(Runtime Constant Pool)是方法区的一部分.

本地方法栈:本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务。用于存放Java通过Native(JNI)关键字调用的本地方法栈帧。

在这里插入图片描述

二、类加载、类加载器以及双亲委派机制
链接: link.

类的加载分为三步:加载、链接(包括验证、准备、解析三个阶段)、初始化

1、加载

加载指的是将类的.class二进制文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。类的加载由类加载器完成,类加载器通常由JVM提供。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
①、从本地文件系统加载.class文件
②、从JAR包加载class文件,例如JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
③、通过网络加载class文件。
④、把一个Java源文件动态编译,并执行加载。

2、链接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。链接包括验证、准备、解析三个阶段

验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。

准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值(根据类型设置的默认值,只是为了占坑,真正初始值在初始化阶段设置)。

解析:将类的二进制数据中的符号引用替换成直接引用。

3、初始化

初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

类加载时机

①、创建类的实例,也就是new一个对象
②、访问某个类或接口的静态变量,或者对该静态变量赋值
③、调用类的静态方法
④、反射(Class.forName(“com.lyj.load”))
⑤、初始化一个类的子类(会首先初始化子类的父类)
⑥、JVM启动时标明的启动类,即文件名和类名相同的那个类

对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

4、类加载器(ClassLoader)

JVM预定义有三种类加载器

根类加载器(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):被称为系统(也称为应用)类加载器。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

5、加载机制:

主要任务:
①、通过“类全名”来获取定义此类的二进制字节流
②、将字节流所代表的静态存储结构转换为方法区的运行时数据结构
③、在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

ClassLoder的具体操作

JVM的类加载机制主要有如下3种。

全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派:其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

三、GC

Java 堆从 GC 的角度还可以细分为:
新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代和永久代。

1、新生代与MinorGC
是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
ServivorFrom:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
ServivorTo:保留了一次 MinorGC 过程中的幸存者。

MinorGC (轻GC)的过程(复制->清空->互换)

MinorGC 采用复制算法
①、eden、servicorFrom 复制到 ServicorTo,年龄+1
首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄已经达到了老年的(默认15次GC)标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);

②、清空 eden、servicorFrom
然后,清空 Eden 和 ServicorFrom 中的对象;

③、ServicorTo 和 ServicorFrom 互换(目的是保证To区是空的)
最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。

3、老年代与MajorGC(重GC,采用标记-整理-压缩算法)

主要存放应用程序中生命周期长的内存对象。

老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象进入老年代,导致老年代的空间不够用时才触发MajorGC 。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC 采用标记清除压缩算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收(相当于两次遍历)。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并(标记清除之后再进行压缩)或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

4、永久代

指内存的永久保存区域,主要存放类加载时提取的类型信息类的元数据(包括类的方法代码,变量名,方法名,访问权限,返回值等)信息,类在被加载的时候这些信息与数据都被放入永久代(方法区)域,它和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。永久带的内存回收的主要目标是针对常量池的回收和类型的卸载

关于永久代和方法区
《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 同时大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。因此,我们得到了结论,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。

5、JAVA8 与元数据

在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。

四、垃圾回收算法
链接: link.

1、如何确定某个对象是“垃圾”

引用计数法
在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。这种方式的特点是实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。

可达性分析法
为了解决这个问题,在Java中采取了可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

2、垃圾收集算法
确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,常用算法如下:

Mark-Sweep(标记-清除)算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
在这里插入图片描述
从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

Copying(复制)算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
在这里插入图片描述
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

Mark-Compact(标记-整理-压缩)算法

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
在这里插入图片描述
3、分代收集

分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为新生代(YoungGeneration)和老生代(Tenured/Old Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

目前大部分 JVM 的 GC 对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,因此要复制的操作比较少。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。

而老年代因为每次只回收少量对象,因而采用标记压缩 Mark-Compact 算法

当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,EdenSpace 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 FromSpace 进行清理。如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。在进行 GC 后,使用的便是 Eden Space 和 To Space(即当前的To 和From会发生角色互换) 了,如此反复循环。当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代中。

如何分析解决OOM:
①、增加堆内存看是否还会出现OOM
②、内存快照分析工具,定位代码第几行出错。JProfiler

JProfiler的作用:分析Dump的内存文件,快速定位内存泄漏;获得堆中的数据;获得大的对象等等。使用前要先在IDEA上配置好,下载客户端。

使用方法:在I编译运行时在Edit Configuration中设置一下虚拟机参数即可;
-Xms 用于设置初始堆大小,默认为计算机内存的 1/64 例如:-Xms1024m 设置初始化内存为1024M
-Xmx 用于设置最大堆大小 默认为计算机内存的 1/4
-XX:+PrintGCDetails 设置打印输出GC信息
-XX:+HeapDumpOnOutOfMemoryError 设置当发生OOM时Dump内存快照。

4、典型的垃圾收集器

垃圾收集算法是 内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。下面介绍一下HotSpot(JDK 7)虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。

Serial/Serial Old收集器 是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

ParNew收集器 是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

Parallel Scavenge收集器 是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

Parallel Old收集器 是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

CMS(Concurrent Mark Sweep)收集器 是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。

G1收集器 是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

5、 JAVA 四种引用类型

强引用

在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

虚引用

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值