JVM 的掌握是每个高级开发工程师必备的技能,我未能在找工作前好好研究,结果可想而知。后来没办法只能花了几天,将《深入理解Java虚拟机》这本书给啃了。
下面是一些简单的总结。
JVM参考
一、Java内存模式
1、内存模型图
-
Java 堆:存放对象实例和数组,线程共享
-
jdk 1.7 时将字符串常量池从方法区移出,存放在堆中
-
Java堆是垃圾收集器管理的主要区域,也被人称为**“GC堆”**
-
细分为:新生代、老年代
- 年轻代内存:1个Eden区和2个Survivor区(分别叫from和to);按照8:1:1的比例来分配;
-
可扩展,通过-Xmx和-Xms控制,在堆无法再扩展时抛出OOM异常(OutOfMemoryError)
-
-
方法区:存储已被虚拟机加载的类信息、常量(编译期字面量和符号引用)、静态变量,即时编译器编译后的代码等信息;
- 线程共享,永久代
- 运行时常量池:方法区的一部分,用于存储编译期生成的各种字面量和符号引用,这部分内容在类加载后进入运行时常量
- 是动态性的,常量在非编译期也可以放入,比如:String类的intern()方法
- 无法申请到足够内存,就会抛出OOM异常
- JDK 1.7后 字符串常量池移出永久代,存放在堆中了
- JDK 1.8 取消方法区,改为元空间,存储在本地内存(还包含一个直接内存)
-
虚拟机栈:调用一个方法至返回,对应一个栈帧,线程私有,生命周期与线程相同
-
每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
-
存储局部变量表、操作数栈、方法出口等信息
-
局部变量表:
- 存放基本数据类型、对象引用类型和 returnAddress类型
- long和double数据占两个局部变量空间,其他占1个
- 编译期间分配,进入方法时分配完成,方法运行期间局部变量表大小不会改变
-
线程请求的栈深度大于虚拟机允许的深度,将抛出SOF 异常(StackOverflowError)
-
如果扩展时没有申请到足够内存,就会抛出OOM异常(OutOfMemoryError)
-
-
本地方法栈:调用native方法(非java代码)
- 与虚拟机栈相似,也会抛出SOF和OOM异常
-
程序计数器:当前线程所执行的字节码的行号,线程私有,
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
扩展:
**常量池:**在方法区中
除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:类和接口的全限定名;、字段的名称和描述符;、方法和名称和描述符。
新生代内存不够用时候发生MGC也叫YGC,JVM内存不够的时候发生FGC
内存分配策略:
- 对象在Eden区申请内存,内存不足,进行GC,任然不足
- Eden区将存活的对象放入survivor区(如果survivor区用的是复制算法,从from复制到to)
- Survivor中只有经历16 次Minor GC还能存活的对象,才会被送到老年代。
- 年轻代空间回收称作 **Minor GC **,老年代的垃圾回收称作 Full GC
注意:
Class文件: 除了有类的版本、字段、方法、接口等描述信息外,还有一个常量池(存放编译期生成的各种字面量和符号引用),这部分内容将在类加载后进入方法区的运行时常量池存放。
“永久代”,
- 与方法区并不等价
- 是HotSpot虚拟机设计团队选择把GC收集扩展至方法区或者说使用永久代实现方法区而已
- 其他虚拟机不存在永久代的概念
- 相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。
扩展:
直接内存:
- JDK 1.4 新加入的NIO(基于通道channel与缓冲区buffer的I/O通讯方式)使用native函数库直接分配的堆外内存,通过一个存储在java堆中的DirectByBuffer对象作为这块内存的引用进行操作
- 和元空间一起在本地内存;
- 也会抛出OOM异常
String类的intern()方法:
作用:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象引用;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
JDK 1.8 内存有所变化:主要体现在方法区
在JDK1.8中,HotSpot已经没有“PermGen space”这个区间了,取而代之是Metaspace(元空间)
Metaspace(元空间)
- 在JDK1.8中,永久代已经不存在,存储的类信息、编译后的代码数据等已经移动到了MetaSpace(元空间)中,
- 元空间直接占用的本地内存(NativeMemory)。
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
注意:
JDK1.8中,堆除了存放对象实例和数组外,还将常量池从永久代中分离到堆中
二、类的加载
主要关注点:
- 什么是类的加载
- 类的生命周期
- 类加载器
- 双亲委派模型
类的加载机制:
虚拟机将描述类的数据从.class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
加载.class文件的方式:
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件
1、类的生命周期
加载、验证、准备、解析、初始化、使用和卸载5个阶段;验证准备解析3部分统称连接;
1.1 加载:获取二进制字节流,类的静态信息存储至方法区,在堆区生成一个class对象
此过程完成3件事
- 通过全限定名获取此类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成此类的Class对象,作为方法区这个类的各种数据访问入口
1.2 验证:校验二进制字节流是否符合JVM标准
- 大致分 4 阶段:
- 文件格式验证:是否符合class文件格式规范
- 元数据验证:语义分析验证,保证其符合Java语言规范的要求;例如:是否继承final修饰的类
- 字节码验证:通过数据流和控制流分析确定程序语义是否合法
- 符号引用验证:对类自身以外的信息验证
1.3 准备:类的静态变量分配内存,并设置类变量的初始值(数据类型默认零值,final修饰过的直接赋值)
1.4 解析:把类中的符号引用转换为直接引用
-
主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符 7 类符号引用
-
符号引用:以一组符号(任一形式字面值,)描述目标的引用
-
直接引用:句柄或直接指针描述目标引用
1.5 初始化:静态块,非final 静态变量赋值,(只有static修饰的才能被初始化);
- 两种初始值途径:
- 声明类变量是指定初始值
- 使用静态代码块为类变量指定初始值
- 此阶段就是执行类构造器()的方法过程;赋真正的值;此过程是线程同步的,同一个类加载器只会初始化一次
1.6 **使用:**new出对象程序中使用
1.7 **卸载:**执行垃圾回收
结束生命周期方式:
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
注意:
数组类加载:
数组类本身不通过类加载器创建,由Java虚拟机直接创建,但数组类的元素类型由加载创建(基本数据类型除外)
如果类的字段属性同时被final和static修饰,那么在准备阶段变量被初始化为指定的值。
-
假设上面的类变量value被定义为: public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。这种情况。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中
扩展:
():是编译器自动收集类中的所有类变量的赋值动作和静态块中的语句合并产生。这也是为什么静态块最先输出的原因;
JVM初始化步骤:
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
2、类初始化的时机
- 使用new 实例化对象时
- 调用静态变量时(常量除外)、静态方法
- 通过反射调用
- 初始化一个类如果父类没有初始化,先触发父类的初始化
- 执行main方法的启动类
注意:
- 子类调用父类的静态变量,子类不会被初始化。只有父类被初始化。。对于静态字段,只有直接定义这个字段的类才会被初始化.
- 通过数组定义来引用类,不会触发类的初始化
- 访问类的常量,不会初始化类
类加载分:
- 静态加载:
- 编译时刻加载的类是静态加载类
- new关键字来实例对象,编译时执行
- 动态加载:
- 运行时刻加载的类是动态加载类。
- 反射方式加载就是动态加载
扩展:
初始化有且只有以上五种,其他方式都为被动调用不会初始化
被动调用:
- 通过子类引用父类的静态字段,不会导致子类初始化,只有定义了这个静态字段的类才会被初始化
- 通过数组定义的引用类,不会触发此类的初始化
- 常量在编译期会存入到调用类的常量池,本质上不会直接引用到定义常量的类,因此也不会触发定义常量的类的初始化
接口和类的的初始化区别: 只需要真正使用到的父接口初始化,不需要所有父接口都初始化
思考:
1、JVM初始化步骤 ?
2、类初始化时机 ?
3、哪几种情况下,Java虚拟机将结束生命周期?
3、类加载器
类加载器定义:
将类加载阶段中的“通过一个类的全限定名获取此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便于应用程序自己决定如何获取所需要的类,实现这个动作的代码模块称为类加载器。
数组类本身不通过类加载器创建,由Java虚拟机直接创建,但数组类的元素类型由加载创建(基本数据类型除外)
1. 类与类加载器区别
- 每个类加载器都独立的类名称空间,一个类可以由不同的加载器加载,
- 两个类是否相等,取决于是否是同一个类加载器加载,
2. 类加载器种类
- 启动类加载器:负责加载存放在
JDK\jre\lib
下 ,像rt.jar的加载 - 扩展类加载器:
JDK\jre\lib\ext
类的加载 - 应用程序类加载器:它负责加载用户类路径(ClassPath)所指定的类
- 自定义加载器
3. JVM类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
4. 双亲委派模型
加载器之间相互配合进行加载的层次关系,其原理是:一般不会自己尝试加载,而请求委派给父类加载,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器无法加载,才自己尝试加载
双亲委派模型优点:
- 避免同一个类被多次加载;
- 每个加载器只能加载自己范围内的类
- 保证java程序的稳定运行
使用双亲委派模型原因j
- 例如 java.lang.Object,无论哪一个类加载器要加载该类,最终都是委托给处于顶端的启动类加载器,因此object在程序的各种类加载器环境中都是同一个类.
- 如果没有使用双亲委派模型,那么假如用户自定义了一个称为java.lang.Object的类,并放在classPath中,那么系统将会出现多个不同的Object类,则java类型体系中最基础的行为都无法保证.
破坏双亲委派模型:
3次被破坏,第三次由用户对程序动态性的追求而导致的,如:代码热替换、模块热部署等
扩展:
类加载有三种方式:
- 命令行启动应用时候由JVM初始化加载
- 通过Class.forName()方法动态加载
- 通过ClassLoader.loadClass()方法动态加载
用户自定义类加载器:
-
如果要符合双亲委派规范,则重写findClass方法(用户自定义类加载逻辑);
-
要破坏的话,重写loadClass方法(双亲委派的具体逻辑实现)。
反射中Class.forName()和ClassLoader.loadClass()的区别
-
Class.forName:将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。
-
classloader只将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
-
Class.forName(name,initialize,loader)带参数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象。
启动类加载器:
- C++实现的,是虚拟机的一部分,其他的加载器都是java实现的
热部署
-
是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为
-
热部署步骤:
-
1、销毁自定义classloader(被该加载器加载的class也会自动卸载);
-
2、更新class
-
3、使用新的ClassLoader去加载class
Class被卸载
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
- 加载该类的ClassLoader已经被GC。
- 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
思考:
可以不可以自己写个String类
答案:不可以,因为 根据类加载的双亲委派机制,会去加载父类,父类发现冲突了String就不再加载了;
5. 对象的访问
对象的访问2种方式,使用过栈上的直接指针或句柄进行的,即对象引用地址
- 句柄:使用句柄访问,Java堆会划分出一块内存做句柄池,存储对象的句柄地址,包含对象实例数据与类型数据各自的具体地址信息
- 直接指针:直接就是对象地址
直接指针和句柄方式的区别:
直接指针:速度快,句柄:数据稳定
Hotspot: 使用直接指针进行对象访问
三、GC算法-垃圾回收
主要关注点:
-
对象存活判断
-
GC算法
-
垃圾回收
1、GC的对象存活判定
判断对象是否存活一般有两种方式:引用计数算法,可达性分析算法
引用计数: 每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
可达性分析(Reachability Analysis):通过一系列的名为“GC Root”的对象作为起点,如果与对象之间没有任何引用链相连,则证明此对象是不可用的
扩展:
GC Roots的对象包括下面几种:
- 虚拟机栈中引用的对象。
- 方法区中类静态变量引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象。
2、四种引用
无论通过引用计数算法还是可达性分析算法,来判读对象是否存活,都与引用相关;
引用分:强引用、软引用、弱引用、虚引用;引用强度依次减弱
- 强引用:类似“Object obj = new Object()”的引用,强引用永远不会被垃圾收集器回收
- 软引用:用来描述一些还有用但是但是并非必须的对象, 内存不足回收
- 弱引用:只能生存到下一次垃圾收集之前,垃圾收集器回收内存
- 虚引用:无影响,为了能在对象被回收时收到一个系统通知
扩展:
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类
判断“无用的类”规则:
- 该类所有的实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类的Class对象没有任何地方引用,无法在任何任何地方通过反射访问该类的方法
满足上述3个条件,虚拟机将会对无用类进行回收
3、垃圾收集算法
共有4种:标记-清除算法、复制算法、标记-整理算法、分代收集算法
- 标记-清除算法:算法分为“标记”和“清除”两个阶段,产生大量不连续的内存碎片
- 效率不高,产生大量不连续的内存碎片,碎片多,不足够大,提前触发垃圾收集动作。
- 复制算法
- 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
- 将内存缩小为原来的一半,对持续复制长生存期的对象则导致效率降低。不需要考虑内存碎片等复杂情况过程简单,运行高效,
- 标记-整理算法
- 先标记,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
- 分代收集算法
- 把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
- 新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法
- 老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
扩展:
对象标记判定:
-
第一次被标记:如果对象在可达性分析后没有与GC Roots相连接的引用链,将第一次被标记,并进行一次筛选,筛选条件为是否有必要执行finalize()方法
"没必要执行"的两种情况:当对象没有覆盖finalize(),或者此方法已经被虚拟机调用过
- 如果对象被判定为没有必要执行finalize()方法,便将这对象放置F-Queue队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(指触发这个方法);
-
第二次标记:稍后GC将对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中成功拯救自己(重新与引用链上的任一对象建立关联),它将被移出“即将回收”集合,否则,被回收
总结:根据向下搜索方法判断引用链不可达 -> 判断是否执行finalize()方法(当没有覆盖finalize方法或者已经被执行过一次,则需要执行finalize方法),进入f-queue队列 ->再次判断与GCRoot有链接 ->回收
4、垃圾收集器
Serial收集器、ParNew收集器、Parallel Scavenge收集器、Serial Old 收集器、Parallel Old 收集器、CMS收集器、G1收集器
Serial收集器: 单线程 用户线程停顿
PartNew收集器: 多线程的Serial收集器
ParallelScavenge:并行收集器: 自动调整停顿时间 和 最大的吞吐量
上述三种新生代收集器,复制算法
串行收集器:串行收集器使用一个单独的线程进行收集,GC时服务有停顿时间
并行收集器:次要回收中使用多线程来执行
SerialOld 、ParallelOld :老年代收集器, 标记整理算法
CMS: 老年代收集器,标记清除算法,并发
- 是一种以最短停顿时间为目标的收集器
- 4步:初始标记、 并发标记、 重新标记、 并发清除 (初始标记、重新标记需要“Stop The World”)
- 优点:并发低停顿;缺点:对CPU资源非常敏感 无法清除浮动垃圾,预留内存空间给线程使用,产生空间碎片
G1收集器:标记整理算法
- 不牺牲吞吐量的情况下完成低停顿 极力避免对全区域进行回收,划分成多个区域region,优先回收垃圾最多的区域
- 4步:初始标记、并发标记、最终标记、筛选回收
- 特点:并行与并发、分代收集、空间整合、可预测的停顿(思想化整为零)
响应优先选择CMS,吞吐量高选择G1
5、Java 内存分配策略
Java 程序运行时的内存分配策略有三种
- 静态分配:静态存储区(也称方法区)
- 静态数据、全局 static 数据和常量
- 栈式分配:栈区
- 方法体内的局部变量(其中包括基础数据类型、对象的引用)
- 堆式分配:堆区。
- new 出来的内存,也就是对象的实例
堆区内存分配策略
- 对象优先分配在Eden区,内存不够,Minor GC
- 任然不够,存活对象进入Survivor区,大对象直接进入老年代
- Survivor中同年龄的对象总和大于一半,存活年龄大于等于15的对象直接进入老年代。(每次Minor GC对象年龄加1)
- 当发生MinorGC时,survivor空间不够时,需要老年代分配担保,让survivor无法容纳的对象进入老年代
内存分配2种方式
- 指针碰撞:在内存是绝对规整情况下,内存分已用内存和空闲内存,中间由一个指针做为分界点的指示器相连;当需要分配内存时,指针向空闲内存移动一段与对象大小相等的距离
- 空闲列表:在内存不规整的情况下,虚拟机有一个维护列表,用于记录哪块内存可用;在分配时,从列表找一块空间足够的内存分给对象实例;
分配方式由堆是否规整决定,而java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
分配方式—>java堆是否规整—>垃圾收集器是否有压缩整理功能
一般系统采用指针碰撞,使用CMS(内存管理系统:基于Mark-Sweep算法的收集器),采用空闲列表
以上两种分配方式不具备线程安全,采用两种解决方法:
- 虚拟机采用CAS配上失败重试的方式,保证更新操作的原子性
- 把内存分配的动作按线程划分在不同的空间,即每个线程预先分配一小块内存(这小块叫:本地线程分配缓冲:thread local allocation buffer,TLAB);线程需要的内存在自己的TLAB 上分配,用完同步锁定重新分配新的TLAB;虚拟机是否使用TLAB 通过-XX:=/-UerTLAB 参数设定
扩展:
MinorGC,FullGC触发条件
MinorGC:Eden区满时,触发MinorGC
FullGC:
- System.gc,系统建议执行FullGC,但不必然执行
- 老年代空间不足(minorGC完15代及以上需要进入老年代 或 复制至to survivor空间不足需要老年代担保分配 ,老年代内存空间不足)
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
四、GC分析–命令调优
主要关注点:
- GC日志分析
- 调优命令
- 调优工具
1、调优命令
Sun JDK监控和故障处理命令有jps、 jstat、jmap、jhat、jstack、jinfo
- jps -l -v 查看JVM进程 启动参数
- jstat -gcutil 查看内存各个区域使用情况和垃圾收集信息
- jmap -heap 查看堆配置信息 -histo查看对象统计信息 命令用于生成heap dump文件
- jstack 查看线程堆栈
- jinfo 实时查看和动态调整JVM运行参数无需重启
- jhat 与jmap配合使用分析dump文件
jvm成熟的调优工具:jconsole、VisualVM、Analyzer
2、JVM 异常
1> 如何找到CPU飙升的原因
问题分析步骤:
- 首先,需要知道哪个进程占用CPU比较高,
- 其次,需要知道占用CPU高的那个进程中的哪些线程占用CPU比较高,
- 然后,需要知道这些线程的stack trace。
问题解决步骤:
-
1、top和pgrep来查看系统中Java进程的CPU占用情况。
- 命令如下:top -p
pgrep -d , java
- pgrep:进程号,
top -p
:进程的信息。记录下CPU占用率最高的那个进程号。
- 命令如下:top -p
-
2、top来查看进程中CPU占用最高的那些线程
- top -Hp 12345
- 假定12345为占用CPU高的进程号。-H是显示该进程中线程的CPU占用情况。同样,记录下CPU占用率高的那些线程号。
-
3、ctrl+H 切换到线程模式,找到占用cpu最高的线程。并把线程号转化为十六进制,printf “%x\n” <线程ID>
-
4、通过jstack导出Java应用中线程的stack trace(堆栈轨迹)
- jstack 12345
-
注意:因为top中显示的线程号是10进制,jstack的输出结果中的线程号是16进制,所以只需要把top中看到线程号转换成16进制
-
小结一下,我们通过top和jstack来找到CPU占用高的线程的stack trace
可以使用Eclipse Memory Analyzer插件分析
2> Java堆溢出和泄漏
-
内存溢出:程序在申请内存时,没有足够的内存空间供其使用
- **危害:**容易受攻击
- 影响因素如下几大类:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
- 代码中存在死循环或循环产生过多重复的对象实体
- 解决方案:
- 修改JVM启动参数,直接增加内存
- 检查错误日志,是否有其他异常或错误;
- 对代码进行走查和分析,找出可能发生内存溢出的位置
- 重点排查:数据库的取值,死循环和递归调用
-
内存泄漏:无法释放已申请的内存空间
-
危害:频繁GC、运行崩溃
-
影响因素如下几大类:
-
静态集合类引起内存泄露
-
当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
Set<Person> set = new HashSet<Person>(); Person p3 = new Person("唐僧","pwd1",25); p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变 set.remove(p3); //此时remove不掉,造成内存泄漏
-
监听器。释放对象时没有删除监听器。
-
各种连接 ,比如数据库连接
-
单例对象持有外部对象的引用
-
-
**解决办法:**使用工具jconsole分析
-
堆的最小值-Xms参数,最大值-Xmx参数
//代码实现堆溢出:---> 无限循环创建 对象
List list =new ArrayList();
int i=0;
while(true){
list.add(new byte[5*1024*1024]);//----------->就是这一步
System.out.println("分配次数:"+(++i))
}
3> Java栈溢出
栈溢出SOF定义:线程请求的栈深度超过虚拟机允许的最大深度
- 无论是由于栈帧太大还是栈容量太小,当内存无法分配时都是OOM异常。
- 虚拟机栈溢出:深度溢出:递归方法;广度溢出:大数组,建立多线程
//代码实现栈泄漏---> 方法无限递归调用
public void add(int i){
add(i+1);
}
扩展:
GC参数
JVM的GC日志的主要参数包括如下几个:
-XX:+PrintGC
输出GC日志-XX:+PrintGCDetails
输出GC的详细日志-XX:+PrintGCTimeStamps
输出GC的时间戳(以基准时间的形式)-XX:+PrintGCDateStamps
输出GC的时间戳(以日期的形式,如 2017-09-04T21:53:59.234+0800)-XX:+PrintHeapAtGC
在进行GC的前后打印出堆的信息-Xloggc:../logs/gc.log
日志文件的输出路径
在生产环境中,根据需要配置相应的参数来监控JVM运行情况。
3、GC分析工具
GChisto是一款专业分析gc日志的工具,可以通过gc日志来分析:Minor GC、full gc的时间、频率等等,通过列表、报表、图表等不同的形式来反应gc的情况。
GC Easy:推荐此工具进行gc分析;这是一个web工具,在线使用非常方便,进入官网,讲打包好的zip或者gz为后缀的压缩包上传,过一会就会拿到分析结果。
4、JVM启动参数
-Xmx设置最大堆容量
-Xms设置初始堆容量
-Xmn新生代大小
-Xss参数设定每个线程的栈大小
-XX:newRatio 新生代与老年代的比例
-XX:SurvivorRatio Eden区与Survivor的比例
-XX:PermSize 永久代的初始大小
-XX:MaxPermSize 永久代的最大空间
-XX:MaxTenuringThreshold=0设置垃圾最大年龄
知识扩展
一、Class文件结构
魔数+版本号+常量池+访问标志+类或父类索引和接口索引集合+字段表集合+方法表集合+属性表集合
- 魔数:每个class文件的头4个字节,用于确定这个文件
- 版本号:第5、6和字节是次版本号,7、8个字节是主版本号
- 常量池:字面值和符号引用(类和接口的全限定名名,字段或方法的名称和描述)
- 访问标志:用于识别类或接口层次的访问信息,如:是类还是接口,是否是public类型、是否是abstract类型、是否是final
- 类或父类索引和接口索引集合:
- 字段表集合:指全局变量,非局部变量
二、字节码执行引擎
执行引擎分两种:解释执行和编译执行
解释执行:通过解释器执行
编译执行:通过编译器产生的本地代码执行
1、运行时栈帧结构
栈帧:
- 是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机栈的栈元素,
- 存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息,
- 每个方法从调用到完成的过程,都是对应着一个栈帧在虚拟机栈里入栈到出栈的过程
局部变量表:存放方法参数和方法内部定义的局部变量
操作数栈:简称操作栈:是一个后入先出的栈,每个元素可以是任一java数据类型,包括long和double;32位占一个栈容量,64位2个
动态连接:
方法返回地址:分正常或异常两种方式
扩展:
局部变量表的容量以变量槽为最小单位,每个槽都能存一个Boolean、byte、char、short、int、float、reference、或者returnAddress类型数据
reference;指对象引用地址;
- 用于直接或间接找到堆的数据存放的起始地址索引
- 直接或间接找到对象所属数据类型在方法区中的存储的类型信息
2、方法调用
不等同于方法执行,是为了确定被调用方法的版本,不进行具体运行过程
**解析:**调用版本中的方法调用
主要包括静态方法和私有方法,适合在类加载阶段进行解析
**分派:**指选择方法的版本
静态分派:体现是重载
动态分派:体现是重写
扩展:
调用版本:指在类加载的解析阶段前,方法有一个可确定的调用版本,此版本在运行期是不可改变的
3、解释执行
基于栈的字节码解释执行引擎
java编译器输出的指令流是基于栈的指令集架构,其中大部分都是零地址指令
三、对象的内存布局
对象在内存中存储布局分3块:对象头、实例数据、对齐填充
- 对象头:分两部分
- 一部分:存储自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID和偏向时间戳,官称“Mark Word”
- 一部分:类型指针,即对象指向它的类元数据的指针,通过这个指针确定对象是哪个类的实例
- 如果数组,则还有一块用于记录数组长度的数据
- 实例数据:对象真正存储的有效信息,就是代码中定义的各种类型的字段内容,无论父类还是子类的,都会记录
- 对齐填充:不是必然存在的,起占位符作用,没有特殊意义
四、面试题
- 1、详细jvm内存模型
- 2、讲讲什么情况下回出现内存溢出,内存泄漏?
- 3、说说Java线程栈
- 4、JVM 年轻代到年老代的晋升过程的判断条件是什么呢?
- 5、JVM 出现 fullGC 很频繁,怎么去线上排查问题?
- 6、类加载为什么要使用双亲委派模式,有没有什么场景是打破了这个模式?
- 7、类的实例化顺序
- 8、JVM垃圾回收机制,何时触发MinorGC等操作
- 9、JVM 中一次完整的 GC 流程(从 ygc 到 fgc)是怎样的
- 10、各种回收器,各自优缺点,重点CMS、G1
- 11、各种回收算法
- 12、OOM错误,stackoverflow错误,permgen space错误
- 13、GC收集器有哪些?CMS收集器与G1收集器的特点。