独占区(线程私有区):
程序计数器:
是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。
虚拟机栈:
是线程私有的,与线程的生命周期相同。每个方法创建的时候都会自动创建一个栈帧,用于存储局部变量,操作栈,动态链接等,局部变量表存放了编译器可知的八种基本数据类型,对象引用等。
本地方法栈:
虚拟机栈为执行虚拟机执行java方法服务,而本地方法栈则为native方法服务。
共享区(线程共享区):
java堆:
是所有线程共享的一块内存区,在虚拟机启动时创建的。
可以细分为新生代和老年代,再细分则时eden,from ,to等
jdk1.8堆的变化:
方法区:
和java堆一样,是各个线程的共享区。用来存放已被加载的类信息,变量,常量,及时编译器编译后的代码等。
又被人称为永久代:实际上不存在永久代的概念,只不过用永久代来实现方法区而已
运行时的常量池:
是方法区的一部分。用于存放编译期生成的各种字面量以及符号的引用。
(线程私有区内容是JVM每遇到一个线程就进行分配私有内容,线程终止,三者占用内存被释放。线程共享区域与java程序运行生命周期相同)
Spring的bean在进程结束的时候进行销毁。
对象的分配
1.检查加载
检查对象是否经过类的加载,如果没有则进行类加载
2.分配内存
根据方法区的信息确定为该类分配的内存空间大小
指针碰撞- 如果java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞
空闲列表 如果java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表
并发安全-CAS机制
a.对分配内存空间的动作进行同步处理
b.分配缓冲 把内存分配的动作按照线程划分在不同的空间之中进行。(buffer容量不够的时候从eden中申请一块继续使用)
3.内存空间的初始化
内存分派完成后,虚拟机需要将分配到的内存空间都初始化为零值(int值为0,boolean为false)
4.设置
将对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息存放在对象的对象头之中。
5.对象的初始化 new
对象内存布局
1.对象头:hashCode,GC,对象分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等
2.类型指针:对象指向它的类数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例
3.对齐填充:占位符作用,对象大小必须是8字节的整数倍。
对象的访问定位
1.句柄
java堆中会划分出一块内存作为句柄池,reference中存储的就是句柄池,句柄池中包含对象实例数据和类型数据的具体信息
2.直接指针
reference中存储的直接就是对象地址。
使用第一种方式,在对象被移动时只会改变句柄的示例数据指针,reference不需要进行修改
第二种方式速度快,节省了一次指针定位的时间开销,对象的访问频繁,比较耗费内存
堆的分配策略
新生代
eden,from,to 8:1:1(可调整) 复制回收算法
对象优先在eden中分配
大对象直接进入老年代(判断是否是大对象可以自己进行调整)
长期存活对象直接进入老年代(15岁)
对象年龄动态判定survivor空间中相同年龄所有对象大小的综合大于survivor空间的一半,大于或者等于该年龄的对象直接进入老年代
空间分配担保 判断老年代最大空间是否足够,如果足够使用miniorGC,不足够查看HandlePromotionFailure设置值是否允许可以担保。可以就检查老年代最大可用的连续空间是否大于历次到老年代对象的平均大小,如果大于,将尝试使用minorGC,担保失败使用fullGC。小于或不允许冒险采用FullGC
GC的方式
1.标记整理
2.标记清除
3.复制算法
4.分代收集
GC触发:
1.老年代空间不足:新生对象转入大对象或大数组时会出现空间不足的情况,执行FullGC后仍然空间不足会抛出异常OutOfMemoryError
2.永生区空间不足:class信息,常量,静态变量,加载的多的时候会导致系统不足
3.堆内存当中分配很大的对象
GC时判断对象是否存活
a.使用引用计数器,当对象增加一个引用,引用计数器+1,引用失效时-1,当引用计数器为0的对象可回收(对象相互引用的时候很难判断是否存活)
b.可达性分析
以“GC Roots"的对象作为起始点,从这些节点开始往下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
GC Roots对象:
1.当前虚拟机栈中局部变量表中的引用的对象。
2.当前本地方法栈中局部变量表中的引用的对象。
3.方法区中类静态属性引用的对象。
4.方法区中的常量引用的对象。
java对象得引用分为四种类型
强引用:默认情况下,对象得引用多数都为强引用
软引用:比较适合缓存场景使用
弱引用:在GC时一定会被GC回收
虚引用:用来得知对象是否被GC
并行:垃圾收集的多线程的同时进行。
并发:垃圾收集的多线程和应用的多线程同时进行。
CMS是一款优秀的收集器,它的主要优点是:并发收集、低停顿,但他有以下3个明显的缺点:
1.CMS收集器对CPU资源非常敏感
因为并发阶段多线程占据cpu资源,如果cpu资源不足,效率会明显降低
2. CMS处理器无法处理浮动垃圾
CMS在并发清理阶段线程还在运行, 伴随着程序的运行自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法再当次过程中处理,所以只有等到下次gc时候在清理掉,这一部分垃圾就称作“浮动垃圾” ,jdk1.6留出8%放置浮动垃圾。
3. CMS是基于“标记--清除”算法实现的,所以在收集结束的时候会有大量的空间碎片产生。空间碎片太多的时候,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象的,只能提前触发 full gc。
为了解决这个问题,CMS提供了一个开关参数,用于在CMS顶不住要进行full gc的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片没有了,但是停顿的时间变长了
G1(Garbage First)是一款面向服务端应用的垃圾收集器。G1具备如下特点:
5、G1运作步骤:
1、初始标记(stop the world事件 CPU停顿只处理垃圾);
2、并发标记(与用户线程并发执行);
3、最终标记(stop the world事件 ,CPU停顿处理垃圾);
4、筛选回收(stop the world事件 根据用户期望的GC停顿时间回收)
与其他GC收集器相比,G1具备如下特点:
G1使用标记整理,CSC使用标记清楚
1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,
上面几个步骤的运作过程和CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短,并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。
而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这一阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
类加载机制
类从被加载到虚拟机当中一直到卸载出内存为止包括七个步骤:
加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析统称为连接。
加载阶段:
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个与这个类同名,代表这个类的class文件,作为方法区中这个类的各种数据的访问入口。
验证阶段:
是链接阶段的第一步。这一阶段要保证class字节流中包含的信息符合jvm虚拟机的要求,并且不会威胁jvm虚拟机自身的安全。验证阶段有四个检验动作:文件格式验证,符号引用验证,元数据验证,字节码验证
准备阶段:
正式为类变量分配内存并设置类变量初始值(初始零值)的阶段,这些变量所使用的内存将在方法区中分配。
解析阶段:
是虚拟机将常量池中符号引用替换成直接引用的过程。
类初始化阶段:
1.遇到new,getstatic,putstatic,invokestatic四条指令,如果之前没有初始化,需要立刻对类进行初始化操作,
2.使用反射(reflect)的方法对类进行反射调用的时候,如果之前没有进行初始化,需要立刻对类进行初始化操作。
3.当初始化一个类的时候,发现其父类还未出现初始化操作,则立刻对其父类进行初始化操作。
4.虚拟机启动时,用户需要指定一个主方法类,虚拟机会优先初始化主类。
5.使用jdk1.7的时候,如果一个methodhandle实例最后的解析结果是ref_getstatic.ref_putstatic,ref_invokestatic句柄,并且这个句柄所对应的类没有进行过初始化,则首先需要进行初始化
类初始化阶段是类加载机制的最后一个阶段,除了加载阶段用户应用程序可以通过自定义类加载器参与外,其余所有动作完全需要虚拟机主导和控制。
双亲委派模式:
双亲委派模型要求除顶层的启动类加载器外,其余的类加载器应该有自己的父类加载器。这里类加载器的父子关系一般不会以继承的关系实现,而是使用组合关系来复用父类加载器的代码。
工作过程是当一个类收到了一个类加载器的请求,它首先不会自己去加载,而是把这个类委派给父类来进行加载,每一个层次的类加载器都如此,因此所有的加载请求都应该传送到顶层的启动类加载器当中,只有当父加载器反馈自己无法加载这个类的时候,子类才会尝试进行加载。
java类可以随着他的类加载器一起具备这种带有优先级的关系,这样无论哪个类来加载都是委派给启动类来进行加载。
破坏双亲委派式:热模部署,重启一次对于商用软件造成很大影响。
OOM 内存溢出:
栈溢出(1m):方法死循环递归调用,不断建立线程
堆溢出:不断创建对象,分配对象大于最大堆的大小
直接内存:JVM分配本地直接内存大小大于JVM限制
方法区溢出:CGLIB字节码增强,动态语言,大量JSP
内存泄露:
程序在申请内存后,无法释放已申请的内存。(30m内存当中有10m内存被无用内存占用并且GC无法始终无法回收)
长生命周期的对象持有短生命周期对象的引用
链接未关闭(数据库链接,网络链接,IO链接,关闭后GC才会继续进行回收)
变量作用域不合理(一个变量的定义的作用范围大于使用范围,没有及时的把对象设置未NULL)
内部类持有外部类(内部类生命周期长于外部类生命周期,就会造成内存泄漏)->解决方法:在内部类的内部显示持有一个外部类的软引用(或弱引用),并通过构造方法的方式传递进来,在内部类的使用过程中,先判断一下外部类是否被回收。
Hash值的改变(改变hashcode,改变位置GC就无法进行回收)
内存泄露与内存溢出的不同
内存溢出:实实在在的内存空间不足导致(检查代码以及设置足够的空间)
内存泄漏:该释放的对象没有释放,常见于使用容器保存元素的情况下。(检查代码)
内存溢出多数是由内存泄漏造成的
串行收集器和并行收集器对比
1.串行采用单线程处理,适用于单CPU或并发能力弱的系统,当回收器启动后会暂停工作线程,更由于是单线程,导致其STW的时间会很长
2.并行采用多线程处理,适用于多CPU或并发能力强的系统,当回收器启动后也会暂停其他工作线程,但由于是多线程,会减少其STW时间
volatile
java内存模型中,原子性,可见性,有序性
volatitle 是比synchronized更轻量级的同步机制,表示可见性,是指线程当中的可见性,一个线程的修改是对另一个线程可见的。
深拷贝和浅拷贝:
浅拷贝只是增加了一个指针,指向已存在的内存地址。
深拷贝是增加了一个指针并新增了一个新的内存,使这个指针指向新的内存。
假设B复制了A,修改B。如果A跟着发生了变化就是浅拷贝,如果A没有发生变化就是深拷贝。