先给大家看几道面试题?
1、请你谈谈你对JVM的理解?Java8的虚拟机有什么更新?
2、什么是OOM?什么是StackOverFlowError?有哪些方法分析?
3、JVM的常用参数调优你知道哪些?
4、内存快照抓取和MAT分析DUMP文件知道吗?
5、堆里面的分区:Eden,Survival from to,老年代,各自的特点?
6、GC的三种收集方法:标记清除,标记整理,复制算法的原理与特点,分别用在什么地方?
什么是jvm
JVM是Java虚拟机,用来解析和运行Java程序的
JVM的位置
JVM体系结构图
如果你不能够闭着眼睛画出 JVM 的体系结构图,说明你还没有入门 JVM:
所谓JVM的调优,其实就是在调这个区域,而且99%情况下都在调堆
类加载器ClassLoader
我们先来看看一个类加载到 JVM 的一个基本结构:
在如下几种情况下,Java虚拟机将结束生命周期:
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或者错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进行终止
类的加载、连接与初始化
在Java代码中,Class的加载、连接与初始化过程都是在程序运行期间完成的。Runtime!
-
加载: 查找并加载类的二进制数据
-
连接
- 验证:确保被加载的类的正确性
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中的符号引用转换为直接引用
在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道
所引用类的地址,多以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化
成为真正的地址的阶段。 -
初始化:为类的静态变量赋予正确的初始值
从代码来理解:
class Test{
public static int a = 1;
}
//我们程序中给定的是 public static int a = 1;
//但是在加载过程中的步骤如下:
1. 加载阶段
编译文件为 .class文件,然后通过类加载,加载到JVM
2. 连接阶段
第一步(验证):确保Class类文件没问题
第二步(准备):先初始化为 a=0。(因为你int类型的初始值为0)
第三步(解析):将引用转换为直接引用
3. 初始化阶段:
通过此解析阶段,把1赋值为变量a
类的加载
类的加载指的是将类的.class文件中二进制数据读入到内存中,将其放在运行时数据区内的方法区内,然后再内存中创建一个 java.lang.Class 对象用来封装类在方法区内的数据结构。
查看类的加载信息,并打印出来:
常量池的概念
ClassLoader分类
有两种类型的类加载器
1、Java虚拟机自带的加载器
- 根类加载器(BootStrap)(BootClassLoader) sun.boot.class.path (加载系统的包,包含jdk核
心库里的类) - 扩展类加载器(Extension)(ExtClassLoader) java.ext.dirs(加载扩展jar包中的类)
- 系统(应用)类加载器(System)(AppClassLoader) java.class.path(加载你编写的类,编译后的类)
2、用户自定义的类加载器
- Java.long.ClassLoader的子类(继承),用户可以定制类的加载方式
双亲委派机制
双亲委派机制的工作原理:一层一层的 让父类去加载,最顶层父类不能加载往下数,依次类推。
- 类加载器收到类加载的请求;
- 把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;
- 启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛出异常,通知子加载器进行加载。
大家所熟知的String 类,直接告诉大家,String 默认情况下是启动类加载器进行加载的。假设我也自定义一个String 。现在你会发现自定义的String 可以正常编译,但是永远无法被加载运行。
这是因为申请自定义String 加载时,总是启动类加载器,而不是自定义加载器,也不会是其他的加载
器。
双亲委派机制可以确保Java核心类库所提供的类,不会被自定义的类所替代。
Native方法
凡是带了native关键字的,说明 java的作用范围达不到,去调用底层C语言的库!
JNI:Java Native Interface (Java本地方法接口)
凡是带了native关键字的方法就会进入本地方法栈;
Native Method Stack 本地方法栈
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 在 Native Method Stack 中登记native方法,在 ( ExecutionEngine ) 执行引擎执行的时候加载Native Libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍!
程序计数器
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。是一个非常小的内存空间,几乎可以忽略不计
方法区
Method Area 方法区 是 Java虚拟机规范 中定义的运行时数据区域之一,它与堆(heap)一样在线程之间共享。
Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
JDK7 之前(永久代)用于存储已被虚拟机加载的类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据。每当一个类初次被加载的时候,它的元数据都会被放到永久代中。永久代大小有限制,如果加载的类太多,很可能导致永久代内存溢出,即 java.lang.OutOfMemoryError:
PermGen。
JDK8 彻底将永久代移除出 HotSpot JVM,将其原有的数据迁移至 Java Heap 或 NativeHeap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace)。
元空间(Metaspace):元空间是方法区的在 HotSpot JVM 中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码、符号引用等。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 配置内存大小。
如果Metaspace的空间占用达到了设定的最大值,那么就会触发GC来收集死亡对象和类的加载器。
栈(Stack)
栈:后进先出 / 先进后出
队列:先进先出(FIFO : First Input First Output)
Stack 栈是什么
栈管理程序运行
存储一些基本类型的值、对象的引用、方法等。
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。
思考:为什么main方法最后执行!为什么一个test() 方法执行完了,才会继续走main方法!
说明:
1、栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放。
2、对于栈来说不存在垃圾回收问题,只要线程一旦结束,该栈就Over,生命周期和线程一致,是线程私有的。
3、方法自己调自己就会导致栈溢出(递归死循环测试)
栈运行原理
没搞明白,以后再做了解
堆(Heap)
Java7之前
Heap 堆,一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为
三部分:
- 新生区 Young Generation Space Young/New
- 养老区 Tenure generation space Old/Tenure
- 永久区 Permanent Space Perm
堆内存逻辑上分为三部分:新生,养老,永久(元空间 : JDK8 以后名称)
GC垃圾回收主要是在 新生区和养老区,又分为 轻GC 和 重GC,如果内存不够,或者存在死循环,就会导致 java.lang.OutOfMemoryError: Java heap space
新生区
新生区是类诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区又分为两部分:伊甸区(Eden Space)和幸存者区(Survivor Space),所有的类都是在伊甸区被new出来的,幸存区有两个:0区 和 1区,当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC)。将伊甸园中的剩余对象移动到幸存0区,若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区,那如果1区也满了呢?(这里幸存0区和1区是一个互相交替的过程)再移动到养老区,若养老区也满了,那么这个时候将产生MajorGC(Full GC),进行养老区的内存清理,若养老区执行了Full GC后发现依然无法进行对象的保存,就会产生OOM异常 “OutOfMemoryError ”。
如果出现 java.lang.OutOfMemoryError:java heap space异常,说明Java虚拟机的堆内存不够,原因如下:
1、Java虚拟机的堆内存设置不够,可以通过参数 -Xms(初始值大小),-Xmx(最大大小)来调整。
2、代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)或者死循环
永久区(Perm)
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
如果出现 java.lang.OutOfMemoryError:PermGen space,说明是 Java虚拟机对永久代Perm内存设
置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包,例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
注意:
Jdk1.6之前: 有永久代,常量池1.6在方法区
Jdk1.7: 有永久代,但是已经逐步 “去永久代”,常量池1.7在堆
Jdk1.8及之后:无永久代,常量池1.8在元空间
实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载
的:类信息+普通常量+静态常量+编译器编译后的代码,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名,叫做Non-Heap(非堆),目的就是要和堆分开。
对于HotSpot虚拟机,很多开发者习惯将方法区称之为 “永久代(Parmanent Gen)”,但严格本质上说两者不同,或者说使用永久代实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,Jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。
常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本,字段,方法,接口描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放!
堆内存调优
了解完基本的堆的信息之后,我们就可以简单学习下关于堆内存调优的说明了!我们是基于 HotSpot 虚拟机的,JDK1.8;
堆内存调优
-Xms :设置初始分配大小,默认为物理内存的 “1/64”
-Xmx :最大分配内存,默认为物理内存的 “1/4”
-XX:+PrintGCDetails :输出详细的GC处理日志
IDEA中进行JVM调优参数设置,然后启动
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
也可以对垃圾回收参数做一些调整
Dump内存快照
在运行java程序的时候,有时候想测试运行时占用内存情况,这时候就需要使用测试工具查看了。在
eclipse里面有 Eclipse Memory Analyzer tool(MAT)插件可以测试,而在idea中也有这么一个插件,就是JProfiler,一款性能瓶颈分析工具!
作用:
- 分析Dump文件,快速定位内存泄漏;
- 获得堆中对象的统计数据
- 获得对象相互引用的关系
- 采用树形展现对象间相互引用的情况
- …
而且这个软件跨平台:
安装JProfiler
内存快照监控用的,以后再做学习,面试说听说过
GC详解
回顾一下 GC 的作用域
此GC按照回收的区域又分了两种类型,一种是普通的GC(minor GC),一种是全局GC (major GC or Full GC)
普通GC:只针对新生代区域的GC
全局GC:针对老年代的GC,偶尔伴随对新生代的GC以及对永久代的GC
GC面试题
1、JVM内存模型以及分区,需要详细到每个区放什么
2、堆里面的分区:Eden,Survival from to,老年代,各自的特点。
3、GC的三种收集方法:标记清除,标记整理,复制算法的原理与特点,分别用在什么地方?
4、Minor GC 与 Full GC 分别在什么时候发生?
很多的问题其实很简单,只是大家没有去研究而已,下面我们来聊聊几种垃圾回收方法!
GC四大算法
引用计数法(不重要)
每个对象有一个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次,则计数器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。
目前虚拟机基本都是采用可达性算法,从GC Roots 作为起点开始搜索,那么整个连通图中的对象边都是活对象,对于GC Roots 无法到达的对象变成了垃圾回收对象,随时可被GC回收。
复制算法(Copying)
简单来说,两片区域,一片空,一片放对象,每次gc交换位置
好处:没有内存碎片,快
坏处:浪费内存空间,极端情况出问题(100%存活)
标记清除(Mark-Sweep)
说明:老年代一般是由标记清除或者是标记清除与标记整理的混合实现
什么是标记清除?
回收时,对需要存活的对象进行标记;
回收不是绿色的对象
标记压缩(Mark-Compact)
在整理压缩阶段,不再对标记的对象作回收,而是通过所有存活对象都像一端移动,然后直接清除边界以外的内存。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉,如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
标记、整理算法不仅可以弥补 标记、清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价;
标记清除压缩(Mark-Sweep-Compact)
望文生义
小总结
内存效率:复制算法 > 标记清除算法 > 标记整理算法 (时间复杂度)
内存整齐度:复制算法 = 标记整理算法 > 标记清除算法
内存利用率:标记整理算法 = 标记清除算法 > 复制算法
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记整理算法相对来说更平滑一些 , 但是效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记清除多了一个整理内存的过程。