学习资料
0、为什么学习JVM?
为什么学习JVM?
- 1 、可以知道电脑是怎么识别我们编写的Java程序的,规避它在使用中的 Bug;
- 2、Java 虚拟机提供了许多配置参数,用于满足不同应用场景下,对程序性能的需求,你可以针对自己的应用,最优化匹配运行参数
重点学习内容:
1、需了解 内存模型各部分作用,保存哪些数据
2、类加载双亲委派加载机制,常用加载器分别加载哪种类型的类
3、GC分代回收的思想和依据 以及不同垃圾回收算法的回收思路和适合场景
4、性能调优常有JVM优化参数作用,参数调优的依据,常用的JVM分析工具能分析哪些问题以及使用方法
1、JVM基本原理
# 最左列是偏移;中间列是给虚拟机读的机器码;最右列是给人读的代码 0x00: b2 00 02 getstatic java.lang.System.out 0x03: 12 03 ldc "Hello, World!" 0x05: b6 00 04 invokevirtual java.io.PrintStream.println 0x08: b1 return
1、JVM的内存结构和内存分配?jmm
1、jvm的组成
- JVM包含两个子系统和两个组件:
- 两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
- Class loader(类加载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area;
- Execution engine(执行引擎):执行classes中的指令;
- Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口;
- Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
流程 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
1、java内存模型:
- Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:
按是否线程独占来划分
- 线程独占: 栈,本地方法栈,程序计数器
- 线程共享: 堆,方法区
按逻辑上划分
- 主要有三大块:①方法区(常量池是方法区的一部分)、②堆内存,③栈内存
1)方法区
- 线程共享,存储已被虚拟机加载的
- 类信息:包括类加载器的加载;
- 常量池:编译期生成的字面量【final常量】和符号引用 ;
- 静态变量、编译器编译后的静态变量数据;
- 即时编译器优化后的代码
- 内存回收目标主要是常量池的回收和对类型的卸载
- 1.7的永久代和1.8的元空间都是方法区的一种实现
2)堆内存
- 是JVM中最大的一块,由年轻代和老年代组成,属于线程共享
- 目的:堆中存放被创建的实例对象、数组
- 堆是GC管理的主要区域,根据对象的存活周期不同,JVM把对象进行分代管理,由垃圾回收器进行垃圾的回收管理;
- 当堆没有可用空间时,会抛出OOM异常,可以通过 -Xmx 和 -Xms 来控制
- 新生代(1/3堆空间) 老年代(2/3堆空间) 通过参数-XX:NewRatio来指定(JDK1.8时,只有新生代和老年代,没有持久代了)
3)栈内存(先进后出)
虚拟机栈:
- 线程私有,生命周期与线程相同
- 描述的是Java方法执行的内存模型:线程执行方法时,都会同时创建一个栈帧,用于存储局部变量表、操作数栈、动态连接(指向常量池)、方法返回地址等信息。
- 方法返回时执行出栈
栈帧的数据 | 详情 |
1、局部变量表 | 局部变量:方法参数和方法内部定义的变量,局部变量表所需的内存空间在编译期间完成分配;编译期可知的8大基本数据类型 对象or数组引用(作为参数) 返回地址类型 |
2、操作数栈 | 先入后出,初始为空,在方法的执行过程中加入数据:算术运算、方法参数、方法返回值 |
3、动态链接 | 每个栈帧都包含一个指针:指向运行时常量池中–所属方法的符号引用,在运行期间将符号引用转化为直接引用称为动态链接 |
4、方法返回地址 | 正常完成出口:是执行引擎遇到一个return的字节码指令,恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,然后调用程序计数器执行后一条指令;异常完成出口:方法执行过程中遇到了异常,异常没有在方法体内得到处理,返回地址由异常处理器确定 |
Action1:一个方法调用另一个方法,会创建很多栈帧吗? |
- 会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面
Action2:栈指向堆是什么意思?
- 就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址
Action3:递归的调用自己会创建很多栈帧吗?
- 递归的话也会创建多个栈帧,就是在栈中一直从上往下排下去
本地方法栈:
- 虚拟机执行Native方法时使用本地方法栈
- native关键字修饰的大部分源码都是C和C++的代码
4)程序计数器
- 概念:
1、线程私有,一块较小的内存空间;
2、当前线程所执行的字节码的行号指示器;
3、每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响;
4、是虚拟机中唯一没有规定 OutOfMemoryError 情况的区域。 - 使用原因:因为线程是不具备记忆功能,为了线程切换后能恢复到正确的执行位置
- 作用:程序计数器指向线程下一步执行的位置
为了线程切换后能恢复到正确的执行位置 (线程时程序运行最小的执行单位) - 执行本地方法时,程序计数器为空
Demo1:
线程A在看直播,突然,线程B来了一个视频电话,就会抢夺线程A的时间片,就会打断了线程A,线程A就会挂起;然后,视频电话结束,这时线程A究竟该干什么?
如果有线程计数器,此时线程A就会想起来在干什么。
2、Java内存分配:
- 基础数据类型直接在栈空间分配;
- 方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收;
- 引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量;
- 方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完后从栈空间回收;
- 局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC 回收;
- 方法调用时传入的实际参数,先在栈空间分配,在方法调用完成后从栈空间释放;
- 字符串常量在DATA 区域分配,this 在堆空间分配;
- 数组既在栈空间分配数组名称,又在堆空间分配数组实际的大小!
3、如何查看JVM的内存使用情况?
1)虚拟机性能检测工具 与jvm调优相结合 个推 (结合JVM启动参数常见配置,jstat等命令)
定位项目问题时,知识,经验是关键基础,数据是依据,工具是运用知识处理数据的手段。数据包括:运行日志/GC日志/线程快照//堆转储快照等
- jps:虚拟机进程状态工具
- jstat:虚拟机统计信息监控工具
jstat是用于监视虚拟机各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程的类加载、内存、垃圾回收、JIT编译等运行数据
选项:-gcutil 最重要的参数是GC时间(YGC和FGC)次数和收集时间(YGCT和FGCT) - jinfo:java配置信息工具
实时查看和调整虚拟机各项参数 - jmap:java内存映像工具
用于生成堆转储快照heapdump或dump,还可以查询finalize执行队列、java堆和永久代的详细信息 - jhat:虚拟机堆转储快照分析工具,让用户可以在浏览器上查看分析结果
使用VisualVM或专业的分析dump文件的eclipse memoryAnalyzer工具更强大 - jstack:java堆栈跟踪工具
用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致长时间等待
2)jdk的可视化工具(Jconsole和visualVM)
- Jconsole:java监视与管理控制台,在jdk目录下可以看见
内存监控:内存标签页相当于可视化的jstat命令,用于监控受收集器管理的虚拟机内存的变化趋势;
线程监控:线程标签页相当于可视化的jstack命令,遇到线程停顿时可以使用这个页签进行监控分析; - visualVM:多合一故障处理工具,可以做到
- 显示虚拟机进程以及进程的配置、环境信息(jps/jinfo)
- 监视应用程序的cpu、gc、堆、方法区以及线程的信息(jstat/jstack)
- dump以及分析堆转储快照(jmap/jhat)
- 程序运行性能分析,找出被调用最多、运行时间最长的方法
3)递归时,jvm栈里面有一个栈帧还是n个栈帧?
栈帧的作用:用于存储局部变量表、操作数栈、动态连接(指向常量池)、方法返回地址等信息,递归时会创建n个栈帧,当递归层数过多时,会导致虚拟机出现stackoverflowError的错误
4、为什么有两个幸存空间? 解决碎片问题**
- 降低gc频率,如果活着的对象全部进入老年代,老年代很快被填满,Full GC的频率大大增加
- 解决碎片问题
- Eden空间快满时young GC,频率得以降低
- 缺点:两个Survivor,10%的空间浪费、复制对象开销内存释放机制:1、如果对象在新生代gc之后任然存活,暂时进入幸存区;以后每过一次gc,对象年龄 +1,直到某个设定的值15或直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中;2、Eden: From Survivor : To Survivor空间大小设成8:1:1,对象总是在Eden区出生,From Survivor保存当前的幸存对象,To Survivor为空。一次gc发生后:
- 1、Eden区活着的对象+From Survivor存储的对象被复制到To Survivor;
- 2、清空 Eden 和 From Survivor ;
- 3、颠倒 From Survivor和To Survivor的逻辑关系:From变To,To变From。
JAVA内存模型
规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行。在 java 的内存模型中每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。 ***
5、JVM指令重排、内存屏蔽概念
JMM是定义程序中变量的访问规则,线程对于变量的操作只能在自己的工作内存中进行,而不能直接对主内存操作.由于指令重排序,读写的顺序会被打乱,因此JMM需要提供原子性,可见性,有序性保证。
1)什么是重排序?
- 在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的,两个语句的赋值操作的顺序被颠倒了。常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。可以增强程序执行的能力
指令重排demo 后续补充
public class DataHolder { private int a,b, c,d,f,g; // volatile 禁止指令重排 volatile long e; public void operateData() { a += 1; b += 1; c += 1; d += 1; e += 1; f += 1; g += 1; } private int counter; public void check() { if (g > e) { System.out.println("counter:" + counter++); } } } public class VolatileMain { public static void main(String[] args) { DataHolder dh = new DataHolder(); Thread thread = new Thread(() -> { while (true) { dh.operateData(); } }); thread.start(); Thread checker = new Thread(() -> { while (true) { dh.check(); } }); checker.start(); } }
2)as-if-serial语义
- 所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。
3)内存访问重排序与内存可见性
- 计算机系统中,为了尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能,可以使用volatile关键字来保证内存可见性。
4)内存访问重排序与Java内存模型
- Happens-before原则:前后两个操作不会被重排序且后者对前者的内存可见**(8大原则)
原则 | 特点 |
1、程序次序法则 | 单线程内,书写在前面的操作happen-before后面的操作 |
2、解锁先于锁定 | 同一个锁的unlock操作happen-before此锁的lock操作 |
3、volatile变量法则 | 对volatile变量的写入操作先于每一个后续对同一个变量的读写操作。 |
4、传递性原则 | 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作 |
5、线程start方法优先 | 同一个线程的start方法happen-before此线程的其它方法 |
6、线程中断 | 对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。 |
7、线程终结 | 线程中的所有操作都happen-before线程的终止检测。(通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行) |
8、对象创建 | 先初始化,后finalize;一个对象的初始化完成先于他的finalize方法调用。 |
5)内存屏障 Memory Barrier
- 是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题,Java编译器也会根据内存屏障的规则禁止重排序
内存屏障的类型 | 特点 |
1、LoadLoad屏障 | 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕 |
2、StoreStore屏障 | 对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 |
3、LoadStore屏障 | 对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 |
4、StoreLoad屏障 | 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。(在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。) |
- 为了保证final字段的特殊语义,也会在下面的语句加入内存屏障。
x.finalField = v; StoreStore; sharedRef = x;
6、heap和stack有什么区别?
从以下几个方面阐述:
1)申请方式:
- stack:由系统自动分配 在栈中
- heap:需要程序员自己申请,并指明大小
2)申请后系统的响应
- stack: 只要栈的剩余空间大于所申请空间,就会分配,否则栈溢出
- heap: 操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表。寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
3)申请大小的限制
- stack: 栈是向低地址扩展的数据结构,是一块连续的内存的区域 栈的大小固定
- heap: 堆是向高地址扩展的数据结构,是不连续的内存区域。堆的大小受限于计算机系统中有效的虚拟内存。
4)申请效率的比较:
- stack:由系统自动分配,速度较快。但程序员是无法控制的。
- heap: 由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
5)heap和stack中的存储内容
- stack: 在函数调用时,第一个进栈的是主函数中的下一条指令的地址,然后是函数的各个参数
- heap: 一般是在堆的头部用一个字节存放堆的大小。
6)数据结构层面的区别
- 这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权
- 栈实际上就是满足先进后出的性质的数学或数据结构
补充:
- 栈有一个很重要的特殊性,就是存在栈中的数据可以共享
- 字面值的引用与类对象的引用不同,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变
- String str = “abc” 的内部工作
1、先定义一个名为str的对 String类的对象引用变量:String str
2、在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o 的字符串值指向这个地址
3、将str指向对象o的地址
结论: 我们在使用诸如 String str = “abc”;的格式定义类,对象可能并没有被创建!唯一可以肯定的是,指向 String类的引用被创建了
7、对象的创建/布局/访问,以hotspot和java堆为例说明 周志明
1)对象的创建
- 检查new指令的参数是否能在常量池中定位到一个类的符号引用,按如下步骤执行类加载:
规划可用空间: 使用指针碰撞或空闲列表方法,选择哪种分配方式由java堆是否规整决定,在使用Serial、ParNew等带标记-整理过程的收集器时,系统采用指针碰撞法,在使用CMS这种基于标记-清除算法的收集器时采用空闲列表。
并发情况下线程安全的解决方法:CAS配上失败重试保证更新操作的原子性,第二种:本地线程分配缓冲TLAB,内存分配完后,jvm将分配的内存空间初始化为零,以保证不赋初始值就可直接使用。
2)对象的内存布局(对象头/示例数据,对齐填充)
- 对象头信息(对对象进行必要的设置)
1、Mark Word 6个: hash码、GC年龄、锁状态、持有的锁、偏向锁线程ID、偏向锁时间戳
2、类型指针:判断对象属于哪个类的实例,指向所属类的指针 - 实例数据 存储真正有效数据(对象按程序员的意愿进行实例化)
1、字段的分配策略:相同宽度的字段总是被分配到一起,便于之后取数据;
2、父类定义的变量会出现在子类前面 - 对齐填充 (占位符的作用,非必须)
经过上述步骤:从虚拟机的角度,一个新的对象已经产生
3)对象的访问定位:通过栈上的引用数据来操作堆中具体对象
- 使用句柄地址:二次定位
java堆中将会划出内存作为句柄池,reference存储对象的句柄地址,通过句柄再查找对象地址
优点:对象被移动时,只改变实例数据指针(垃圾回收时) - 直接指针访问对象,reference中存储的就是对象地址
优点:只进行了一次指针定位,节省了时间,而这也是HotSpot采用的实现方式;由于对象访问比较频繁,这个较好
2、Java程序是否会内存溢出,内存泄露情况发生?针对hotspot虚拟机。个推
1、什么是内存溢出?
- 除程序计数器外,虚拟机内存的其他几个运行时区域都有发生OOM异常的可能
- 研究内存溢出的意义:当实际工作中遇到内存溢出异常时,能根据异常的信息快速判断是哪个区域的内存溢出,什么样的代码可能导致这些区域内存溢出,以及如何处理
2、内存溢出分三种情况(堆/栈/方法区)
1)java堆空间
- 当对象创建太多超出了最大堆容量限制,且GCroots到对象之间路径可达,就会发生内存溢出异常(循环或递归中大量的new对象)
- 一般的异常信息:java.lang.OutOfMemoryError:Java heap space
Java堆溢出解决方案
- 通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析 ,先分清是内存泄漏还是内存溢出。内存溢出:实例过多、数组过大、大量的new对象;内存泄露:没被引用的对象(垃圾)过多
- 若是内存泄漏: 通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象与GC Roots相关联路径。
- 若是内存溢出:(对象确实还活着) 检查虚拟机的参数(-Xmx最大堆与-Xms最小堆)的设置是否适当,代码上检查是否某些对象生命周期过长。
2)虚拟机栈和本地方法栈溢出原因:(hotspot不区分两者,用-xss表示) 阿里
- 递归太深、死循环导致栈帧创建过多(递归或无限递归) stackoverflow异常
- 线程请求的栈深度大于虚拟机允许的最大深度,抛出StackOverflowError异常
- 如果 Java 虚拟机栈可以动态扩展, 并且扩展的动作已经尝试过,在扩展栈无法申请到足够的内存(函数体内的数组过大) 则抛出OutOfMemoryError异常
Java堆溢出解决方案(减少内存的手段):多线程下,栈的大小越大,可分配的线程数就越少,通过减少最大堆和最大栈容量换取更多的线程
3)方法区和运行时常量池溢出
- 方法区作用:存储已被JVM加载的类信息、常量池、静态变量等。编译器编译后的代码,线程共享(1.7之前) 而是用 jdk1.7 之后,开始逐步去永久代, 就不会产生内存溢出。
- 溢出原因:CGlib字节码增强和动态语言填满了方法区(静态变量大、类加载过多)
- 异常信息:java.lang.OutOfMemoryError:PermGen space (jdk1.6)
- 解决方法:-XX:PermSize和-XX:MaxPermSize 设置方法区的大小 (jdk1.6)
4)本机直接内存溢出(-XX来指定)
- java.lang.OutOfMemoryError抛异常时没有向操作系统申请分配资源,直接内存导致的内存溢出在dump文件中看不到明显异常
- -XX:MaxDirectMemorySize指定,默认和Java堆最大值相同
3、在开发中遇到过内存溢出么?原因有哪些?解决方法有哪些? 个推
项目中引起内存溢出的原因有很多种,常见的有以下几种:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
举例:在后台管理商品时,要把商品的信息导入到EasyUI控件中,刚开始没有设置mysql的分页,数据量太大,从而导致内存溢出 *** - 集合类(缓存)中有对对象的强引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 使用的第三方软件中的BUG;****
- 启动参数内存值设定的过小;
解决方案:
第一步,修改JVM启动参数(-Xms,-Xmx),直接增加内存。
第二步,检查错误日志,查看“OOM”错误前是否有其它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出,因此对于数据库查询尽量采用分页的方式查询。检查代码中是否有死循环或递归调用,检查是否有大循环重复产生新对象实体,检查List、MAP等集合对象是否有使用完后,未清除的问题
第四步,使用内存查看工具 (如Memory Analyzer)动态查看内存使用情况。
4、内存泄漏的问题:
1)概念:一个对象已经不需要再使用,本该被回收,另外一个正在使用的对象持有它的引用从而导致它不能被回收。停留在堆内存中,这就产生了内存泄漏
2)例如:对象连接资源未关闭造成的内存泄漏,集合容器中的内存泄露,出栈时,栈中对象不会被当作垃圾回收,通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。
3)如何避免:写代码时:保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期
例子1:静态集合类 使用set,vector,hashmap等集合类时,当这些类被定义为静态时,由于他们的生命周期和应用程序一样长,这时候就可能发生内存泄漏。
class static test{ private static vector v = new vector(10); public void init(){ for object obj = new object(); v.add(obj); obj = null; } }
例子2:关于匿名内部类,非静态内部类会造成内存泄露的隐患。
Thread 是一个匿名内部类。
public void run() { while (true) { SystemClock.sleep(1000); }
例子3:
监听器listener 物理连接 如数据库连接和网络连接 除非显式地关闭了连接,否则不会自动被GC回收。
单例模式:对象初始化后在整个jvm生命周期中存在,持有的外部对象的引用,那么这个外部对象就不能被回收,导致内存泄漏。
5、你有哪些手段来排查OOM的问题? 美团
- 增加两个参数
-XX:+HeapDumpOnOutOfMemoryError
-
XX:HeapDumpPath=/tmp/heapdump.hprof
- 作用:当 OOM 发生时自动 dump 堆内存信息到指定目录。
- 同时 jstat 查看监控 JVM 的内存和 GC 情况, 先观察问题大概出在什么区域。
- 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 Guava cache 做缓存未清理, 时间长了就会内存溢出, 可以把改为弱引用。
3、JVM相关面试题
3.1、java编译原理:“Java是解释执行”,这句话正确吗?(效率优化LJIT,AOT)
对于“Java是解释执行”这句话,这个说法不太准确。Java 虚拟机需要将字节码翻译成机器
码。如下图所示:
方式1:解释执行,即逐条将字节码翻译成机器码并执行;
方式2:提供了JIT(Just-In-Time)编译器,也就是通常所说的动态编译器。JIT:能够在运行时将热点代码(有些方法和代码块高频调用,提前将字节码编译成机器码,之后遇到同类代码直接可以执行)编译成机器码。
- 对于小部分热点代码,我们可以将其编译成机器码,以达到理想的运行速度。
- 在计算资源充足的情况下,字节码的解释执行和即时编译可同时进行。编译完成后的机器码会在下次调用该方法时启用,以替换原本的解释执行。
- AOT编译器:java9提供的直接将所有代码编译成机器码执行
3.2、Java与c/c++最大的不同点
c/c++编程时面向操作系统的,需要开发者关心不同操作系统之间的差异性;而java平台通过虚拟机屏蔽了操作系统的底层细节,不需关注操作系统差异。
3.3、如何用Java分配一段连续的1G的内存空间?需要注意些什么?
ByteBuffer.allocateDirect(1024 * 1024 * 1024);
- -Xms指定,默认是物理内存的1/64,
- JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4
- 一般情况下,一个JVM的内存堆大小,最大不要超过1024M
补充知识点:TLAB
TLAB(Thread Local Allocation Buffer,对应虚拟机参数 -XX:+UseTLAB
,默认开启)
- 每个线程可以向 Java 虚拟机申请一段连续的内存,比如 2048 字节,作为线程私有的 TLAB
- 这个操作需要加锁,线程需要维护两个指针(实际上可能更多,但重要也就两个),一个指向 TLAB 中空余内存的起始位置,一个则指向 TLAB 末尾
3.4、Java有自己的内存回收机制,但为什么还存在内存泄露的问题呢?(经典问题)
内存泄漏的问题:一个对象已经不需要再使用,本该被回收,另外一个正在使用的对象持有它的引用从而导致它不能被回收。停留在堆内存中,这就产生了内存泄漏。
原因:1、对象连接资源未关闭造成的内存泄漏,集合容器中的内存泄露,代码上的问题
通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。
写代码时:保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期
有些人你永远不用爱,有哪个谈爱发了财;有些手机你永远不必等,您拨的用户比你更郁闷