JVM 一些基础知识

本文详细介绍了JVM的内存结构,包括类装载子系统、执行引擎、运行时数据区和本地接口。阐述了Java程序运行过程,强调了主存与工作内存的区别,以及堆和栈的特性。同时,详细讲解了对象分配内存的方式、内存溢出异常、垃圾回收的基本原理和不同算法。最后,讨论了类加载机制,特别是双亲委派模型,并列举了一些常见的JVM调优参数。
摘要由CSDN通过智能技术生成

JVM 主要组成部分及其作用

在这里插入图片描述
JVM主要包含两个子系统和两个组件
两个子系统为:

  • 类装载子系统(里面含有类加载器)
  • 执行引擎(即时编译器,GC垃圾回收器)

两个组件为:

  • 运行时数据区(JVM内存)
  • 本地接口

Java程序运行过程:
1.编译器将Java代码转为字节码
2.类加载器将字节码加载到内存中
3.再将其放入运行时数据区(JVM)内存的方法区内
4.将字节码传入执行引擎,将其编译为系统指令
5.将系统指令传输至本地方法接口,再使用本地方法库执行。

为什么分主存与工作内存

主内存主要包括本地方法区和堆。每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)。
1.所有的变量都存储在主内存中(虚拟机内存的一部分),对于所有线程都是共享的。
2.每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
3.线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。
在这里插入图片描述

简述JVM内存模型

  • 程序计数器
    当前线程所执行的字节码的行号指令器,字节码通过改变计数器的值,来选取下一条需要执行的字节码,每行程序指令都需要这个计数器来完成
  • 本地方法栈
    本地方法栈是为虚拟机调用Native方法服务的。
  • 虚拟机栈
    用于存储局部变量表、操作数栈、动态链接、方法出口等。
  • 方法区
    用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

  • Java虚拟机内存中最大的一块,是被所有线程共享,几乎所有的对象实例都在这里分配。

堆与栈的区别

  • 物理地址
    堆的物理地址分配对象是不连续的。因此性能较栈慢。在GC时也要考虑到这些不连续的分配数据,所以需要GC回收算法辅助。
    栈为数据结构中的栈,物理地址是连续分配的,所以更快
  • 内存分配
    堆的因为是不连续的,所以分配是在运行时期确认,大小不固定,一般堆大小要远大于栈
    栈是连续的,所以在编译时期就确定了大小(固定)
  • 存放内容
    堆的存放是对象实例与数组,该区更关注数据存储。
    栈存放的是局部变量,操作数栈,返回结果。该区更关注程序方法执行。
  • 程序可见度
    堆对于整个程序是共享可见的。
    栈只对线程可见,同时也是线程私有的。

对象分配内存方式

  • 指针碰撞
    如果Java堆内存是规整的,即所有使用过的内存放在一边,空闲的放在另一边。分配内存时就可以将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便能完成内存分配工作。
  • 空闲列表
    如果Java堆的内存不规整,则需要由虚拟机维护一个列表来记录哪些内存是可用的,这样分配是可以从列表中查询到足够大的内存分配给对象,并在分配后更新记录。
    选择哪种分配方式由Java堆是否规整来决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

内存溢出异常

内存泄漏是指不再被使用的对象或变量一直在被占据在内存中,理论上来说Java是有GC垃圾回收机制的,也就是说不再使用的对象,会被GC自动回收掉。
但是,如果长生命周期的对象持有短生命周期对象的引用就很有可能发生内存泄漏,尽管短生命周期对象不再需要,长生命周期对象持有它的引用而导致不能回收。

垃圾回收器基本原理

程序员创建对象时,GC就开始监控这个对象的地址,大小以及使用情况。
通常,GC采用有向图方式记录和管理堆中的所有对象。通过这种方式确定哪些对象是可达的,哪些对象是不可达的。当GC确定一些对象不可达时,GC就会回收这些空间。

Java中都有引用类型

  • 强引用:发送GC时不会被回收
  • 软引用:有用但不是必须的对象,发生内存溢出之前会被回收
  • 弱引用:有用但不是必须的对象,在下一次GC时会被回收。
  • 虚引用:无法通过虚引用获得对象,虚引用用途是在GC时返回一个通知。
    ps:为什么ThreadLocal会出现OOM
    在threadlocal的生命周期中,都存在这些引用. 看下图: 实线代表强引用,虚线代表弱引用.
    在这里插入图片描述
    1.ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。
    2.也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
    3.ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

