尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JVM随记

JVM随记 一、JVM垃圾的来源平时开发写的接口数据流转一般是从前端发起HTTP请求到后端前端 ↓ Controller ↓ Service ↓ Dao ↓ 数据库我们在对接口进行实现的时候会创建很多对象例如User user new User(); UserDTO userDTO new UserDTO();这些对象一般是存放在堆内存中的。而我们在方法中定义的局部变量比如User user new User();这里的user是一个对象引用这个引用一般存放在当前线程的栈帧中而真正的User对象存放在堆中。可以简单理解为栈 user ─────────────→ 堆 User对象当方法执行结束以后如果没有其他地方再引用这个User对象那么这个对象就没有办法再被程序访问到了。这时候这个对象就会变成垃圾对象。但是需要注意对象变成垃圾并不代表JVM马上就会把它回收。什么时候回收是由垃圾回收器决定的。二、JVM如何识别和清除垃圾Java中的线程是共享堆内存的但是每一个线程都有自己的栈内存。每一个线程在执行方法的时候都会创建一个对应的栈帧。栈帧主要用来保存当前方法执行过程中需要使用的数据例如局部变量对象引用操作数等例如public void test() { User user new User(); }可以简单理解为线程栈 test()栈帧 ┌─────────────┐ │ user │ ──────────→ User对象 └─────────────┘ ↑ │ 堆JVM在判断一个对象是不是垃圾的时候并不是简单地看“有没有一个变量指向它”而是使用可达性分析算法。它会从一些特殊的对象也就是GC Roots开始往下寻找。例如GC Roots ↓ User引用 ↓ User对象 ↓ Address对象那么User对象 → 存活 Address对象 → 存活因为它们都可以从GC Roots找到。如果一个对象GC Roots ↓ 找不到这个对象那么这个对象就会被认为是不可达对象也就是垃圾对象。常见的GC Roots包括当前线程栈帧中的引用静态变量引用常量引用一些JNI引用等所以这里需要注意GC Roots不只是线程栈中的引用只是线程栈中的局部变量引用是我们最容易理解的一种。三、JVM垃圾分类与分代处理Java程序运行的时候会创建大量对象。但是这些对象的生命周期是不一样的。有一些对象刚创建没多久就没用了。例如用户注册 ↓ 创建User对象 ↓ 创建UserDTO对象 ↓ 数据处理完成 ↓ 请求结束 ↓ 这些临时对象可能就不再使用了这些对象的生命周期比较短。还有一些对象可能会存活很长时间。例如用户登录 ↓ 用户信息保存到Session ↓ 长时间存在所以JVM根据对象存活时间的不同把堆内存进行了分代。经典的分代模型堆 │ ├── 新生代 │ ├── Eden │ ├── Survivor 0 │ └── Survivor 1 │ └── 老年代可以简单理解成新创建的对象 ↓ 新生代 ↓ 存活时间越来越长 ↓ 老年代四、新生代的特点以及它的回收算法新生代最大的特点就是对象创建非常频繁但是大部分对象很快就会变成垃圾。例如一次HTTP请求请求进来 ↓ 创建A对象 创建B对象 创建C对象 创建D对象 ↓ 请求处理完成 ↓ A、B、C、D可能很快就没有用了所以新生代里面虽然对象创建得很多但是经过一次GC之后真正存活下来的对象通常比较少。因此新生代比较适合使用复制算法。1. Eden区新创建的对象通常会首先进入Eden区。例如Eden [A][B][C][D][E]当Eden空间不够的时候就会进行一次Young GC。JVM会进行可达性分析。假设A → 存活 B → 垃圾 C → 存活 D → 垃圾 E → 存活那么Eden [A][B][C][D][E] ↓ ↓ ↓ 存活 垃圾 存活 ↓ 复制到Survivor最终Eden Survivor 清空 [A][C][E]Eden里面的垃圾直接被回收存活的对象复制到Survivor区。五、为什么会有S0和S1Survivor区实际上还有两个S0 S1它们主要是为了配合复制算法使用。例如第一次GCEden S0 ↓ 找到存活对象 ↓ 复制到S1然后下一次GCEden S1 ↓ 找到存活对象 ↓ 复制到S0再下一次Eden S0 ↓ 复制到S1所以它们会不断进行交换Eden ↓ S0 ↓ S1 ↓ S0 ↓ S1 ↓ ......这里的核心思想就是每次GC只把存活的对象复制到另外一个Survivor区然后把原来的区域整体清空。六、对象什么时候进入老年代对象每经过一次Young GC如果还存活那么它的年龄一般就会增加。例如第一次Young GC 对象还活着 年龄 1 第二次Young GC 对象还活着 年龄 1 第三次Young GC 对象还活着 年龄 1这样不断增加。当对象达到一定的年龄之后就可能从年轻代进入老年代。很多资料中会说对象年龄达到15岁就进入老年代。这个说法不够准确。更准确地说经典HotSpot情况下年龄阈值常见是15但并不是说所有情况下都一定15次GC之后进入老年代。还可能因为Survivor空间不够对象太大垃圾收集器的具体策略导致对象提前进入老年代。所以记成新对象 ↓ Eden ↓ Young GC ↓ Survivor ↓ 继续存活 ↓ 年龄增加 ↓ 满足晋升条件 ↓ 老年代就可以了。七、老年代的特点以及它的回收算法老年代里面的对象一般都是存活时间比较长的对象。也就是说新生代 创建很多对象 ↓ 大部分很快死亡 ↓ 少部分存活而老年代长期存活的对象比较多 ↓ 所以存活对象比例通常比较高因此老年代的回收和新生代是不太一样的。传统的垃圾回收算法里面老年代经常会使用标记-清除标记-整理八、标记-清除算法标记-清除其实很好理解。假设老年代里面有[A][B][C][D][E][F]经过可达性分析之后A → 存活 B → 垃圾 C → 存活 D → 垃圾 E → 存活 F → 垃圾第一步就是标记垃圾对象。[A][X][C][X][E][X]第二步清除垃圾对象。[A][空][C][空][E][空]这就是标记-清除。它的问题就是清除垃圾之后会产生内存碎片。例如[A][空][C][空][E][空]虽然这里总共有3块空间是空的。但是如果现在来了一个需要连续空间的大对象[大对象]可能还是放不进去。所以就出现了明明还有空间但是因为空间不连续所以无法存放对象。这就是内存碎片。九、标记-整理算法为了解决标记-清除产生的内存碎片问题就可以使用标记-整理。还是[A][空][C][空][E][空]先标记存活对象[A][C][E]然后把存活对象移动到一起[A][C][E][空][空][空]这样存活对象集中在前面 空闲空间集中在后面这样就可以减少内存碎片。所以标记-清除 [A][空][C][空][E][空] ↓ 产生内存碎片而标记-整理 [A][空][C][空][E][空] ↓ [A][C][E][空][空][空]这样空间就更加连续。十、把整个JVM垃圾回收串起来整个过程其实可以理解成Java程序运行 ↓ 不断创建对象 ↓ 对象进入Eden ↓ Eden空间不足 ↓ Young GC ↓ ┌──────────────┐ │ │ 垃圾对象 存活对象 ↓ ↓ 回收 Survivor ↓ 再次Young GC ↓ 年龄增加 ↓ 满足晋升条件 ↓ 老年代 ↓ 老年代GC ↓ 标记 / 清除 / 整理
返回列表