JVM内存体系

其中灰色区为线程私有区,橙色区为线程公用区。GC的作用域也是橙色区公用区。
- 程序计数器:简而言之:程序计数器就是一个记录程序的执行位置的东西。以便多线程切换执行时到正确的位置。
- Java栈(也叫虚拟机栈):Java栈所要做的事就是:每个方法在执行的同时都会创建一个栈帧,这个栈帧存储的是局部变量表,操作数栈,动态链接,方法出口等等信息。每个方法从调用到执行完成的过程就对应Java栈的入栈和出栈的过程。
- 本地方法栈:本地方法栈与Java栈类似,它们之间的区别是Java栈执行的是Java方法,而本地方法栈为虚拟机使用的Native方法服务。
- 方法区(也称为永久代):和堆一样,方法区也是线程共享的区域。它存储已被虚拟机加载的类信息,常量,静态变量,编译后的代码等等
- 常量池: 常量池是方法区的一部分,它就是用来存放常量的。如String的intern()方法。
-
Java堆:Java堆是被所有线程共享的一块内存区域,几乎所有的对应都在Java堆中存放。Java堆是垃圾收集器工作的主要区域,因此也称为GC堆
从java8开始用元空间Metaspace VM Metadata 替换了Permanent Generation
java堆体系

垃圾收集算法理论
- 引用计数法:每个对象都维护一个引用计数器,每当一个地方引用它时,计数器加1,每当引用失效时,计数器减少1.当计数器的数值为0时,也就是对象无法被引用时,表明对象不可在使用,这种方法难以解决对象之间的相互循环引用的问题。JVM不使用这种方法。
-
可达性分析算法
JVM回收对象时,通过一系列名为GC Roots的对象作为起点集合,从这些GC Roots开始往下搜索,如果对象和GC Roots没有任何引用链相连时,表明这个对象已死。即使这个对象与其他对象(也是不可达对象)有相互引用关系。
image.png
可以作为GC Roots的对象 - 虚拟机栈中引用的对象(当前所有正在被调用的方法的引用类型的参数/局部变量/临时值)
- 方法区中的类静态属性对象
- 方法区中的常量引用对象
- 本地方法栈中引用的对象
public class Test1 {
private byte[] arr = new byte[1024*1024*1024];
private static Test1 t2 ;//2 静态对象 属于GC Root对象
private final Test1 t3 =new Test1() ;//3 常量对象 属于GC Root对象
// 4 native 方法中引用的对象
public static void main(String[] args) {
m1();
}
public static void m1(){
Test1 t = new Test1();//1虚拟机栈中的对象 属于GC Root对象
System.gc();
System.out.println("完成一次GC");
}
}
具体的垃圾收集算法
-
标记清除算法:分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。
缺点:
1 效率问题,标记和清除过程效率不高 。
2 空间问题,标记清除之后会产生大量不连续的内存碎片。
image.png
复制算法:主要用于回收新生代。
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉,这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆订的指针,按顺序分配内存即可,实现简单,运行高效。
缺点
1 效率问题:在对象存活率较高时,复制操作次数多,效率降低;
2 空间问题:內存缩小了一半;需要額外空间做分配担保(老年代)

复制算法用于清理新生代的垃圾, 但是清理新生代垃圾的时候,真正是这样做的。将新生代分为三部分, Eden区,Survivor1区,Survivor2区 其大小比例是8:1:1。每次使用Eden和一块Survivor区,当发生回收时将Eden和Survivor区还生存的对象一次性复制到另一块Survivor区上,然后再清理Eden和Survivor区。这样的回收操作进行15(这个数字是可以设置的)次之后,如果对象依然存活,那么就将对象放入到老年代区
-
标记-清除-整理(标记整理):主要用于回收老年代。
标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动。
优点:效率高
缺点:耗内存
image.png
分代的垃圾回收策略
分代的垃圾回收策略是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。新生代中 每次都有大量的对象死去,只有少量存活,因此适合复制算法,在老年代中由于对象存活率高,另外没有其他的内存对它进行担保,那么就是呀标记-整理算法。

