JVM补充篇
1.对象分配原则
1)对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC
2)大对象直接进入老年代(大对象是指需要大量连续内存空间的对象),这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)
3)长期存活的对象进入老年代,虚拟机为每个对象定义了一个年龄计数器,如果对象经过了一次Minor GC那么对象会进入Survivor区,之后没经过一次Minor GC,那么年龄会增加1,直到达到阈值对象进入老年代
4)空间分配担保。每次进行minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小,则进行一次Full GC,如果小于检查HandleFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC
对象的内存布局:
1.对象头
2.实例数据
3.对齐填充
对象头:
在32位系统下,对象头8字节,64位则是16字节[未开启压缩指针,开启后12字节]
markword很像网络协议报文头,划分为多个区间,并且会根据对象的状态复用自己的存储空间
为什么这么做: 省空间,对象需要存储的数据很多,32bit/64bit是不够的,它被设计成非固定的数据结构以便于在极小的空间存储更多的信息
假设当前的为32bit,在对象未被锁定情况下,25bit为存储对象的哈希码,4bit用于存储分代年龄,2bit用于存储锁标志,1bit固定为0
锁状态分为四种:无锁状态,偏向锁,轻量级锁,重量级锁
HotSpot 底层通过markOop实现Mark Word 具体实现位于 markOop.hpp文件
markOop中提供了大量方法用于查看当前对象头的状态,以及更新对象头的数据,为synchronize锁的实现提供了基础。
比如说我们知道synchronize锁的是对象而不是代码,而锁的状态保存在对象头,进而实现锁住对象
线程A在进入同步代码块前,先检查MarkWord中的线程ID是否与当前线程ID一致,如果一致(还是线程A获取锁对象),则无需使用CAS来加锁、解锁。
如果不一致,再检查是否为偏向锁,如果不是,则自旋等待锁释放。
如果是,再检查该线程是否存在(偏向锁不会主动释放锁),如果不在,则设置线程ID为线程A的ID,此时依然是偏向锁。
如果还在,则暂停该线程,同时将锁标志位设置为00即轻量级锁(将MarkWord复制到该线程的栈帧中并将MarkWord设置为栈帧中锁记录)。线程A自旋等待锁释放。
如果自旋次数到了该线程还没有释放锁,或者该线程还在执行,线程A还在自旋等待,这时又有一个线程B过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
如果该线程释放锁,则会唤醒所有阻塞线程,重新竞争锁。
实例数据
存放对象程序中各种类型的字段类型,不管是从父类中继承下来的还是在子类中定义的
分配策略:相同宽度的字段总是在一起,比如double和long
对齐填充:
这部分没有特殊的含义,仅仅起到占位符的作用满足JVM要求
由于HotSpot规定对象的大小必须是8的整数倍,,对象头刚好是整数倍,如果实例数据不是的话,就需要占位符对齐填充
对象的访问定位
java程序需要通过引用ref 数据来操作堆上面的对象,那么如何通过引用定位,访问到对象的具体位置
对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式
1.句柄访问对象
2.直接指针访问对象(Sun HotSpot使用这种对象)
句柄访问:
简单来说,java堆划出一块内存作为句柄池,引用中存储对的句柄地址,句柄中包含对的实例数据,类型数据的地址信息
优点:引用中存储的是稳定的句柄地址,在对象被移动 (垃圾收集时,移动对象是常态),只需要改变句柄中实例数据的指针不需要改动引用ref本身
直接指针;
与句柄访问不同的是,ref中直接存储的就是对象的实例数据,但是类型数据跟句柄访问的方式一样
优势:就是速度快,相比于句柄访问少了一次指针定位的开销时间 可能是出于java对象的访问时十分频繁的,平时我们常见的JVM HotSpot采用此种方式
2.解释内存中的栈(stack),堆(heap)和静态区(static area)的用法
通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间,而通过new关键字和构造器创建的对象放在堆空间,程序中的字面量(literal)如直接书写的100,“hello”和常量都是放在静态区中,栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,理论上整个内存没有被其他进行使用的空间甚至硬盘上的虚拟内存都可以被当成堆空间来使用
String str=new String(“hello”)
上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆中,而 “hello”这个字面量放在静态区中
3.Perm Space 中保存什么数据? 会引起OutOfMemory吗?
Perm Space 保存的是加载的class文件,会引起OutOfMemory,出现异常可以设置-XX:PermSize 的大小,jdk8以后,字符串常量不存放永久代,而是放在堆中,jdk8以后没有永久代概念,而是元空间替代,元空间不存在虚拟机中,而是使用本地内存
4.什么是类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口
- 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
- 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
双亲委派机制:类加载器收到类加载请求,自己不加载,向上委托给父类加载,父类加载不了,再自己加载。优势就是避免Java核心API篡改。
自定义类加载的意义:
加载特定路劲的class文件,
加载一个加密的网络class文件
热部署加载class文件
如何打破双亲委派模型:
不仅要继承ClassLoader 类,还要重写loadClass findClass发放
5.java对象创建过程
1)JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用,然后加载这类
2)为对象分配内存。一个办法是指针碰撞 一个是空闲列表 本地线程缓冲分配
3)将除对象头外的对象内存空间初始化0
4)对对象头进行必要的设置
6.如何判断一个对象是否应该被回收
判断对象是否存活一般有两种方式:
- 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
- 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
思考:哪些地方是GC ROOTS开始的地方?
虚拟机栈(栈帧中的本地变量表)中的引用的对象,就是平时所指的java对象,存放堆中
方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中
方法区中的常量引用的对象
本地方法栈中JNI(native方法)引用的对象
即使可达性算法中的不可达的对象,也不是一定要马上被回收,还有可能被抢救一下,
要真正宣告对象死亡需要经历两个过程:
1.可达性分析后么有发现引用链
2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意一下这个finalize只执行一次,这就出现一样的代码第一次自救成功第二次失败的情况,如果类重写finalize且还没有调用过,会将这个对象放到一个叫做F-queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果死循环的话可能是F-Queue 队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。
枚举根节点算法:
GC Roots 被虚拟机用来判断对象是否存活
可作为GC Roots 的节点主要是一些全局引用[如常量 静态属性],执行上下文[如帧中的本地变量表]中,那么如何在这么多全局变量和本地变量表中找到【枚举】根节点将是问题
可达性分析算需要考虑的问题:
1.如果方法区几百兆,一个个检查里面的引用,将耗费大量资源
2.在分析时,需要保证这个对象引用关系不再变化,否则结果将不准确,因此GC进行时需要停掉其他所有java执行线程 sun把这种行为 成为 Stop the word
即使是号称几乎不会停顿的CMS收集器,枚举根节点时也需要停掉线程
解决办法:实际上当系统停下来后,JVM不需要一个个检查引用,而是通过OopMap数据结构 [HotSpot的叫法]来标记对象引用
虚拟机先得知道哪些地方存放对象的引用,在类加载完时,HotSpot把2对象内什么偏移量什么类型的数据算出来,在jit编译过程中,也会在特定位置记录下栈和寄存器哪些位置是引用,这样GC在扫描时就可以知道这些信息
OopMap可以帮助HotSpot 快速且准确完成GC Roots枚举以及确定相关信息,但是也存在一个问题,可能导致引用关系变化
这个时候有个safepoint(安全点)的概念
HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入,GC时对一个java线程来说,它要么处在safepoint,要么不在safepoint。
safepoint 不能太少,否则GC等待的时候会很久
safepoint 不能太多,否则将增加运行GC的负担
安全点主要存放的位置
1.循环的末尾
2.方法临返回前/调用方法的call指令后
3.可能抛异常的位置
7 引用的分类
- 强引用:GC时不会被回收
- 软引用:描述有用但不是必须的对象,在发生内存溢出异常之前被回收
- 弱引用:描述有用但不是必须的对象,在下一次GC时被回收
- 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用PhantomReference实现虚引用,虚引用用来在GC时返回一个通知。
8调优命令
Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
- jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
- jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
- jmap,JVM Memory Map命令用于生成heap dump文件
- jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
- jstack,用于生成java虚拟机当前时刻的线程快照。
- jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。
9 调优工具
常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。
- jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控
- jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
- MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
- GChisto,一款专业分析gc日志的工具
10 jstack 是干什么的?jstat? 如果线上程序周期性地出现卡顿,你怀疑可能是GC导致
jstack 用来查询java进程的堆栈信息
jvisualvm监控内存泄露,跟踪垃圾回收,执行时内存,cpu分析,线程分析
11 你有没有遇到过OutOfMemory 问题? 你是怎么来处理问题的? 处理过程中有些方式
permgen space heap space错误
常见原因:
1)内存加载的数据量太大,一次性从数据库取太多数据
2)集合类中有对象的引用,使用后未清空,GC不能进行回收
3)代码中存在循环产生过多的重复对象
4)启动参数堆内存设置太小
12 jdk8以后Perm Space有哪些改动? MataSpace大小是无限的吗?
JDK 1.8后用元空间替代了 Perm Space;字符串常量存放到堆内存中。
MetaSpace大小默认没有限制,一般根据系统内存的大小。JVM会动态改变此值。
-XX:MetaspaceSize:分配给类元数据空间(以字节计)的初始大小(Oracle逻辑存储上的初始高水位,the initial high-water-mark)。此值为估计值,MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。
-XX:MaxMetaspaceSize:分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。
13 StackOverFlow 异常遇到过没?一般你会猜测有什么情况下被触发?
栈内存溢出,一般由栈内存的局部变量过爆了,导致内存溢出,出现递归方法,参数个数过多,递归过深,递归没有出口
14 JVM相关知识
java内存模型规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存,不同的线程之间无法直接访问对方工作内存中的变量,线程间的传递均需要自己的工作内存和主存之间进行数据同步进行。
指令重排序:
public class PossibleReordering { static int x = 0, y = 0; static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { Thread one = new Thread(new Runnable() { public void run() { a = 1; x = b; } }); Thread other = new Thread(new Runnable() { public void run() { b = 1; y = a; } }); one.start();other.start(); one.join();other.join(); System.out.println(“(” + x + “,” + y + “)”); }
运行结果可能为(1,0)、(0,1)或(1,1),也可能是(0,0)。因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待3。通过乱序执行的技术,处理器可以大大提高执行效率。而这就是指令重排。
内存屏障
内存屏障 也叫内存栅栏,是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题
- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
happen-before 原子
- 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
- volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
- happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
- 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
- 线程中断的happen-before原则 :对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
- 线程终结的happen-before原则: 线程中的所有操作都happen-before线程的终止检测。
- 对象创建的happen-before原则: 一个对象的初始化完成先于他的finalize方法调用
线程安全的本质:
程安全本质是由于多个线程对同一个堆内存中的Count变量操作的时候,每一个线程会在线程内部创建这个堆内存Count变量的副本,线程内所有的操作都是对这个Count副本进行操作。这时如果其他线程操作这个堆内存Count变量,改变了Count值对这个线程是不可见的。当前线程操作完Count变量将值从副本空间写到主内存(堆内存)的时候就会覆盖其他线程操作Count变量的结果,引发线程不安全问题。
15 JVM主要参数
-Xmx3550m: 最大堆大小为3550m。
-Xms3550m: 设置初始堆大小为3550m。
-Xmn2g: 设置年轻代大小为2g。
-Xss128k: 每个线程的堆栈大小为128k。
-XX:MaxPermSize: 设置持久代大小为16m
-XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。
-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。
-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20: 配置并行收集器的线程数
-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
-XX:+PrintGC 输出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails 输出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs
16 怎么打印出线程栈信息
- 输入jps,获得进程号。
- top -Hp pid 获取本进程中所有线程的CPU耗时性能
- jstack pid命令查看当前java进程的堆栈状态
- 或者 jstack -l > /tmp/output.txt 把堆栈信息打到一个txt文件。
- 可以使用fastthread 堆栈定位,fastthread.io/
17 GC是怎么判断对象是被标记的
通过枚举根节点的方式,通过JVM提供的一种oopMap的数据结构,简单来说就是不要再通过去遍历内存的东西,而是通过oopMap的数据结构去记录该记录的信息,比如说它可以不用去遍历整个栈,而是扫描栈上引用的信息并记录下来
总结:通过oopMap把栈上代表引用的位置全部记录下来,避免全栈扫描,加快枚举根节点的速度,除此之外还有一个极为重要的作用,可以帮助HotSpot实现准确GC
18 什么时候触发GC
GC算法区域满了或者将满了
minor GC(young GC):当年轻代中eden区分配满的时候触发[值得一提的是因为young GC后部分存活的对象会已到老年代(比如对象熬过15轮),所以过后old gen的占用量通常会变高]
full GC:
①手动调用System.gc()方法 [增加了full GC频率,不建议使用而是让jvm自己管理内存,可以设置-XX:+ DisableExplicitGC来禁止RMI调用System.gc]
②发现perm gen(如果存在永久代的话)需分配空间但已经没有足够空间
③老年代空间不足,比如说新生代的大对象大数组晋升到老年代就可能导致老年代空间不足。
④CMS GC时出现Promotion Faield[pf]
⑤统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间。
这个比较难理解,这是HotSpot为了避免由于新生代晋升到老年代导致老年代空间不足而触发的FUll GC。
比如程序第一次触发Minor GC后,有5m的对象晋升到老年代,姑且现在平均算5m,那么下次Minor GC发生时,先判断现在老年代剩余空间大小是否超过5m,如果小于5m,则HotSpot则会触发full GC(这点挺智能的)
Promotion Faield:minor GC时 survivor space放不下[满了或对象太大],对象只能放到老年代,而老年代也放不下会导致这个错误。 Concurrent Model Failure:cms时特有的错误,因为cms时垃圾清理和用户线程可以是并发执行的,如果在清理的过程中 可能原因: 1 cms触发太晚,可以把XX:CMSInitiatingOccupancyFraction调小[比如-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC)] 2 垃圾产生速度大于清理速度,可能是晋升阈值设置过小,Survivor空间小导致跑到老年代,eden区太小,存在大对象、数组对象等情况 3.空间碎片过多,可以开启空间碎片整理并合理设置周期时间
19 cms收集器是否会扫描年轻代
如果担保失败,会检查一个配置(HandlePromotionFailire),即是否允许担保失败。
如果允许:继续检查老年代最大可用可用的连续空间是否大于之前晋升的平均大小,比如说剩10m,之前每次都有9m左右的新生代到老年代,那么将尝试一次minor gc(大于的情况),这会比较冒险。
如果不允许,而且还小于的情况,则会触发full gc。【为了避免经常full GC 该参数建议打开】
这边为什么说是冒险是因为minor gc过后如果出现大对象,由于新生代采用复制算法,survivor无法容纳将跑到老年代,所以才会去计算之前的平均值作为一种担保的条件与老年代剩余空间比较,这就是分配担保。
这种担保是动态概率的手段,但是也有可能出现之前平均都比较低,突然有一次minor gc对象变得很多远高于以往的平均值,这个时候就会导致担保失败【Handle Promotion Failure】,这就只好再失败后再触发一次FULL GC,
21 stop the world 有没有办法避免