JVM(复习,上课笔记)

JAVA虚拟机 JVM

JAVA的大阶段
1.5以前
1.6-1.7-1.8
1.9之后

图
jvm分为
内存区域(栈,堆,程序计数器,本地方法区,本地方法栈),类加载系统,字节码执行引擎,直接内存

线程私有,生命周期和线程一致。
每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

一个栈帧就是一个方法,就是一个线程
栈空间是动态分配的,java默认规定的栈空间为1M
栈中如果有对象的话,栈中存对象的地址,堆中存对象的实例

栈帧分为局部变量表,操作数栈,动态链接,方法出口

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)。
操作数栈:进行操作,赋值等的栈空间
动态链接:将类元信息返回的代码存入
方法出口:记的是程序接下来要执行的行号

永久代

永久代不回收
由于常量池与静态池容易爆满,以及方法区很多方法没用还不回收,造成永久代各种问题
jdk1.6-1.7 存在,其中包括方法区,static池(静态池),常量池,计数器
1.7常量池移到了metaspace中,1.8之后将方法区,静态池,计数器都移到了堆的metaspace(元空间),等于都不在永久代了,所以现在永久代为metaspace

方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
javac编译出的代码都放在方法区中
常量,静态变量,类元信息,及时编译器编译后的代码
方法区线程共享,内存回收效率低

静态池:自己写的
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError

常量池:字符串常量

计数器:引用计数器 1.4之后就取消了
程序计数器 相当于CPU的CS:IP的IP,CS:段基址,IP:段偏移量

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

堆分为metaspace,年轻代,老年代,GCRoots
堆是线程共享,垃圾回收的主要场所,在虚拟机启动时创建

GCRoots为垃圾不回收的对象:java虚拟机栈引用的对象,本地方法栈中引用的对象,方法区中常量引用的对象,方法区中的静态属性引用的对象
垃圾对象:没有GCRoots指向的对象为无用对象
full gc(old区满后的垃圾回收)

新生代:发生在新生代的垃圾回收动作,频繁,速度快。
老年代:发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。

先将内存存在年轻代中,gc每扫一次,不回收则+1,16次后进入老年代
年轻代与老年代一般为3:7的比例或者2:8的比例
年轻代分为Eden区与幸存代,S0:S1:Eden比例为1:1:8,8为Eden区
Eden区满一次,minor gc一次

对象

对象的创建
遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载。
类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)。
前面讲的每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。
内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成。

对象的内存布局
在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头
Test t = new Test();
t指向栈,栈帧;new Test();指向堆内存,指向对象头
对象头如哈希编码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程 ID,偏向时间戳,也可包含类型指针
64位对象头

25bit unused空的为0
31bit的hashcode 哈希编码与ClassInfo做对应用
1bit Cms-free被垃圾回收的状态
4bit为被垃圾回收扫过不回收的次数,16次晋升,一共4个位,只能16次,把25bit的unuserd用上可以超过16次
1bit锁偏向位,无锁为0
2bit锁标志位,01为无锁,00为轻量级锁,10为重量级锁,11为GC标记

实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。
对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。

对象的访问定位
使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。

通过句柄访问
Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。详情见图。

通过句柄访问对象
使用直接指针访问
reference 中直接存储对象地址

通过直接指针访问对象
比较:使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。

垃圾回收

在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。
为什么垃圾回收:不回收就会不断的占用内存,占用到一定的程度后,Linux会直接杀死内存

1、 垃圾标记算法
1)引用计数法
给对象添加一个引用计数器。但是难以解决循环引用问题。

2)根扫描法
通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用

3)weak指针法
C使用

4)可作为 GC Roots 的对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象

2、四种引用
下面四种引用强度一次逐渐减弱

1)强引用
平时我们编程的时候例如:Object object=new Object();
那object就是一个强引用了。
如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它,强引用必须断开指针才能被回收。
当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2)软引用
如果一个对象只具有软引用,那就类似于可有可无的生活用品,会尽量不释放。
如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

3)弱引用
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
弱引用只要垃圾回收就会被回收。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4)虚引用
虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。虚引用不叫引用,称为通知。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用
虚引用一般做监控(染色)用的,类似于日志
当监控对象被回收,系统会向引用队列投放一个引用对象

3、回收方法区
在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。
永久代垃圾回收主要两部分内容:废弃的常量和无用的类。
判断废弃常量:一般是判断没有该常量的引用。
判断无用的类:要以下三个条件都满足
该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
加载该类的 ClassLoader 已经被回收
该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法

4、垃圾回收的三大算法
1)标记清除算法
必须STW,进行一次根扫描
空闲链表指针会连在堆上,不使用的内存会被空闲链表标记,然后回收
特点:运行效率高
缺点:空间会产生大量碎片,回收效率低,性能最差

2)复制算法
将内存分为f(from)和t(to),内存在f中使用,继续被使用的内存放入t中,然后f变为t,t变为f,
不断的执行下去,使用的内存永远是f,收集继续使用的的内存永远是t
解决前一种方法的不足,但是会造成空间利用率低下。
特点:可用性高
缺点:效率低,需要拷贝,而且要分一半内存出来,浪费内存

