今天整理的内容为 直接内存和垃圾回收
正常来说需要一步一步才能访问到jvm的堆内存
但是直接内存: 过 java.nio(java I/O库)直接缓冲区直接分配 绕过堆,由操作系统直接管理的本地内存区域,主要用于提升高性能I/O操作的速度 回收成本高不受JVM内存回收管理 读写性能高
这样就可以直接访问jvm的堆内存
其语句为:
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(); 开辟分配直接内存
同样这样也会造成内存溢出问题
我们来验证其直接内存的过程原理:
public class 直接内存 {static int _1G = 1024 * 1024 * 1024;public static void main(String[] args) throws IOException {ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1G);System.out.println("分配");//等待用户输入EnterSystem.in.read();System.out.println("释放");//失去所有对直接缓冲区的引用byteBuffer = null;System.gc();System.in.read();} }
大致就是看到直接内存如何分配和gc回收是否会释放掉它(注:我们需要在任务管理器中查看进程变化) 创建分配出来
然后gc回收 发现释放掉了
关于过程:
需要我们进入直接内存分配方法中
创建了DirectByteBuffer
对应关联了cleaner
然后GC触发后 就执行cleaner.run UNSAFE.freeMemory释放直接内存
大致流程就是: 触发GC回收->DirectByteBuffer->关联Cleaner->Cleaner.run() UNSAFE.freeMemory->释放native堆上内存
关于这个是重点也是难点 我尽量理解这样的
首先一个对象能不能回收JVM如何知道
1.引用计数算法 给对象添加一个引用计数器 引用失效减1 当变为0可被回收 但是如果两个对象循环引用 就永远不会为0 JVM不采用这个 2.可达性分析算法 从GC Roots 为开始点 然后搜索 能搜索到的对象都是存活的 不可搜索到的可被回收 JVM采用此算法 线程栈中的局部变量 静态字段(static) 锁(synchronized)/JNI、本地线程 ThreadLocal 上的值
可达性分析算法 就是从根开始找 找不到的对象就是不可存活对象 可回收掉
new 对象(); 即为强引用 被强引用关联的对象不可被回收 之前关于字符串常量池那些整理也有
上述五种引用我们来关联垃圾回收验证其内存溢出问题:
验证强引用-软引用内存溢出问题: public class Ying_yong {static final int _4MB = 4 * 1024 * 1024;public static void main(String[] args) {//强引用ArrayList<byte[]> list = new ArrayList<>();for (int i = 0; i < 5; i++) {list.add(new byte[_4MB]);} // soft();}//软引用 内存不足后 触发GC回收掉了public static void soft() {ArrayList<SoftReference<byte[]>> list = new ArrayList<>();for (int i = 0; i < 5; i++) {SoftReference<byte[]> sf = new SoftReference<>(new byte[_4MB]);list.add(sf);}for (SoftReference<byte[]> softReference : list) {System.out.println(softReference.get());}} }
注:我们设置堆大小20M
首先运行强引用 内存溢出 垃圾回收未触发
运行软引用 触发了GC垃圾回收 并打印出了东西 前面几个对象已经被回收掉了
标记存活的对象(即GC Roots搜索到的对象) 清除掉未被标记(即可回收)的对象 速度效率很快 但是缺点在于内存空间造成不连续大量碎片 这个很简单理解 GC Roots标记存活对象 没被标记的即可回收的对象 被回收
所有存活对象向一侧移动 然后移动的时候清理掉可回收的对象 速度效率慢 但是整体紧凑 不会出现大量碎片空间
分成两块内存大小一样 然后将第一块标记好的存活对象复制到第二块 清除掉第一块只剩下的可回收对象 交换以此反复 缺点就是占用双倍内存空间 也不会造成内存碎片 至于from区和to区是举例用的 后面分代回收会着重讲
接下来是重点即分代回收 首先分为新生代(年轻代)和老年代
新生代存放新创建的对象 采用复制算法 Minor GC 老年代存放多次使用存活时间长的对象 采用标记-清除/整理 算法 Full GC
分代回收的过程在复制算法讲过就是如下:
首先将eden和from区可存活的对象放入to区 年龄+1 若对象寿命达到阈值则移到老年代 然后清空eden和from区可回收的垃圾 交换from和to区 以此反复
大部分时间只会进行 Minor GC 只有内存极端不足或老年代对象占用过多时才触发 Full GC Minor GC 通过复制算法快速清理大部分垃圾,避免了 Full GC 的开销 Full GC 只在必要时才触发,采用 mark‑compact(标记‑压缩)以解决老年代碎片问题
我们当然需要代码实际中来验证:
public class Test_GC_options {static final int _7MB = 7 * 1024 * 1024;static final int _10MB = 10 * 1024 * 1024;public static void main(String[] args) {} }
加入参数 打印参数观察
堆大小20M 新生代10M 单线程垃圾收集器 打印GC基础信息 -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
不加入任何对象 运行
我们拿出来打印信息Heap堆def new generation新生代 我们设置的10M total 9216K, used 2352K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)eden space 8192K, 28% used [0x00000000fec00000, 0x00000000fee4c010, 0x00000000ff400000)from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)tenured generation 老年代 10M total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000) Metaspace 元空间 used 579K, committed 768K, reserved 1114112K class space used 36K, committed 128K, reserved 1048576K
我们发现eden区已经用了28%
这个时候我们强引用加入存活对象 看变化
ArrayList<byte[]> list = new ArrayList<>();list.add(new byte[_7MB]);
加入7M后 因为eden区不够了 放入了from区 实际上是放到了to区 然后from区和to区互换了
这个时候我们在加入10M 这个时候新生代肯定顶不住了
list.add(new byte[_10MB]);
首先肯定溢出了 总共才20M 然后我们发现新生代不够了 就放入了老年代
至此验证完毕
关于GC参数大概就是jvm的设置参数 我记得的: -Xms 初始堆大小 -Xmx堆最大大小 两个设为相同 避免运行时扩容带来的停顿 -Xmn 新生代大小 -XX:+PrintGCDetails -verbose:gc 打印详细GC信息 基础GC打印 -XX:+UseSerialGC 单线程垃圾收集器
更多参考:https://pdai.tech/md/java/jvm/java-jvm-param.html
1.串行垃圾回收器Serial(复制算法) Serial Old(标记-整理算法) -XX:+UseSerialGC 只使用一个线程进行垃圾回收工作 前者工作于新生代 后者老年代 2.吞吐量优先垃圾回收器 -XX:+UseParallelGC 大多数jdk8+版本默认并行GC 并行多线程工作 3.响应时间优先处理器 CMS 收集器 XX:+UseConcMarkSweepGC 采用标记-清除算法 JDK18+已移除 四个阶段 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象 速度很快 需要停顿 并发标记: 进行 GC Roots Tracing 的过程 它在整个回收过程中耗时最长 不需要停顿 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 需要停顿 并发清除: 不需要停顿
这里还需要介绍一个G1(Garbage-First):
首先我们需要区别一个东西 一中总结的GC(1)表示的这次GC的序号 而不是接下来我们要说的G1
核心就是:把堆划分为若干相等大小的Region (整体标记-整理算法实现 局部(两个Region之间)复制算法实现)
关于每个Region区 过程类似分代回收但也有区别 1.先在Young Region对象分配 达到阈值分配到老年代 触发Minor GC 2.若老年代也达到阈值就触发并发标记 3.有条件性的垃圾回收
另外插一嘴:大小达到甚至超过分区大小一半的对象称为巨型对象 GC会把它直接放进老年代 可减少(新生代处理时)停顿
我就说俩问题 具体调优不是一成不变的 会根据环境和机器的情况调整 【新生代是否越大越好吗 不一定 可能会导致新生代的Minor GC停顿时间变长 若晋升率过高可能触发更多的Full GC
Full GC和Minor GC频繁 空间内存不足导致 解决方案均衡新生代和老年代大小比例】
更多调优参考:https://pdai.tech/md/java/jvm/java-jvm-oom.html
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/995462.shtml