JVM的组成
方法区和堆是共享区所有线程共享的,本地方法栈、虚拟机栈、程序计数器是每个线程独有的。
程序计数器
执行class文件到达哪一行了,线程私有的,不会发生内存泄漏。
堆
内存管理最大的一部分,当程序里new出一个对象,或者声明一个数组时,都会在堆内存中申请出一块空间。
分为新生代、年老代、永久代。
发生异常时是因为往里面添加的对象太多。
虚拟机栈
线程私有,与线程共存。
每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,包含操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程,当线程请求的栈深度(局部变量表越来越大)超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;
发生异常时是因为方法进去后无法出栈。
本地方法栈
直接跟操作系统打交道,不加载Java方法,与虚拟机栈互相调用。
方法区
存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); //true
String s3 = new String("hello");
System.out.println(s1 == s3); //false
String s4 = new String("hello");
System.out.println(s4 == s3); //false
//在运行过程中加入常量池
System.out.println(s1.intern());
System.out.println(s1 == s3.intern()); //true
}
堆和栈的概念和区别(重要)
栈内存 是一块系统自动分配的一块连续的区域,存放着局部变量,先加载函数才能进行局部变量的定义,所以方法需要先进栈,然后再定义变量,变量才有了自己的作用域,方法创建了一个栈针用来维护局部变量表,一旦方法出栈,定义的变量也就失去的作用,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。
堆内存 是人为申请分配的不连续的内存空间,存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
主函数里的语句 int [] arr=new int [3];在内存中是怎么被定义的
主函数先进栈,在栈中定义一个变量arr,接下来为arr赋值,但是右边不是一个具体值,是一个实体。
实体创建在堆里,在堆里首先通过new关键字开辟一个空间,内存在存储数据的时候都是通过地址来体现的,地址是一块连续的二进制,然后给这个实体分配一个内存地址。数组都是有一个索引,数组这个实体在堆内存中产生之后每一个空间都会进行默认的初始化(这是堆内存的特点,未初始化的数据是不能用的,但在堆里是可以用的,因为初始化过了,但是在栈里没有),不同的类型初始化的值不一样。
所以堆和栈里就创建了变量和实体,堆分配了一个地址,把堆的地址赋给arr,arr就通过地址指向了数组。
所以arr想操纵数组时,就通过地址,而不是直接把实体都赋给它。这种我们不再叫他基本数据类型,而叫引用数据类型。称为arr引用了堆内存当中的实体。(可以理解为c或c++ 的指针,Java成长自c和c++很像,优化了c++)
如果当int [] arr=null;arr不做任何指向,null的作用就是取消引用数据类型的指向。
当一个实体,没有引用数据类型指向的时候,它在堆内存中不会被释放,而被当做一个垃圾,在不定时的时间内自动回收,因为Java有一个自动回收机制,(而c++没有,需要程序员手动回收,如果不回收就越堆越多,直到撑满内存溢出,所以Java在内存管理上优于c++)。自动回收机制(程序)自动监测堆里是否有垃圾,如果有,就会自动的做垃圾回收的动作,但是什么时候收不一定。
内存分配与回收策略
判断对象已死的算法
引用计数器算法(Java中现在不使用)
可达性分析算法(目前在使用)
再谈引用
JVM中哪些是共享区
方法区和堆是共享区所有线程共享的,本地方法栈、虚拟机栈、程序计数器是每个线程独有的。
哪些地方可以作为gc root
垃圾回收过程中需要找到没有被调用的的对象进行回收,但是直接找没有调用的对象是非常耗时的,所以可以反过来找正在被调用的对象,这样没有没调用的对象就可以被回收掉。那么就需要从根去寻找,一路上找到的对象就是正常的对象,根 有一个特点就是它只会引用别人,而别人不能引用它,
包括:栈中的局部变量、方法区中的静态变量、本地方法中的变量,正在运行的线程等。
垃圾回收算法
标记-清除算法(容易产生内存碎片)
标记-整理算法
复制算法
分代垃圾回收
垃圾收集器
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+ Serial Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+ Serial Old(老年代)
jdk1.9 默认垃圾收集器G1
Serial收集器
Serial:串行(-XX:+UseSerialGC)>为单线程环境设计,且使用一个线程回收垃圾,会暂停所有的用户线程,不适合服务器环境(例如:用户用餐,餐厅让用户出去,要叫一个清洁工打扫,打扫完再回来吃)
PerNew收集器(Serial收集器的多线程版本)
Parallel Scavenge收集器
Parallel:并行(-XX:+UseParallelGC)>多个并行垃圾收集线程工作,此时用户线程是暂停的,适用于科学计算、大数据处理首台处理等若交互环境(例如:用户用餐,餐厅让用户出去,要叫多个清洁工打扫,打扫完再回来吃)
可控的吞吐量,吞吐量优先
Serial Old收集器
Parallel Old收集器
CMS收集器(边污染边清除)
CMS:(-XX:UseConcMarkSweepGC)>用户线程和垃圾收集线程同时执行(并不一定是并行,可能交替执行),不需要停顿用户线程,适用对响应时间有要求的场景(例如:用户用餐,餐厅叫多个清洁工进来打扫,边吃边打扫)
G1收集器
G1:(garbage first)(-XX:UseG1GC)>G1垃圾回收器将堆内存分割成不通的区域然后并发的对其进行垃圾回收 java11默认GC回收器是ZGC
CMS与G1的区别
- 使用范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用 - STW时间不一样
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型,可配置) - 垃圾碎片
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。 - 回收过程不一样
CMS收集器 G1收集器- 初始标记 1.初始标记
- 并发标记 2. 并发标记
- 重新标记 3. 最终标记
- 并发清除 4. 筛选回收
常见参数分配
GC文字描述
我们都知道在Java虚拟机中进行垃圾回收的场所有两个,一个是堆,一个是方法区。
在堆中存储了Java程序运行时的所有对象信息,而垃圾回收其实就是对那些“死亡的”对象进行其所侵占的内存的释放,让后续对象再能分配到内存,从而完成程序运行的需要。关于何种对象为死亡对象,在下一部分将做详细介绍。
Java虚拟机将堆内存进行了“分块处理”,从广义上讲,在堆中进行垃圾回收分为新生代(Young Generation)和老生代(Old Generation);
从细微之处来看,为了提高Java虚拟机进行垃圾回收的效率,又将新生代分成了三个独立的区域(这里的独立区域只是一个相对的概念,并不是说分成三个区域以后就不再互相联合工作了),
分别为:Eden区(Eden Region)、From Survivor区(Form Survivor Region)以及To Survivor(To Survivor Region),而Eden区分配的内存较大,其他两个区较小,每次使用Eden和其中一块Survivor。
Java虚拟机在进行垃圾回收时,将Eden和Survivor中还存活着的对象进行一次性地复制到另一块Survivor空间上,直到其两个区域中对象被回收完成,当Survivor空间不够用时,需要依赖其他老年代的内存进行分配担保。
当另外一块Survivor中没有足够的空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老生代,在老生代中不仅存放着这一种类型的对象,还存放着大对象(需要很多连续的内存的对象),当Java程序运行时,如果遇到大对象将会被直接存放到老生代中,长期存活的对象也会直接进入老年代。
如果老生代的空间也被占满,当来自新生代的对象再次请求进入老生代时就会报OutOfMemory异常。新生代中的垃圾回收频率高,且回收的速度也较快。
就GC回收机制而言,JVM内存模型中的方法区更被人们倾向的称为永久代(Perm Generation),保存在永久代中的对象一般不会被回收。
其永久代进行垃圾回收的频率就较低,速度也较慢。永久代的垃圾收集主要回收废弃常量和无用类。以String常量abc为例,当我们声明了此常量,那么它就会被放到运行时常量池中,如果在常量池中没有任何对象对abc进行引用,那么abc这个常量就算是废弃常量而被回收;判断一个类是否“无用”,则需同时满足三个条件:
(1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
(2)加载该类的ClassLoader已经被回收
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的是可以回收而不是必然回收。
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC;
同理,当老年代中没有足够的内存空间来存放对象时,虚拟机会发起一次Major GC/Full GC。
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full CG。
虚拟机通过一个对象年龄计数器来判定哪些对象放在新生代,哪些对象应该放在老生代。
如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将该对象的年龄设为1。对象每在Survivor中熬过一次Minor GC,年龄就增加1岁,当他的年龄增加到最大值15时,就将会被晋升到老年代中。
虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中所有相同年龄的对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。如何排查 JVM 问题
对于还在正常运行的系统:
可以使用jmap来查看 JVM 中各个区域的使用情况
通过jmap命名查看堆中对象 jmap -histo:live 7869 | head -20 定位前20个
可以通过 jstack 来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
jstack -l 26045 > ./26045.stack
cat 26045.stack | grep '65be' -C 20
可以通过 jstat 命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
- 通过各个命令的结果,或者jvisualvm等工具来进行分析
- 首先,初步猜测频繁发送fulgc的原因,如果频繁发生fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc 过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
- 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存
对于已经发生了OOM的系统:
- 一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件(-X.+HeapDumpOnOutOfMemoryEror-XXHeapDumpPath=/utr/local/base)
- 我们可以利用jsisualvm等工具来分析dump文件
- 根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
- 然后再进行详细的分析和调试
总之,调优不是一蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
GC调优
原则
- 大多数的java应用不需要GC调优
- 大部分需要GC调优的的,不是参数问题,是代码问题
- 在实际使用中,分析GC情况优化代码比优化GC参数要多得多;
- GC调优是最后的手段
-
目的
GC的时间够小,GC的次数够少。
发生Full GC的周期足够的长,时间合理,最好是不发生。注意
如果满足下面的指标,则一般不需要进行GC:
Minor GC执行时间不到50ms;
- Minor GC执行不频繁,约10秒一次;
- Full GC执行时间不到1s;
-
调优
日志分析
- 调整GC类型和内存分配
- 不断地调试找出最优点
-
插曲
导出导致的OOM问题
SXSSFWorkbook是streaming版本的XSSFWorkbook,它只会保存最新的excel rows在内存里供查看,在此之前的excel rows都会被写入到硬盘里(Windows电脑的话,是写入到C盘根目录下的temp文件夹)。被写入到硬盘里的rows是不可见的/不可访问的。只有还保存在内存里的才可以被访问到。参考资料
- 垃圾回收讲解
- CMS和G1的区别,以及Parallel
- Dump文件分析
- dump文件生成和分析查看
- 内存溢出线上问题定位案例
- XSSFWorkbook生成大excle,避免内存溢出
- 深入理解JVM——第六章 JVM调优和深入了解性能优化笔记
- SXSSFWorkbook & XSSFWorkbook 效率比拼