大数据学习之路 JVM篇

大数据学习之路(一) JAVA篇(1)

正文篇

JVM (总体概述)

什么是 JVM ?

JVM它是Java Virtual Machine 的缩写,主要是通过在实际计算机模仿各种计算机功能来实现的,组成部分包括堆、方法区、栈、本地方法栈、程序计算器等部分组成的,其中堆和方法区是共享区,也就是谁都可以使用,而栈和程序计算器、本地方法栈区是归JVM的。Java能够被称为“一次编译,到处运行”的原因就是Java屏蔽了很多的操作系统平台相关信息,使得Java只需要生成在JVM虚拟机运行的目标代码也就是所说的字节码,就可以在多种平台运行。

JVM

JDK、 JRE、JVM 的关系是什么

什么是 JVM ?

英文名称 ( Java Virtual Machine ),就是 JAVA 虚拟机, 它只识别 .class 类型文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。

什么是 JRE ?

英文名称( Java Runtime Environment ),Java 运行时环境。它主要包含两个部分:JVM 的标准实现和 Java 的一些基本类库。相对于 JVM 来说,JRE多出来一部分 Java 类库。

什么是 JDK? 英文名称( Java Development Kit ),Java 开发工具包。JDK 是整个 Java 开发的核心,它集成了 JRE 和一些好用的小工具。例如:javac.exe、java.exe、jar.exe 等。

这三者的关系:一层层的嵌套关系。JDK > JRE > JVM。

JVM体系架构

在这里插入图片描述

类装载器(ClassLoader)

类装载器负责加载class文件。每个编译好的class文件在文件开头有特定的文件标识。ClassLoader 将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构(类的元数据)并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

在这里插入图片描述

四种 类装载器 以及 双亲委派机制

在这里插入图片描述

启动(Bootstrap)类加载器
启动类加载器是用本地代码实现的类加载器,它负责将 < JavaRuntimeHome >/lib 下面的类库加载到内存中。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用。

标准扩展(Extension)类加载器
扩展类加载器负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

应用程序类加载器(Application)
应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

双亲委派机制

当一个类收到了类加载请求时,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一层次的类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class)(从最顶层的Bootstrap -> Extension -> AppClassLoader 再到自定义的类加载器)子加载器才会尝试自己加载

在这里插入图片描述

双亲委派机制的作用

为了保证自己写的代码不污染java出厂自带的源代码。如果有人想替换系统级别的类:String.java。篡改它的实现,但是在这种机制下这些系统的类已经被Bootstrap classLoader加载过了,所以并不会再去加载,从一定程度上防止了危险代码的植入。

  • 1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
  • 2、保证了使用不同的类加载器最终得到的都是同一个Object对象
本地接口(Native Interface)

本地接口的作用就是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。Java诞生的时候是C/C++横行的时候,想要立足,必须调用C/C++程序;于是就在内存中专门开辟了一块区域来处理标记为native的代码。它的具体做法是 本地方法栈(Native Method Stack)中登记native方法,在 Execution Engine执行时加载native libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领区间的通信很发达,比如可以使用Socket通信,也可以使用Web Service。

本地方法栈(Native Method Stack)

native 方法就放在本地方法栈(Native Method Stack)中

