在面试的时候,很多面试官都会问到关于jvm相关的面试题,但自己做了那么长时间的java开发,往往忽略了这方面的知识.理解底层是对一个程序员尤为重要的.这是我跟着b战狂神学习的笔记.
首先出几道面试题:
- jvm内存模型,分区。详细到每个区都放什么。
- 堆里面的分区:伊甸园,from,to老年区。说说他们的特点。
- Gc常用算法,标记清除,标记压缩,复制算法引用计数器分带收集算法。怎么使用。
- 软引用弱引用强引用虚引用.
- gc根据什么判断是垃圾
- 垃圾收集器有什么,分别说一说。
- 新创建对象那么他一定会在新生代么?不是,大的对象会直接分配在老年代。
感觉很懵,一道题都不会.那就由浅入深,学习jvm吧.
JVM基础知识
1.jvm的位置
首先我们要知道jvm试运行在操作系统上的,程序运行在jvm中,jre包含jvm.
2.jvm的体系结构
基本的执行流程.
在这个区域一定不会有垃圾回收,所谓的jvm调优99%都是在堆中
jvm内存结构可以分为线程独占的和县城共享的。
线程独占:本地方法栈,栈,程序计数器。
线程共享的,方法区堆。
方法区他是java虚拟机的一个模型规范。具体实现是元空间和永久代永久代是jdk1.7之前。1.8后只有元空间。元空间是脱离jvm虚拟机内存的。在计算机中。
3.类加载器
类加载器的作用:
加载class文件.当我们new 一个对象的时候,引用在栈,实例在堆中.
当我们 new Car 对象的时候.
首先通过classLoader 加载 car.class类.在通class类实例化出car对象.
栈中存放car的引用地址.堆中存放car的实例.
其实这只是简单的流程.实际流程要复杂得多.比如加载器:
- 虚拟机自带加载器
- 启动(根加载器)
- 扩展类加载器
- 应用程序加载器
比如上述car使用的加载器是应用程序加载器.applicationClassLoader
应用程序加载器的父加载器是扩展类加载器,他们都是放在rt.jar里面的
扩展类加载器的父加载器通过java程序class.getParent()方法是获取不到的,但这并不能说明他没有,扩展类放在ext目录下
4.双亲委派机制
他的作用是保证安全性.防止出现bug.
比如我们自己new 一个java.lang.String类.添加main方法.但在执行的时候会报错.原因就是双亲委派机制.
原理就是当我们执行Stringmain方法.他会被appclassloader加载,但不会马上执行,他会层层递进至扩展类加载器,根加载器.如果有相同的String类则不会执行apploader中的String方法.执行顺序:
根加载器>扩展类加载器>app加载器.
整体流程:
- 类加载器收到类加载的请求
- 将这个类委托给父加载器去完成,一直委托,直到启动加载器.
- 启动类加载器检查是否能加载此类,能加载就结束使用当前加载器,否则抛出异常,通知子类加载器加载.
- 重复此步骤.
- 如果都无法加载则抛出 class not found异常.
5.沙箱安全机制
沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?
CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
6.Native
方式调用到了native关键字的方法,说明java的作用范围达不到了,回去调用c语言底层的库.会进入本地方法栈.他会去找JNI(java native interface)接口,也就是本地方法接口.本地方法接口调用本地方法库.
jvm在内存中开辟了一块标记区域本地方法栈native method stack 登记native 方法.在执行的时候加载本地方法通过jni.
7.pc寄存器
PC寄存器( PC register ):
-
每个线程启动的时候,都会创建一个PC(Program Counter,程序计数器)寄存器。PC寄存器里保存有当前正在执行的JVM指令的地址。 每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。保存下一条将要执行的指令地址的寄存器是 :PC寄存器。PC寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
-
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
-
这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
-
如果执行的是一个Native方法,那这个计数器是空的。
8.方法区
方法区是被所有线程共享.所有字段和方法字节码.以及一些特殊方法,构造器,接口等.简单的说就是所有的方法的信息都保存在该区域.此区域属于共享区间.
静态变量,常量类信息(构造方法,接口定义)运行时的常量池存在方法区中,但实例变量存在堆内存中.和方法区无关.
都保存在该区域.此区域属于共享区间.
其实存储的就是static final Class 常量池.
当我们new User 一个对象的时候 首先在栈中存放引用地址.堆中产生该实例,User属性name 如果常量池中有,则引用常量池中的数据,如果没有则产生新的数据.
9.栈
每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈和出栈。
Java帧上的所有数据都是此线程私有的。任何线程都不能访问另一个线程的栈数据,因此我们不需要考虑多线程情况下栈数据的访问同步问题。当一个线程调用一个方法时,方法的的局部变量保存在调用线程Java栈的帧中。只有一个线程能总是访问那些局部变量,即调用方法的线程。
10.三种jvm
-
Sun公司的HotSpot 是目前使用范围最广的Java虚拟机。
-
BEA公司的JRockit(原来的 Bea JRockit)电脑软件,系列产品是一个全面的Java运行时解决方案组合。
-
IBM公司的J9 VM 是一个高性能的企业级 Java 虚拟机。
11.堆
一个jvm只有一个堆内存.,堆内存是可以调节的.
类加载器读取文件后通常会把
类,方法,常量,变量放在堆中.保存我们引用类型的真实对象.
堆内存主要细分三个区域.
新生区老年区永久区。
12.新生区.老年区
首先新生区:
在堆内存中新生区包括:
- 伊甸园区和幸存区
- 当new出新对象后经过请gc未被回收掉的会到幸存区。
- 对象诞生成长甚至会死亡的地方。
- 所有的对象都是在伊甸园区new 出来的
当伊甸园区满了之后他会经历一次轻gc,移到幸存区,有两个幸存区,他们两个互相交换。当都满了之后会触发重GC。活下来的进入老年区。99%都会被回收所以很少看见堆内存溢出。
13.永久区
永久区是常驻在内存中的存放Javajdk自带的class对象。interface,元数据,存储java运行环境
jdk6之前:常量池永久代在方法区中
jdk7 有永久代但慢慢退化了。去永久带,常量池在堆中。
Jdk8以后,无永久代常量池叫元空间。
那么永久代和元空间有什么区别呢
- 永久代在1.7 ,并且必须指定大小 。容易发生oom,元空间他可以指定也可以不指定。受限于本机内存
14.堆内存调优
jvm默认分配的总内存是电脑内存的1/4初始化内存是1/64.
我们可以通过命令调优:
-Xms8m -Xmx1024m XX:+PrintGCDetails
8m 和1024m是最小和最大堆内存参数。
假如项目中报了OOM:
- 要看待代码哪行出现错误:内存快照分析工具。Mat,Jprofiler
- Debug 一行行分析。
Mat,Jprofiler作用:
- 分析dump文件快速定位内存泄露。
- 获得堆中的数据
- 获得大的对象。
15.GC垃圾回收
Gc垃圾回收主要是在养老区和伊甸园区。假设内存满了就会报OOM内存溢出错误。
GC作用区只在方法区和堆
JVM进行gc时,并不是对三个区域统一回收,大部分是回收新生区
- 新生区
- 幸存区
- 老年区
轻gc 回收新生区
重gc都回收一遍
1常用算法
15.1 引用计数器算法
原理就是多个对象标记他们被引用的次数 引用0的或引用少的则被回收,需要有计数器,计数器也占用资源所以不被广泛使用。
15.2 复制算法
复制算法主要针对在新生代。
伊甸园区经过轻gc后存活的对象会去to区。伊甸园区变为空。from区所有活着对象复制到to区并按照地址排序。
缺点:
-
复制算法缺点就是占用空间大,实际用到存放对象的只有开辟区域的1/2。
-
假如对象存活率特别高的话很占用空间。并且复制所花费的时间会更多
一个对象经历了十五次gc都还没有死就会进入老年区。这个参数可调。
15.3 标记清除算法
他经常被用于老年区。
他就是对对象进行标记,未被标记的对象会被清除。这种方法会对所有对象进行两次扫描,浪费时间,会产生内存碎片,无法存储大的对象在碎片中。
优点:不需要额外空间。
15.4 标记压缩
他和标记清除联用,防止内存碎片的产生。
多了移动成本。
联用通常称为标记清除压缩算法。
15.5 算法总结
我们从三个方面分析:
- 内存效率:复制>标记清除>标记压缩
- 内存整齐度:复制=标记压缩>标记清除算法
- 内存利用率:标记压缩>标记清除>复制
16.JMM
Java Memory Model
java 内存模型.
【JMM】(Java Memory Model的缩写)允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了volatile或synchronized明确请求了某些可见性的保证。
其实就是个缓存一致性协议.用于定义数据的读写规则.
JMM定义了线程和主内存的抽象关系线程之间的共享变量存在主内存中,每个线程都有一个私有内存
解决共享对象可见性问题:volitale