Java内存区域
运行时内存区域
- 方法区 Method Area:又称为持久代(PremGen),是各个线程共享的内存区域。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 堆 Heap:是各个线程共享的内存区域,对于绝大多数应用来说,是Java虚拟机管理的内存中最大的一块。目的是存放对象实例、数组,垃圾回收主要发生在这个区域。
Java堆又分为新生代和老生代,比例1:2,其中新生代以8:1:1的比例,分为Eden空间、From Survivor空间、To Survivor空间。Java堆在物理内存上可以不连续,在逻辑上连续。
当Java堆中有实例没有被分配内存,且堆也无法扩展时,会抛出OutOfMemoryError异常。 - 虚拟机栈:线程私有。生命周期和线程一致。描述的是Java方法执行的内存模型:每个方法在执行时会创建一个栈帧(Stack Frame)用于存放局部变量表、操作数栈、动态链接、方法出口。每个方法从执行到结束,对应一个栈帧从入栈到出栈的过程。
局部变量表:存放了编译期可知的基本数据类型、对象引用(指向堆中的对象)、returnAddress类型(指向一条字节码指令的地址)。
线程请求栈的深度大于虚拟机所允许的深度(无线递归),抛出StackOverflowError异常。
虚拟机扩展时无法申请到足够的内存,抛出OutOfMemoryError异常。
可以通过-Xss来设置大小,默认为1M
优化,一般将其调整小一点。 - 本地方法栈:和虚拟机栈类似,区别是虚拟机栈服务于java方法, 本地方法栈服务于虚拟机用到的Native方法,也会抛出StackOverflowError和OutOfMemoryError异常。
- 程序计数器:内存空间小,线程私有,唯一不会抛出OutOfMemoryError异常的区域。
作用是当前线程所执行的字节码的行号指示器。字节码解释器工作通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
Java类加载机制
类加载是将.class文件读入内存,将其放在方法区内,然后再Java堆中创建一个对象,用来封装方法区里的数据结构。
类的生命周期
- 加载:查找并加载二进制数据,在Java堆中创建java.lang.Class对象
- 验证:确保类加载的正确性。分为文件格式验证、元数据验证、字节码验证、符号引用验证四个阶段
- 准备:为静态变量分配内存,并初始化默认值
- 解析:将类中的符号引用转化为直接引用
- 初始化:为静态变量赋予正确的初始值
- 使用:new出对象程序中使用
- 卸载:执行垃圾回收
类加载器
- 启动类加载器:负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
- 扩展类加载器:该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器:ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
类的加载
类的加载有三种方式:
- 启动时由jvm加载
- Class.forName()动态加载
- ClassLoader.loadClass() 动态加载
双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
- 当 AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当 ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加载
- 若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 ClassNotFoundException
双亲委派的意义:
- 系统类防止内存中出现多份同样的字节码
- 保证Java程序安全稳定运行
垃圾回收
内存的垃圾回收主要集中在Java堆和方法区中,在程序运行时,这两部分的内存是动态变化的。
对象存活判断
-
引用计数:对各对象有个属性用于记录引用次数。被引用时加1,引用释放时减1.当次数为0时,可回收。无法解决对象循环相互引用的问题。
-
可达性分析:从GC Roots开始向下搜索,搜索走过的路径成为引用链。当一个对象和GC Roots之间没有任何引用链时,证明该对象没有被引用,可以被回收。
GC Roots包括: -
虚拟机栈中引用的对象
-
方法区中静态属性实体引用的对象
-
方法区中常量引用的对象
-
本地方法栈(Native方法)中JNI引用的对象
垃圾回收算法
-
标记-清除算法:标记出所有需要回收的对象,然后回收。
缺点:效率不高,会产生空间碎片 -
复制算法:将内存划分为两块,每次只使用其中的一块。当一块内存用完之后,将存活的对象复制到另一块内存,然后将使用过的内存空间一次性清除。
解决了标记-清楚算法的缺点,但会造成空间的浪费。大多数的新生代对象都不会熬过第一次GC,可以将堆空间按照8:1:1的比例划分为一个Eden和两个Survivor空间。每次使用Eden和一个Survivor空间,当GC时,将存活的对象复制到另一个Survivor空间上,然后清理其他两个空间。这样每次浪费10%的Survivor空间。如果存活对象大于10%,多出来的对象进入老年代。 -
标记-整理算法:针对于老年代的特点,把存活对象标记,然后移动到内存的一边,再清楚掉边界以外的内存。
-
分代回收算法:把java堆分为新生代和老年代。新生代中,每次GC都有大量对象死去,采用复制算法。老年代中,对象存活率高,使用标记-清除或标记-整理算法。
垃圾回收器
- Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
- ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
- Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
- Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
- CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
- G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
内存分配策略
- 优先分配到Eden区
- 大对象直接进入老年代
- 长期存活的对象进入老年代(默认15)
- 空间分配担保(复制算法,存活对象大于10%时,多出的对象进入老年代)
java内存模型分为新生代,老生代和永久代(jdk1.8以后替换为元空间)。java堆对应的是新生代和老生代,方法区对应的是永久代。
- Minor GC:发生在新生代的gc,复制算法
- Major GC / Full GC:发生在老年代的gc,标记-整理,标记-清除