程序计数器PC(PC寄存器)

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码,(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。这块内存区域很小,他是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
如果执行的是一个Native方法,那这个计数器是空的。
PC用以完成分支,循环,跳转,异常处理,线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误。

方法区(Method Area)

供各线程 共享 的运行时内存区域。----存储了每一个类的结构信息(类的 元数据)。例如:运行时常量池(Runtime Constant Pool)、字段和 方法数据、构造函数和普通方法的字节码内容。
方法区就是一个规范,在不同的虚拟机里实现是不一样的,最典型的就是 永久代(PermGen space) 和 元空间 (Metaspace)
永久代:JDK1.7
元空间:JDK1.8
但是两者都不存在方法区中,就像上面所讲,方法区存的是模板,而永久代和元空间是这套模板的实例,后面在堆中讨论。所以,实例变量存储在堆内存中,和方法区无关。

JAVA Stack(Java 栈)

口诀:栈管运行,堆管存储
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,他的生命周期是跟随线程的生命周期,线程结束那么栈内存也就随之释放, 对于栈来说不存在垃圾回收问题 ,只要线程已结束该栈就结束,是线程私有的。
注意: 栈保存哪些东东?
8种数据基本类型:byte,short,int,long,char,float,double,boolean
8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。

栈存储什么? (Java 栈中存储的基本单位为 栈帧)

栈帧中主要保存 3 类数据:

  1. 本地变量 (Local Variables) : 入参和出参 以及方法内的变量;
  2. 栈操作 (Operand Stack) : 记录出栈 和 入栈的操作;(可理解为pc寄存器的指针)
  3. 栈帧数据 (Frame Data) : 包括类文件、方法等

栈的运行原理

栈中的数据都是以栈帧 (Stack Frame) 的格式存在,栈帧是一个内存区块,是一个有关方法和运行期数据的数据集,
当一个方法A被调用时就产生了栈帧 F1,并被压入到栈中,
A方法又调用 B方法,于是产生栈帧F2 ,也被压入栈,
B方法又调用 C方法, 于是产生栈帧F3,也被压入栈
……
执行完毕后,先弹出F3栈帧,再弹出 F2栈帧,再弹出 F1栈帧 ……
遵循 “先进后出” / “后进先出” 原则。
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表,操作数据栈,动态连接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的操作过程。
栈的大小和具体jvm的实现有关,通常在 256K ~ 756K 之间,约等于 1Mb左右。

在这里插入图片描述

栈帧里面包括:局部变量表,操作数栈,指向运行时常量池的引用,方法返回地址,动态链接

如果栈空间满了,就会报SOF错误:Java.Lang.StackOverflowError;注意,不是异常而是 错误 Error

栈 堆 方法区 三者之间的关系

hotspot 是使用指针的方式来访问对象

  • 使用直接指针访问,那么Java堆中的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址
  • 使用直接指针访问的最大好处是 速度更快,它节省一次指针定位时间的开销(节省了句柄访问方法中的句柄池寻找实例池中的寻址时间),由于对象的访问在Java中非常频繁,因此这类开销积少成多时也是一项非常可观的成本。
  • 一般的虚拟机都是通过指针直接访问对象而通过句柄的这种方法并不是很常用。
    *** 举个例子 ***
    a b c是三个整型变量,Student是学生类,变量表中存储着 基本数据类型和引用类型的reference,主要以Student类来说明三者调用关系
    三者之间的调用
    由图可知,每个线程都要有一个线程栈,栈内存放栈帧,内部为线程中的变量表,既然是在栈中都要按照先入后出的原则进行入栈和出栈。
    变量表中student指向堆中的student类实例的地址,同时指向方法区中Student类的类型数据(类型数据存储类信息),内部还有其他引用类型的数据,这里举例String类型的name变量,student实例内部存储name由于是引用类型,也会使用指针,在堆中开辟存储空间存储name的内容存储内容“xiaoming”,实质是String类型,在方法区中也有对应类型指向。

大数据学习之路 JAVA篇(堆)


上一节我们介绍了jvm体系架构中的 类加载器ClassLoader 方法区Method Area java Stack 本地方法栈native stack PC程序计数器本地接口

这一节我们介绍非常重要的 内存

堆(heap)

什么是堆(heap)?

java堆在虚拟机启动的时候建立,它是java程序最主要的内存工作区域。几乎所有的java对象实例都存放在java堆中。堆空间是所有线程共享的,这是一块与java应用密切相关的内存空间。

堆的划分

在这里插入图片描述

在逻辑上,堆内存可以划分为三个部分:

  • 1.新生代(Young)
    • 伊甸园区(Eden)
    • 幸存区(Survivor):From 和 To
  • 2.老年代(Old)
  • 3.永久代/元空间(JDK7:永久代;JDK8:元空间)
    在物理上,堆内存可以划分为两个部分:新生代+老年代

在这里插入图片描述

堆内存分代的意义

Java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代和永久代(对HotSpot虚拟机而言),这就是JVM的内存分代策略。
  堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。给堆内存分代是为了提高对象内存分配和垃圾回收的效率。试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率。
  有了内存分代,情况就不同了,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是内存分代带来的好处。

新生代(Young Generation)

新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。
  HotSpot将新生代划分为三块,一块较大的Eden(伊甸园区)空间和两块较小的Survivor(幸存者)空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
  GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

老年代(Old Generation)

在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。

元空间/永久代

永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。

永久代和方法区

1、方法区

方法区(Method Area)是jvm规范里面的运行时数据区的一个组成部分,jvm规范中的运行时数据区还包含了:pc寄存器、虚拟机栈、堆、方法区、运行时常量池、本地方法栈。主要用来存储class、运行时常量池、字段、方法、代码、JIT代码等。运行时数据区跟内存不是一个概念,方法区是运行时数据区的一部分。方法区是jvm规范中的一部分,并不是实际的实现,切忌将规范跟实现混为一谈。

2、永久代

永久带又叫Perm区,只存在于hotspot jvm中,并且只存在于jdk7和之前的版本中,jdk8中已经彻底移除了永久带,jdk8中引入了一个新的内存区域叫metaspace。并不是所有的jvm中都有永久带,ibm的j9,oracle的JRocket都没有永久带,永久带是实现层面的东西,永久带里面存的东西基本上就是方法区规定的那些东西。

元空间与永久代的最大的区别

永久代使用JVM的堆内存,但是Java8以后的元空间并不在虚拟机中,而是使用本机物理内存
因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory, 字符串池和类的静态变量放入java堆中,这样可以加载对少类的元数据就不再由MaxPerSize控制,而由系统的实际可用空间来控制。

3、区别
  方法区是规范层面的东西,规定了这一个区域要存放哪些东西,永久带或者是metaspace是对方法区的不同实现,是实现层面的东西。
  
4、hotspot jdk8中移除了永久带以后的内存结构

元空间是方法区的一种具体实现

  • 可以把方法区理解为java中定义的一个接口,把元空间/永久代看做是这个接口的具体实现类
  • 方法区是规范, 元空间/永久代是hotspot针对该规范进行的实现。

GC(垃圾回收)

GC(垃圾回收机制)。这一部分将会介绍 如何判断一个对象是否是垃圾常用的三种垃圾回收算法Java的分代收集算法

如何判断垃圾

第一种 引用计数器算法
每当对象被引用一次计数器加1,对象失去引用计数器减1,计数器为0就可以判断对象死亡了
优点:简单高效
缺点:无法解决循环引用;需要更多额外的开销,JAVA几乎不使用

第二种 可达性分析算法-根搜索算法
所谓可达性是指,顺着GCRoots根一直向下搜索(顺藤摸瓜),整个搜索的过程就构成了一条“引用连”,只要在引用链上的对象叫做可达,在引用链之外的叫不可达,不可达的对象就可以判断为可回收的对象。

哪些对象可以作为GCRoot?????????
- 1.虚拟机栈帧上本地变量表中的引用对象(方法参数、局部变量、临时变量)
- 2.方法区中的静态属性引用类型对象、常量引用对象
- 3.本地方法栈中的引用对象(Native方法的引用对象)
- 4.Java虚拟机内部的引用对象,如异常对象、系统类加载器等
- 5.所有被同步锁(synchronize)持有的对象
- 6.Java虚拟机内部情况的注册回调、本地缓存等

垃圾回收算法

1. 标记-清除
在这里插入图片描述

首先标记出所有需要被回收的对象,然后对标记的对象进行统一清除,清空对象所占用的内存区域
缺点:

  1. 执行效率不可控:如果堆中大部分的对象都是可回收的,收集器要执行大量的标记,收集操作
  2. 产生了许多内存碎片,被回收后的区域内存并不是连续的,当有大对象要分配而找不到满足大小的空间时,要触发下一次垃圾收集。

2. 标记-复制
在这里插入图片描述

针对标记-清除算法执行效率与内存碎片的缺点,计算机科学家又提出了一种“半复制区域”的算法
标记-复制算法将内存分为大小相同的两个区域:运行区域,预留区域。
所有创建的新对象都分配到运行区域,当运行区域内存不够时,进行垃圾回收,将运行区域中存活对象全部复制到预留区域,然后再清空整个运行区域内存,这是两块区域的角色互换

缺点:预留一半的内存区域太浪费,并且如果内存中大量的都是存活对象,只有少量的垃圾对象,收集器要执行更多次的复制操作才能释放少量的内存空间,得不偿失。

3. 标记-整理
在这里插入图片描述

标记复制算法要浪费一半的内存空间,并且在 大多数状态为存货状态时使用效率会很低。
标记-整理算法:将存活的对象想内存空间的一端移动,然后将存活对象边界以外的空间全部清空

缺点:
虽然标记-整理算法解决了内存碎片化问题,也不存在内存空间的浪费问题,看上去挺美好的。但是,当内存中存活对象多,并且都是一些微小对象,而且垃圾对象较少时,要移动大量的存活对象才能换取少量的内存空间。

JAVA的 分代收集算法

  一块独立的内存区域只能使用一种垃圾回收算法,根据对象生命周期特征,将其划分到不同的区域,再对特定区域使用特定的垃圾回收算法,只有这样才能将垃圾回收算法的优点发挥到极致,这种组合的垃圾回收算法称之为:分代收集算法(分代回收算法)
   JAVA中的分代收集算法: 新生代使用:复制算法
              老年代使用:标记-清除 或者 标记-整理

在这里插入图片描述

JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
因此GC按照回收的区域又分了两种类型:一种是普通GC (Minor GC),一种是全局GC(Major GC or Full GC

Minor GC 和 Full GC的区别

  • 普通GC:只针对新生代区域的GC,指发生在新生代的垃圾回收动作。因为大多数java对象存活率都不高,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • 全局GC(Full GC or Major GC):指发生在老年代的垃圾回收动作。出现了Major GC, 经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上

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

1.Eden、SurvivorFrom复制到SurvivorTo,年龄+1
  首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则复制到老年代区),同时把这些对象的年龄+1

2. 清空Eden、SurvivorFrom
  然后,清空Eden和SurvivorFrom中的对象。注意复制之后有交换,谁空谁是To

3. SurvivorTo和SurvivorFrom互换
  最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

对象进入老年代的两种情况

  • 第一种情况:直接分配到老年代
      创建了大对象,直接超过了Eden区大小的对象
  • 第二种情况:从年轻代晋升
    1.分配担保机制
      新生代分配担保,在执行MinorGC时要将Eden区存活的对象复制到Survivor区,但是Survivor区默认空间是只有新生代的2/10,实际使用的只有1/10,当Survivor区内存不够所有存活对象分配时,就需要将Survivor无法容纳的对象分配到老年代去,这种机制就叫分配担保机制。
    2.对象年龄超过虚拟机MaxTenuringThreshold的设置值,最大为15
    3.Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半(TargetSurvivorRatio),年龄大于或等于该年龄的对象直接进入老年代。

JAVA8 JVM参数调整

public static void main(String[] args)
    {
    	//返回Java虚拟机试图使用的最大内存量(最大堆内存大小)。
    	long maxMemory = Runtime.getRuntime().maxMemeory();
    	//返回Java虚拟机中的内存总量(初始大小)
    	long totalMemory= Runtime.getRuntime().totalMemory();
    	//大小为字节
    }

参数调整之前,先模拟OOM异常(java.lang.OutOfMemoryError:Java heap space )

 public class OOM {
    public static void main(String[] args) {
    String str = "www.atguigu.com";
    while (true)
    {
    str += str+new Random().nextInt(888888888)+ new Random().nextInt(999999999);
    }
    //byte[] bytes = new byte[80*1024*1024];
    }
    }
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    	at java.util.Arrays.copyOf(Arrays.java:3332)
    	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
    	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
    	at java.lang.StringBuilder.append(StringBuilder.java:208)
    	at org.javaTest.OOM.main(OOM.java:10)

JVM 参数调整

  • -Xms :设置初始分配大小,默认为物理内存的“1/64” -Xms:1024m
  • -Xmx :最大分配内存,默认为物理内存的“1/4” -Xmx:1024m
    但是实际生产中,初始内存大小与最大内存大小要保持一致,避免内存忽高忽低,产生停顿。
  • -XX :+PrintGCDetails: 输出详细的GC处理日志 -XX:+PrintGCDetails

配置VM options= -Xms1024m -Xmx1024m -XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen: 216589K->35125K(305664K)] 216589K->85811K(1005056K), 0.0469169 secs] [Times: user=0.03 sys=0.01, real=0.07 secs] 
[GC (Allocation Failure) [PSYoungGen: 242888K->34755K(305664K)] 428713K->220580K(1005056K), 0.0549642 secs] [Times: user=0.03 sys=0.03, real=0.06 secs]
[GC (Allocation Failure) [PSYoungGen: 182879K->1368K(305664K)] 774124K->660191K(1005056K), 0.0475239 secs] [Times: user=0.09 sys=0.05, real=0.05 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1368K->0K(305664K)] [ParOldGen: 658823K->339122K(699392K)] 660191K->339122K(1005056K), [Metaspace: 4025K->4025K(1056768K)], 0.0632486 secs] [Times: user=0.13 sys=0.00, real=0.06 secs] 
[GC (Allocation Failure) [PSYoungGen: 140302K->32K(305664K)] 614565K->609434K(1005056K), 0.0315208 secs] [Times: user=0.08 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 32K->0K(305664K)] [ParOldGen: 609402K->271552K(699392K)] 609434K->271552K(1005056K), [Metaspace: 4025K->4025K(1056768K)], 0.0434879 secs] [Times: user=0.16 sys=0.00, real=0.04 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(262656K)] 541832K->541832K(962048K), 0.0040964 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 0K->0K(262656K)] [ParOldGen: 541832K->406692K(699392K)] 541832K->406692K(962048K), [Metaspace: 4025K->4025K(1056768K)], 0.0497864 secs] [Times: user=0.16 sys=0.00, real=0.05 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(303616K)] 406692K->406692K(1003008K), 0.0019701 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(303616K)] [ParOldGen: 406692K->406635K(699392K)] 406692K->406635K(1003008K), [Metaspace: 4025K->4025K(1056768K)], 0.1020535 secs] [Times: user=0.17 sys=0.01, real=0.10 secs] 
Heap
 PSYoungGen  total 303616K, used 7629K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 256000K, 2% used [0x00000000eab00000,0x00000000eb273550,0x00000000fa500000)
  from space 47616K, 0% used [0x00000000fd180000,0x00000000fd180000,0x0000000100000000)
  to   space 45568K, 0% used [0x00000000fa500000,0x00000000fa500000,0x00000000fd180000)
 ParOldGen   total 699392K, used 406635K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 58% used [0x00000000c0000000,0x00000000d8d1acd8,0x00000000eab00000)
 Metaspace   used 4056K, capacity 4568K, committed 4864K, reserved 1056768K
  class spaceused 450K, capacity 460K, committed 512K, reserved 1048576K

MinorGC GCDetails 详情查看
在这里插入图片描述

Major(Full)GC GCDetails 详情查看
在这里插入图片描述

JAVA 内存模型:JMM

在这里插入图片描述

JMM(Java Memory Model)是Java内存模型,JMM定义了程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存读取变量这样的底层细节。

设计JMM主要的目的是:屏蔽各种硬件和操作系统的内存访问差异,以实现让JAVA程序在各种平台下都能达到一致的内存访问效果。

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程私有的数据区域,而Java内存模型中规定所有变量都存储在 主存中 ,主存是共享内存区域,所有线程都可以访问,

但是线程对变量的操作(读取赋值等)必须在工作内存中进行, 首先要将变量先从主存拷贝到线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主存,

不能直接操作主内存中的变量,各个线程中的工作内存中存储着主存中的 变量副本,因此 不同线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主存来完成。

JVM 代码的加载顺序

类的静态块最先执行,且只执行一次
接下来执行类的构造块,按顺序执行
最后再执行构造方法
(一切程序的入口都是主方法所在的类,因此该类先加载)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值