1. 内存模型以及分区,需要详细到每个区放什么
JVM分区Hotspot虚拟机是基于栈的虚拟机(1) 线程私有区
程序计数器:记录正在执行的虚拟机字节码的地址;
虚拟机栈:方法执行的内存区,每个方法执行时(敲黑板💯)会在虚拟机栈中创建栈帧;(PS:在代码中的方法调用过程中,往往需要从一个方法跳转到另一个方法,执行完再返回,那么在跳转之前需要在当前方法的基本信息压入栈中保存再跳转)
本地方法栈:虚拟机的Native方法执行的内存区;
(2) 线程共享区
Java堆:对象分配内存的区域;
方法区:存放类信息、常量、静态变量、编译器编译后的代码等数据;
常量池:存放编译器生成的各种字面量和符号引用,是方法区的一部分。
线程在运行的时候,代码在运行时,是通过程序计数器不断执行下一条指令。真正指令运算等操作时通过控制操作栈的操作数入栈和出栈,将操作数在局部变量表和操作栈之间转移。
2. 堆里面的分区:Eden,survival from to,老年代,各自的特点
堆里面的分区HotSpot虚拟机的分代收集,分为一个Eden区、两个Survivor去以及老年代,其中Eden以及Survivor共同组成新生代。
(1) Eden区
Eden区位于Java堆的新生代,是新对象分配内存的地方,由于堆是所有线程共享的,因此在堆上分配内存需要加锁。而Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上进行分配。如果Eden区内存也用完了,则会进行一次Minor GC(敲黑板💯)。
(2) Survival from to
Survival区与Eden区相同都在Java堆的新生代。Survival区有两块,一块称为from区,另一块为to区,这两个区是相对的,在发生一次Minor GC后,from区就会和to区互换。在发生Minor GC时,Eden区和Survival from区会把一些仍然存活的对象复制进Survival to区,并清除内存(敲黑板💯)。Survival to区会把一些存活得足够旧的对象移至老年代。
(3) 老年代
老年代里存放的都是存活时间较久的,大小较大的对象,因此老年代使用标记整理算法。当老年代容量满的时候,会触发一次Major GC,回收老年代和新生代中不再被使用的对象资源(敲黑板💯)。
3. 对象创建方法,对象的内存分配,对象的访问定位
(1) 类加载检查:检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程
(2) 为对象分配内存:对象所需内存的大小在类加载完成后便完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。由于堆被线程共享,因此此过程需要进行同步处理(分配在TLAB上不需要同步)
(3) 内存空间初始化:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
(4) 对象设置:JVM对对象头进行必要的设置,保存一些对象的信息(指明是哪个类的实例,哈希码,GC年龄等)
(5) init:执行完上面的4个步骤后,对JVM来说对象已经创建完毕了,但对于Java程序来说,我们还需要对对象进行一些必要的初始化。
Java对象的内存分配有两种情况,由Java堆是否规整来决定(Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定):
(1) 指针碰撞(Bump the pointer):如果Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离
(2) 空闲列表(Free List):如果Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
对象的访问形式取决于虚拟机的实现,目前主流的访问方式有使用句柄和直接指针两种:
(1) 使用句柄:
如果使用句柄访问,Java堆中将会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息:
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
(2) 直接指针:
如果使用直接指针访问对象,那么对象的实例数据中就包含一个指向对象类型数据的指针,引用中存的直接就是对象的地址:
优势:速度更快,节省了一次指针定位的时间开销,积少成多的效应非常可观。
4. GC的两种判定方法:引用计数与引用链
基于引用计数与基于引用链这两大类别的自动内存管理方式最大的不同之处在于:前者只需要局部信息,而后者需要全局信息
(1) 引用计数顾名思义,就是记录下一个对象被引用指向的次数,当该计数器的值降到0就认为对象死亡。因为缺乏全局对象图信息,所以无法处理循环引用的状况。
(2) 引用链需要内存的全局信息,当使用引用链进行GC时,从对象图的“根”(GC Root,必然是活的引用,包括栈中的引用,类静态属性的引用,常量的引用,JNI的引用等)出发扫描出去,基于引用的可到达性算法来判断对象的生死。
5. GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?
标记清除 | 复制算法 | 标记整理 |
---|---|---|
暂停用户线程,通过GC Root使用可达性算法标记存活对象,清除未被标记的垃圾对象 | 暂停用户线程,标记活动空间的存活对象,把活动空间的存活对象复制到空闲空间去,清除活动空间 | 暂停用户线程,标记所有存活对象,移动所有存活对象,按内存地址次序一次排列,回收末端对象以后的内存空间 |
效率较低,需要暂停用户线程,清除垃圾对象后内存空间不连续,存在较多内存碎片 | 需要浪费一定的内存作为空闲空间,如果对象的存活率很高,则需要复制大量存活对象,导致效率低下 | 标记整理算法与标记清除算法相比,整理出的内存是连续的;而与复制算法相比,不需要多片内存空间。然而标记整理算法的第二步整理过程较为麻烦,需要整理存活对象的引用地址,理论上来说效率要低于复制算法 |
标记算法如今使用的较少了 | 复制算法一般用于新生代的Minor GC,主要是因为年轻代的大部分对象存活率都较低 | 标记整理算法一般引用于老年代的Major GCw2 |
6. GC收集器有哪些?CMS收集器与G1收集器的特点
常见的GC收集器如图所示,连线代表可搭配使用具体见http://blog.csdn.net/ccutwangning/article/details/53840967
7. Minor GC与Full GC分别在什么时候发生
Minor GC也叫Young GC,当新生代内存满的时候会触发,会对新生代进行GC
Full GC也叫Major GC,当老年代满的时候会触发,当我们调用System.gc时也可能会触发,会对新生代和老年代进行GC
8. 几种常用的内存调试工具:jmap、jstack、jconsole
...
9. 类加载的五个过程:加载、验证、准备、解析、初始化
...
10. 双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader
类加载器的双亲委托模型11. 分派:静态分派与动态分派
静态分派:
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,其典型应用是方法重载(重载是通过参数的静态类型而不是实际类型来选择重载的版本的)
public class StaticAssignTest {
public static void main(String[] args) throws Exception {
// Car 为静态类型,Car 为实际类型
Car car1 = new Car();
// Car 为静态类型,Bus 为实际类型
Car car2 = new Bus();
// Car 为静态类型,Jeep 为实际类型
Car car3 = new Jeep();
showCar(car1);
showCar(car2);
showCar(car3);
}
private static void showCar(Car car) {
System.out.println("I have a Car !");
}
private static void showCar(Bus bus) {
System.out.println("I have a Bus !");
}
private static void showCar(Jeep jeep) {
System.out.println("I have a Jeep !");
}
static class Car {}
static class Bus extends Car {}
static class Jeep extends Car {}
}
输出结果:
I have a Car !
I have a Car !
I have a Car !
动态分派:
与静态分派类似,动态分派指在在运行期根据实际类型确定方法执行版本,其典型应用是方法重写(即多态)
public class DynamicAssignTest {
public static void main(String[] args) throws Exception {
// Car 为静态类型,Car 为实际类型
Car car1 = new Car();
// Car 为静态类型,Bus 为实际类型
Car car2 = new Bus();
// Car 为静态类型,Jeep 为实际类型
Car car3 = new Jeep();
car1.showCar();
car2.showCar();
car3.showCar();
}
static class Car {
public void showCar() {
System.out.println("I have a Car !");
}
}
static class Bus extends Car {
public void showCar() {
System.out.println("I have a Bus !");
}
}
static class Jeep extends Car {
public void showCar() {
System.out.println("I have a Jeep !");
}
}
}
输出结果:
I have a Car !
I have a Bus !
I have a Jeep !
网友评论