《深入理解Java虚拟机》学习及日常问题总结

写此博客的初衷:对周志明老师的《深入理解Java虚拟机》学习做个梳理总结,以加深自己的理解;总结自己平时开发中遇到的问题。
Java语言对于内存的管理不像C++那样,由程序员自己控制。它交给JVM自动管理。内存如何分配,初始化及回收对于程序员来说是透明的。了解虚拟机的工作原理对于Java开发至关重要:解决最常见的OOM问题;虚拟机集群选择(32/64);选择适合的垃圾收集器;根据具体业务场景选择合适的数据结构等都需要我们了解JVM的工作原理。
## 一.Java内存区域与内存溢出异常 ##
这里写图片描述
线程共享区域:方法区;堆。
线程不共享区域:虚拟机栈;本地方法栈;程序计数器。
1.程序计数器:当前线程所执行的字节码的行号指示器。唯一 一块无内存溢出的区域。
2.虚拟机栈:每个线程独占。方法调用的同时会创建一个栈帧,栈帧里存放的是局部变量表(编译期已确定大小),动态链接,方法的出口信息。线程执行方法的过程即栈帧入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设定),就会抛出StackOverflowError;如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError。
模拟StackOverflowError例子(-Xss128k):

Public class JavaVMStackSOF{
    private int stackLength = 1;
    public void stackLeak(){
    stackLength ++;
    stackLeak();
}
}

在单线程下,无论是因为栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机都会抛出StackOverflowError。
创建更多的线程导致内存溢出(-Xss2M):

public class JavaVMStackOOM{
    public void stackLeakByThread(){
        while(true){
        Thread thread = new Thread(
        public void run(){
        ...
}
);
}
}
}

创建更多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量来换取更多的线程。

3.堆:由虚拟机启动时创建,存放对象实例,也是GC关注的主要对象 。可以通过-Xmx和-Xms参数设置堆得大小。如果堆中没有内存完成实例的分配,并且堆也无法扩展时将会抛出OutOfMemoryError。

//VM args -Xms 20m -Xmx 20m
public class HeapOOM{
    public class OOMObject(){
        List<Object> list = new ArrayList<Object>();
        while(true){
        list.add(new Object());
}
}
}

4.方法区:方法区存放的是已经被虚拟机加载的类信息,常量,静态变量和即时编译器编译后的代码等数据。通过参数-XX:MaxPermSize设置其大小。其中常量池包含静态编译常量和运行时常量(如:String.intern())。

public class RuntimeConstantPoolOOM{
    public void oom(){
     List<Object> list = new ArrayList<Object>();
     int i = 0 ;
     while(true){
         list.add(String.valueof(i++).intern());
    }
}
}

5.直接内存(Direct Memory):它并不是运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域。但是这部分内存也被频繁使用,也会导致OutOfMemoryError(使用NIO读写)。其容量可通过-XX:MaxDirectMemorySize 指定,如果不指定,默认与-Xmx 一样。
开发中遇到的OOM问题
1.循环创建对象如:

public class TestOom1{
    public void testOom1(){
    while(true){
     Object object = new Obeject();//循环不结束,所有对象都存在obect的引用,无法回收
    }
}
}

// 改如下方式创建对象
public class TestOom1{
    Object object = null;
    public void testOom2{
        while(true){
        object  = new Object();//始终只有一个引用指向当前对象
}
}           
}

2.ThreadLocal导致OOM:ThreadLocal是以ThreadLocalMap为数据结构保存数据的,key设计为WeakReference,当只有ThreadLocalMap引用的时候自动回收key。
ThreadLocal对象作为key,value为实际添加的对象。ThreadLocal是线程私有的,其生命周期和当前线程一样,但是在使用线程池的时候线程是不会销毁的,即使我们设置ThreadLocalMap的value的引用为null,由于ThreadLocalMap一直对其保持引用,所以就不会回收value,导致内存泄漏。
解决方法:调用其remove()方法。

二.垃圾收集器与内存分配策略

