【面试】JVM
关于内存、JVM、类加载机制
about conception:
JVM内存模型:
从大的方面讲,JVM的内存模型分为两大块:
永久区内存(Permanent space)和堆内存(heap space)。
栈内存(stack space)一般不归在JVM内存模型中,因为栈内存属于线程级别。
每个线程都有个独立的栈内存空间。
Permanent space是存放加载的Class类级对象 如class本身,method,field等等。
heap space主要存放对象实例和数组。
heap space由new Generation和old Generation组成,old Generation存放生命周期长久的实例对象,而新的对象实例一般存放在New Generation。
New Generation还可以再分为Eden。Survivor区,新的对象实例总是首先放在Eden区,Survivor区作为Eden区和old区的缓冲,可以向old区转移活动的对象实例。
下图是JVM在内存空间(堆空间)中申请新对象过程的活动图:
没错,我们常见的OOM(out of memory)内存溢出异常,就是堆内存空间不足以存放新对象实例时导致。
永久区内存溢出相对少见,一般是由于需要加载海量的class数据,超过了堆内存的容量导致。通常出现在web应用刚刚启动时,因此web应用推荐使用预加载机制,方便在部署时就发现并解决该问题。
栈内存也会溢出,但是更加少见。
堆和栈:
java栈是与每一个线程关联的,JVM在创建每一个线程的时候,会分配一定的栈空间给线程。它主要用来存储线程执行过程中的局部变量,方法的返回值,以及方法调用上下文。栈空间随着线程的终止而释放。StackOverflowError:如果在线程执行的过程中,栈空间不够用,那么JVM就会抛出此异常,这种情况一般是死递归造成的。
Java堆是由所有的线程共享的一块内存区域,堆用来保存各种java对象,比如数组,线程对象等。
堆和栈分离的好处:面向对象的设计,当然除了面向对象的设计带来的维护性,复用性和扩展性方面的好处外,我们看看面向对象如何巧妙的利用了堆栈分离。如果从java内存模型的角度去理解面向对象的设计,我们就会发现对象它完美的表示了堆和栈,对象的数据放在堆中,而我们编写的方法一般是运行在栈中的,因此面向对象的设计是一种非常完美的设计方式,它完美的统一了数据的存储和运行。
堆内存优化:
调整JVM启动参数-Xms -Xmx -XX:newSize -XX:MaxNewSize,如调整初始堆内存和最大堆内存 -Xms256M,-Xmx512M,或者调整初始New Generation的初始内存和最大内存 -XX:newSize=128M -XX:MaxNewSize=128M。
永久区内存优化:
调整PermSize参数 如-XX:PermSize=512M -XX:MaxPermSize=512M。
栈内存优化:
调整每个线程的栈内存容量,如 -Xss2028K。
最终,一个内存中JVM所占的内存=堆内存+永久区内存+所有线程所占的栈内存总和。
一个bean被new出来后,在内存空间的走向?
Student s = new Student();
1)加载Student类文件到栈内存,开辟空间;
2)在栈内存为s开辟空间;
3)在堆内存为Student对象开辟空间;
4)给Student的成员变量分配默认值;
5)如果成员变量有给定值则用给定值覆盖默认值;
6)通过构造方法给成员变量赋值;
7)把Student对象在堆内存的地址值赋给s变量。
成员变量的初始化在构造函数之前。
类加载器:
java类装载方式:
1)隐式装载:程序在运行过程中碰到通过new等方式生成对象时,隐式调用类加载器加载对应的类到jvm中;
2)显示装载:通过class.forName()等方式,显示加载需要的类。
类加载的动态体现:
一个应用程序是由n多个类组成,java程序启动时,并不是一次性把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其他类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统开发的,内存宝贵,这是一种可以理解的机制,而用到时再加载也是java动态性的一种体现。
java类装载机制:
1)Bootstrap loader:Bootstrap加载器是用C++写的,它是在虚拟机启动后初始化的,主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classess中的类;
2)ExtClassLoader:BootStrap loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrap loader。ExtClassLoader是用java写的,主要加载%JAVA_HOME%/jre/lib/ext路径下所有的classes目录以及java.ext.dirs系统变量指定的路径中类库。
3)AppClassLoader:BootStrap Loader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为ExtClassLoader。AppClassLoader也是用java写的。classLoader中有个getSystemClassLoader方法,此方法返回的正是AppClassLoader,AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,是java程序默认的类加载器。
关系如图;
为什么要有三个类加载器,一方面是分工,各自负责各自的区域,另一方面是为了实现委托模型。
类加载器之间是如何协调工作的?
java采用了委托模型机制,这个机制简单来说,就是“类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,如果Parent找不到,那么才由自己按照自己的搜索路径搜索类”。
描述一下JVM加载class文件的原理机制?
【加载、验证、准备、解析、初始化、使用、卸载 7个阶段】
1)装载:查找和导入Class文件;
2)链接:其中解析步骤是可以选择的;
- 检查:检查载入的class文件数据的正确性
- 准备:给类的静态变量分配存储空间
- 解析:将符号引用转成直接引用
3)初始化:对静态变量,静态代码块执行初始化工作。
java装载类使用“全盘负责委托机制”,“全盘负责”是指当一个ClassLoader装载一个类时,除非显示的使用另外一个ClassLoader,该类所依赖及引用的类也由这个ClassLoader载入;“委托机制”是指先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。这一点是从安全方面考虑的,试想一个人如果写了一个恶意的基础类(java.lang.String)并加装到JVM将会引起严重的后果,但有了全盘负责制,java.lang.String永远是由根装载器来装载,避免了以上情况的发生。除了JVM默认的三个ClassLoader外,第三方可以编写自己的类加载器,以实现一些特殊的需求。类文件被装载解析后,在JVM中有一个对应的java.lang.Class对象,提供了类结构信息的描述。数组、枚举,及基本数据类型,甚至void都拥有对应的Class对象。Class类没有public的构造方法,Class对象是在装载类时由JVM调用类装载器中的defineClass()方法自动构造的。
为什么要使用这种双亲委托模式呢?
主要有两方面的原因,第一是避免重复加载,当父加载器已经加载了该类的时候,就没必要子加载器再来加载一次;
另一方面就是考虑到安全因素,我们试想一下,如果不使用这种委托机制,我们可以随时使用自定义的String动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的ClassLoader。
定义自己的ClassLoader?
java中提供的默认类加载器,只能加载指定目录下的jar和class,如果我们想加载其他位置的类或者jar时,比如,我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这种情况下,默认的类加载器已经不能满足我们的需求了,需要定义自己的ClassLoader。
定义自己的类加载器分为两步:
1)继承java.lang.ClassLoader;
2)重写父类的findClass方法。
如何让栈溢出,如何让方法区溢出?
栈溢出:死循环;
方法区溢出:借助CGLib使方法区出现内存溢出异常。方法区用于存放Class的相关信息,对于这些区域测试的基本思路:运行时产生大量的类去填满方法区,直到溢出。
参考:
https://www.cnblogs.com/kivi/p/3197825.html
https://blog.csdn.net/wang740209668/article/details/65474752
https://www.cnblogs.com/doit8791/p/5820037.html
写出几个JVM优化配置参数?
-Xms:初始堆大小。只要启动,就占用的堆大小。
-Xmx:最大堆大小。java.lang.OutOfMemoryError:Java heap这个错误可以通过配置-Xms和-Xmx参数来设置。
-Xss:栈大小分配。栈是每个线程私有的区域,通常只有几百K大小,决定了函数调用的深度,而局部变量、参数都分配到栈上。当出现大量局部变量,递归时,会发生栈空间OOM(java.lang.StackOverflowError)之类的错误。
-XX:NewSize=n :设置新生代大小的绝对值。
-XX:NewRatio=n :设置年轻代和年老代的比值。比如设置为3,则新生代:老年代=1:3,新生代占总heap的1/4。
-XX:MaxPermSize=n :设置持久代大小。java.lang.OutOfMemoryError:PermGenspace这个OOM错误需要合理调大PermSize和MaxPermSize大小。
-XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意,Survivor区有form和to两个。比如设置为8时,那么eden:form:to=8:1:1。
-XX:HeapDumpOnOutOfMemoryError:发生OOM时转储堆到文件,这是一个非常好的诊断方法。
-XX:HeapDumpPath:导出堆的转储文件路径。
-XX:OnOutOfMemoryError:OOM时,执行一个脚本,比如发送邮件报警,重启程序。后面跟着一个脚本的路径。