文章目录
- 前言
- 一、从面试题出发
- 二、JVM体系结构与组成成分
- 1. jvm 位置
- 2. 体系结构
- 3. 上下四部分
- 3.1 类装载器ClassLoader
- 3.2 Execution Engine 执行引擎
- 3.3 Native Interface本地接口
- 4. 运行时数据区
- 4.1 本地方法栈:Native Method Stack
- 4.2. 程序计数器:program counter register
- 4.3. java 栈:java stack
- 4.4 方法区:Method Area
- 4.5 堆:heap(java 7之前)
- 4.6 堆:heap(java 8)
- 三、堆内存 调优参数
- 1. 查看:堆内存初始化信息
- 2. VM 设置参数
- 2.1 设置:堆内存初始信息
- 2.2 设置:新生代中三个区、新生代与老年代的各个比例
- 2.3 设置:栈的大小
- 2.4 设置:方法区内存
- 2.5 设置:直接内存 配置
- 2.3 Java性能分析神器:MAT 和 Jprofiler
- 四、垃圾回收
- 1. 引用计数法
- 2. 标记清除法
- 3. 复制算法(目前新生代使用)重点
- 4. 标记压缩法(目前老生代使用)重点
- 5. 分代算法
- 6. 分区算法
- 五. 再谈 堆内存参数 设置
- 1. 测试在Eden区的对象
- 2. 说一下垃圾收集器
- 3. 设置:对象经过多少GC的次数进入老年代,默认15
- 4. 设置:进入老年代的对象大小
- 4.1 注意TLAB区域
前言
为面试而准备,学习java虚拟机。
主要是学习JDK7,JDK8与 7 仅有少量不同之处。
此文章是看了视频和很多博客才写出来的。
一、从面试题出发
- java虚拟机的内存体系结构。
- 请谈谈你对JVM的理解?java8版有什么了解?
- 谈谈JVM中你对ClassLoader类加载器的认识?
- 什么是OOM?写代码使得分别出现StackOverflowError和OutOfMemoryError
- JVM的常用参数调优你了解吗?
- 内存快照抓取和MAT分析hprof文件干过吗?
二、JVM体系结构与组成成分
1. jvm 位置
JVM是运行在操作系统之上的,它与硬件没有直接的交互
2. 体系结构
根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。
如图所示:
最下面:虚拟机的最底层,是 执行引擎
和本地方法接口
,在往下,就是 本地方法调用的C++。
最上面:虚拟机的最上层,是 类装载器子系统
加载 .class 的字节码文件
到 java虚拟机 运行内存中。
中间:虚拟机的 运行时数据区,包括5个:
-
堆、方法区
属于线程共享。 -
java 栈、本地方法栈、程序计数器
属于线程私有。
下面就从图中的 8部分开始叙述:
3. 上下四部分
3.1 类装载器ClassLoader
负责加载 class 字节码
文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
ClassLoader 加载 类的过程图:
虚拟机自带的加载器:
- 启动类加载器(Bootstrap)C++ (爷爷辈)
- 扩展类加载器(Extension)Java (爸爸辈)
- 应用程序类加载器(App)Java,也叫系统类加载器,加载当前应用的classpath的所有类 (儿子辈)
用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式
示意图:
3.2 Execution Engine 执行引擎
Execution Engine执行引擎
负责解释命令,提交 操作系统
执行。
3.3 Native Interface本地接口
- Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。
- 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合 C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为Native的代码,它的具体做法是Native Method Stack中登记Native方法,在Execution Engine 执行时加载Native libraries。
- 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用WebService等等,不多做介绍。
-
Native Interface本地方法接口
就在数据运行区
中的 本地方法栈
中。
4. 运行时数据区
4.1 本地方法栈:Native Method Stack
它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。
4.2. 程序计数器:program counter register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来**存储指向下一条指令的地址,也即将要执行的指令代码**),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
4.3. java 栈:java stack
-
栈也叫栈内存
,主管Java程序的运行,是在线程创建时创建
,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题。 - 只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。基本类型的变量、实例方法、引用类型变量都是在函数的栈内存中分配。
出现的异常:
Exception in thread “main” java.lang.StackOverflowError
4.4 方法区:Method Area
-
方法区是线程共享
的。 - 通常用来保存装载的类的元结构信息。
比如:运行时常量池
+静态变量
+常量
+字段
+方法字节码
+ 在类/实例/接口初始化用到的特殊方法
等。 - 通常和
永久区
关联在一起(Java7之前),但具体的跟JVM的实现和版本有关。
4.5 堆:heap(java 7之前)
-
堆是线程共享的。
- 一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。
- 类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
堆内存逻辑
上分为三部分:新生+养老+永久
4. 新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区
(Eden space)和幸存者区
(Survivor pace) 。
所有的类都是在伊甸区被new出来的
。
幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。
- 过程,新生区->养老区->永久区
当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区.若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx
来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。 - 逻辑上,堆包括新生区、养老区、永久区。
实际上,堆只包括新生区、养老区。
堆的示意图:
4.6 堆:heap(java 8)
JDK 1.8之后将最初的永久代取消
了,由元空间取代。
堆的示意图:
三、堆内存 调优参数
1. 查看:堆内存初始化信息
查看本机 分配给 jvm虚拟机内存的 初始值大小
、可用大小
、最大值大小
。
代码测试: 以下测试都在本类中测试
本机内存 为 8G。
测试可看出:
最大内存 :18014 约为 8G
总内存:12364 约为 8G
2. VM 设置参数
2.1 设置:堆内存初始信息
在VM 中设置参数: 后在进行测试,后在后面打印出 详细的 GC 日志
VM参数:
vm参数详解:
java代码测试:
java 8:
java 7:
2.2 设置:新生代中三个区、新生代与老年代的各个比例
模拟一个 内存溢出
的情况。来查看GC信息。(上面是因为内存够用,GC回收正常,所以没打印)
IDEA 设置参数位置:
VM 配置参数:
Java测试代码:
- 第一次配置的日志:
分析:
打印出GC信息。
并曝出内存溢出异常,java heap space。
在堆内存信息中可看出:
eden/from = eden/to 约等于 2:1
eden + from + to 约等于 5M
- 第二次配置的日志:
- 分析:
与第一次配置相同,仅改变 初始大小。内存设未20,可以满足代码所需的10M,这里不报错。 - 第三次配置的日志:
- 分析:
老年代与新生代的比值 和配置的相同,为 2: 1
2.3 设置:栈的大小
一般不配置,Java 给优化好了,了解即可
java虚拟机提供了参数 -Xss 来指定线程的最大栈空间,整个参数也直接决定了函数可调用的最大深度:
参数详解:
代码测试:
日志分析:
2.4 设置:方法区内存
一般不配置,Java 给优化好了,了解即可
和java 堆一样,方法区 是一块所有线程共享的内存区域,它用于保存系统的类信息,方法区(永久区)可以保存多少信息可以对其进行配置,在默认情况下,-XX:MaxPersSize 为 64MB,如果系统运行时生产大量的类,就需要设置一个相对合适的方法区,以避免永久区内存溢出的问题。
参数详解
2.5 设置:直接内存 配置
一般不配置,Java 给优化好了,了解即可
直接内存也是java程序中非常重要的组成部门,特别是广泛用在NIO中,直接内存跳过了java堆,使java程序可以直接访问原生堆空间,因此在一定程度上加快了内存空间的访问速度,但是说直接内存一定就可以提高内存访问速度也不见得,具体情况具体分析。
相关配置参数:-XX:MaxDirectMemorySize。如果不设置,默认值为最大堆空间,即-Xmx。直接内存使用达到上限时,就会触发垃圾回收,如果不能有效的释放空间,也会引起系统的OOM。
参数详解
2.3 Java性能分析神器:MAT 和 Jprofiler
分析java 内存性能需要工具,eclipse 是 MAT(Eclipse Memory Analyzer Tool)
Idea 就是 Jprofiler。
现在经常用的工具是 IDEA,这里我也没学过 MAT,我就学习Jprofiler了。
四、垃圾回收
- 垃圾回收(Garbage Collection),简称 GC。
- GC中的垃圾,是特指存于内存中、不会再被使用的对象,而 回收就是相当于把垃圾“倒掉”。
- 这里的内存就是指
堆内存
(一般内存都是指堆内存),对象被new出来时,一般存放在堆中
。 - 垃圾回收有很多算法:如引用计数法、标记压缩法、复制算法、分代、分区的思想。
1. 引用计数法
引用计数法
:这是个比较古老而经典的垃圾收集算法,其核心就是在对象被其他所引用时计数器加1,而当引用失效时则减1,但是这种方式有非常严重的问题:无法处理循环引用的情况、还有就是每次进行加减操作比较浪费系统性能。
2. 标记清除法
标记清除法
:就是分为 标记 和 清除 两个阶段进行处理内存中的对象,当然这种方式也有非常大的弊端,就是 空间碎片问题,垃圾回收后的空间不是连续的,不连续的内存空间的工作效率要低于连续的内存空间。
3. 复制算法(目前新生代使用)重点
复制算法
:其核心思想就是将内存空间分为两块,,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存留对象复制到未被使用的内存块中去,之后去清除之; 前正在使用的内存块中所有的对象,反复去交换俩个内存的角色,完成垃圾收集。(java中新生代的from和to空间就是使用这个算法)
4. 标记压缩法(目前老生代使用)重点
标记压缩法
:标记压缩法 在标记清除法基础之上做了优化,把存活的对象压缩到内存一端,而后进行垃圾清理。(java中老年代使用的就是标 记压缩法)
考虑一个问题:为什么新生代和老年代使用不同的算法?
因为新生代的对象死得快,被销毁的快,而经过15次的销毁后还存留下来的就放在 老年代。
5. 分代算法
分代算法
:就是根据对象的特点把内存分成N块,而后根据每个内存的特点使用不同的算法。对于新生代和老年代来说,新生代回收频率很高,但是每次回收耗时都很短,而老年代回收频率较低,但是耗时会相对较长,所以应该尽量减少老年代的GC.
6. 分区算法
分区算法
:其主要就是 将整个内存分为N多个小的独立空间,每个小空间都可以独立使用,这样细粒度的控制一次回收都少个小空间和那些个小空间,而不是对整个空间进行GC,从而提升性能,并减少GC的停顿时间。
垃圾回收器的任务
是 识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以高效的执行,大部分情况下,会要求系统进入一个停顿的状态。停顿的目的是终止所有应用线程,只有这样系统才不会有新的垃圾产生,同时停顿 保证了系统状态在某-一个瞬间的一致性,也有益于更好低标记垃圾对象。因此在垃圾回收时,都会产生应用程序的停顿。
五. 再谈 堆内存参数 设置
更细粒度的设置 堆内存参数,
主要针对 堆内存中的 新生区和 老年区的参数设置。
1. 测试在Eden区的对象
测试代码:
日志分析:
2. 说一下垃圾收集器
默认使用的是 PSYoungGen 收集器,这是JDK自带的。
上面示例有时候设置参数为:-XX:+UseSerialGC,这是使用 串行收集器。
3. 设置:对象经过多少GC的次数进入老年代,默认15
一般而言对象首次创建会被放置在新生代的Eden区,如果没有GC介入,则对象不会离开Eden区,那么Eden区的对象如何进入老年代?一般来讲,只要对象的年龄达到一定的大小,就会自动离开年轻代进入老年代,对象年龄是由对象经历数次GC决定的,在新生代每次GC之后如果对象没有被回收则年里加1,虚拟机提供了一个参数来控制新生代对象的最大年龄,当超过这个年龄范围就会晋升老年代。
参数分析:
测试代码:
日志分析:
事与愿违啊!!!
4. 设置:进入老年代的对象大小
另外,大对象(新生代Eden区无法装入时,也会直接进入老年代)。JVM里有个参数可以设置对象的大小超过在指定的大小之后,直接晋升老年代。
可以指定进入老年代的对象大小,但是要注意TLAB区 域优先 分配空间。
代码测试、参数分析:
日志分析:
第一次配置参数:
**第二次配置参数: 比第一次配置 多了一个配置::-XX:-UseTLAB 就是禁用 TLAB 区域 **
加了-XX:-UseTLAB 参数后,老年区的使用率变大。
4.1 注意TLAB区域
- TLAB全称是Thread Local Allocation Buffer即线程本地分配缓存,从名字上看是一一个线程专用的内存分配区域,是为了加速对象分配而生的。每- -个线程都会产生-一个TLAB,该线程独享的工作区域,java虚拟机使用这种
- TLAB区来避免多线程冲突问题,提高了对象分配的效率。TLAB空间- -般不会太大,当大对象无法在TLAB分配时,则会直接分配到堆上。
代码测试、参数分析:
日志分析: