JVM虚拟机相关

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_37465188/article/details/85051975

Java内存模型:

在这里插入图片描述

  • 程序计数器:
    用于保存当前正在执行的程序的内存地址,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。

  • JAVA栈:
    Java栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的Java栈,在这个Java栈中又会包含多个栈帧(Stack Frame),这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量、操作栈和方法返回值等信息。
    在这里插入图片描述

  • 堆 Heap:
    堆是JVM所管理的内存中最大的一块,是被所有Java线程所共享的,不是线程安全的,在JVM启动时创建。堆是存储Java对象的地方,这一点Java虚拟机规范中描述是:所有的对象实例以及数组都要在堆上分配。Java堆是GC管理的主要区域。从内存回收的角度来看,由于现在GC基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。

  • 方法区Method Area:
    方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当在程序中通过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。方法区是被Java线程锁共享的,不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。方法区也是堆中的一部分,就是我们通常所说的Java堆中的永久区 Permanet Generation,大小可以通过参数来设置,可以通过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。

  • 常量池Constant Pool:
    常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,并保存在已编译的.class文件中。一般分为两类:字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定位到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。

  • 本地方法栈Native Method Stack:
    本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

主内存和工作内存:

JMM规定了所有的变量都存储在主内存(Main Memory)中每个线程有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
在这里插入图片描述

线程1和线程2要想进行数据的交换一般要经历下面的步骤:

1.线程1把工作内存1中的更新过的共享变量刷新到主内存中去。
  2.线程2到主内存中去读取线程1刷新过的共享变量,然后copy一份到工作内存2中去。
3.JAVA线程工作内存指的应该就是栈吧?

JAVA垃圾回收机制

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:
(1)发现无用信息对象;
(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。
第一步有两种方法:

  • 引用计数法
    引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。

  • 可达性分析法
    java中可作为GC Root的对象有:
    1.虚拟机栈中引用的对象(本地变量表)
    2.方法区中静态属性引用的对象
    3.方法区中常量引用的对象
    4.本地方法栈中引用的对象(Native对象)

第二步有三种方法:

  • 标记-清理
  • 标记-复制
  • 标记-整理

JVM实际的GC

1.现实是复杂的,也是有趣的(为什么GC要分代?)

如我们上面所见,不同的回收算法各有其优缺点。适用的对象有所不同,所以JVM不会那么傻,只用一种方法来进行垃圾回收,它很聪明的使用了“分代”这种方式来对不同类型的对象进行不同的管理。

2.分代是怎样的:

内存主要被分为三块:新生代(Youn Generation)旧生代(Old Generation)持久代(Permanent Generation)。三代的特点不同,造就了他们使用的GC算法不同,新生代适合生命周期较短,快速创建和销毁的对象,旧生代适合生命周期较长的对象,持久代在Sun Hotpot虚拟机中就是指方法区(有些JVM根本就没有持久代这一说法)。
在这里插入图片描述

在这里插入图片描述

3.分代内部具体回收又是怎样的:

年轻代(因为每次存活的对象都比较少,所以采取 标记-复制 算法对其进行清理):

所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。

年老代:(因为存在对象大多存活时间都比较久,所以采用标记-整理或者标记-清理算法)
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

持久代:

用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或 者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

4.什么时候触发垃圾回收:

既然进行了分代,那么不同代肯定写满的时间不一样,所以垃圾回收的时间节点肯定也就不一样,所以呢,这里就将垃圾回收分为了 Scavenge GC 和 Full GC。

Scavenge GC:

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC:

对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

  • 年老代(Tenured)被写满
  • 持久代(Perm)被写满
  • System.gc()被显示调用
  • 上一次GC之后Heap的各域分配策略动态变化

在这里插入图片描述

几种垃圾回收器

几种垃圾回收器的简单介绍

类加载机制

在这里插入图片描述
双亲委派好处

  • 避免同一个类被多次加载;
  • 每个加载器只能加载自己范围内的类;

类加载过程:
类加载分为三个步骤:加载,连接,初始化;
在这里插入图片描述

参考链接:
浅谈JAVA内存模型
深入理解垃圾回收机制

猜你喜欢

转载自blog.csdn.net/qq_37465188/article/details/85051975