java内存模型 JMM
JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
- 线程A从主内存中将共享变量读入线程A的工作内存后并进行操作,之后将数据重新写回到主内存中;
- 线程B从主存中读取最新的共享变量
内存间交互操作
- lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
- unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用;
- load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本
- use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用;
- write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
内存模型三大特性
- 原子性
- 有序性
- 可见性
synchronized:原子性、有序性、可见性
volatile:有序性、可见性
final:可见性
学习Java虚拟机(JVM)面试题(2020最新版),记录的笔记
Java内存区域
JVM 的主要组成部分及其作用
- Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
- Execution engine(执行引擎):执行classes中的指令。
- Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
- Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
Java程序运行机制
- 首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
- 再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;
- 运行字节码的工作是由解释器(java命令)来完成的。
java源码文件(.java),经过编译器(javac命令)编译生成字节码文件(.class),通过解释器(java命令)运行字节码文件
JVM 运行时数据区
1、线程共享
- java堆:jvm中内存最大的一块,存储大部分的对象示例,可能产生oom。GC堆,垃圾回收堆
- 方法区:存储已经被虚拟机加载的类信息、常量、静态变量等,可能产生oom
2、线程私有:
- java虚拟机栈:存储局部变量表、操作树栈、动态链接、方法出口等,可能产生oom
- 本地方法栈:处理native方法,可能产生oom
- 程序计数器:记录字节码程序执行到的行号。jvm内存中唯一不会产生oom的区域
PS:
静态变量放在方法区
静态的对象还是放在堆。
HotSpot虚拟机对象
对象的创建方式
方式 | 特点 |
---|---|
new关键字 | 调用构造器 |
Class类的newInstance方法 | 调用构造器 |
Constructor类的newInstance方法 | 调用构造器 |
clone | 不使用构造器 |
反序列化 | 不使用构造器 |
示例参见:https://www.cnblogs.com/liululee/p/11570353.html
内存分配方式
- 指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
- 空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。
选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
对象的访问定位
- 直接指针:虚拟机栈中的对象的引用,存储的是对象所在堆内存的地址;优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。
- 句柄:简单理解为指针的指针。虚拟机栈中的引用,存储的是句柄的地址,句柄中存储的是对象的地址。优势:GC时效率高,不用修改引用,只需要修改句柄内的值(对象地址)
垃圾收集器
java中的引用
- 强引用:GC时不会被回收
- 软引用:有用但不是必须的对象,发生内存溢出之前会被回收,即内存不够时
- 弱引用:有用但不是必须的对象,只要发生GC,就会回收
- 虚引用:无法通过虚引用获取对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。
如何确认垃圾
- 引用计数法:引用计数为0时,可以回收。有循环依赖,计数一直大于0而不能回收,导致内存泄露
- 可达性分析:从GC root根开始搜索,没有被引用的对象就是不可达对象,被标记两次后会被回收
垃圾回收算法
- 标记-清除算法:标记处可回收的对象,然后回收。缺点:标记、清除过程效率低,容易产生碎片
- 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。优点:效率高;缺点:内存使用率不高,只有原来的一半。
- 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
- 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
java堆划分
垃圾回收器
新生代:Serial,Parnew,Parallel Scavenge
老年代:Serial Old,CMS,Parallel Old
不分代,整堆回收:G1,ZGC,Epsilon,Shenandoah
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
详细介绍CMS垃圾回收器——面试常问
- CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间(STW,Stop The World)的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
- CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
- CMS步骤:
- 初始标记:标记GCRoot可以直接引用的对象,不再进行深层次扫描,所以速度很快,STW时间很短。
- 并发标记:在初始标记的基础上对整个堆中的对象进行逐层扫描,耗时较长,但是因是和用户程序并发执行,并不影响用户体验。
但是并发标记的过程都会有个问题,和应用程序一起执行,应用程序的状态会发生变化,可能存在漏标和多标的情况存在,漏标的问题可以通过重新标记解决,多标的问题会产生“浮动垃圾”,下一次GC时再解决。 - 并发预清理:标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。
- 重新标记:会STW,重新标记的过程可以解决并发标记过程中的漏标问题。主要是通过三色标记的增量更新方法来处理的。三色:黑色、灰色和白色
黑色:对象已经被垃圾收集器扫描过,并且这个对象的所有引用都已经扫描过了,所以它不可能指向白色对象;
灰色:对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用;
白色:表示对象没有被垃圾收集器访问过,即表示不可达。 - 并发清理:和用户线程一起,不会STW
- 重置:CMS清除内部状态,为下次回收做准备。
虚拟机类加载机制
类装载的执行过程
- 加载:根据查找路径找到相应的 class 文件然后导入;
- 验证:检查加载的 class 文件的正确性;
- 准备:给类中的静态变量分配内存空间;
- 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
- 初始化:对静态变量和静态代码块执行初始化工作
加载Class文件的方式
类加载器的功能是把.class文件加载到内存,类装载方式有两种:
- 隐式装载:new关键字,隐式调用类装载器加载对应的类到jvm中
- 显示装载:通过Class.forname()等方法,显式加载需要的类
双亲委派模型?
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。.
类加载器分类:
- 启动类加载器:是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
- 扩展类加载器:负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;
- 应用程序类加载器:负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
- 自定义类加载器:通过继承 java.lang.ClassLoader类的方式实现。
双亲委派模型:
如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
JVM调优
JVM 调优工具
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
- jconsole:用于对 JVM 中的内存、线程和类等进行监控;
- jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
常用的 JVM 调优的参数
-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。