3)标记整理算法
不同于针对新生代的复制算法,针对老年代的特点,创建该算法。
STW,然后进行标记,回收后,后面继续使用的内存往前面搬,处理空隙,有依赖的指针指向下一块的,内存的指针先存起来,等下一块内存搬过来了,再继续指向下一块内存的位置,中间的内存要是被回收了,直接指向下一块不回收内存的位置,主要是把存活对象移到内存的一端。
特点:内存利用率最高
缺点:性能最差

4)分代回收
后来使用了分代算法(分代垃圾回收)
将内存分为了年轻代与老年代,比例为3:7或2:8
年轻代由于垃圾回收的次数比较多,内存容易爆满,所以将年轻代分为了eden和幸存代,eden:S0:S1=8:1:1,eden区gc过一次后进入幸存代,继续使用的就在幸存代中交替,年轻代经历了16次垃圾不回收后,进入老年代,eden区放不下的内存直接进老年代
所以复制算法是解决了分代之间用的缝隙问题,根据各个年代的特点制定相应的回收算法,(当年)老年代使用的算法是标记整理法(fullGC)
年轻代用的算法不唯一

新生代
每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。
老年代
老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除 或者 标记 —— 整理算法回收

5、垃圾回收器
收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。

1)Serial 收集器(串行回收器)
年轻代使用串行化回收器,老年代也使用串行化回收器
年轻代使用复制算法,老年代使用标记整理
在工作时,内存满了,老年代与年轻代一起stw,垃圾回收,回收完在一起开始工作

这是一个单线程收集器。意味着它只会使用一个CPU或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。
Serial 收集器

2)ParNew 收集器
因为老年代被认为很少回收,没必要和年轻代一起stw
所以提出了一个新的算法,parnew(并行回收)给年轻代,使用并行回收(算法不变,线程很多)
parnew使用的是复制加标记清除算法

可以认为是 Serial 收集器的多线程版本。
parnew/serial
3)Parallel Scavenge 收集器
由于年轻代回收太频繁,老年代容易累死(老年代只有一个线程)
于是出现了并行回收器(parallel)可以在年轻代和老年代同时使用
这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。
CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。
作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。
ParNew 收集器并行:Parallel
指多条垃圾收集线程并行工作,此时用户线程处于等待状态

并发:Concurrent
指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。

4)于是出现了三种配对:
1.年轻代使用串行回收器,老年代只能使用串行回收器

Serial Old 收集器
收集器的老年代版本,单线程,使用 标记 —— 整理。

Serial+Serial
2.年轻代使用parnew(并行回收),老年代只能使用串行回收器
Parnew+Serial

3.年轻代使用并行回收器(parallel),老年代既可以使用串行回收器,也可以使用并行回收器(parallel)

Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用标记 —— 整理

Parallel Old 收集器

5)CMS 收集器
后来发现老年代的垃圾回收也很频繁,所以在老年代也带入了一个算法(CMS——标记清除法),专门为老年代设计的,是一种以获取最短回收停顿时间为目标的收集器
CMS不支持并行(parallel),支持串行(serial)和并行(parnew)

于是出现了两套搭配
parnew+cms,适合于大吞吐量的内存激增,如tomcat的
如修改的次数也多的,吞吐量非常大的应用,也是用parnew+cms
缓存老年代不怎么回收的,读吞吐量,写吞吐量大,改吞吐量小,所以一般使用parallel,parallel+parallel

由于老年代使用的CMS有使用标记清除法,会产生缝隙,所以给这个标记清除叫做“major gc”
而年轻代只要使用parnew,就称为“minor gc”
为什么这么叫,因为这时候在老年代存在两个算法,并行回收器CMS,会默认加上一个回收器,叫全局回收器
这个回收器不光回收老年代,年轻代和老年代甚至幸存代都回收,这个gc称为Full gc
从这一年开始,fullgc不再是老年代的代名词,fullgc包括minor,major,永久代的清缝工作都由full gc来做
(永久代回收的是classloader后不再使用的那些,所以后来永久代变成了metaspace,metaspace的回收也不归CMS管,归full gc管)

cms算法:
考虑到了stw的问题
于是算法被分成
产生垃圾->root scan根扫描(STW)->并发标记阶段(产生垃圾和标记垃圾是都在做的)->重新标记阶段(STW)->回收阶段(STW)->继续执行程序
所以很长时间内可以不STW
所以比以前安全,因为以前stw时,请求是等待的,容易请求爆满再gc一次,fullgc的次数多了,内存容易被暴死

