一、运行时数据区域
- 程序计数器:通过改变值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都要依赖此计数器。此内存区域是唯一一个没有OutOfMemoryError的区域。
- Java虚拟机栈:非线程共享,每个线程维护一个栈,存放编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(非对象本身)、和returnAddress类型(指向一条字节码指令地址)。会抛出StackOverflowError和OutOfMemoryError。
- 本地方法栈:为Native方法服务。
- Java堆:被所有线程共共享,存储所有的对象实例以及数组,被所有线程共享;java堆是垃圾收集器管理的主要区域(GC堆);会抛出OutOfMemoryError。
- 方法区:用于存储已被虚拟机加载的类信息、常量(String常量池也在这里)、静态变量(static变量、static方法)、即时编译器编译后的代码等数据;属于堆的一个逻辑部分,被各个线程共享,被GC管理,会抛出OutOfMemoryError。
- 运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量(常量)和符号引用。
二、垃圾回收
1、可达性分析算法:通过一系列成为GC Roots的对象作为起始点向下搜索,走过的路径成为引用链,当一个对象到GC Roots没有任何引用链时,证明此对象不可用。
可作为GC Roots的对象包括:
- 虚拟机栈中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中引用的对象。
2、垃圾收集算法
新生代:复制法
将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
老年代:标记-整理法
标记-清除
这种会造成大量内存碎片,所以不建议使用。
另:
- 对象优先在新生代分配;
- 大对象直接进入老年代;
- 新生代发生垃圾回收时,如果另一个survival不够大,则存活对象进入老年代;
- 长期存活的对象进入老年代。
三、JVM启动参数
Xms Xmx 堆的最小、最大容量
Xmn 新生代大小
Xss 每个线程的栈大小
四、JVM调优
常用的JVM调优工具:
- jps:虚拟机进程状况工具,可以输出JVM启动时的指定参数;
- jstat:虚拟机统计信息监视工具,可以查看新生代、老年代内存使用情况,垃圾回收次数;
- jmap:Java内存印象工具,可以生成堆转储快照;
- jhat:虚拟机堆转储快照分析工具,分析jmap生成的快照;(分析同样一个dump快照,MAT需要的额外内存比jhat要小的多的多,所以建议使用MAT来进行分析)
- jstack:Java堆栈跟踪工具,可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源
- jinfo:Java配置信息工具,这个命令作用是实时查看和调整虚拟机运行参数, 之前的jps -v口令只能查看到显示指定的参数,如果想要查看未被显示指定的参数的值就要使用jinfo口令。
参考:https://www.cnblogs.com/warehouse/p/9479104.html https://www.jianshu.com/p/aaee11115f37
一次调优:
1、匿名内部类引起的内存泄漏:
匿名内部类或者普通的内部类,会持有外部类的引用。如果内部类的生命周期比外部类长,就会导致外部类也无法被回收。解决办法:将内部类改成静态内部类,如果需要外部类的引用,则手动持有一个外部类的弱引用。
public void doSmthing(T t){
redis.addListener(new Listener(){
public void onTimeout(){
if(t.success()){
//执行操作
}
}
});
}
由于listener在回调后不会进行释放,而且回调是个超时的操作,当某个事件超过了设定的时间(1分钟)后才会进行回调,这样就导致了T这个对象始终无法回收,所以内存中会存在这么多对象实例。
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
当这个Activity
被finish
掉的时候,Handler
发送的这个Message
会继续在主线程的message queue
中存在1000*60*10ms
才会被处理掉。这个Message
持有了activity
中Handler
对象的引用,并且Handler
内部类又隐式地持有它的外部类SampleActivity
的引用,在Message
被处理之前,这个引用链会一直存在,从而会阻止activity
的context
被垃圾回收器回收,这样就会导致这个activity
引用的所有resources
造成内存泄漏。(Message
被处理掉之后,再遇到GC,该Message
对象就会被回收,其引用的Handler
对象也会被回收,相应的activity
也就可以被回收了,如果没有其他引用继续引用它时)。代码中的new Runnable
也是同理。非静态的匿名内部类也会隐式地持有外部类的引用,也会造成内存泄漏。
public class SampleActivity extends Activity {
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
2、查全表
3、新生代太小导致youngGC频繁
-Xmn350M -> -Xmn800M
-XX:SurvivorRatio=4 -> -XX:SurvivorRatio=8 //Eden区和两个Survivor比例从4:3:3改为8:1:1
-Xms1000m ->-Xms1800m
五、new一个对象的过程
虚拟机遇到一条new指令时,检查是否能在运行时常量池中定位到类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就必须先执行相应的类加载过程,然后初始化对象、创建对象。
一、加载和初始化
1、加载
使用双亲委派模型加载对象,由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例;
2、验证
格式验证:验证是否符合class文件规范
语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否可以通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
3、准备
为类中的所有静态变量分配内存空间,并为其设置一个初始值;
4、解析
将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。·
5、初始化(先父后子)
给静态变量赋值、执行static代码块
二、创建对象
1、在堆区分配对象需要的内存(指针碰撞法或空闲列表法)
- 指针碰撞,当虚拟机使用复制算法或标记整理算法实现的垃圾收集器时,内存区域都是规整的,这时候使用指针碰撞分配内存,用过的内存放在一边,空闲的内存在另一边,中间用一个指针作为分界点,当需要为新对象分配内存时只需把指针向空闲的一边移动一段与对象大小相等的距离。
- 空闲列表,当虚拟机使用标记清除算法实现的垃圾收集器时,内存都是碎片化的,那虚拟机就要记录哪块内存是可用的,当需要分配内存时,找一块足够大的内存空间给对象实例,并更新记录。
2、对所有实例变量赋默认值
3、执行实例初始化代码
初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法
4、如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它
六、GC停顿以及SafePoint
GC停顿(Stop The World)的原因是,在进行垃圾回收时,需要做可达性分析。在枚举那些可以作为GC Root的类时,不可以出现对象引用关系还在发生变化的情况,这就是导致GC进行时必须停止所有java执行线程的一个重要原因,即使是号称几乎不会发生停顿的CMS收集器,枚举根节点时也是必须要停顿的。发生停顿的这个点叫做SafePoint。
可作为GC Root的对象主要在全局引用和执行上下文中,逐个检查会消耗很多时间。HotSpot中一个叫做OopMap的数据结构,直接存放了对象引用的地址。在OopMap的帮助下可以快速准确的完成GC Root的枚举。但是导致对象引用关系变化(或者说导致OopMap内容变化)的指令有很多,不可能为每一条指令都生成OopMap,只是在特定位置记录了这些内容,这些特定的位置就是SafePoint。
SafePoint的选取标准是那些会导致程序长时间执行的指令,比如方法调用、循环跳转、异常跳转等,这些指令才会产生SafePoint。
需要执行GC时,线程不会立刻中断,而是执行到下一个安全点才中断。
七、垃圾收集器
1、serial
单线程,且在进行垃圾收集的时候必须停止其他线程;用在新生代时使用复制算法,用在老年代时使用 “标记-整理“ 算法;简单高效,单CPU环境下没有线程开销,仍可用在Client模式下的新生代。
2、parnew
多线程版本的serial,但在发生垃圾回收时仍然要停止其他线程;
3、serial old
老年代版本的serial
4、parnew old
老年代版本的parnew
5、CMS(concurrent mark sweep)
JDK1.5发布,旨在缩短垃圾收集的停顿时间,垃圾回收和用户线程可以并发执行,采用 “标记-清除” 算法,常用在老年代。其垃圾回收经历四个步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
初始标记只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段会深入查找GC Roots关联到的对象,比较耗时;重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记,这个过程的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记时间短。
优点:
- 并发收集
- 低停顿
缺点:
- 比较耗费CPU资源
- 无法处理浮动垃圾(由于CMS并发清理阶段用户线程还在运行,会有新的垃圾不断产生)
- 由于是基于 “标记-清除” 算法,在垃圾收集结束之后会产生大量空间碎片。
6、G1
JDK1.7提出。
垃圾回收的过程:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
优点:
- 并发回收,和用户线程同时执行,低停顿;
- 不用和其他收集器配合,独自管理新生代和老年代;
- 采用“ 标记-整理 ”算法,不会产生碎片;
- 可预测的停顿。
关于可预测的停顿,在G1之前的收集器的收集范围是整个新生代或者老年代,而G1则是将整个Java堆划分成多个大小相等的Region(新生代和老年代的概念还在,但是不再是物理隔离,它们都是一部分Region的集合)。之所以能建立可预测的时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾回收,先回收价值最大的Region。