新生代(Young Generation)
- 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象
- 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0/survivorFrom,survivor1/survivorTo)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。
年老代 (Old Generation)
- 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
- 内存比新生代也大很多(新生代占堆的三分之二,老年代占堆的三分之一),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
永久代(Permanent Generation)
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。(Oracle JDK8的HotSpot VM去掉“持久代”,以“元数据区”(Metaspace)替代之。)
新生代发生的GC也叫做Minor GC
新生代发生的GC过程如下:
1 回收时先将eden区存活对象复制到一个Form区,然后清空eden区;
2 当这个Form区也存放满了时,则将eden区和Form区存活对象复制到另一个To区,然后清空eden和这个From区;
3 此时From区是空的,然后将From区和To区交换,即保持To区为空
4 如此往复循环。From区和To区交换15(由JVM参数MaxTenuringThreshold决定,默认为15)次后,对象如果还存货,就将其存入老年代
我们知道在HotSpot虚拟机中存在三种垃圾回收现象,minor GC、major GC和full GC。对新生代进行垃圾回收叫做minor GC,对老年代进行垃圾回收叫做major GC,同时对新生代、老年代和永久代进行垃圾回收叫做full GC。许多major GC是由minor GC触发的,所以很难将这两种垃圾回收区分开。major GC和full GC通常是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是major GC
元空间与永久代的最大区别:
永久代使用堆内存,元空间直接使用本机物理内存,所以元空间仅受本地内存限制
JVM参数类型
- 标配参数 :这些参数基本不会变动,比如 java -version ,java -help等
- x 参数(了解) :如java -Xint -version,java -Xint -version 等等。
- xx参数 分两种:Boolean类型和 K--V类型
如何查看一个运行中程序的JVM参数是否开启(Boolean类型),值是多少(k-v类型)?
public class Test2 {
public static void main(String[] args) throws Exception{
System.out.println("--------------");
TimeUnit.MILLISECONDS.sleep(Integer.MAX_VALUE);
}
}

编辑参数,再次运行


K-V类型的参数:查看元空间(7 之前的永久代)大小,约20.79MB

修改后再看


或者使用jinfo -flags 59780 一次性查看这个进程的所有重要参数

坑:-Xms200m -Xmx200m 是属于什么参数?标配?x参数,xx参数?
Xms 是 -XX :initialHeapSize 的缩写,XmX 是 -XX :MaxHeapSize 的缩写
为什么一般来说Xms和-Xmx设置的值一般要一样?
如果二者不等,最开始的时候堆大小是Xms,随着heap内存消耗,jvm很有可能需要申请更大的空间直到Xmx;相似的,jvm在申请到Xmx空间时可能又用不了,这时会缩小jvm空间,这样,虽然可以动态调整jvm堆申请的大小,但是每一次调整都需要一定的系统开销,生产环境意味着一台机器或者一个容器只有一个服务,独占机器意味着没有必要调整jvm大小,直接分配Xmx就行了。否则每一次调整都可能会有开销。
-
Xss 设置单个线程栈的大小
-XX:+PrintFlagsFinal
-XX:+PrintFlagsInitial
-XX:+PrintCommandLineFlags -
-XX:PrintGCDetails 输出GC详细收集日志信息
-Xms10m -Xmx10m -XX:+PrintGCDetails 设定内存大小未10m
public class Test4 {
public static void main(String[] args) throws Exception{
byte[] ar = new byte[20*1024*1024];//尝试分配50m空间
System.out.println("--------------");
}
}
日志信息的查看

- -XX:SurvivorRatio=8 eden区与survivor区的比例,默认是8比1
- -XX:NewRatio=5 设置老年代的占比 剩下1给新生代(默认为2),这里就是老年代占5 新生代占1,新生代占整个堆的六分之一大小。
- -XX:MaxTenuringThreshold 设置垃圾的最大年龄(默认15,值的范围就是0-15)
强引用,软引用,弱引用,虚引用

