JVM在运行的时候在对所管理的内存空间划分了不同的区域,有些是线程私有的,有些是线程共享的。
线程私有的有
1.程序计数器,指示了当前线程执行的字节码的行号。字节码解释器通过改变程序计数器的值来选取下一条执行的指令。这个区域是唯一不会发生OOM的区域。
2.虚拟机栈,由栈帧构成,每一个栈帧包括一个方法执行需要的内存,每次方法调用会有一个栈帧入栈,方法返回会从栈顶出栈。栈帧里面存储方法参数,局部变量表(基本类型的局部变量值,和对象的引用值),方法的返回地址(主调方法的PC计数器的值),以及动态连接(一个指向运行时常量池中该栈帧所属方法的引用,解决Java多态的问题)。
Note:动态连接是什么?
栈帧中保存了一个引用,相当于C语言中的指针,指向该方法在运行时常量池中的位置。
通过运行时常量池的符号引用(指向堆),完成将符号引用转化为直接引用。
问题:虚拟机栈中可能发生的问题?如果栈内存溢出,会发生stackoverflow。如果支持动态扩展虚拟机栈容量的话,申请不到内存时会发生OOM。
局部变量是否线程安全?因为虚拟机栈是线程私有的,所以一般来说是线程安全的。但是如果局部变量的作用域逃离了方法,比如说这个值被返回了,或者说这个局部变量是个对象的引用。
3.本地方法栈,是本地方法运行需要的空间,和虚拟机栈类似,也有stackoverflow和oom。
线程共享的两个区域:
1.堆,用来存储对象实例的一块空间。jdk 1.7之后字符串常量池和静态变量被移动到了堆里。
2.方法区,存有类型信息,常量,静态变量,即时编译器的代码缓存。方法区中有一个运行时常量池,存有常量池中的符号引用和对应的直接引用。
常量jdk 1.8之前用永久代实现,jdk1.8以后用元空间实现,存在于本地内存中,只要本地内存足够就不会OOM。
类加载的过程 一.加载Loading1.根据类的全限定名获取定义该类的二进制字节流。
这个动作由类加载器完成。
2.将二进制字节流代表的静态存储结构转换为方法区的运行时数据结构。
3.在堆中生成一个Java.lang.Class对象,作为程序访问方法区的类型数据的接口
二.连接 linking连接包括验证,准备和解析。
验证:对Class文件的字节流是否符合JVM的要求进行验证。
验证1.文件格式2.元数据,类、字段、方法定义3.字节码,对方法体的代码进行验证.4.符号引用,判断根据符号引用是否能够访问到对应的类,方法和字段。
准备:
为静态变量分配内存,设置默认初始值(0),如果是静态常量的话设置初始值。
解析:
将常量池内的符号引用替换为直接引用。 符号引用是在class文件里类的全限定名,方法的描述符和名称之类的,但是没有这些东西在在内存中的实际存储位置。而直接引用是指向在内存中的实际位置的指针。
三.初始化 Initialization执行
类加载的加载动作中的从类的全限定名获得类的二进制字节流这个动作由类加载器执行。
一个类加载器在执行加载任务的时候,首先委派父加载器进行加载,只有父加载器不能加载的情况下才由自身加载。每个加载器只能加载固定目录下的类。
这样做的好处是可以在不同的类加载环境下保证加载的是同一个类。
打破双亲委派模型1.双亲委派模型出现之前的自定义类加载器并没有去委派父类加载器加载,是直接覆盖loadClass()方法。双亲委派模型出来之后,新的ClassLoader的loadClass方法内部会执行委派父类加载器加载的动作,加载失败再调用自身的findClass方法,之后的用户自定义加载器要去实现findClass()。
2.第二次破坏双亲委派模型是因为,双亲委派模型是分层的,用户代码的系统类加载器在低层,可以调用父加载器去加载基础的类。 但是基础的类,没办法调用低层的类加载器加载用户代码。比如JNDI服务需要调用其他厂商部署在classpath下提供的接口代码。JDBC 的DriverManager.getConnection(),DriverManager在java.sql包下,但是加载的其他厂商提供的Connection类不在java包下。
JAVA提供了线程上下文类加载器,没有设置的话默认是应用程序加载器。JNDI可以用应用程序类加载器来加载自己需要的类。
3.Tomcat给每一个web应用创建了一个类加载器实例(WebAppClassLoader),优先加载当前应用目录下的类,加载不到再一层层网上找。
对象的创建过程1.在常量池中检查类是否已经被加载,解析和初始化,如果没有的话先执行类加载过程。
2.分配内存,根据垃圾回收器是否进行空间压缩整理来进行指针碰撞和空闲列表的分配方式
3.将分配的内存空间初始化为0值
4.对对象头进行设置
5.执行
一.哪些内存需要回收
线程私有的程序计数器,虚拟机栈和本地方法栈不需要考虑如何回收,这些区域随着线程结束而被回收。栈帧的内存大小在类结构确定时就确认了,方法调用结束后被回收。
堆和方法区需要垃圾回收器来回收。
堆是垃圾回收的最主要的区域,要对不会再被使用的对象进行回收。
判断需要被回收的对象:
1.引用计数算法(循环引用)
2.可达性分析算法,以GC Root作为根结点,不能被引用到的就可以被回收。
GC Root 包括
1.虚拟机栈中引用的对象
2.类变量引用的对象
3.类常量引用的对象
4.本地方法栈中引用的对象
5.虚拟机内部的引用
6.被同步锁持有的对象
方法区也可以进行回收,不再使用的常量和类型。
常量没有引用可以被回收
类型被回收需要满足三个条件:
1.类的实例全部被回收
2.类的类加载器被回收
3.类的class对象没有被引用
二.如何进行垃圾回收
分代收集理论:
垃圾收集器将堆中的对象按照年龄分配在不同的区域。这基于两个假说:
1.大部分对象存活时间很短
2.如果一个对象存活了越多轮垃圾回收,说明它越难消亡。
3.跨代引用相对于同代引用来说仅占极少数。不用为了少量的跨代引用扫描整个老年区,有一块记忆集记录老年代哪块内存存在跨代引用,进行GC Roots的扫描。
垃圾回收算法
标记-清除, 标记-复制, 标记-整理
标记-清除:随对象数量变多而变慢,会产生内存碎片。(Eden区,CMS)
标记-复制:对象存活率高的时候复制操作昂贵,用于新生代。(Survivor区)
标记-整理:常用于老年代垃圾回收
垃圾收集器
CMS(Concurrent Mark Sweep)垃圾收集器
1)初始标记(CMS initial mark)
只标记GC Roots直接关联的对象,暂停用户线程
2)并发标记(CMS concurrent mark)
并发标记,遍历整片区域。
3)重新标记(CMS remark)
暂停用户线程,修正并发标记阶段因为程序运行导致的标记变动的对象的标记记录。
4)并发清除(CMS concurrent sweep)
清除掉已死亡的对象。
特点:
1.目标是最短响应时间,但是需要占用处理器资源,总吞吐量下降。
2.CMS收集器无法处理“浮动垃圾”(Floating Garbage),即并发标记和并发清理阶段新产生的垃圾,有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生
3.CMS基于标记-清除,会产生内存碎片。
G1 垃圾收集器
G1的堆内存区域是基于Region的,每个Region都可以作为Survivor,Eden,老年代。
G1垃圾收集器跟踪每个Region的垃圾回收的价值(回收到的空闲空间和所需时间),根据用户设置的允许的收集停顿时间,选择具有高价值的那些Region, 把这些Region中的存活对象移动到空的Region里。
特点:
1.在延迟可控的情况下尽量追求高吞吐量。
2.G1收集器的内存布局下堆全部由G1收集器管理。
3.不会产生内存碎片
内存溢出与内存泄漏问题内存泄漏的根本原因是长生命周期的对象持有了短生命周期的对象,这样短生命周期的对象不会被使用了也不被回收。
场景:
静态的容器,提供了close()的对象,单例模式的对象
怎么排查,去获取堆转储快照文件,然后去查看GC Roots的引用链。
内存溢出
堆溢出:存活的对象过多,超过堆的最大容量
虚拟机栈和本地方法栈:如果不支持动态扩展栈内存的话,会抛StackOverFlow,不支持的话会抛OOM
方法区:运行时生成大量动态类的应用场景。JDK1.8之后用元空间来实现方法区,直接使用本地内存,所以只要本地内存够,不会抛OOM
本地直接内存溢出: Unsafe类下提供了分配本地内存的方法,如果不够也会抛OOM



