JMM (Java内存模型):
1 线程 私有区域:
(1) 程序计数器 (PC寄存器): 比较小的内存空间, 是当前 线程所执行的 字节码的行号指示器.
(2) 虚拟机栈: 平时所说的栈, 每个方法 执行的同时 都会创建一个 栈帧 (Frame) 用于存储 局部变量, 操作数栈, 执行环境上下文, 方法出口 等信息. 可能会抛出 StackOverFlowError (SOF栈溢出)异常 和 OutOfMemoryError (OOM内存溢出)异常. 栈的 可用空间并不大, 只能为 大小确定的 数据分配内存. 灵活性 不高, 但是栈的执行效率很高.
(3) 本地方法栈: 和上面作用一样, 而本地方法栈为 JVM执行的 Native本地方法服务. (JVM 是一个基于 C++ 实现的程序. 在 Java 程序执行过程中, 本质上也需要调用 C++ 提供的一些函数和操作系统底层进行一些交互)
2 线程 共享区域:
(1) Java堆: 在JVM 启动时创建, 所有的 对象实例(实例变量和成员函数), 数组(属于对象), new出来的对象 都要在堆上分配. 当没有引用指向该对象时,会变成可回收对象. 如果在堆中没有足够的内存完成实例分配并且堆也无法再拓展时, 将会抛出 OOM异常. 能存储大量数据, 而且能 动态分配 存储空间. 灵活性 高, 但堆的执行效率不如栈高.
其中的 字符串常量池: 存储 字符串对象, 或是字符串对象的引用.
(2) 方法区 (1.7位于java进程中). 元数据区(元空间区) (1.8位于本地内存): 用于存储 已被虚拟机加载的 类信息, 类变量(静态变量), 源代码中的常量命名, String常量. 即时编译器 编译后 的代码等.
其中 运行时常量池: 并不要求常量一定只能在编译期产生, 运行期间也可能产生 新的常量.
(如 final常量, 基本数据类型, 包装类, String类), 被放在 运行时常量池中.
其中 类常量池: 存放 类的版本、字段、方法、接口等描述信息.
(3) 直接内存: 新加入了 NIO(New Input/Output)类, 是一种 面向缓冲的, 非阻塞的, 新的I/O方式. 受到本机总内存影响, 可能会出现 OOM异常.
总结: 基础数据类型 直接在栈分配空间.
引用数据类型, 在栈空间分配一个 地址空间, 又在堆空间分配 对象的存储空间.
jdk1.7.
jdk1.8.
类加载: .java源文件通过javac命令编译后生成 .class文件.
(1) 类只会被加载一次.
(2) JVM会保证类加载时是 线程安全的.
1 类的 生命周期 (7个阶段) : 加载 验证 准备 解析 初始化(前五个是类加载过程) 使用和卸载.
2 类加载5个步骤: 加载 验证 准备 解析 初始化.
(1) 加载. 通过一个类的 全限定名称获取定义此类的二进制字节流, 在Java堆中生成一个代表该类的 java.lang.Class对象, 作为方法区域的数据访问入口.
(2) 验证. 保证 Class文件的字节流包含的信息 符合JVM规范.
(3) 准备. 给各种由 static 修饰的变量 设置默认值.
(4) 解析. 是将 常量池内的 符号引用替换成直接引用.
(5) 初始化. 给类变量 赋初值.
注意: 非静态代码则在类的使用阶段 (也就是实例化一个类的时候) 才会被初始化, 这个阶段已经不属于类加载了.
3 类加载器: 自定义类加载器 (必须继承ClassLoader), 应用程序类加载器, 扩展类加载器, 启动类加载器.
4 类加载机制: 双亲委派机制.
过程: 类加载器接收到 类加载的请求:
(1) 先从下往上, 找到父类加载器然后加载,每一层加载器都是如此, 直到找到 顶层的启动类加载器.
(2) 然后从上往下, 当启动类加载器表示自己无法加载这个类的时候, 子类加载器就会一层一层的加载直到加载完成. 当回到开始的发起者加载器还无法加载时, 并不会继续往下加载,而是抛出 ClassNotFound异常.
a 启动类加载器. c++编写, 涉及到虚拟机本地实现细节, 开发者无法直接获取到启动类加载器的引用, 所以不允许直接通过引用进行操作.
b 扩展类加载器. java编写, 指定位置中的类, 开发者 可以直接使用 标准扩展类加载器.
c 应用程序类加载器. java编写, 加载程序所在的目录, 如果应用程序中没有自定义类加载器, 一般情况下这个就是程序中默认的类加载器.
d 自定义类加载器. java编写, 可加载指定路径的 class文件.
双亲委派机制作用:
(1) 防止重复加载同一个 .class文件.
(2) 保证核心 .class文件 不被篡改.
GC: Java垃圾回收, Java进程在启动后会 自动创建单独的垃圾回收线程.
1 为什么Java 要有 GC机制? (GC的优点)
(1) 减少内存泄漏.
(2) 减少程序员工作量 (程序员 不需要考虑, 也不能 实时的调用垃圾收集器对某个对象进行垃圾回收).
2 GC的原理.
垃圾回收器通常是作为一个单独的低级别的线程运行, 不可预知的情况下 对内存堆中 已经死亡的或者长时间没有使用的 对象进行清楚和回收.
3 为什么有GC 还存在内存泄漏问题?
因为有的对象属于 持久态, 如hibernate 的Session(一级缓存), 这些对象不能被 GC 回收. 如果不及时关闭(close) 或者 清空(flush) 会导致内存泄漏.
4 Java中的引用类型.
(1) 强引用: 发生GC时 不会被回收. Java 虚拟机宁愿抛出 OutOfMemoryError 错误, 使程序异常终止, 也不会靠随意回收具有强引用的对象来解决内存不足问题.
(2) 软引用 (使用最多): 发生GC时 会被回收. 如果内存空间足够, 垃圾回收器就不会回收它, 如果内存空间不足了, 就会回收这些对象的内存.
(3) 弱引用: 发生GC时 会被回收. 和软引用比拥有更短暂的生命周期, 在垃圾回收器线程扫描它所管辖的内存区域的过程中, 一旦发现了只具有弱引用的对象, 不管当前内存空间足够与否, 都会回收它的内存. 不过由于垃圾回收器是一个优先级很低的线程, 不一定会很快发现那些只具有弱引用的对象.
(4) 虚引用: 如果一个对象仅持有虚引用那么它就和没有任何引用一样, 在任何时候都可能被垃圾回收器回收, 主要用来 跟踪对象被垃圾回收器回收的活动.
5 垃圾回收策略 (如何判断对象已死):
(1) 引用计数算法: 不能解决 循环引用算法.
(2) 可达性分析算法 (这是 Java采用的): GC Roots引用链(解决循环引用算法问题). 当一个对象到 GC Roots 没有任何引用链相连的话 (没有任何引用指向它), 就是 可回收对象.
6 什么时候发生GC:
(1) 大多数情况下, 直接在 Eden 区中进行分配. 如果 Eden区域 没有足够的空间, 那么就会发起一次 Minor GC.
(2) 如果 老年代没有足够空间的话, 那么就会进行一次 Full GC.
7 System.gc(): 此方法的调用是 建议JVM进行 GC,只是建议而非一定. 一般不使用此方法, 而是让虚拟机自己去管理它的内存.
8 finallize()(自救): 当一个已经不被任何变量引用的对象, 垃圾回收器准备回收该对象所占用的内存时, 先判断 是否有 finalize()方法, 有就执行finalize() 方法. 任何一个对象的 finalize()方法 都只会被系统自动调用一次. 如果相同的对象在逃脱一次后又面临一次回收, 它的 finalize()方法不会 被再次执行. 一般避免使用 finalize方法, 因为不确定性, 运行代价太大.
9 垃圾回收的内存划分: 堆(新生代+老年代), 方法区(永久代).
(1) 线程私有区域(虚拟机栈, 程序计数器, 本地方法栈) 无需考虑内存回收问题.
(2) 垃圾回收 也会发生在永久代, 永久代满了会触发一次 Full GC.
(3) 新生代 = eden(8/10) + from(1/10) + to(1/10).
Full GC >= Major GC.
Minor GC 和 Full GC 的区别.
1 垃圾回收的时机: Minor GC 和 Majar GC 的触发条件.
2 垃圾回收算法(4个):
(1) 标记-清除算法 (老).
特性: 效率不高, 易产生空间碎片问题 (大量不连续的内存碎片).
(2) 复制算法 (新).
优点: 效率高.
缺点: 内存使用率不高, 只有50%.
新生代算法 (新生代对象存活率低). 在对象 存活率较高时会进行比较多的复制操作, 效率会变低.
(3) 标记-整理算法 (老).
提高了内存的利用率. 标记过程与(1) 一致, 但接着不是直接对可回收对象进行清理, 而是让所有存活对象都向一端移动, 然后直接清理掉端边界以外的内存.
(4) 分代收集算法.
把Java堆分为 新生代和老年代, 每个代采用 不同的垃圾回收算法.
3 垃圾回收过程:
Minor GC 的处理过程. 内存分配与回收策略:
(1) 新创建的对象 优先在Eden分配. 当内存不够下一个新的对象的内存时,就会触发GC,先把 edan区 + 一个s区 的存活的对象转移到另一个s区. 然后创建对象…重复进行上述过程.
(2) 大对象直接进入老年代: 很长的字符串以及很长的数组.
(3) 长期存活的对象将进入老年代: 对象在Survivor空间中每"熬过"一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将晋升到老年代.
-> 动态对象年龄判定: 相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代而无需保证上面的大于等于15岁.
(4) 空间分配担保: 转移时一旦另一个s区内存不够时, 把全部的要转移的对象通过 分配担保机制 转移到老年代. 如下所示:
4 垃圾收集的影响: 用户线程的暂停. Stop-The-World(STW).
(1) >单次暂停时间和 总的暂停时间是反比.
(2) 吞吐量和 单次暂停时间成正比.
(3) 吞吐量(单次暂停时间) 和 用户体验成 反比.
5 垃圾收集器:
(1) 新生代垃圾收集器和 老年代垃圾收集器 可以搭配使用. Parnew 经常搭配 CMS使用. MSC是CMS出现错误的备案.
(2) 上面的是复制算法(新), 下面的是标记-清除算法和标记-整理算法(老).
(3) Serial 是串行的, Paraller 和 CMS 是并发的.
(4) 吞吐量优先:Parallel Scavenge + Parallel Old.
(5) 用户体验优先: (1) ParNew + CMS (2) G1.
(6) CMS 和 G1 是分阶段执行的. 有的阶段是 需要暂停用户线程 (单次暂停时间不长), 有的阶段可以 同时执行. 而其它5种收集器 都会暂停用户线程.
6 CMS优点: 高并发, 低停顿, 用户体验优先.
标记-清除算法, 整体来看垃圾回收和用户线程是 并发执行的.
CMS缺陷: 抢占CPU资源, 无法处理浮动垃圾(可以加一次major gc解决), 空间碎片问题(算法决定的).
7 G1收集器 (唯一一款 全区域(新生代+老年代) JDK1.7 以后提供的的垃圾回收器).
整体 标记整理算法, 局部 复制算法.
8 什么是内存泄漏?
有些内存空间使用完毕之后没有释放(没有被收回), 导致一直占据着该部分内存, 直到程序运行结束.
引起 内存泄漏 的原因.
1 使用单例造成的内存泄漏: 因为单例的静态特性导致他的生命周期和应用的生命周期一样长,如果一个对象已经不需要再使用了,而单例对象还持有这这个对象的引用,就会导致该对象不能被正常回收.
2 资源未关闭会造成内存泄漏;
3 使用完集合容器后, 没有把引用从集合中清空.
9 引起 内存溢出 的原因.
(1) 内存中加载的 数据过多, 如一次从数据库中取出过多数据.
(2) 集合中有对对象的引用, 使用完后未清空, 使JVM不能回收.
(3) 代码有 死循环 或产生 过多的对象实体.
(4) 启动 参数内存值 设置的过小.
解决方法.
(1) 修改 JVM启动参数, 直接增加内存. (-Xms, -Xmx 参数设置)
(2) 检查 错误日志, 查看OOM 错误之前是否有其它异常或错误.
(3) 分析代码, 找出可能出错的位置.
10 查看GC日志.
点击三角键, -> Edit运行程序 -> VM options 中设置JVM参数. 然后运行.
JVM参数如下:
-XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
//堆:20m,新生代10m,eden8m,s区1m,t区10m
public class EdenAllocate {
private static final int _1MB = 1024 * 1024;
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
// 出现Minor GC:对应的GC日志是[GC 或者[Full GC
//回收前:allocation1,2,3在e区
//回收后:1,2,3在老年代
allocation4 = new byte[4 * _1MB];
//allocation4对象创建好以后,gc日志信息为:Heap ...
}
public static void main(String[] args) throws Exception {
testAllocation();
}
}
11 常用JVM监控工具(重点):
cmd运行:
(1) jps. 显示进程pid + 进程名称.
(2) jstack. 结合 jps 检测线程状态, 死锁等情况.
(3) jmap / jhat. 结合 jps 查询 堆内存相关信息.