1#JVM 中的内存可以划分为若干个不同的数据区域,主要分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区

1.1 程序计数器
在多线程环境下,为每个线程记录一个当前方法执行到的位置,当 CPU 切换回某一个线程上时,则根据程序计数器记录的数字,继续向下执行指令。
每条线程内部都有一个私有程序计数器。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
1.2 虚拟机栈
虚拟机栈也是线程私有的,与线程的生命周期同步。每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧;
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧
1.3 本地方法栈
本地方法栈和上面介绍的虚拟栈基本相同,只不过是针对本地(native)方法。
1.4 堆
Java 堆(Heap)是 JVM 所管理的内存中最大的一块,该区域唯一目的就是存放对象实例,几乎所有对象的实例都在堆里面分配,因此它也是 Java 垃圾收集器(GC)管理的主要区域
1.5 方法区
方法区(Method Area)也是 JVM 规范里规定的一块运行时数据区。方法区主要是存储已经被 JVM 加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。该区域同堆一样,也是被各个线程共享的内存区域。
2# GC 回收机制与分代回收策略
程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作,这几个区域内不需要过多考虑回收的问题。而堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。
可达性分析

对象M和K虽然被对J 引用到,但是并不存在一条引用链连接它们与 GC Root,所以当 GC 进行垃圾回收时,只要遍历到 J/K/M 这 3 个对象,就会将它们回收。
GC Root 对象
在 Java 中,有以下几种对象可以作为 GC Root:
Java 虚拟机栈(局部变量表)中的引用的对象。
方法区中静态引用指向的对象。
仍处于存活状态中的线程对象。
Native 方法中 JNI 引用的对象。
什么时候回收
不同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收。
1)Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
2)System.gc():在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。
如何回收垃圾
1)标记清除算法(Mark and Sweep GC)
Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
Sweep 清除阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清除。
优点:实现简单,不需要将对象进行移动。
缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。
2)标记-压缩算法 (Mark-Compact)
Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。
优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。
3)复制算法(Copying)
将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收.

优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
JVM分代回收策略
Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代.对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短。如果经过多次回收仍然存活下来,则将它们转移到老年代中.
新生代
新生代又可以继续细分为 3 部分:Eden、Survivor0(简称 S0)、Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代,一般采用的 GC 回收算法是复制算法
当 Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1是空的。
下一次 Eden 区满时,再执行一次垃圾回收。此次会将 Eden和 S0区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0变为空。
如此反复在 S0 和 S1之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。
老年代
一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),大于Eden区或者所设置的jvm参数时,则这个大对象会直接被分配到老年代上。
-XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法
另外,老年代中维护了一个 512 byte 的 card table,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个 card table 即可,大大提高了性能。
Java 命令的参数:
例如 VM agrs: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
当创建新的对象放不下Eden区时,会执行一次gc,尝试把Eden区的内容放进S1区,当S1区的空间还是不足时,会直接将当前Eden区的内存移至老年代,然后把新的对象放到Eden区。
再谈引用

内存不足时,频繁的对软弱引用的回收会导致”GC overhead limit exceeded”异常
利用引用队列进行监听优化
private static class Test{byte[] data = new byte[_1KB];}private static final int _1KB = 1024;private static final int _1MB = 1024 * 1024;private static final int _1GB = 1024 * 1024 * 1024;private static Set<SoftReference<Test>> set = new HashSet<>();private static ReferenceQueue<Test> queue = new ReferenceQueue<>();public static void main(String[] agrs) {for (int i = 0; i < _1GB; i++) {Test test = new Test();set.add(new SoftReference<Test>(test,queue));}}private void removeUnuesdCacheSoftReference(){Reference<? extends Test> poll = queue.poll();while (poll != null){set.remove(poll);poll = queue.poll();}}
