JVM

图来自JavaGuide

程序计数器

  • 程序计数器是线程私有的,每个线程一份,是线程安全的;
  • 内部保存的字节码的行号,用于记录正在执行的字节码指令的地址。

java堆

  • java堆是线程共享的区域(线程不安全),主要用来保存对象实例、数组等,内存不够会抛出OutOfMemoryError异常

  • 一个JVM只有一个堆内存,堆内存大小可以调节

  • 组成:年轻代+老年代

    • 年轻代分为三部分:Eden区和两个大小严格相同的Survivor区(8:1:1)
    • 老年代主要保存一些生命周期长的对象。
  • JDK1.7 和1.8 的区别

    • 1.7 堆中有一个永久代,存储类信息、静态变量、常量、编译后的代码,不存在垃圾回收,关闭虚拟机就会释放这个区域的内存
    • 1.8中堆中移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出。

字符串常量池的变化:

  • 1.6 在方法区
  • 1.7在堆区
  • 1.8 在元空间(1.8方法区变成了本地内存)

方法区是所有线程共享的内存,在java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制,在java8中移除了永久代的内容,方法区由元空间(Meta Space)实现,并直接放到了本地内存中,不受JVM参数的限制(当然,如果物理内存被占满了,方法区也会报OOM),并且将原来放在方法区的字符串常量池和静态变量都转移到了Java堆中。

所有的对象都是在Eden区new出来

OOM解决方法:

  • 扩大堆内存
  • 分析内存,看哪里出现问题

永久代逻辑上存在,物理上不存在

堆内存调优

-Xms 1m 设置初始化内存分配大小 默认本机内存1/64

-Xmx 1m 设置最大分配内存 默认本机内存1/4

-XX:+PrintGCDetails 打印GC垃圾回收信息

-XX:+HeapDumpOnOutOfMemoryError: 导出OOM异常文件

虚拟机栈

  • 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
  • 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 8大基本类型、对象引用、实例方法

正在执行的方法一定在栈的顶部

运行时栈帧包含的结构:局部变量表、操作数栈、动态连接、返回地址、附加信息

垃圾回收是否涉及栈内存

不涉及,垃圾回收主要指堆内存,当栈帧弹栈后,内存就会释放

栈内存分配越大越好吗?

  • 未必,默认栈内存1024k,栈帧过大会导致线程数变少

方法内的局部变量是否线程安全

  • 若方法内局部变量没有逃离方法的作用范围,它是线程安全的;
  • 若局部变量引用了对象并逃离方法的作用范围,需要考虑线程安全

什么情况下会导致堆内存溢出(StackOverflow)

  • 栈帧过多,典型问题:递归调用
  • 栈帧过大

堆、栈的区别

  • 栈内存一般用来存储局部变量和方法调用,堆内存用来存储java对象和数组。堆会GC垃圾回收,而栈不会。
  • 栈内存是线程私有的,堆内存是线程共有的
  • 两者异常错误不同,但如果栈内存或堆内存不足都会抛出异常。

方法区

  • 方法区是各个线程共享的内存区域
  • 主要存储静态变量、变量、类的信息、运行时常量池
  • 虚拟机启动的时候创建,关闭虚拟机时释放。
  • 若方法区中的内存无法满足分配请求,则会抛出OutOfMemoryError:Metaspace

运行时常量池

  • 常量池可以看做一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型等信息
  • 当类被加载时,它的常量池信息会放入运行时常量池,并将里面的符号地址变为真实地址。

直接内存

  • 直接内存并不属于JVM的内存结构,不由JVM进行管理。是虚拟机的系统内存。
  • 常见于NIO操作,用于数据缓冲区。读写性能高 ,不受JVM内存回收管理

GC垃圾回收

发生在堆。

垃圾回收算法

标记-清除算法

首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

缺点:

  • 效率不高
  • 产生大量不连续的内存碎片
复制算法

内存分为大小相同的两块,每次使用其中的一块,当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。

作用于新生代的Survivor区

缺点:

  • 可用内存变小,缩小为原来的一半
  • 不适用于老年代
标记整理算法

标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

根据老年代的特点提出的一种标记算法,多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。

分代收集算法

对象的生命周期不同,故根据对象的存货周期在堆中分为新生代、老年代,根据其特点选择合适的垃圾收集算法。

  • 新生代:标记-复制算法(每次收集都有大量对象死去,只需要付出少量对象的复制成本就可以完成垃圾清除)
  • 老年代:标记-清除或标记整理算法(对象存活几率高,要清除的少)

类的加载过程

主要分为七个过程

  1. 加载

    • 通过类名,获取二进制流,
    • 解析类的二进制数据流为方法区内的数据结构(Java类模型)
    • 创建java.lang.Class类的实例,作为方法区这个类的各种数据的访问入口
  2. 验证

    验证类是否符合JVM规范

  3. 准备

    为类变量分配内存并设置初始值

    • static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成

    • static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成

    • static变量是final的引用类型,那么赋值也会在初始化阶段完成

  4. 解析

    把类中的符号引用转变为直接引用

  5. 初始化

    对类的静态变量、静态代码块进行初始化操作

    • 从上到下
    • 优先初始化父类
  6. 使用

    JVM 开始从入口方法开始执行用户的程序代码

    • 调用静态类成员信息(比如:静态字段、静态方法)
    • 使用new关键字为其创建对象实例
  7. 卸载

    程序代码执行完毕后,JVM销毁Class对象,JVM也退出内存

双亲委派机制

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,下一级才会去加载这个类。

优点:

  • 避免某一个类被重复加载,保证唯一性。
  • 为了安全,保证类库API不会被修改

JVM调优

JVM调优主要是调整年轻代、老年代、元空间的内存大小及使用的垃圾回收器。

  • 设置堆内存的初始化、最大内存
java 复制代码
-Xms : 设置堆的初始化内存大小
-Xmx :设置堆的最大内存大小
  • 设置年轻代中Eden区和Survivor区的大小比例(默认8:1:1)
java 复制代码
-XX:SurvivorRatio=3,表示年轻代中的分配比率survivor:survivor:eden = 1:1:3
  • 设置年轻代与老年代的大小比例(默认1:2)
java 复制代码
-XX:newSize=n   设置年轻代的初始大小
-XX:MaxNewSize   设置年轻代的最大大小,  初始大小和最大大小两个值通常相同
  • 线程堆栈的设置

    默认1M,但128k就够用了

java 复制代码
-Xss   对每个线程stack大小的调整,-Xss128k
相关推荐
ThisIsClark33 分钟前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉1 小时前
【jvm】内存泄漏与内存溢出的区别
jvm
大G哥3 小时前
深入理解.NET内存回收机制
jvm·.net
泰勒今天不想展开3 小时前
jvm接入prometheus监控
jvm·windows·prometheus
东阳马生架构1 天前
JVM简介—3.JVM的执行子系统
jvm
程序员志哥1 天前
JVM系列(十三) -常用调优工具介绍
jvm
后台技术汇1 天前
JavaAgent技术应用和原理:JVM持久化监控
jvm
程序员志哥1 天前
JVM系列(十二) -常用调优命令汇总
jvm
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭1 天前
聊聊volatile的实现原理?
java·jvm·redis