Log[JVM]_20.01.15

一、简介
JVM(Java虚拟机),主导Java程序运行,不同的操作系统有不同版本的JVM。Java程序在编译器被编译为字节码文件(.class),字节码在不同的OS上由不同的JVM解释,从而得以在此平台运行。
二、JVM体系结构与工作方式

  1. JVM的结构
    由四部分组成:
    类加载器(详见xxx):JAVA源文件经过编译变成class文件,由类加载器负责将class文件加载到JVM中
    执行引擎:负责执行class文件中的字节码指令,相当于实体机上的CPU
    运行时数据区:堆,方法区,PC寄存器、Java栈、本地方法栈
    2、JVM内存管理
    2.1.物理内存(RAM )与虚拟内存
    在计算机中还有一个存储单元叫寄存器,用于存储计算单元执行指令的中间结果。
    地址总线连接CPU和RAM(或者寄存器),所以地址总线的位数也决定了最大寻址空间,同样也就决定了RAM的内存空间
    要运行程序,就要先向操作系统申请内存空间,通常操作系统管理内存的申请空间是按照进程管理的,每个进程拥有一段独立的地址空间(逻辑上独立)
    虚拟内存:为了改善内存不够用的问题,使得多个进程在同时运行时可以共享物理内存,这里的共享只是空间共享,逻辑上仍不能互相访问
    2.2. JVM内存结构的划分
    JVM是按照运行时数据的存储结构来划分内存结构的。JVM在运行JAVA程序时,将他们划分成几种不同格式的数据,分别存储在不同的区域,这些数据统称为运行时数据。
    运行时数据划分为6种:PC寄存器数据 Java栈 本地方法区 堆 方法区 运行时常量池
    3.1 PC寄存器
    用于保存当前正常执行的Java方法的字节码指令的内存地址(如果执行的是一个Native方法,这个计数器值为空)(每个线程有一个)
    3.2 Java栈
    每当创建一个线程时,JVM就会为这个线程创建一个JAVA栈(堆栈),栈里包含多个栈帧,栈帧里有内部变量和操作栈的返回值方法等信息。Java栈的数据不是线程共享的。
    栈会产生的两种异常:
    线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常
    虚拟机在扩展栈时无法申请到足够的内存空间,OOM异常
    3.3. 本地方法栈
    顾名思义,是为JVM运行Native方法准备的空间,和Java栈的作用类似。(每个线程一个)
    3.4 堆
    堆是存储Java对象的地方,这个区域是应用程序与内存关系最为密切的区域。
    堆上可能会出现内存泄漏(泄露对象无法被GC,此时对象已经无用)内存溢出(内存对象确实还应该存活,此时就要考虑是否应该将堆内存调大)
    3.5 方法区
    JVM方法区是存储类结构信息的地方。在class被加载到JVM中时,常量池、域、方法数据、方法体、构造函数、包括类中的专用方法、实例初始化、接口初始化都存储在这个区域。这个区域属于Java堆的一部分,区域内存储的数据被所有线程共享。
    3.6 运行时常量池(已经归并到堆中)
    代表运行中每个class文件中的常量表
    在这里插入图片描述
    三、JVM垃圾回收与内存分配
    3.1垃圾回收-对象
    主要是Java堆(存储几乎所有对象实例)与方法区这两个区域
    3.1.1 判断对象是否应该被回收的方法
    1.1 引用计数法
    为每个对象增加一个引用计数器,对象被引用时,计数器+1;引用失效时,计数器-1。任何时刻计数器为0的对象就是不能再被使用的。(但这种方法并没有在JVM中使用,因为无法解决对象的循环引用问题)
    1.2 可达性分析算法
    通过一系列称为GC Roots的对象作为起始点,从这些结点开始向下搜索,搜所走过的路径称为引用链,当一个对象到GC Roots 没有任何的引用链相连时,证明此对象是不可用的
    在这里插入图片描述
    在实际的GC过程中,检测到对象与任何一个GC Roots都没有引用链时,先不能直接回收对象,要判断其是否有必要执行finalize()方法(判定条件:对象覆盖了finalize()方法并且没有被调用过),注意,finalize()方法只能被执行一次,此方法一般用于释放非Java资源,如数据库连接、打开的文件等。
    3.2 垃圾回收-方法区
    方法区的垃圾回收主要收集两部分:废弃常量和无用的类(常量池在后来被改进了堆中)
    回收常量:没有任何一个地方引用此常量,常量就会被系统清理出常量池
    回收类:满足三个条件1.该类的所有实例已经被回收 2.加载该类的ClassLoader已经被回收 3.该类对应的class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法
    3.3 垃圾回收算法
    3.3.1 标记-清除算法
    首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
    这种算法一是效率不太高,二是会产生大量不连续的内存碎片,导致如果有较大的对象需要存入内存时,会因无法找到足够连续的内存而触发另一次垃圾收集
    3.3.2 复制算法
    为了解决标记-清除算法的效率问题,诞生了复制算法
    按容量把可用内存分为两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域存活的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉,这样做的好处是每次都是对整个半区进行内存回收,内存分配时也不用考虑内存碎片等复杂情况
    3.3.3 标记-整理算法
    标记方式与3.1一样,标记完后让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存
    3.3.4 分代收集算法
    根据对象存活周期的不同将内存分为几块。
    一般是把Java堆分为新生代和老年代。在新生代每次垃圾回收都会回收掉大批量对象,因此采用复制算法。在老年代中对象存活率高,没有额外空间对它进行分配担保,就必须采用标记-整理或者标记-清除算法
    3.4 垃圾收集器发展史:
    Serial:新生代的单线程收集器,使用复制算法,在收集时需要暂停所有线程
    ParNew:Serial的多线程版本,使用多条线程进行垃圾收集,但仍然需要STW
    Parallel Scavenge:新生代多线程收集器,目标是达到一个可控制的吞吐量(吞吐量=用户运行代码时间/用户运行代码时间+GC时间)高吞吐量适合用在后台,而低吞吐量适合用在与用户交互频繁的场景。
    Serial Old:老年代的单线程收集器,使用标记-整理算法
    Parallel Old:Parallel Scavenge的老年代版本,标记-整理算法
    CMS收集器:老年代的多线程收集器,目标为获取最短回收停顿时间,基于标记-清除算法,分为四个步骤:
    1.初始标记:标记GC Roots能直接关联到的对象结点,需要STW
    2.并发标记:进行GC Roots Tracing,耗时较长
    3.重新标记:修正并发标记期间因用户线程的运行而产生的新的变化,需要STW
    4.并发清除:清除对象
    G1收集器:全区域的垃圾收集器,把heap划分为很多的region块,并行的对存活率低的块进行垃圾回收,整体基于标记-整理算法,局部(两个region之间)基于复制算法。region区的种类:E伊甸区 Ssurvivor T永久区 H用于储存大对象
    步骤:
    1.初始标记:与CMS不同的是G1的初始标记工作与minor GC 一并进行
    2.并发标记:与CMS不同的是,在发现T区中对象的存活率很小的时候,G1在这个阶段直接将其回收
    3.最终标记:跟CMS一样,只不过用的算法不同
    4.筛选回收:对对象存活率低的region进行回收

