JVM基础
1.JVM堆的基本结构。
Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。
2.Java内存结构(堆结构,新生代[S0/S1/Elden],年老代,持久代)
创建的对象,首先放入Eden和Survivor1(可能是短时间)的当Eden满了会启动minor gc,回收新生代中不再使用的对象,还要用的就放到Survivor2中移完之后eden和Survivor1中剩下的就是不再使用的对象,就将他们清理掉。Survivor1和Survivor2交换角色。
那就是原来的Survivor1成了备用的了,也就是原来的Survivor2多次在Survivor区没有被清理掉的,说明它是长时间使用的,那么将它移动到老年代。
由于对象越New越多,minor时发生备用的Survivor区满了,new的对象直接放入老年代,很可能快速的给老年代占满,白白的浪费老年代的空间,就会触发Full GC,回收老年代的对象。(年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。)
老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。
注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。
3.JVM的垃圾算法有哪几种?CMS收集算法的流程?
引用计数法:堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),引用结束时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。缺点:无法检测出循环引用。
可达性分析算法:可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
a) 虚拟机栈中引用的对象(栈帧中的本地变量表);
b) 方法区中类静态属性引用的对象;
c) 方法区中常量引用的对象;
d) 本地方法栈中JNI(Native方法)引用的对象。
复制(Copying):将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块,然后把已使用的内存空间一次清理掉,当存活的对象较少时,复制算法会比较高效(新生代的Eden区就是采用这种算法),其带来的成本是需要一块额外的空闲空间和对象的移动。
标记-清除(Mark-Sweep) :标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除动作不需要移动对象,且仅对不存活的对象进行清理,在空间中存活对象较多的时候,效率较高,但由于只是清除,没有重新整理,因此会造成内存碎片。
标记-整理算法(Mark-compact):标记出所有需要被回收的对象,完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存,因此成本更高,但是却解决了内存碎片的问题。
分代收集算法 Generational Collection(分代收集)算法 :将堆区划分为老年代和新生代,在堆区之外还有一个代就是永久代。
老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收。
新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间(一般为8:1:1),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法。
数据会首先分配到Eden区 当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)),当Eden没有足够空间的时候就会 触发jvm发起一次Minor GC。如果对象经过一次Minor GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空 间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代 中了,当然晋升老年代的年龄是可以设置的。老年代满了就执行:Full GC 因为不经常执行,因此采用标记-整理算法清理.
4. JVM内存结构,为什么需要GC?
JVM主要包括四个部分:
1.类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。
2.执行引擎:负责执行class文件中包含的字节码指令;
3.内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域:
-
-
- 方法区(Method Area):用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。。
- java堆(Heap):存储java实例或者对象的地方。这块是GC的主要区域。从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的。
- java栈(Stack):java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是现成私有的。(栈帧:每个方法在执行时都会创建一个栈帧,随着方法的调用而创建,随着方法的结束而消亡。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程)。https://blog.csdn.net/qq_32258777/article/details/81353638
- 程序计数器(PC Register):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
- 本地方法栈(Native Method Stack):和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。
-
4.本地方法接口:主要是调用C或C++实现的本地方法及返回结果。
5.内存分配
java一般内存申请有两种:静态内存和动态内存。编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。
有3个是不需要进行垃圾回收的:本地方法栈、程序计数器、虚拟机栈。因为他们的生命周期是和线程同步的,随着线程的销毁,他们占用的内存会自动释放。所以,只有方法区和堆区需要进行垃圾回收,回收的对象就是那些不存在任何引用的对象。
6.CMS算法的过程,CMS回收过程中JVM是否需要暂停
老年代使用CMS垃圾回收器,需要添加虚拟机参数-"XX:+UseConcMarkSweepGC"。
CMS收集器是一种基于"标记-清除"算法实现的收集器,整个过程分为四步
1.初始标记(CMS initial mark),需要暂停所有的工作线程.该过程分为两步:
-
- 标记GC Roots可达的老年代对象
- 遍历新生代对象,标记可达的老年代对象
2.并发标记(CMS concurrent mark)
3.重新标记 (CMS remark)
4.并发清除 (CMS concurrent sweep)
初始标记、重新标记着两个步骤任然需要"Stop The World",初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC Roots Tracing 的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产品变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些。,但远比并发标记的时间短。
常见的B/S架构的应用就适合这种收集器,因为其高并发、高响应的特点,CMS是基于标记-清楚算法实现的。
CMS收集器的缺点:
1.对CPU资源非常敏感,在并发阶段虽然不会导致用户停顿,但是会占用CPU资源而 导致应用程序变慢,总吞吐量下降。
2.无法处理浮动垃圾(由于CMS并发清理阶段用户线程还在运行着,伴随着程序的运行自然就还会有新的垃圾不断产生,这一部分产生的垃圾,CMS无法在当次收集中处理掉他们,只要留待下一次GC再进行处理。这一部分产生的垃圾成为"浮动垃圾"),可能出现“Concurrnet Mode Failure”,失败而导致另一次的Full GC。
3.CMS收集器是基于标记-清除算法的实现,因此也会产生碎片。
一旦发生old区满了,并且浮动垃圾未清除完的情况,就会使用Serial Old(单线程,使用的垃圾回收算法是标记-压缩-清理算法)进行垃圾清除,效率极低。
7.JVM有哪些常用启动参数可以调整?
7.为什么JAVA类加载要用双亲委派(从子到父,再从父到子的过程)?
Java类加载器(ClassLoader):双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。
双亲委派模式:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
基于双亲委派模型设计,那么Java中基础的类,Object类似Object类重复多次的问题就不会存在了,因为经过层层传递,加载请求最终都会被Bootstrap ClassLoader所响应。加载的Object类也会只有一个,否则如果用户自己编写了一个java.lang.Object类,并把它放到了ClassPath中,会出现很多个Object类,这样Java类型体系中最最基础的行为都无法保证,应用程序也将一片混乱。
双亲委派模型的好处:
1.主要是为了安全性,避免用户自己编写的类动态替换JAVA的一些核心类,比如String。
2.同时也避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类。
在JVM中表示两个class对象是否为同一个类对象存在两个必要条件:
1.类的完整类名必须一致,包括包名。
2.加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
8.如何打破双亲委派机制:
1.jdk1.2之前,自定义ClassLoader都必须重写loadClass()。 重写loadClass方法即可打破双亲委派机制。
2.ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定。
3.热启动,热部署 。 osgi tomcat都有自己的模块制定classLoader,可以加在同一类的不同版本。
9.对象的创建过程。
1.Loading:把一个class文件装入到内存。
2.Linking:分三步:1.校验,是否是class文件,2.将class文件静态变量赋默认值(eg:int i = 0)。 3. 把class文件常量池里面用到的符号引用转换为直接内存地址。
3.Initializing:初始化,调用类初始化代码,给静态变量赋初始值,同时执行静态语句块。
4.申请对象内存。
5.成员变量赋默认值。
6.调用构造方法,成员变量顺序赋初始值。
10.对象怎么分配?
1.对象头,8字节。 2.classpointer指针,8字节。 3.实例数据,8字节。 4.pading对齐。8的倍数
对象头:
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。这里我们着重说明64位的。
https://blog.csdn.net/lkforce/article/details/81128115
2.指向类的指针:该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。Java对象的类数据保存在方法区。
3.实例数据:对象的实例数据就是在java代码中能看到的属性和他们的值。
4.对齐填充字节:因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。
数组对象: 1.对象头。 2.classpointer指针。 3.数组长度:4字节(该数据在32位和64位JVM中长度都是32bit。) 4.数组数据。 5.pading对齐。8的倍数
11.Object object = new Object();占多少字节?
一个object对象有16个字节,原因:
1.头部 8个字节 2.class指针:被压缩了是4个字节(默认是8个字节) 。 3.Padding:4个字节
12.对象如何定位?
1.句柄池:T t = new T(); t找到T,先通过一个间接指针,这个间接指针,一个指向对象,一个指向T.class对象。
2.直接指针:直接指向某个对象,这个对象中再有一个指针指向T.class.