目录
JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。
java内存模型
程序计数器
他是线程私有的,是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
Java虚拟机栈
他也是线程私有的,每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧 (Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。所以会有教科书上很经典的用一个方法交换两个int变量。但在main方法里两个变量其实没有交换的这个问题。 Java内存区域笼统地划分为堆内存( Heap )和栈内存(Stack)而“栈”通常就是指这里讲的虚拟机栈,或 者更多的情况下只是指虚拟机栈中局部变量表部分。 局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean 、 byte 、 char 、 short 、 int、 float 、 long 、 double )、对象引用(reference或 returnAddress 类型(指向了一条字节码指令的地址))。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,局部变量表所需的内存空间在编译期间完成分配。
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。有的 Java 虚拟机(譬如 Hot-Spot虚拟机)直接 就把本地方法栈和虚拟机栈合二为一。
为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
Java堆
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java “ 几乎 ” 所有的对象实例都在这里分配内存。 java堆是垃圾收集器管理的内存区域。所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区。将Java堆这么细分的目的只是为了更好地回收内存,或者更快地分配内存。
方法区
是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。 运行时常量池(Runtime Constant Pool )是方法区的一部分。 Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表( Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
对象
对象的创建
当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,所以jvm还有一个相应的类加载过程后面细说。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。jvm有两种内存分配方式。
第一种是指针碰撞,假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离
第二种称为空闲列表,当Java堆中的内存不是规整的,已被使用的内存和空闲的内存相互交错在一起,这时候虚拟机就会维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表“。
对象内存分配
对象在堆内存中的存储布局可以划分为三个部分:对象头、实例数据和对齐填充.。
对象的对象头部分包括两类信息。第一类被官方称为Mark Word。是一种有着动态定义的数据结构。的是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,对象头的另一部分是类型指针,也就是对象指向类型对象数据的指针,用来判断该对象是哪个类的实例。接下来的实例数据才是对象真正的有效信息,对齐填充不是必然存在的,他起到了一个占位符的作用。
对象的访问定位
主流的对象访问,有使用句柄和直接指针两种。
如果使用句柄,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,使用句柄的最大好处就是在垃圾回收的时候,对象被移动的时候只会改变句柄池中实例数据指针而不用改变reference。
直接指针就是reference直接指向java堆中的对象实例数据,使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销。
垃圾回收
判断对象是否存活
判断对象是否存活常有引用计数法和可达性分析
引用计数法的思路是这样的,给对象添加一个引用计数器,有地方引用时,计数器就加1;当引用失效时就减1;当计数为0的时候就判定对象需要被回收,但引用计数法有一个难以解决的问题就是相互循环引用问题。
比如:
obj1.instance=obj2.instance;obj2.instance=obj1.instance;
可达性分析算法这个算法的基本思路是通过一些列称为“GC Roots”的对象作为起始点,从这些点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明对象需要被回收.
Java可作为GC Roots的对象包括下面几种:
-
虚拟机栈中引用的对象
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中JNI引用的对象
强引用、软引用、弱引用、虚引用的区别?
1)强引用
我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
2)软引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
SoftReference<String> softRef=new SoftReference<String>(str); // 软引用用处: 软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
如下代码:
Browser prev = new Browser(); // 获取页面进行浏览SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用 if(sr.get()!=null){ rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取}else{ prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了 sr = new SoftReference(prev); // 重新构建}3)弱引用
具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
String str=new String("abc"); WeakReference<String> abcWeakRef = new WeakReference<String>(str);str=null;等价于str = null;System.gc();4)虚引用
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
垃圾回收算法
回收对象的年龄——对象熬过垃圾收集过程的次数
标记清除算法
标记出所有需要回收的对象,标记完后统一回收掉所有标记的对象,标记的过程就是判断对象是否为垃圾。
缺点:
1、执行效率不稳定。标记和清楚两个过程的执行效率都随对象数量增长而降低
2、内存空间的碎片化。会产生大量不连续的内存碎片,需分配大对象时不得不提前触发另一次垃圾收集
标记复制算法
将可用内存按容量划分成相同的两块,每次只用一块,当这一块用完把还存活着的复制到另一块,在清楚使用过的内存空间。
优点:实现简单,运行高效
缺点大量的空间浪费
标记整理算法
标记出所有还存活的对象,让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
分代算法
根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
分代收集理论存在三种假说
1、弱分代假说(WeakGenerationalHypothesis):绝大多数对象都是朝生夕灭的。
2、强分代假说(StrongGenerationalHypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
3、跨代引用假说(IntergenerationalReferenceHypothesis):跨代引用相对于同代引用来说仅占极少数。
分代收集eden区,survial区
分代收集把堆区分为了新生代和老年代;新生代又分为eden区,from survivor区,to survivor区。
一般把空间大小设为8:1:1,对象总是eden区出生,from suvivor区保存当前的幸存对象,to为空。在eden快满的时候发生minor gc,eden活着的+from保存的复制到to,清空eden与from,然后把to和fromi调换。
由于大部分的新生代都是朝生夕灭的,很少可以活过第一轮收集,所以内存分配大小比例一般是8:1:1,先放8的那部分,满了后活下来的和其中一个1里活下来的复制到另一个1里,然后清空。
JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。
共享内存区 = 持久带 + 堆
持久带 = 方法区 + 其他
Java堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定)Survivor区中的对象被复制次数为15(对应虚拟机参数 -XX:+MaxTenuringThreshold)
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1。
Minor GC与Full GC
Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常平凡,一般回收速度也比较i快。
Minor GC发生:当jvm无法为新的对象分配空间的时候就会发生Minor gc,所以分配对象的频率越高,也就越容易发生Minor gc。
Major GC/Full GC 是老年代GC,指的是发生在老年代的GC,出现Major GC一般经常会伴有Minor GC,Major GC的速度比Minor GC慢的多,因为他会发生stop the world。
Full GC:发生GC有两种情况,①当老年代无法分配内存的时候,②当发生Minor GC的时候可能触发Full GC,由于老年代要对年轻代进行担保,由于进行一次垃圾回收之前是无法确定有多少对象存活,因此老年代并不能清除自己要担保多少空间,因此采取采用动态估算的方法:也就是上一次回收发送时晋升到老年代的对象容量的平均值作为经验值,这样就会有一个问题,当发生一次Minor GC以后,存活的对象剧增(假设小对象),此时老年代并没有满,但是此时平均值增加了,会造成发生Full GC、
垃圾回收器
Serial收集器: 单线程的收集器,收集垃圾时,必须stop the world,也就是除垃圾收集器外其他所有线程都被挂起,使用复制算法。
ParNew收集器: Serial收集器的多线程版本,也需要stop the world,复制算法。
Parallel Scavenge收集器: 新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。
Serial Old收集器: 是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。CMS(Concurrent Mark Sweep) 收集器: 是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。
G1收集器: 标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。
CMS收集器和G1收集器的区别:
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
CMS收集器以最小的停顿时间为目标的收集器;G1收集器可预测垃圾回收的停顿时间
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
类加载机制
类加载过程
分为加载,验证,准备,解析,初始化,五个步骤。
加载阶段会在内存中生成一个class对象,用来作为在方法区中这个类各个数据的入口。
验证阶段是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求
准备阶段正式为类变量分配内存并设置类变量的初始值。这个的初始值指的是比如int类型默认为0
解析虚拟机将常量池中的符号引用替换成直接引用。
符号引用,引用的目标并不一定已经加载到内存中。
直接引用是可以指向目标的指针,相对偏移量或是一个能间接定位到的句柄,表示引用的目标必定已经在内存中存在。
初始化阶段是执行类构造器方法的过程,虚拟机会保证在类构造器方法执行之前父类的类构造器已经执行完毕
类的主动引用和被动引用的区别
类的主动引用(一定会发生类的初始化)
—— new一个类的对象
—— 调用类的静态成员(除了final常量)和静态方法
—— 使用java.lang.reflect包的方法对类进行反射调用
—— 当虚拟机启动,先启动main方法所在的类
—— 当初始化一个类,如果父类没有被初始化,则先初始化它的父类
类的被动引用(不会发生类的初始化)
—— 当访问一个静态域时,只有真正声明这个域的类才会被初始化
—— 通过子类引用父类的静态变量,不会导致子类初始化
—— 通过数组定义类引用,不会触发此类的初始化
—— 引用final常量不会触发此类的初始化(常量在编译阶段就存入类的常量池中)
类加载器——双亲委派机制
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。