JVM内存模型
JMM,长下面这个样子:
其中,堆和栈区自然不做介绍了,主要介绍:
程序计数器:线程私有的,记录正在执行的字节码地址,换言之,它告诉我们某线程执行到了那里,分支、循环等也会依赖这个来执行,这一区域不会发生OOM问题
栈:就是正常所指的栈,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame )用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程,这一区域会发生StackOverflow问题
堆:就是正常所指的堆,这里是GC的主要区域。
方法区:线程私有的,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,运行时常量池也包含在里面。
String常量池:单独拿出来说一下,这部分在jdk1.7之前是包含在运行时常量池中的,之后归属于堆中。
OOM与StackOverflow Error
OOM(OutOfMemory)问题可能比较常见,引发的诱因包括栈溢出、堆溢出和方法区内存溢出,一般都是不恰当的引用造成了达到最大内存或是业务中村你在内存泄漏、堆内存分配不合理引起业务内存不足等,举个最简单栗子:
class OOM { //其中 设定java -Xmx12m 以下操作将直接引起内存分配不足溢出 static final int SIZE=2*1024*1024; public static void main(String[] a) { int[] i = new int[SIZE]; } }
引发内存泄露的例子:
public class KeylessEntry { static class Key { Integer id; Key(Integer id) { this.id = id; } @Override public int hashCode() { return id.hashCode(); } } public static void main(String[] args) { Map<Key,String> m = new HashMap<Key,String>(); while(true) { for(int i=0;i<10000;i++) { if(!m.containsKey(new Key(i))) { m.put(new Key(i), "Number:" + i); } } } } }
主函数如果重复执行,HashMap中的hashCode因为已经被重写,并不能判断重复的key值,所以每次执行主函数,map中的元素会越来越多,直到溢出。
StackOverFlow是请求新的栈帧时,栈所剩空间不足,如果说OOM是广度空间不足,nameStackOverFlow便是深度空间不足,引发的原因一般是方法调用链过于复杂,常见于复杂的或者陷于死循环的递归方法调用。
Java的类加载
类加载器负责读取 Java 字节代码,并转换成java.lang.Class类的一个实例,JavaClassLoader(类加载器)包含如下几类:
Bootstrp loader,Bootstrp loader是用C++语言写的,是在Java虚拟机启动后初始化的,它主要负加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。
ExtClassLoader,Bootstrp loader之后加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader.ExtClassLoader是用Java写的,具体来说就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库。
AppClassLoader,Bootstrp loader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为 ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是 sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。
他们的继承顺序如下:
类的一致性
相关问题,类的一致性判断并不只是凭字节码的一致性判断的,而是类加载器,不同的类加载器加载出的类也是不同的类,包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字等判断出来的结果。
双亲委派
类加载符合双亲委派原则,双亲委派指的是加载某个类时将优先从其父类加载,这样做可以避免一个类被多次加载,也能保护程序的安全性,譬如当一名攻击者试图通过修改java底层类并使用自定义的类加载器进行类的加载来进行攻击时,双亲委派模型将不允许这种操作(比如覆盖加载String类)。
浅谈GC
GC判断存活的方式
在这里简单提一下引用计数法,就是给每一个对象添加对应的计数器,每被引用一次计数+1,释放引用则-1,然而这种方式并不能用于复杂的对象之间相互引用的方式,因此JVM并不是依赖引用计数开判断对象的存活与否的。
JVM使用的是可达性分析:
这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。在Java语言中,可以作为GCRoots的对象包括下面几种:
虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
方法区中的类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(Native方法)引用的对象。
而对于方法区内资源的回收,主要包含常量池内无用的常量和不用的方法。判断一个方法是否可用主要看以下三点:
该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。