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区
缺点:
- 可用内存变小,缩小为原来的一半
- 不适用于老年代
标记整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
根据老年代的特点提出的一种标记算法,多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。
分代收集算法
对象的生命周期不同,故根据对象的存货周期在堆中分为新生代、老年代,根据其特点选择合适的垃圾收集算法。
- 新生代:标记-复制算法(每次收集都有大量对象死去,只需要付出少量对象的复制成本就可以完成垃圾清除)
- 老年代:标记-清除或标记整理算法(对象存活几率高,要清除的少)
类的加载过程
主要分为七个过程
-
加载
- 通过类名,获取二进制流,
- 解析类的二进制数据流为方法区内的数据结构(Java类模型)
- 创建java.lang.Class类的实例,作为方法区这个类的各种数据的访问入口
-
验证
验证类是否符合JVM规范
-
准备
为类变量分配内存并设置初始值
-
static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成
-
static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成
-
static变量是final的引用类型,那么赋值也会在初始化阶段完成
-
-
解析
把类中的符号引用转变为直接引用
-
初始化
对类的静态变量、静态代码块进行初始化操作
- 从上到下
- 优先初始化父类
-
使用
JVM 开始从入口方法开始执行用户的程序代码
- 调用静态类成员信息(比如:静态字段、静态方法)
- 使用new关键字为其创建对象实例
-
卸载
程序代码执行完毕后,JVM销毁Class对象,JVM也退出内存
双亲委派机制
当某个类加载器需要加载某个.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,下一级才会去加载这个类。
优点:
- 避免某一个类被重复加载,保证唯一性。
- 为了安全,保证类库API不会被修改
JVM调优
JVM调优主要是调整年轻代、老年代、元空间的内存大小及使用的垃圾回收器。
- 设置堆内存的初始化、最大内存
-Xms : 设置堆的初始化内存大小
-Xmx :设置堆的最大内存大小
- 设置年轻代中Eden区和Survivor区的大小比例(默认8:1:1)
-XX:SurvivorRatio=3,表示年轻代中的分配比率survivor:survivor:eden = 1:1:3
- 设置年轻代与老年代的大小比例(默认1:2)
-XX:newSize=n 设置年轻代的初始大小
-XX:MaxNewSize 设置年轻代的最大大小, 初始大小和最大大小两个值通常相同
-
线程堆栈的设置
默认1M,但128k就够用了
-Xss 对每个线程stack大小的调整,-Xss128k