文章目录
JVM位置
JVM体系结构
双亲委派机制
- 说明:当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,直到BootStrap启动类加载器,如果上级的类加载器没有加载,自己才会去加载这个类。(向上委托,向下加载)
- 作用:
- 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
- 类加载器类别:(由上至下)BootstrapClassLoader(启动类加载器)-> ExtClassLoader (标准扩展类加载器)-> AppClassLoader(系统类加载器)-> CustomClassLoader(用户自定义类加载器)
native关键字(主要用于方法上)
- 一个native方法就是一个Java调用非Java代码的接口。一个native方法是指该方法的实现由非Java语言实现,比如用C或C++实现。
- 在本地方法栈中(native method stack)登记native方法,在执行引擎之行的时候加载本地库。
- 在定义一个native方法时,并不提供实现体(比较像定义一个Java Interface),因为其实现体是由非Java语言在外面实现的。主要是因为JAVA无法对操作系统底层进行操作,但是可以通过JNI(java native interface java本地方法接口)调用其他语言来实现底层的访问。
- 举例:Thread类中的start() 方法中调用一个start0()的native方法。
PC寄存器
程序计数器
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法中的方法字节码(用来存储指向一条指令的地址,也即将要之行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
方法区
方法区:Method Area方法区
- 方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,
- 方法区在物理上存在堆中,为了和堆区分开,也称非堆
- 简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
- 静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池(static,final,Class,常量池)存在方法区中,但是实例变量存在堆内存中,和方法区无关
栈
- 栈:先进后出,后进先出
- 为什么main()先执行,最后结束:每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在;在这个线程上正在执行的每一个方法都各自对应一个栈帧;栈帧是一个内存区块,是一个数据集维系着方法执行过程中的各种数据信息
- 栈:8大基本类型+对象引用+实例的方法
- 栈是运行时的单位,Java 虚拟机栈,线程私有,生命周期和线程一致。
- 描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。(方法头开始入栈,结束出栈,方法里面调用别的方法 新的方法就会把旧的压在底下,最上面永远是正在执行的方法,也对应先入后出。)
堆
- Heap,一个jvm只有一个堆内存,堆内存的大小是可以调节的
- new一个对象时将对象的引用放在Java栈中,把类的具体事例、方法、常量、变量放在堆中,保存我们所有引用类型的真实对象。
- 堆内存细分:(下图为jdk8之前的,jdk8以后永久存储区的名字改为“元空间”)
- 永久区:这个区域常驻内存的。用来存放JDK自身携带的class对象,interface元数据,储存的是java运行时的一些环境或类信息,这个区域不存在垃圾回收,关闭VM虚拟机就会释放这个区域的内存
- Jdk1.6之前:永久代,常量池在方法区
Jdk1.7:永久代,慢慢退化,去永久代,常量池在堆中
Jdk1.8之后:无永久代,常量池在元空间
- 什么时候出现永久区满:一个启动类,加载了大量的第三方jar包,tomcat部署了太多的应用,大量动态生成的反射类,不断的被加载,直到内存满,就会出现OOM
堆结构
元空间逻辑上在堆内存中,物理上不在堆内存中
OOM内存溢出,解决方法
解决方法:
- 尝试扩大堆内存看结果
- 分析内存,看一下哪个地方出现问题(专业工具,如JProfiler等)
JProfiler工具分析OOM原因
JProfiler可以分析Dump内存文件,快速定位内存泄漏
JProfiler使用步骤
- 编写一个能够产生OOM的方法用来测试
import java.util.ArrayList; public class Demo { byte[] array=new byte[1*1024*1024]; public static void main(String[] args) { ArrayList<Demo> list=new ArrayList<Demo>(); int count=0; try { while (true){ list.add(new Demo()); count++; } } catch (Error e) { System.out.println("count"+count); e.printStackTrace(); } } }
- 安装JProfiler(官网下载即可)
- 在IDEA中引入JProfiler的插件
重启idea完成后可以看到右上角有了JProfiler的标志,代表成功安装。
- 修改idea当前类的虚拟机的配置参数为
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
几个不同的配置参数的含义:-Xms
:设置初始化内存分配大小-Xmx8m
:设置最大分配内存-XX:+HeapDumpOnOutOfMemoryError
:当发生OOM错误时自动生成dump文件-XX:+PrintGCDetails
:打印GC垃圾回收信息
- 这时候运行main方法,若出现OOM错误就会自动生成DUMP文件。
进入当前项目的的父文件夹下就可以看到
- 点击目录下第一个生成的文件就会自动跳转进入JProfiler的页面,从下面两张图中我们可以分别看出占用最多内存的Object和代码中发生错误的行数。
jvm调优
对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。
JVM性能调优方法和步骤:
- 监控GC的状态;
- 生成堆的dump文件;
- 分析dump文件;
- 分析结果,判断是否需要优化;
- 调整GC类型和内存分配;
- 不断的分析和调整
垃圾回收GC
只在堆和方法区中存在垃圾回收
JVM在进行GC时,并不是对这三个区域统一回收,大部分时候,回收都是新生代。
- 新生代
- 幸存区(from、to)幸存区中谁空谁就是幸存to区,位置会交换。
- 老年区
- 每一次垃圾回收之后都会将Eden区活的对象移到幸存区中,一旦Eden区被GC后,就会是空的。
- 当一个对象经历了15次(默认,可以通过设置MaxTenuringRhreshold来设定进入老年代的时间)GC后还没有被回收,就会进入老年区
GC的两种类型:轻GC(普通的GC,针对新生代,偶尔清一下幸存区)、重GC(全局GC,所有的区都清)
GC常见算法
- 引用计数器(JVM一般不会采用,不高效)
- 为每个对象分配一个计数器,该对象引用了几次就加几
- 计数器本身也会有消耗
- 计数为0的就会被清除
- 标记清除法
- 过程:
- 标记:扫描对象,对活着的对象进行标记
- 清除:对没有标记的对象进行清除
- 优点:不需要额外的空间(比复制算法需要的空间少)
- 缺点:两次扫描,严重浪费时间,会产生内存碎片
- 过程:
- 标记整理/压缩
- 在标记清除的基础上再优化,进行压缩。再次扫描,向一段移动存活的对象,防止内存碎片的产生。
- 多了一个移动成本。可以进行优化:先标记清除几次,再进行压缩。
- 复制
- 幸存区(from、to),幸存区中谁空谁就是幸存to区,位置会交换。复制算法会将from区中的东西复制到to区中,复制完成之后to区就变成了from区,from区就变成了to区
- 好处:没有内存碎片
- 坏处:浪费了内存空间,多了一半空间(to区)永远是空的。假设对象100%存活,每次需要全部复制,成本太高。
- 最佳使用场景:对象存活度较低的时候,适用于新生区中(新生区中使用的就是复制算法)
总结
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法
没有最好的算法,只有最合适的
GC也被称为分代收集算法:
- 年轻代存活率低,适合复制算法
- 老年代区域大,存活率高,适合标记清除+标记压缩混合实现
JMM:Java Memory Model
Java内存模型
常见面试题
- JVM的内存模型和分区
- 堆里面的分区(Eden、from、to、老年区)
- GC算法
- 轻GC和重GC在什么时候发生