1.对象是否存活判断方法:
引用计数法:对象头里添加引用计数器,每引用一次就加一,为零说明此对象已死。缺点:不能解决对象之间的循环引用问题。
可达性分析算法:判断此对象与GC Roots之间是否存在引用链,即是否可达。若可达,说明此对象无法被回收。静态变量,常量,本地方法引用的对象可作为GC Roots。
强引用:无论何时都不会被回收。
软引用:内存不够用的时候才会被回收。
弱引用:无论内存是否够用,下一次GC的时候被回收。*
判定对象最终死亡:如果一个类重写了finalize()方法,虚拟机就会调用此对象的finalize()方法,如果没有重写,则认为“没必要执行”。一个对象的finalize()方法只执行一次。
2.回收方法区
判断类是否是“无用的类”:该类的所有实例是否被回收;
加载该类的ClassLoader是否被回收;
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
3.垃圾收集算法:
标记—清除:标记不存在引用的对象后回收内存。缺点,标记效率不高,存在内存碎片。
复制算法:把内存划分为相等的两块区域,开始至使用一半内存,当内存不够用的时候把存活的对象复制到另一半内存,全部清除刚才使用过的内存。适用范围:新生代区域(98%的对象是朝生夕灭)。如果有大量的存活对象,采用复制算法效率同样很低(如果采用了复制算法,且活着的对象很多时,可以考虑增加参数去掉survivor区,让新生代中存活的对象在第一次Minor GC 后直接进入老年代)
标记—整理:类似标记清除算法,多了一步内存碎片的整理。
分代收集算法:新生代采用复制算法。老年代采用标记清除算法或标记整理算法。
4.垃圾收集器:
Serial 收集器:单线程收集器,GC过程中会发生”Stop The World”。
CMS收集器:基于标记—清除算法实现。目标是获取最短回收停顿时间。GC线程和用户线程并发执行。无法处理浮动垃圾,即无法处理GC标记之后用户线程产生的垃圾。当剩余内存不够用户线程分配时,会出现“Concurrent Mode Failure”失败而导致一次Full GC ,此时虚拟机会临时采用Serial Old 重新对老年代进行收集。可以通过-XX:CMSInitingOccupancyFraction参数设置触发CMS收集的阈值,此参数设置太高容易导致大量”Concurrent Mode Failure”失败,性能反而降低,JDK1.6默认值为92%。

三.虚拟机类加载机制

类加载的时机
1.遇到new ,getstatic,putstatic或invokestatic这四条字节码指令时,如果发现类没有进行初始化,则先初始化。
2.使用java.lang.reflect包对其类进行反射调用的时候,如果类没有初始化,则触发初始化。
3.初始化一个类时,如果发现其父类没有初始化,则先初始化其父类。
类加载过程:
1.加载:
根据类全限定名来获取此类的二进制字节流。
内存中生成代表这个类的java.lang.Class对象(存放在方法区),作为访问这个类数据的入口。
2.验证:校验字节码是否符合虚拟机规范。只有通过了验证,字节流才会进入方法区进行存储。将占用虚拟机加载类的大部分时间。
3.准备:在方法区为类变量分配内存空间,并给定初始值。fianl static变量除外,编译时已经赋予值,所以此时的值为赋值后的值。如:
static final int value =123;准备阶段value的值为123,而不是0;
4.解析:将常量池的符号引用替换成直接引用。
5.初始化:执行初始化语句。
类加载器:
1.Bootstrap ClassLoader启动类加载器,由C++实现,是虚拟机的一部分。其他的类加载器都是由java实现的,斗继承java.lang.ClassLoader,独立于虚拟机外部。负责加载\lib类库。
2.Extension ClassLoader扩展类加载器,负责加载\lib\ext目录中的类库。
3.Application Classloader 应用程序类加载器,负责加载ClassPath上的类库。
双亲委派模型:非继承而是组合关系。当一个类加载器收到类加载的请求时,首先不会自己去加载,而是把这个请求委派给父类加载器去完成。如果父类加载器无法加载(它的搜索范围中找不到此类),子类加载器才会尝试自己去加载。
同一个类经不同的加载器加载,会生成不同的类。双亲委派模型保证了类加载后的唯一性,保证了Java程序的稳定运行。

四.Java内存模型

这里写图片描述
Working Memory存放主内存的副本,为每个线程私有,线程无法彼此访问工作内存,线程之间的数据共享通过主内存。
1.volatile变量的特殊规则:
可见性:被volatile修饰的变量所有线程之间是可见的。即每个线程使用volatile变量之前都会刷新,立即同步主内存(volatile,synchronized和final修饰的变量能保证可见性)。
禁止指令重排序优化:多线程之间被volatile修饰的变量顺序执行。(场景:先加载配置文件,后执行后续的操作)。

五.虚拟机性能监控

给一个系统定位问题的时候,知识,经验是关键基础,数据是依据。数据包括:运行时日志,异常堆栈,GC日志,线程快照(threaddump),堆转储快照(heapdump)等。
JDK命令行工具:
jps:列出正在运行的虚拟机进程。(包括PID和主方法对应类名)
jstat:显示虚拟机类加载,内存,垃圾收集,JIT编译等运行数据。
如:jstat -gc 2764 250 20表示每250毫秒查询一次进程2764垃圾收集情况,查询20次。
jmap:生成dump文件,查询finalize执行队列,Java堆和永久代详细信息,如空间使用率,当前使用的是哪种收集器。
jstack:生成线程快照(threaddump):生成线程快照的目的是为了定位线程长时间停顿的原因:锁等待(死锁和活锁-waiting),死循环,请求外部资源(数据库连接,网络资源,设备资源等)等都是导致线程长时间停顿的原因。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值