Native情景引入
看一段线程代码
public class MyTest {
public static void main(String[] args) {
Thread t=new Mythread();
t.start();
}
}
class Mythread extends Thread{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"====>"+i);
}
}
}
我们点击start方法查看源码 ,发现里面有个native关键字
Native关键字是什么?
native :凡是带了native 关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!
会进入本地方法栈
调用本地方法接口(JNI Java Native Interface)
JNI:扩展java使用,融合不同的变成语言,c、c++ python等语言
Java诞生的时候C、C++横行,想要立足,必须要有调用c、C++的程序~
它在内存区域中专门开辟了一块标记区域: Native Method Stack,登记native方法
在最终执行的时候,通过JNI加载本地方法库中的方法
关于栈
栈:栈内存,主管程序的运行,生命周期和线程同步;
程序运行,最先就是把main方法压入栈
线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题,—旦线程结束,栈就Over ! |
线程是最小的执行单位,他不具备记忆功能,他只负责去干,那这个记忆就由:程序计数器来记录
程序计数器是用在多线程的情况下,他会记录每个线程的执行位置,当cpu切换回来的时候,就可以找到之前的位置,继续执行
栈存放:8大基本类型+对象引用+实例的方法
8大基本类型:int short long byte double float boolean char
栈对应的是线程,栈帧对应的线程里的方法
栈里有多个栈帧
子帧调用子程序,父帧回到原来程序的执行位置,并继续执行
栈内存和堆内存的区别:
1、栈内存用来存放基本类型的变量和引用变量,堆内存用来存储java中的对象,无论是成员变量,局部变量,还是类变量,他们指向的对象都存储在堆内存中。
2、栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属的线程中可见,即栈内存可以理解成线程的私有内存;堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。一个JVM只有一个堆内存
3、如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出java.lang.stackoverflowerror 错误(SOF);如果是堆内存内没有可用的空间存储生成的对象,jvm会抛出java.lang.outofmemoryerror错误(OOM)。
4、栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈会很快充满。-Xss设置每个线程堆栈内存的大小 -Xms可以设置堆内存开始时的大小。
总结:
JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个 JVM 的所有线程共享。
关于堆
首先我们要知道目前有三大Java虚拟机:HotSpot,oracle JRockit,IBM J9。
JRockit和J9不存在永久代这种说法。这里只讨论HotSpot虚拟机,这也是目前使用的最多的JVM。
堆又可以分为三个区域
jvm 堆内存结构图
JDK8之前
JDK8以后再堆中移除了永久代(区),把这个区域移到了本地内存中,即元空间!!
在HotSpot虚拟机中存在三种垃圾回收现象,minor GC、major GC和full GC。对新生代进行垃圾回收叫做minor GC,对老年代进行垃圾回收叫做major GC,同时对新生代、老年代和永久代进行垃圾回收叫做full GC。许多major GC是由minor GC触发的,所以很难将这两种垃圾回收区分开。major GC和full GC通常是等价的,收集整个GC堆。
年轻代
也叫新生代,顾名思义,主要是用来存放新生的对象。新生代又细分为 Eden区、SurvivorFrom区、SurvivorTo区。
新创建的对象都会被分配到Eden区(如果该对象占用内存非常大,则直接分配到老年代区), 当Eden区内存不够的时候就会触发MinorGC(Survivor满不会引发MinorGC,而是将对象移动到老年代中),
在Minor GC开始的时候,对象只会存在于Eden区和Survivor from区,Survivor to区是空的。
Minor GC操作后,Eden区如果仍然存活(判断的标准是被引用了,通过GC root进行可达性判断)的对象,将会被移到Survivor To区。而From区中,对象在Survivor区中每熬过一次Minor GC,年龄就会+1岁,当年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认是15)的对象会被移动到年老代中,否则对象会被复制到“To”区。经过这次GC后,Eden区和From区已经被清空。
“From”区和“To”区互换角色,原Survivor To成为下一次GC时的Survivor From区, 总之,GC后,都会保证Survivor To区是空的。
奇怪为什么有 From和To,2块区域?
这就要说到新生代Minor GC的算法了:复制算法
把内存区域分为两块,每次使用一块,GC的时候把一块中的内容移动到另一块中,原始内存中的对象就可以被回收了,
优点是避免内存碎片。
老年代
随着Minor GC的持续进行,老年代中对象也会持续增长,导致老年代的空间也会不够用,最终会执行Major GC(MajorGC 的速度比 Minor GC 慢很多很多,据说10倍左右)。Major GC使用的算法是:标记清除(回收)算法或者标记压缩算法。
标记清除(回收):1. 首先会从GC root进行遍历,把可达对象(存过的对象)打标记
2. 再从GC root二次遍历,将没有被打上标记的对象清除掉。
优点:老年代对象一般是比较稳定的,相比复制算法,不需要复制大量对象。之所以将所有对象扫描2次,看似比较消耗时间,其实不然,是节省了时间。举个栗子,数组 1,2,3,4,5,6。删除2,3,4,如果每次删除一个数字,那么5,6要移动3次,如果删除1次,那么5,6只需移动1次。
缺点:这种方式需要中断其他线程(STW),相比复制算法,可能产生内存碎片。
标记压缩:和标记清除算法基本相同,不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩,这样就可以解决内存碎片问题。
当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
永久代(元空间)
永久代又被人称为‘非堆’。方法区是抽象概念, 永久代和元空间都是对方法区的具体实现
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间,Metaspace)的区域所取代。
因此常量池平时存放在堆中,运行时的常量池存放在永久代(非堆)中。
这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息~,这个区域几乎不存在垃圾回收! 关闭VM虚拟就会释放这个区域的内存~
值得注意的是:元空间并不在虚拟机中,而是使用本地内存(之前,永久代是在jvm中)。这样,解决了以前永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出,毕竟是和老年代共享堆空间。java8后,永久代升级为元空间独立后,也降低了老年代GC的复杂度。
参考文章:
jvm之年轻代(新生代)、老年代、永久代以及GC原理详解、GC优化 - 走看看
如果出现了OOM怎么办?
解决方案:
1、修改JVM启动参数,直接增加内存。
JVM默认可以使用的内存为64M,Tomcat默认可以使用的内存为128MB,对于稍复杂一点的系统就会不够用。在某项目中,就因为启动参数使用的默认值,经常报“Out Of Memory”错误。因此,-Xms,-Xmx参数一定不要忘记加。
2、找出可能发生内存溢出的位置,并解决:
检查代码中是否有死循环或递归调用。
检查是否有大循环重复产生新对象实体。
检查对数据库查询中,是否有一次获得全部数据的查询。
检查List、Map等集合对象是否有使用完后,未清除的问题。
使用内存查看工具动态查看内存使用情况。
Dump文件
怎么去分析找到出现的内存溢出(OOM)问题?
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfHemoryError
OutOfHemoryError出现OOM错误,会在同级目录下生成个 文件
-Xms设置初始化内存分配 默认大小1/64
-Xmx设置最大分配内存,默认1/4
-XX: +PrintGCDetails //打印GC垃圾回收信息
-XX : +HeapDumponoutofMemoryError //oom DuMP