- 强引用:当内存不足的时候,JVM开始回收垃圾,对于强引用对象,就算出现了OOM也不会对该对象进行回收,死都不收,因此强引用是造成Java内存写漏的主要原因之一。对于普通对象,如果没有其他引用关系,只要超过了引用的作用域或者显示的将强引用赋值为null,一般认为就是可以被收集的(当然:回收时机还是要看垃圾收集器的策略的)。
- 软引用:对于软引用对象来说,当系统内存充足的时候 软引用对象不会被回收,当系统内存不足时,软引用对象会被回收。软引用和弱引用通常会被使用与内存铭感的程序中,如 高速缓存。
- 弱引用:对于弱引用对象来说,只要垃圾回收开始执行,不管JVM空间是否足够,都会回收该对象占用的内存。
- 虚引用:虚引用必须和引用队列联合使用(ReferenceQueue),虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是对象进入finalization阶段可以被gc回收,换句话说 就是这个对象被收集器回收的时候收到一个系统通知以便后续处理。
public class Test1 {
public static void main(String[] args) throws Exception{
// test1();
// test2();
// test3();
// test4();
// hashMap();
test5();
}
//强引用测试
public static void test1(){
Object o1 = new Object();
Object o2 = o1;
o1 = null;
System.out.println("************");
System.gc();
System.out.println(o2);//o2是强引用 ,GC不会回收
}
// 软引用测试 1:系统内存足
public static void test2(){
Object o1 = new Object();
SoftReference<Object> softReference = new SoftReference<>(o1);
System.out.println(softReference.get());
o1 = null;
System.out.println("************");
System.gc();
System.out.println(softReference.get());//系统内存足时,这个对象打印不为null
}
// 软引用测试 2:系统不足
public static void test3(){
Object o1 = new Object();
SoftReference<Object> softReference = new SoftReference<>(o1);
System.out.println(softReference.get());
o1 = null;
System.out.println("************");
try {
/**
* 设置内存大小 -Xms5m -Xmx5m -XX:+PrintGCDetails
*/
byte[] arr = new byte[30*1024*1024];//制造30m大对象,撑爆内存
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("软引用对象:"+softReference.get());
}
}
// 弱引用测试
public static void test4(){
Object o1 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(o1);
System.out.println(weakReference.get());
o1 = null;
System.gc();
System.out.println("************");
System.out.println(weakReference.get());
}
//weakHashMap 测试
public static void hashMap(){
HashMap<Integer,String> map = new HashMap<>();
Integer key = new Integer(1);
map.put(key,"HashMap");
System.out.println(map);
key = null;//key引用 置为空 不会影响到Map
System.out.println(map);
System.gc();
System.out.println(map+"--HashMap--"+map.size());
System.out.println("************************");
WeakHashMap<Integer,String> weakHashMap = new WeakHashMap<>();
Integer key2 = new Integer(2);
weakHashMap.put(key2,"WeakHashMap");
key2 = null;
System.out.println(weakHashMap);
System.gc();
System.out.println(weakHashMap+"--WeakHashMap--"+weakHashMap.size());//对于WeakHashMap 存的键是弱键 ,当这些弱键不被引用时 GC回收会将其内存回收,同时弱键会被加入到queue中
}
// 虚引用测试
public static void test5() throws Exception{
Object o1 = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
//当gc释放内存时,会将对象加入到引用队列中,对象被加入到引用队列中 意味着对象将要被回收,可以重写对象的 finailze方法 做对象回收后的事情
//ReferenceQueue 不仅仅适用于虚引用,弱 软 强也可以使用
PhantomReference<Object> phantomReference = new PhantomReference<>(o1,referenceQueue);
System.out.println(phantomReference.get());
System.out.println(referenceQueue.poll());
System.out.println("**************");
o1 = null;
System.gc();
Thread.sleep(500);
System.out.println(phantomReference.get());
System.out.println(referenceQueue.poll());
}
}
常见的OOM异常
- StackOverflowError(栈溢出)
- Heap space(堆溢出)
- GC overhead limit exceeded(GC 作无用功,罢工了)
- unable to create new native Thread(不能再开启线程了,已经到了最大值了)
- Metaspace(元空间不足)
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class Test2 {
public static void main(String[] args) {
// stackOverflowError();
// heapSpace();
// GC_overhead_limit_exceeded();
// unable_to_create_new_thread();
metaspace_error();
}
public static void stackOverflowError() {
stackOverflowError();
}
public static void heapSpace(){
/**
* -Xms5m -Xmx5m
*/
byte[] arr = new byte[30*1024*1024];
System.out.println("-----------------");
}
/**
* GC回收时间过长时抛出的OOM,过长的定义是 超过98%的时间来做GC,并且回收不到2%的内存,连续这样的轮回下,假设GC不抛出 GC_overhead_limit_exceeded情况下的后果是
* 2%的内存被很快填满,GC再次回收 恶性循环,CPU占有率一直是100%,而GC收集却没有任何效果,所以GC罢工了
* 这个例子也是频繁full gc
*/
public static void GC_overhead_limit_exceeded(){
int i =0;
List<String> list = new ArrayList<>();
try {
/**
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
*/
while (true){
list.add(String.valueOf(++i).intern());
}
}catch (Exception e){
System.out.println("**********:"+i);
e.printStackTrace();
}
}
/**
* 一个应用能创建的线程数是有限的,linux 在/etc/security/limits.d/20-nproc.conf下可以配置,线程数
*/
public static void unable_to_create_new_thread(){
int i = 0;
try {
for(;;i++){
new Thread(()->{
try { TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);}catch (Exception e){}
}).start();
}
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("创建的线程数"+i);
}
}
/**
* 类模板
*/
static class OOMTest{}
/**
* -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m //没有效果的话,可以减小值
* 元空间(永久代)存放以下信息
* 虚拟机加载的类信息
* 常量池
* 静态变量
* 即使编译后的代码
* 下面不停的生产类 往元空间灌,撑爆它
*/
public static void metaspace_error(){
int i = 0;
try {
for (; ; i++) {
System.out.println(i);
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o,null);
}
});
}
}catch (Exception e){
System.out.println("i="+i);
e.printStackTrace();
}finally {
}
}
}
查看默认垃圾回收器
java -XX:+PrintCommandLineFlags -version

网友评论