4.JVM如何给对象分配内存
4.1 对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发生一次Minor GC.如果当前Eden的对象无法全部放到survivor中,就将其通过分配担保机制提前转移到老年代。
4.2 大对象直接进入老年代
大对象是指需要大量连续空间的Java对象,比如很长的字符串。如果将大对象直接分配到Eden就会频繁触发Minor GC.虚拟机提供了一个参数,令大于这个值的对象直接在老年代分配。
4.3 长期存活的对象进入老年代
虚拟机给每个对象定义了一个对象年龄计数器。若对象在Eden出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且把对象的年龄设为1.对象在Survivor中每熬过一次Minor GC,年龄就增加一岁,当年龄增加到一定程度就进入老年代。
4.4 动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代。
4.5 空间分配担保
在Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于,则此次Minor GC是安全的。如果小于,则计算历代晋升到老年代的对象的平均大小,如果最大可用连续空间大于这个平均值,则进行Minor GC,但这次Minor GC有风险。
四、类加载器
4.1 类加载过程
step1 加载:通过类的全限定类名从不同的数据源(本地、jar包、网络)找到此类对应的二进制字节码文件
stap2 连接:就是把原本的静态数据转入JVM方法区做处理,以便进行调用。处理包括安全验证,验证字节信息是否符合JVM规范;包括对静态变量进行创建和初始化(但这时候的初始化没有分配内存);包括解析,将常量池中的符号引用替换为直接引用
(直接引用与符号引用:https://blog.csdn.net/u014656992/article/details/51107127)
step3 初始化:执行类初始化的代码逻辑,比如给静态变量进行实际的开辟内存赋值动作,还有执行静态初始化块内的逻辑
4.2 双亲委派模型
4.2.1 概述
类加载器在收到加载类的指令之后,不会立即去加载这个类,而是会把请求传给父类加载器,只有在父类加载器不能加载此类的时候,才会重新传下来。
4.2.2 JVM的三种类加载器
启动类加载器:加载<JAVA_HOME>/lib下的类
扩展类加载器:加载<JAVA_HOME>/lib/ext下的类
应用程序类加载器:加载我们写的类
4.2.3 作用
避免重复加载类
避免核心类库里的类被更改
五. Java内存模型
Java内存模型是为了屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
5.1 主内存与工作内存
内存模型的工作是完成将变量(实例字段、静态字段、构成数组对象的元素)存储到内存和从内存中取出变量这样的底层细节。
内存模型规定所有的变量存储在主内存中。每条线程有自己的工作内存,工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读取主内存中的变量。线程间变量的传递也需要主内存来完成。
5.2 内存间的交互操作
内存模型中定义了8种操作
lock和unlock 作用于主内存的变量,用于把变量标识或解除标识为某条线程独占状态。
read 作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中
load 作用于工作内存的变量,把从主内存传来的变量放入工作内存的变量副本中
use和assign 用于变量在工作内存和执行引擎之间的交互
store和write 与read和load差不多
六.深浅拷贝
要想让对象具有拷贝的功能,必须实现Cloneable接口,并且在类中自定义clone调用Object类提供的继承权限clone方法。
6.1 浅拷贝(对象值拷贝)
对于浅拷贝而言,拷贝出来的对象仍然保留原对象的所有引用。
浅拷贝存在的问题是只要任意一个拷贝对象(或原对象)中的引用发生改变,所有对象均会受到影响。
6.2 深拷贝
拷贝出来的对象产生了所有引用的新的对象。此时修改任意一个对象就不会对其他对象产生影响。
(B树B+树)
七、四大引用
强引用:我们实例化对象的时候引用就是强引用,如果对象具有强引用,那么它在任何时候都不会被GC,内存溢出也不会
软引用:内存空间不够时才会被回收 应用场景:缓存图片或视频
弱引用:不论内存空间是否充足,一旦发现就会被回收
虚引用:相当于没有引用,在任何时候都会被回收(对象销毁前的操作,释放资源之类的)
TIPS:Minor GC和Full GC
Minor GC发生在新生代,发生比较频繁,速度也快。
Full GC(Major GC)发生在老年代,通常会伴有Minor GC,速度比Minor GC慢十倍以上

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值