GC

对象何时被回收?

当前使用对象在应用程序中不可触及时,这个对象就可以被回收了,普通的GC不会发生在永久代,如果永久代满了或者超过了临界值,会触发完全GC(Full GC)Java8移除了永久代,新加了元数据的native 内存区。
ps:为什么移除永久代,增加元空间
为什么移除永久代
永久代经常发生内存不足的情况(OOM)
元空间并不存在在虚拟机中而是使用本地内存,理论上取决于32/64位系统可虚拟的内存大小。

JVM有哪些垃圾回收算法

  • 标记-清除算法:标记无用对象,然后进行清除回收。
    • 缺点:是效率不高
  • 复制算法:按照容量划分为两个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。
    • 缺点:内存利用率不高,只有原来的一半
  • 标记整理:标记无用对象,让所有存活对象都向一端移动,然后直接清除掉边界以外的内存
  • 分代算法:根据对象存活周期的不同将内存划分为几块,一般分为年轻代(endn space, survivor space1 与 survivor space2),老年代与永久代
    这里讲下使用最多的分代算法:
    在这里插入图片描述
    分代回收器工作原理
    分代回收器有两个分区:新生代与老年代,新生代默认占用1/3,老年代默认占用2/3
    新生代使用的是复制算法,新生代里有3个分区,Eden,To Survivor、From Survivor,它们默认的占比是8:1:1。
    执行流程如下:
  • 把Eden + From Survivor 存活的对象放入 To Survivor 区
  • 清空Eden和From Survivor 分区
  • From Survivor 和 To Survivor 分区交换,From Survivor 变为 To Survivor 。
    每次在From Survivor到To Survivor移动时都存活的对象,年龄就+1,当年龄到达15时(默认为15)就会将对象放入老年代。大对象也会直接进入老年代
    老年代当空间占用到达阈值时就会进行全局垃圾回收。一般采用标记整理算法。

内存分配策略

对象优先在Eden区分配

多数情况下对象都在新生代Eden区分配,当Eden区没有足够空间分配时虚拟机会发起一次Minor GC,如果本次GC后还是没有足够空间,则将启用担保机制在老年代中分配内存。
这里谈到Minor GC,其实还有Major GC/Full GC

  • Minor GC 是指发生在新生代的GC,将新生代数据移入老年代,Minor GC非常频繁,一般回收速度也非常快。
  • Major GC/Full GC 指老年代的GC,一般都比Minor GC 慢很多。

虚拟机类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析话,最终形成可以被虚拟机直接使用的java类型。
类加载机制有两种

  • 隐式加载,如常用的new()关键字,生成一个对象,JVM会隐式使用类装载器加载对应的类到JVM中。
  • 显示装载,通过class.forName等方法显式加载需要的类
    Java类加载是动态的,并不会一次性将全部类加载后再运行,在保证基础类装载到JVM中后便可正常运行,其它类是使用时才会加载。

类加载器有分类

  • 启动类加载器:用来加载java核心类库,无法被Java程序直接引用
  • 扩展类加载器:用来加载Java的拓展库。Java虚拟机的实现会提供一个拓展库目录。
  • 系统类加载器:根据应用的类路径来加载Java类。一般来说,Java应用的类都是它来完成加载。
  • 用户自定义类加载器:通过继承ClassLoader类方式实现。

装载类的过程

在这里插入图片描述

双亲委派机制

如果一个类加载器收到了类加载请求,它不会立刻去加载这个类,而是将这个请求委派给父类加载器完成,只有当父加载器无法加载类时才会使用子加载类加载。
在这里插入图片描述
如果都加载不出来就会抛出ClassNotFoundException.
使用双亲委派的好处就是防止一个类加载请求被多个类加载器接收从而导致生成多个相同的类

常用JVM调优参数

  • -Xms2g: 初始化堆大小为2g
  • -Xmx2g: 堆最大内存为2g
  • -XX:NewRatio=4:设置年轻代与老年代比例为1:4
  • -XX:uSurvivorRatio=8: 设置新生代Eden和Survivor比例为8:1
  • -XX:+PrintGC:开启打印GC信息
  • -XX:+PrintGCDetails"打印GC详细信息
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值