JVM虚拟机

类加载器ClassLoader

负责加载clas文件, class文件在文件开头有特定的文件标示,将c1ass文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且 Classloader只负责 class文件的加载,至于它是否可以运行,则由 Execution Engine决定
class里面是二进制字节码
在这里插入图片描述

虚拟机自带的加载器

  • 启动类加载器( Bootstrap)C++
  • 扩展类加载器( Extension)Java
  • 应用程序类加载器( AppclassloaderJava也叫系统类加载器,加载当前应用的 classpath的所有类
    用户自定义加载器
  • Java.lang. Classloader的子类,用可以定制类的加载方式

在这里插入图片描述
sun. mlsc. Launcher它是一个java虚拟机的入口应用

  Object o = new Object();
        System.out.println("启动类加载器( Bootstrap)C++是:"+o.getClass().getClassLoader());
        TestMap testMap = new TestMap();
        System.out.println("扩展类加载器( Extension)Java是:"+testMap.getClass().getClassLoader().getParent());
        System.out.println("应用程序类加载器是:"+testMap.getClass().getClassLoader());

启动类加载器( Bootstrap)C++是爷爷:null
扩展类加载器( Extension)Java是爸爸:sun.misc.Launcher E x t C l a s s L o a d e r @ 1540 e 19 d 应 用 程 序 类 加 载 器 是 自 己 : s u n . m i s c . L a u n c h e r ExtClassLoader@1540e19d 应用程序类加载器是自己:sun.misc.Launcher ExtClassLoader@1540e19d:sun.misc.LauncherAppClassLoader@18b4aac2

双亲委派机制

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

采用双亲委派的一个好处

比如加载位于 rt. ar包中的类 java lang, object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这洋就保证了使用不同的类加载器最终得到的都是同样一个 object对象

JVM
在这里插入图片描述

黄色是线程共享,存在垃圾回收。灰色是私有的

Native Interface本地接口

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

Native Method Stack本地方法栈
它的具体做法是 Native Method Stack中登记 native方法(例如C方法),在 ExecutionEngine执行时加载本地方法库

native是一个关键字,声明有,无实现的方法。底层调用是C C++

PC寄存器
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。

如果执行的是一个 Native方法,那这个计数器是空的。

用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出( Outofmemory=00M)错误

记录了方法之间的调用和执行情况,类似版值日表

方法区

供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池( Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代( Permgen space)和元空间( Metaspace)。

实例变量存在堆内存中,和方法区无关

1.它存了一个关于类的结构信息
2.方法区相当是规范模板,最典型的就是永久代( Permgen space)和元空间( Metaspace)。
空调 k1 = new 格力()
list list = new ArrayList();
方法区 名 = new 堆();


栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就0ver,生命周期和线程致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配

1.栈管运行,堆管存储
2.栈先进后去,例如子弹. 存方法的地方
3.方法=栈帧(参数相当于是子弹壳)
5.main方法在栈底,等其它方法执行完,main才结束

栈帧中主要保存3类数据:
本地变量( Local Variables):输入参数和输出参数以及方法内的变量;
栈操作( Operand Stack):记录出栈、入栈的操作;
栈帧数据( Frame Data):包括类文件、方法等等。
在这里插入图片描述
注: 弯的指针是pc寄存器
图示在一个栈中有两个栈帧
栈帧2是最先被调用的方法,先入栈,

然后方法2又调用了方法1,栈帧1处于栈顶的位置,

栈帧2处于栈底,执行完毕后,依次弹出栈帧1和栈帧2,

线程结束,栈释放。

每执行个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕后会自动将此栈帧出栈
在这里插入图片描述
栈 - 堆 -方法区之间关系
在这里插入图片描述
堆(Java7之前)
一个JWM实例只存在一个堆内存,堆内存的大小是可以调节的类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分
逻辑上三部分 新生区 - 养老区 - 永久区
对应理工男来说, 逻辑上就是物理上
在这里插入图片描述
堆上new对象流程

Eden新生区 = GC = YGC = 轻GC 比喻100个小兵死了(满)自动开启GC,清理了只有2个老兵
0区from = Full GC = FGC 2个老兵,如老兵多了.这里满了第二次开启 FGC
1区from= 老兵经历了15次 . 0和1区清理会进行交换内存后面说
养老区 15次还没死是将军
0区 from和1区 to.里面的内存空间是不固定的,每次GC完,空会相互交换.

对象生命周期与GC交换
在这里插入图片描述

新生代是百分之30多的内存,老年代是60多的内存
新生代里面内存分Eden区百分之8的内存,其它From和to各百分之一内存

方法区
实际而言,方法区( Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有个别叫做Non-Heap(非堆),目的就是要和堆分开.

对于 Hotspot虚拟机,很多开发者习惯将方法区称之为“永久代Parmanent Gen)”,但严格本质上说两者不同,或者说使用永久代来实现方区而已,永久代是方法区(相当于是一个接口 interface)的一个实现,jdk1.7的本中,已经将原本放在永久代的字符串常量池移走
在这里插入图片描述
在这里插入图片描述

水久区(java7之前有)
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class(如jar包), Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

堆参数
在这里插入图片描述
Java8
在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似

元空间与永久代之间最大的区别在于永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存(自己电脑上)。

因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由 Maxpermsize控制,而由系统的实际可用空间来控制
在这里插入图片描述

-Xmx最大的内存如:笔记本16G,除以4是约4G,这是JVM最大的内存
-Xms是JVM出厂的值4G除以4,约1G

计算内存

 long max1 = Runtime.getRuntime().maxMemory();
        long xms2 = Runtime.getRuntime().totalMemory();
        System.out.println("max最大的内存"+max1+"(字节)"+(max1 /(double)1024/1024)+"MB");
        System.out.println("xms默认的内存"+xms2+"(字节)"+(xms2/(double)1024/1024)+"MB");

实际工作调优
二个参数必须相同
理由避免内存呼高呼低,产生停顿

VM参数:-Xms8m-Xmx8mXX:+ Printgcdetails
在这里插入图片描述

在这里插入图片描述
GC日志
在这里插入图片描述
GC图
在这里插入图片描述
Full GC图
在这里插入图片描述在这里插入图片描述
GC算法
次数上频繁收集是Young区
次数少的是Old区
不动的元空间

在这里插入图片描述
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代因此GC按照回收的区域又分了两种类型,一种是普通GC( minor gc),一种是全局GC( major GC or Full GC)

普通GC( minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。

全局GC( major GC or Full GC):指发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次的 Minor GC(但并不是绝对的)。 Major GC的速度一般要比 Minor GC慢上10倍以上
在这里插入图片描述

年轻代中使用的是 Minor GC,这种GC算法采用的是复制算法( Copying)

Minor GC会把Eden中的所有活的对象都移到 Survivor区域中,如果 Survivor区中放不下,那么剩下的活的对象就被移到Odgeneration中,也即一旦收集后,Eden是就变成空的了

当对象在Eden(包括一个 Survivor区域,这里假设是from区域)出生后,在经过一次 Minor GC后,如果对象还存活,并且能够被另外一块 Survivor区域所容納(上面已经假设为from区域,这里应为to区域,即to区域有足够的内存空间来存储Eden和from区域中存活的对象),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor区域(即to区域)中,然后清理所使用过的Eden以及 Survivor区域(即from区域),并且将这些对象的年龄设置为1,以后对象在 Survivor区每熬过一次 MinorGC,就将对象的年龄+1,当对象的年龄达到某个值时(默认是15岁,通过-XX: Maxtenuringthreshold来设定参数),这些对象就会成为老年代

XX: Maxtenuring Threshold一设置对象在新生代中存活的次数

年轻代中的GC,主要是复制算法( Copying)

Hotspot JVM把年轻代分为了三部分:1个Eden区和2个 Survivor区(分别叫from和to)。默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理)这些对象经过第一次 Minor GC后,如果仍然存活,将会被移到 Survivor区。对象在Survivor区中每熬过一次 Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片

在这里插入图片描述
因为Eden区对象一般存話率较低一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推

复制算法它的缺点也是相当明显的。
1、它浪费了一半的内存,这太要命了。
2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。

老年代一般是由标记清除或者是标记清除与标记整理的混合实现

在这里插入图片描述

标记清除优缺点

  • 不需要额外空间
  • 二次扫描耗时严重;
  • 会产生内存片

用通俗的话解释一下标记清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记清理工作接下来便让应用程序恢复运行

在这里插入图片描述
在整理压缩阶段,不再对标记的对像做回收,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

优缺点

  • 好处没有内存碎片,可以利用bunp
  • 需要移动对象的成本高,效率不行,不仅要标记所有存活对象,还要繁理所有存活对象的引用地址从效率上来说,标记/整理算法要低于复制算法。-the-pointer

总结各个垃圾回收的优缺点

1.内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情況不一定如此)
2.内存整齐度:复制算法=标记整理算法>标记清除算法
3.内存利用率:标记整理算法=标记清除算法>复制算法。
4.可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程难道就没有一种最优算法吗?猜猜看,是那种 分代收集算法

1.年轻代( Young Gen)
年轻代特点是区域相对老年代较小,对像存活率低。
这种情况复算法的回收整繁理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适川于年轻代的回收。而复制算法内存利用率不高的问题,通过 hotspot中的两个 survivor的设计得到缓解

2.老年代( Tenure Gen)
老年代的特点限区域较大,对像存活率高
这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。

JMM内存模型

volatile是Java虚拟机提供的轻量级的同步机制,乞丐版的
保证可见性,不保证原子性,禁止指令重排

比喻例如
JVM - GC - JMM
CPU |- 内存| -硬盘
|之间是缓存

在这里插入图片描述

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为战空的),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问

没写完-----------

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java语录精选

你的鼓励是我坚持下去的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值