一、Java内存区域
1.Java虚拟机运行时数据区
- 程序计数器(线程私有):如果执行的是Java方法,则这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果是Native方法,这个计数器值为空。
- Java虚拟机栈(线程私有):每个方法在执行时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口、常量池引用等信息。通过-Xss来指定虚拟机栈内存的大小。
- 本地方法栈:为Native方法服务。
- Java堆(线程共享):所有对象在这里分配内存,是垃圾收集的主要区域。-Xms设置堆初始大小,-Xmx设置堆最大内存。堆可以细分为新生代和老年代,再细致一点可以分为Eden,From Survivor,To Survivor。
- 方法区(线程共享):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2.对象的创建过程
- 类加载检查
- 为对象分配内存
- 将分配到的内存空间初始化置零(不包括对象头)
- 设置对象头
- 初始化成员变量,并执行构造函数
3.OOM异常(内存溢出)
- Java堆溢出
- 虚拟机栈和本地方法栈溢出
- 方法区和运行时常量池溢出
- 本机直接内存溢出
3.1 JVM调优
- 首先把OOM文件dump下来
- 使用jprofiler工具加载dump文件,查看是否有大对象占用很大内存,此外还可以定位到哪行代码出现问题
- 尝试调大堆内存
内存泄漏:程序申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积后果很严重,无论内存多大都会被占光。
二、垃圾收集
1.判断对象是否已死
- 引用计数法:为对象添加计数器,引用为0时可被回收。缺点:当存在循环引用时,所占用的内存永远无法被回收。
- 可达性分析算法:以GC Roots为起点搜索,可达对象都是存货的,不可达对象都被回收。
可作为GC Roots对象的:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中Native方法引用的对象
2.引用
- 强引用:
被强引用关联的对象不会被回收。使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
- 软引用:只有在内存不够的情况下才回收。使用 SoftReference 类来创建软引用。
假设有一个应用需要大量读取本地的图片:每次读取图片都从磁盘读取会严重影响性能,如果一次性全部加载到内存中又可能造成溢出,此时用软引用就可以解决这个问题。
- 弱引用:被弱引用关联的对象一定会被回收。使用 WeakReference 类来创建弱引用。
- 虚引用(幽灵引用/幻影引用):为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。使用 PhantomReference 来创建虚引用。
3.finalize()
在可达性分析中不可达的对象,也并不是一定会被回收的,真正宣告它死亡,至少需要两次标记过程。可达性分析中的不可达对象会被进行第一次标记,并根据此对象是否有必要执行finalize()方法进行一次筛选。如果没有覆盖finalize()方法或已被执行过,则都被视为没有必要执行。
如果有必要执行finalize()方法,会被放入F-Queue中,有可能会通过finalize()方法实现自救。
4.回收方法区
常量池的回收和类的卸载
5.垃圾收集算法
- 标记-清除:会产生很多内存碎片,后续会因为没有连续内存空间分配给大对象而提前进行垃圾收集。
- 复制算法:不会产生内存碎片,但只用到一半的内存。
- 标记-整理算法:没有内存碎片,效率偏低。
6.垃圾收集器
- Serial 收集器(新生代,复制算法;老年代,标记整理算法):
Serial 翻译为串行,也就是说它以串行的方式执行。它是单线程的收集器,只会使用一个线程进行垃圾收集工作。 会暂停所有的用户线程,不适合服务器环境。 - ParNew收集器(新生代,复制算法;老年代,标记整理算法):
Serial收集器的多线程版本。 - Parallel Scavenge收集器:与 ParNew 一样是多线程收集器。其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器
- CMS 收集器(用于老年代,标记-清除算法):
是一种以获取最短停顿回收时间为目标的收集器。
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
并发清除:不需要停顿。 - G1收集器(整体采用标记整理,局部采用复制):G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。解决了CMS内存碎片的问题,同时使停顿时间更短,甚至用户可以自己指定停顿时间。
7.内存分配与回收策略
- 对象优先在Eden分配
- 大对象(需要大量连续内存空间的对象 )直接进入老年代
- 长期存活(默认为15岁,每熬过一次minor GC 算一次)的对象将进入老年代
- 如果Survivor空间中相同年龄所有对象大小总和超过Survivor空间的一半,则年龄大于或等于该年龄的对象就可以直接进入老年代。
三、类加载机制
1.类的生命周期
加载-验证-准备-解析-初始化-使用-卸载
- 加载:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
- 验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
- 准备:类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。
- 解析:将常量池的符号引用替换为直接引用的过程,解析某些时候可以在初始化后
- 初始化:
以下5种情况需要立即对类进行初始化:
1.遇到new、getstatic、putstatic、invokestatic这4条字节码指令时。
2.使用java.lang.reflect包的方法对类进行反射调用时
3.当初始化一个类时,发现其父类还没有初始化
4.虚拟机启动时,会初始化主类(main)
5.如果一个java.lang.invoke.MethodHandle时力最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStaitc方法句柄时。
2.类加载器
- 启动类加载器:加载存放在<JAVA_HOME>\lib中的
- 扩展类加载器:加载<JAVA_HOME>\lib\ext中的
- 应用程序类加载器:加载用户类路径上所指定的类库
3.双亲委派模型
- 工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
- 好处:保证Java程序的稳定运作。使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
网友评论