三色标记算法
一种代表未标记(比如白色)
一种代表没标记完整(栈直接指向堆,有孩子)(比如灰色)
一种代表用完的内存(比如黑色)
栈引用堆,根扫描开始,用三种颜色标记相应的内存,没用的为白色,
非堆指向堆,且有孩子,为灰色,没孩子为黑色
进入并发标记阶段一边造垃圾标记,将灰色标记为黑色,并且将使用完的内存(使用完的孩子),都标记为黑色
也就是说将所有的非堆指向堆的内存都标记为黑
由于在工作,如果有黑的出现了新的孩子(被白的引用),使黑的属性加一(CMS),在重新标记阶段标记为灰的
重新标记阶段:将上述情况不论灰的(被引用)还是白的(孩子内存),都重新标记为黑的(将被回收)
没有工作线程,只有垃圾回收,所以才能重新标记不产生新的依赖
回收阶段:回收垃圾

缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记 —— 清除 算法带来的空间碎片

CMS 收集器

G1 收集器(年轻代与老年代共用)
面向服务端的垃圾回收器。
将内存分为等大的格
开始使用后,将格命名为eden(标记清除法),内存达到了一个阈值后,将此区域改名为S1,相邻的格改名为 S0
开始使用复制算法在两个区域交替,使用到一定的次数后,
将使用的那个区域改名为老年代,不使用的那个区域改回默认未命名状态
通过换名换算法,可以不再内存拷贝了
当你申请的内存大于一个Card(格)时,将两个相邻的内存标记为huge(巨人),存放这个较大的内存,huge代一旦没有堆外引用,优先垃圾回收

垃圾回收过程和CMS很像
产生垃圾->根扫描(STW)->并发标记->重新标记(STW)->回收
也是三色标记法
根标记与并发标记都是一样的
重新标记阶段使用了内存结构快照(snapshort)算法(二进制数组)
在重新标记阶段之前将依赖存入快照中,如1->2->3,A->B
重新标记阶段写新快照(新的引用),如1->2,A->B->3
对比老快照
哪里不一样从哪里标,标灰,没孩子再标为黑,如3标为灰,再标为黑,和刚才(CMS)+1标灰再标黑有明显区别,不用把父内存标为灰(回溯)

优点:并行与并发、分代收集、空间整合、可预测停顿。

G1 收集器

  1. 直接内存
    非虚拟机运行时数据区的部分
    直接内存与堆内存的比较
    直接内存申请空间耗费更高的性能,直接内存读取的性能要优于普通的堆内存

  2. 类加载器
    通过一个类的全限定名来获取描述此类的二进制字节流。

加载器的种类
启动类加载器:负责将存放在<JAVA_HOME>\lib目录中,并且能被虚拟机识别的类库加载到虚拟机内存中。
扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录中的所有类库,开发者可以直接使用扩展类加载器
应用程序类加载器:由于这个类加载器时classloder中的GetSystemClassLoder()方法的返回值,所以一般也称为“系统加载器”,它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器;如果应用程序中没有且定义过自己的类加载器,一般情况下这个就是程序中默认的加载器

类加载的过程:
五个阶段:加载,验证,准备,解析,初始化

加载:通过类的全限定名获取该类的二进制字节流
将二进制字节流所代表的静态结构转化为方法区的运行时的数据结构早内存中创建一个代表该类的Java.lang.class(内存在方法区中,不在堆中)对象作为方法区这个类的各种数据的访问入口
获取二进制字节流:从zip包中获取,如jar,war等
从网络中获取,如Applect
通过动态代理技术生成代理类的二进制字节流
由JSP文件,生成对应的class类
从数据库中读取

验证:是连接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求
文件格式验证
是否以魔数 0xCAFEBABE 开头
主、次版本号是否在当前虚拟机处理范围之内
常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
Class 文件中各个部分集文件本身是否有被删除的附加的其他信息
……
只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。

元数据验证
这个类是否有父类(除 java.lang.Object 之外)
这个类的父类是否继承了不允许被继承的类(final 修饰的类)
如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)
这一阶段主要是对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。

字节码验证
保证任意时刻操作数栈的数据类型与指令代码序列都鞥配合工作(不会出现按照 long 类型读一个 int 型数据)
保证跳转指令不会跳转到方法体以外的字节码指令上
保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)
……
这是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的事件。

符号引用验证
符号引用中通过字符创描述的全限定名是否能找到对应的类
在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
……
最后一个阶段的校验发生在迅疾将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,还有以上提及的内容。
符号引用的目的是确保解析动作能正常执行,如果无法通过符号引用验证将抛出一个 java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

准备
这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。

解析
解析阶段时虚拟机就将常量池内的符号引用替换为直接引用(地址)的过程

初始化
类初始化阶段时类加载过程的最好一步,时执行类构造器&Lt;clint&gt;方法的过程

双亲委派模型
启动类加载器
BootStrapClassLoder
加载 lib 下或被 -Xbootclasspath 路径下的类

扩展类加载器
ExtClassLoder
加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类

引用程序类加载器
ApplicantClassLoder
ClassLoader负责,加载用户路径上所指定的类库。

工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。

  1. 本地方法栈
    区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
    调用低级语言语法,方法

  2. 字节码执行引擎
    改变栈中的程序计数器

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

©️2020 CSDN 皮肤主题: 1024 设计师:上身试试 返回首页