JVM内存与GC简单理解
内存与GC
顺序是:Java程序编译为.class–>加载到运行时数据区–>最后执行
整个内存(运行时数据区)分为:一堆二栈一方法,方法内部常量池
一堆:
- 存放对象,GC堆。
- 分为:新生代+老年代+永久代。新生代:老年代 = 1:2。永久代,是方法区的一种实现
- 新生代又分Eden和两个幸存区。Eden:幸存区 = 8:1:1
GC
- 可达性算法:GC Root引用链不可达的对象都是可回收的。
- GC Root:可以作为GC Root的:
- 栈中引用的对象;
- 静态,常量引用的对象
- Native方法引用的对象
局部GC
- 新生代回收:Minor GC
- 老年代回收:垃圾收集器的CMS就是这种模式
- 收集新生代和部分老年代
Minor GC触发:
当Eden区满的时候,触发
Minor GC表现:
- 将Eden和幸存区from中的对象复制到幸存区to中
- 将from和to换指针,此时,from内是存活的对象,to是空的
- Eden再满的时候执行第一步
对象晋升
- 对象到幸存区to的时候,年龄+1,默认15的时候进入老年代
- 幸存区使用50%的时候,高年龄对象进入老年代
- Minor GC触发,Eden+from对象大小 > to内存大小,溢出对象到老年代
全部GC
- 全部收集:收集整个堆,Full GC
触发条件:
- 老年代内存满了
- System.gc()。在没有线程运行的时候触发;内存大小不足的时候,触发
GC算法
- 标记清除:标记要回收的,进行清除。结果:效率低,空间不连续
- 复制算法:内存分两个区域A和B,将不回收的都移动到一个区域中,清空另一个区域
- 标记整理:标记不回收的,都整理到前面,清除后面的全部内存
新生代使用的就是复制算法:Eden+幸存区
老年代内存大,使用标记清除或者标记整理算法好
大对象,比如大的数组或者字符串等,需要大量连续空间。直接进入老年代
二栈:
Java虚拟机栈 + Native方法栈
- 一个线程持有一个方法栈。
- 栈内元素是栈帧。栈帧对应的是线程中调用的方法。方法执行的时候创建栈帧入栈
可能异常:- 线程调用方法过多,超出栈的深度。比如无限递归,报StackOverFlowErroe
- 栈要扩展的时候,申请不到足够的内存,OOM
一方法:
方法区,是JVM的规范。里面存放加载的类的信息,常量,静态变量等。是线程共享的。
前面提到的永久代就是方法区的一种实现。
- 永久代:1.8以前,方法区的实现是永久代,是与老年代相连的内存
- 元空间:1.8以后,方法区的实现是元空间,使用直接内存。只受本地内存影响
为什么用元空间替代永久代
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的
大小指定比较困难,太小容易出现永久代溢出,太大则容易导致
老年代溢出。(这个可以理解为永久代大小不好确定,导致OOM) - 永久代会为GC 带来不必要的复杂度,并且回收效率偏低。
- Oracle 可能会将HotSpot 与JRockit 合二为一。
常量池:
方法内部常量池,也就是说常量池存放在方法区内部
- class文件常量池:又称静态常量池,存放class文件中的基本类型常量,字符串,符号引用等
- 运行时常量池:class文件加载结束后,将静态常量池内容移动到运行时常量池中。将静态常量池中的部分符号引用转换为直接引用。
将静态常量池中的部分符号引用转换为直接引用。比如一些静态方法,私有方法或者构造方法等。这些不能被重写或者改变的。就会转换为直接引用。
其他方法是在被第一次调用的时候将符号引用转换为直接引用。
符号引用可以理解为我们定义的名称,直接引用可以理解为内存地址。