JVM你看这篇就够了(适合新手)

Java类加载过程

在这里插入图片描述
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
加载:
(1)通过一个类的全限定名来获取其定义的二进制字节流

(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构

(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
验证: 验证的主要作用就是确保被加载的类的正确性。检验过程包括下面四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
准备:
(1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中。

(2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。
解析: 解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。
初始化: 是类加载过程的最后一步,会开始真正执行类中定义的Java代码。

类加载器

启动类加载器: 由C++语言实现,是虚拟机自身的一部分,负责加载存放在<JAVA_HOME>\lib目录中可被虚拟机识别的类库。
其他类加载器: 由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader,可被Java程序直接引用。

  • 扩展类加载器: A.由sun.misc.Launcher$ExtClassLoader实现B.负责加载<JAVA_HOME>\lib\ext目录中的、或者被java.ext.dirs系统变量所指定的路径中的所有类库
  • 应用程序类加载器: A.是默认的类加载器,是ClassLoader#getSystemClassLoader()的返回值,故又称为系统类加载器B.由sun.misc.Launcher$App-ClassLoader实现C.负责加载用户类路径上所指定的类库
  • 自定义类加载器: 如果以上类加载起不能满足需求,可自定义
    执行顺序如下图所示:
    在这里插入图片描述

类加载的三种方式

(1)通过命令行启动应用时由JVM初始化加载含有main()方法的主类。

(2)通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。

(3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。

双亲委派模型

  • 定义:表示类加载器之间的层次关系
  • 前提:除了顶层启动类加载器外,其余类加载器都应当有自己的父类加载器,且它们之间关系一般不会以继承关系来实现,而是通过组合关系来复用父加载器的代码
  • 工作过程:若一个类加载器收到了类加载的请求,它先会把这个请求委派给父类加载器,并向上传递,最终请求都传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载
  • 优点:类会随着它的类加载器一起具备带有优先级的层次关系,可保证Java程序的稳定运作。

JVM内存结构

在这里插入图片描述
程序计数器:较小的内存空间,当前线程执行的字节码的行号指示器线程私有的
堆:Java是内存最大的一块,所有对象实例、数组都存放在java堆,涉及到内存的分配(new关键字,反射等)与回收(回收算法,收集器等),线程公有的
虚拟机栈:线程私有,生命周期和线程,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
本地方法栈:本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法;
方法区:也叫永久区,用于存储已经被虚拟机加载的类信息,常量(“zdy”,"123"等),静态变量(static变量)等数据。(jdk1.8已经将方法区去掉了,改为了元空间,元空间是在JVM外开辟的一块高速内存区,但是还是由jvm来监控与gc)

JDK1.8为什么要移除方法区
1)永久代来存储类信息、常量、静态变量等数据不是个好主意, 很容易遇到内存溢出的问题。JDK8的实现中将类的元数据放入 native memory, 将字符串池和类的静态变量放入java堆中, 可以使用MaxMetaspaceSize对元数据区大小进行调整
2)对永久代进行调优是很困难的,同时将元空间与堆的垃圾回收进行了隔离,避免永久代引发的Full GC和OOM等问题;

直接内存: 不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;

Java堆

堆分代

在这里插入图片描述
1、JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)
2、年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例。
3、内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
4、非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等(jdk1.7,1.8之后就没有永久带了,取而代之的就是元空间

新生代: 新生成的对象优先存放在新生代中,新生代对象存活率很低,Eden和Survivor比例是8:1:1,设置这个比例是为了充分利用内存空间,减少浪费。

老年代: 中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。

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

垃圾收集器

Serial收集器: 单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。
ParNew收集器: Serial收集器的多线程版本,也需要stop the world,复制算法。
Parallel Scavenge收集器: 新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。
Serial Old收集器: 是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
CMS(Concurrent Mark Sweep) 收集器: 是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。
G1收集器: 标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。

CMS收集器和G1收集器的区别

  • CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
  • G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
  • CMS收集器以最小的停顿时间为目标的收集器;
  • G1收集器可预测垃圾回收的停顿时间
  • CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
  • G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。

垃圾回收机制

GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值的对象会被移到老年代中没有达到阀值的对象会被复制到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区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

判断对象是否存活算法

1、引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,引用失效时就减1。任何时刻计数器为0的对象就是不可能再被使用的。这个方法效率挺高,大部分情况下也是很不错的算法。

2、可达性分析算法
是用GC ROOTs 作为对象的起点开始往下搜索,能搜索到这个对象,就表示对象是可达的,不能搜素到表示对象是不可达的

常见的垃圾回收算法

1.标记清除法
GC分为两个阶段,标记和清除。首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。缺点会产生不连续的内存碎片。
2.复制算法
将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点需要两倍的内存空间
3.标记整理法
首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题。

堆溢出

OOM(Out of Memory)异常常见有以下几个原因:
1)老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace
2)永久代内存不足:java.lang.OutOfMemoryError:PermGenspace
3)代码bug,占用内存无法及时回收。
OOM在这几个内存区都有可能出现,实际遇到OOM时,能根据异常信息定位到哪个区的内存溢出。
可以通过添加个参数-XX:+HeapDumpOnOutMemoryError,让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。

JVM内存参数设定

-Xms 初始堆内存大小
-Xmx 最大堆内存大小
-Xss 单个线程栈大小
-XX:NewSize 初始新生代堆大小
-XX:MaxNewSize 生代最大堆大小
-XX:PermSize 方法区初始大小(JDK1.7及以前)
-XX:MaxPermSize 方法区最大大小(JDK1.7及以前)
-XX:MetaspaceSize 元数据区初始值(JDK1.8)
-XX:MaxMetaspaceSize 元数据区最大值(JDK1.8)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值