Jvm由4个部分
组成,分为2个子系统和2个组件,2个子系统为Class loader(类装载)、Execution engine(执行引擎);2个组件为Runtime Data Area(运行时数据区)、Native Interface(本地接口)。
各个组成部分的用途:
Java 代码转换成字节码
(class文件)方法区
内指令集规范
,并不能直接交给底层操作系统
去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行
,而这个过程中需要调用其他语言的 本地库接口(Native Interface) 来实现整个程序的功能。HotSpot虚拟机是是Sun/OracleJDK和OpenJDK中的默认Java虚拟机,是JVM应用最广泛的一种实现。
JDK1.8
时彻底干掉了方法区
,而在直接内存
中划出一块区域作为元空间
,运行时常量池、类常量池都移动到元空间
。-Xms设置堆的最小空间大小。
堆中 年轻代和年老默认有个比如 是 NewRatio = 2 (默认是 2:1)
年轻代中eden和suvivor默认有个比例 8:1:1 (SurvivorRatio = 8) jps查看进程 jmap -heap 进程编号 查看到改参数
-Xmx 设置堆的最大空间大小。
-XX:NewSize 设置年轻代最小空间大小。
-XX:MaxNewSize 设置年轻代最大空间大小。
-XX:PermSize 设置永久代最小空间大小。
-XX:MaxPermSize 设置永久代最大空间大小。
-Xss 设置每个线程的堆栈大小 (64位 默认是1M -XX:ThreadStackSize默认是0)。
-Xms:JVM启动时申请的初始Heap值,默认为操作系统物理内存的1/64,例如-Xms20m
-Xmx:JVM可申请的最大Heap值,默认值为物理内存的1/4,例如-Xmx20m,我们最好将 -Xms 和 -Xmx 设为相同值,避免每次垃圾回收完成后JVM重新分配内存;
-Xmn:设置新生代的内存大小,-Xmn 是将NewSize与MaxNewSize设为一致,我们也可以分别设置这两个参数
-XX:PermSize 设置最小空间
-XX:MaxPermSize 设置最大空间
-XX:MetaspaceSize :分配给类元数据空间(以字节计)的初始大小。MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。
-XX:MaxMetaspaceSize:分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。
-XX:MinMetaspaceFreeRatio:表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最小比例,不够就会导致垃圾回收。
-XX:MaxMetaspaceFreeRatio:表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最大比例,不够就会导致垃圾回收。
Heap(堆)模块
在虚拟机启动时创建 , 堆是被所有线程共享
的最大的一块内存
,几乎所有的对象实例都在这里分配内存(并不是绝对);
-Xmx
和-Xms
控制堆大小根据Java回收机制的不同,Java堆有可能拥有不同的结构。最为常见的一种构成是将整个java堆分为年轻代和老年代
。其中年轻代存放新生对象或者年龄不大的对象,老年代则存放老年对象。
年轻代有分为Eden区、s0区、s1区,s0区和s1区也被称为from和to区
,他们是两块大小相同、可以互换角色
的内存空间。
在绝大多数情况下,对象首先分配在Eden区
,在一次年轻代回收之后,如果对象还存活,则进入s0或者s1
,每经过一次年轻代回收,对象如果存活,它的年龄就会加1
。当对象的年龄达到一定阀值后,就会被认为是老年对象
,从而进入老年代
。
年轻代:新创建的对象——>Eden区
Eden中存活的对象复制
⼀个空的 Survivor中
,并把当前的 Eden和正在使 的Survivor中的不可达对象 清除掉
老年代:对象如果在年轻代存活了足够长的时间而没有被清理掉
(即在几次Young GC
后存活了下来),则会被复制到老年代
长字符串或大数组
),且年轻代空间不足
,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)老年代的空间一般比年轻代大,能存放更多的对象
,在老年代上发生的GC次数也比年轻代少永久代:可以简单理解为方法区(本质上两者并不等价)
Java.lang.OutOfMemoryError: PermGen space
,这种错误将不会出现在JDK1.8中),通过使用本地内存的元空间
来代替永久代堆由年轻代和老年代组成,年轻代又分为Eden区
和survivor(幸存)区,survivor区
中又有from
区和to
区.
new出来的对象一般都放在Eden区,那当Eden区满了之后呢?
600M内存
,那么老年代默认是占2/3
的,也就是差不多400M
,那年轻代就是200M
,Eden区160M
,Survivor区40M
。
一个程序只要在运行,那么就不会不停的new对象,那么总有一刻Eden区会放满
,那么一旦Eden区被放满之后,虚拟机会干什么呢?
minor(咪呢) gc
,就是垃圾收集,来收集垃圾对象并清理的,那么什么是垃圾对象呢?这里就涉及到了一个GC Root根以及可达性分析算法的概念,也是面试偶尔会被问到的。
可达性分析算法
是将GC Roots对象作为起点,从这些起点开始向下搜索引用的对象
,找到的对象都标记为非垃圾对象
,其余未标记的都是垃圾对象
。加粗样式那么GC Roots根对象又是什么呢?
线程栈的本地变量、静态变量、本地方法栈的变量等等它们引用的对象
,说白了就是找到和根节点有联系的对象就是有用的对象,其余都认为是垃圾对象来回收
。minor gc
后,没有被清理的对象就会被移到From区
,如上图。存储GC分代年龄
,一个对象每经历一次gc,那么它的gc分代年龄就会+1,如上图。那么如果第2次
新的对象又把Eden区放满了,那么又会执行minor gc
,但是这次会连着From区一起gc
,然后将Eden区
和 From区
存活的对象都移到To区域
,对象头中分代年龄都+1
,如上图。
那么当第3次
Eden区又满的时候,minor gc
就是回收Eden区
和 To
区域了,TEden区和To区域
还活着的对象就会都移到From区
,如上图。
Survivor区中总有一块区域是空着的
,存活的对象存放是在From区和To区轮流存放,也就是互相复制拷贝,这也就是垃圾回收算法中的复制-回收算法
。如果一个对象经历了一个15次gc
的时候,就会移至老年代。如果还没有到最大年龄且From区或者To区域也慢了,就会直接移到老年代
,这只是举例了两种常规规则,还有其他规则也是会把对象存放至老年代的。
Full gc
了。那当我们老年代满了会发生什么呢?当然是我们上面说过的Full GC
,但是你仔细看我们写的这个程序,我们所有new出来的HeapTest对象都是存放在heapLists中的,那就会被这个局部变量
所引用,那么Full GC就不会有什么垃圾对象可以回收
,可是内存又满了,那怎么办?OOM
下面是个死循环,不断的往list中添加new出来的对象。
public class HeapTest {
byte[] a = new byte[1024 * 100];
public static void main(String[] args) throws InterruptedException {
ArrayList<HeapTest> heapTest = new ArrayList<>();
while(true) {
heapTest.add(new HeapTest());
Thread.sleep(10);
}
}
}
通过JDK自带的Jvm调优工具jvisualvm
观察上面代码执行的内存结构。
打开visual GC
- 对象放入Eden区
- Eden区满发生minor gc
- 第二步的存活对象移至From(Survivor 0)区
- Eden区再满发生minor gc
- 第四步存活的对象移至To(Survivor 1)区
Minor GC(YGC): 它主要是用来对年轻代
进行垃圾回收的方式,使用的复制算法
,因为年轻代的对象大多数生命周期很短,所以GC的频率也会比较频繁,但是回收速度很快。
Major GC(YGC): 它是主要用于对老年代
对象的垃圾回收方式,老年代的对象生命周期都是比较长的,所以对象不会轻易灭亡,Major GC的频率不会像Minor GC那么频繁,况且一次Full GC会比Minor GC需要花费更多的时间、消耗更大,通常出现一次Major GC一般也会出现一次Minor GC(但不绝对)。
Full GC(): Full GC是针对整个年轻代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC,
但是它并不等于Major GC + Minor GC,具体是要看使用什么垃圾收集器组合。一次Full GC 需要花费更多的时间、消耗更大,所以要尽可能减少Full GC的次数
。
特点比较:
复制算法
,需要一块空的内存空间,所以空间使用效率不高
,但是它不会出现空间碎片的问题。标记-清除算法
,容易产生空间碎片
,如果再有对象需要请求连续的空间而无法提供时,会提前触发垃圾回收,
所以它适合存活对象较多
的场景使用也就是老年代
的垃圾回收。内存泄漏是不再被使用的对象或者变量一直被占据在内存中
。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露
,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的发生场景。物理地址
堆的物理地址分配对对象是不连续的
。因此性能慢些。在GC的时候也要考虑到不连续的分配
,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即年轻代使用复制算法,老年代使用标记——压缩)
栈使用的是数据结构中的栈
,后进先出
的原则,物理地址分配是 连续
的。所以性能快。
内存分别
堆因为是不连续的
,所以分配的内存是在运行期
确认的,因此大小不固定
。一般堆大小远远大于栈。
栈是连续的
,所以分配的内存大小要在编译期
就确认,大小是固定的
。
存放的内容
堆存放的是对象的实例和数组
。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果
。该区更关注的是程序方法的执行
。
程序的可见度
该代码声明了一个类,并在main方法中创建了两个SimpleHeap实例。
public class SimpleHeap {
private int id;
public SimpleHeap(int id){
this.id = id;
}
public void show(){
System.out.println("My id is "+id);
}
public static void main(String[] args) {
SimpleHeap s1 = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
s1.show();
s2.show();
}
}
各对象和局部变量的存放情况如下图:
是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)
用于存储局部变量表、操作数栈、动态链接、方法出口
等信息,每个方法从调用直至执行完成的过程,都对应着一个栈帧
在虚拟机栈中入栈到出栈
的过程。
每一个线程都有一个私有的Java栈,一个线程的Java栈在线程创建的时候被创建
java栈中保存着栈帧
信息- 局部变量表:存放了编译器可知的
各种基本数据类型
(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本
),- 局部变量表所需的内存空间在
编译期间完成分配
,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间
)
请求的栈深度
大于虚拟机所允许的栈深度
就会抛出StackOverflowErrorJVM 会在线程被创建时,创建一个线程私有
的虚拟机栈,也叫“线程栈
”。该栈的生命周期和线程是一致
,除了Native方法以外,Java方法都是通过Java 虚拟机栈来实现调用和执行
过程的(需要程序计数器、堆、元空间内数据的配合)。所以Java虚拟机栈是虚拟机执行引擎的核心之一。而Java虚拟机栈中出栈入栈的元素就称为「栈帧」
。
多个栈帧(Frame)
组成,对应着每个方法
运行时所占用的内存
。活动栈帧
,也叫当前栈帧
,对应着当前正在执行的方法
,当方法执行时压入栈
,方法执行完毕后弹出栈
。基本类型
的变量都在栈上,引用变量
的指针在栈上,实例在堆上
。public class Math {
public static int initData = 666;
public static User user = new User();
public int compute() {
int a = 1;
int b = 2;
int c = (a+b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
System.out.println("test");
}
}
math,compute
方法中的a b c
,那么Java虚拟机为了区分不同方法中局部变量作用域范围的内存区域,每个方法在运行的时候都会分配一块独立的栈帧内存区域, 上图中的程序代码执行的内存活动如下。执行main方法中的第1行代码是,栈中会分配main()
方法的栈帧,并存储math局部变量,,接着执行compute()
方法,那么栈又会分配compute()的栈帧区域。
compute()
方法执行完之后,就会出栈被释放,也就符合先进后出
的特点,后调用的方法先出栈。栈帧(Stack Frame)
是用于支持虚拟机进行方法调用和方法执行的数据结构。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息
**。
每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈
的过程**。
简单的理解就是:栈对应线程,栈帧对应方法
局部变量表(Local Variable Table)是一组变量值存储空间
,用于存放方法参数和方法内定义的局部变量
。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型 (指向一条字节码指令的地址)。
StackOverflowError
异常OutOfMemoryError
异常。直接上代码
public int test(int a, int b) {
Object obj = new Object();
return a + b;
}
操作数栈(Operand Stack) 也称作操作栈,是一个后入先出栈(LIFO)
。随着方法执行
和字节码指令
的执行,会从局部变量表或对象实例的字段
中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素 出栈到局部变量表 或者 返回给方法调用者,也就是出栈/入栈操作。
public class OperandStackTest {
public int sum(int a, int b) {
return a + b;
}
}
编译生成.class文件之后,再反汇编查看汇编指令
javac OperandStackTest.java
javap -v OperandStackTest.class
OperandStackTest字节码文件
public int sum(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3 // 最大栈深度为2 局部变量个数为3
0: iload_1 // 局部变量1 压栈
1: iload_2 // 局部变量2 压栈
2: iadd // 栈顶两个元素相加,计算结果压栈
3: ireturn
LineNumberTable:
line 10: 0
动态链接:Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池
中 该栈所属方法的符号引用
,持有该引用是为了支持方法调用过程中的动态链接(Dynamic Linking)
。
方法返回地址/方法出口:无论方法是否正常完成,都需要返回到方法被调用的位置
,程序才能继续进行
无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有3种方式:
那么要讲这个就会涉及到更底层的原理–字节码
。我们先看下我们上面代码的字节码文件。
看着就是一个16字节的文件
,看着像乱码,其实每个都是有对应的含义的,oracle官方是有专门的Jvm字节码指令手册
来查询每组指令对应的含义的。那我们研究的,当然不是这个。
javap
的命令,可以将上述class文件生成一种更可读的字节码文件
。javap -c
命令将class文件反编译并输出到TXT文件中。Compiled from "Math.java"
public class com.example.demo.test1.Math {
public static int initData;
public static com.example.demo.bean.User user;
public com.example.demo.test1.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/example/demo/test1/Math
3: dup
4: invokespecial #3 // Method "":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String test
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
static {};
Code:
0: sipush 666
3: putstatic #8 // Field initData:I
6: new #9 // class com/example/demo/bean/User
9: dup
10: invokespecial #10 // Method com/example/demo/bean/User."":()V
13: putstatic #11 // Field user:Lcom/example/demo/bean/User;
16: return
}
其中方法中的指令还是有点懵,我们举compute()方法来看一下:
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
compute()
方法中的四行代码。大家都知道越底层的代码,代码实现的行数越多,因为他会包含一些java代码在运行时底层隐藏的一些细节原理。
那么一样的,这个Jvm指令官方也是有手册可以查阅的,网上也有很多翻译版本,大家如果想了解可自行百度。
0. 将int类型常量1压入操作数栈
0: iconst_1
1. 将int类型值存入局部变量1
1: istore_1
2. 将int类型常量2压入操作数栈
2: iconst_2
3. 将int类型值存入局部变量2
3: istore_2
4. 从局部变量1中装载int类型值
4: iload_1
5. 从局部变量2中装载int类型值
5: iload_2
操作数栈
中6. 执行int类型的加法
6: iadd
7. 将一个8位带符号整数压入栈
7: bipush 10
8. 执行int类型的乘法
9: imul
9. 将将int类型值存入局部变量3
10: istore_3
将30存入局部变量3,也就是c
10. 从局部变量3中装载int类型值
11: iload_3
11. 返回int类型值
12: ireturn
将操作数栈中的30返回
到这里就把我们compute()方法讲解完了,讲完有没有对局部变量表和操作数栈的理解有所加深呢?说白了赋值号=后面的
就是操作数,在这些操作数进行赋值,运算的时候需要往内存存放,那就是存放在操作数栈
中,作为临时存放操作数的一小块内存区域。
接下来我们再说说方法出口。
方法执行完了之后要出到哪里
,那么我们知道上面compute()方法执行完之后应该回到main()方法第三行
那么当main()方法调用compute()的时候,compute()栈帧
中的方法出口就存储了当前要回到的位置,那么当compute()方法执行完之后,会根据方法出口中存储的相关信息回到main()方法的相应位置。当前帧(位于栈顶)
,它保存着当前函数的局部变量、中间计算结果等数据
。return指令
,另一种是抛出异常
。不管使用哪种方式,都会导致栈帧被弹出。
请求的栈深度大于最大可用栈深度时
,系统会抛出StackOverflowError栈溢出错误
。使用递归,由于递归没有出口,这段代码可能会抛出栈溢出错误,在抛出栈溢出错误时,打印最大的调用深度
public class TestStackDeep {
private static int count =0;
public static void recursion(){
count ++;
recursion();
}
public static void main(String[] args) {
try{
recursion();
}catch(Throwable e){
System.out.println("deep of calling ="+count);
e.printStackTrace();
}
}
}
在进行大约1079次调用之后,发生了栈溢出错误,通过增大-Xss的值,可以获得更深的层次调用,尝试使用参数-Xss256K执行上述代码,调用层次有明显的增加:
结论:函数嵌套调用的层次在很大程度上由栈的大小决定
,栈越大,函数支持的嵌套调用次数就越多。
局部变量表是栈帧的组成部分之一。用于保存函数的参数
以及局部变量
,局部变量表随着函数栈帧的弹出而销毁。
局部变量表膨胀
,从而每一次函数调用就会占用更多的栈空间
,最终导致函数的嵌套调用次数减少
。例如:一个recursion()函数含有3个参数和10个局部变量,因此,其局部变量表含有13个变量,而第二个recursion()函数不再含有任何参数和局部变量,当这两个函数被嵌套调用时,第二个recursion函数可以拥有更深的调用层次。
public class TestStackDeep2 {
private static int count = 0;
public static void recursion(long a,long b,long c){
long e=1,f=2,g=3,h=4,i=5,k=6,q=7,x=8,y=9,z=10;
count ++;
recursion(a,b,c);
}
public static void recursion(){
count++;
recursion();
}
public static void main(String[] args) {
try{
recursion(0L,0L,0L);
//recursion();
}catch(Throwable e){
System.out.println("deep of calling = "+count);
e.printStackTrace();
}
}
}
-Xss128K
递归执行上述代码中的recursion(long a,long b,long c)函数
,输出结果为:-Xss128K
递归执行不带参数的recursion()
函数与虚拟机栈的作用是一样的
,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用本地方法(Native方法)服务的
线程私有,StackOverflowError、OutOfMemoryError
。new Thread().start();
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
其中底层调用了一个start0()
的方法,本地方法,底层通过C语言
实现的
private native void start0();
那java代码里为什么会有C语言实现的本地方法呢?
大家都知道JAVA出来之前一个公司的系统百分之九十九都是使用C语言实现的,但是java出现后,很多项目都要转为java开发,那么新系统和旧系统就免不了要有交互,那么就需要本地方法来实现了,底层是调用C语言中的dll库文件
,就类似于java中的jar包
,当然,如今跨语言的交互方式就很多了,比如`thrift,http接口方式,webservice等,当时并没有这些方式,就只能通过本地方法来实现了。
程序计数器/*PC寄存器(Program Counter Register):可以看做是当前线程所执行的字节码的行号指示器
,每个线程都有一个程序计数器来保存当前执行指令的地址
,一旦该指令被执行,程序计数器会被更新至下条指令的地址
。程序计数器是Java虚拟机规定的唯一不会发生内存溢出的区域
。
这是一块较小
的内存空间(可忽略不记
),用于记录当前线程所执行的字节码的行号指示器
,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令
,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器
来完成;
特点:线程私有
异常规定:无
那么Jvm虚拟机为什么要设置程序计数器这个结构呢?
因为Jvm的多线程是通过线程轮流切换并分配处理器执行时间(cpu时间片)来的方式来实现的
,也就是任何时刻,一个处理器(对于多核处理器来说是一个内核)
都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置
,每个线程都有独立的程序计数器
。
它被设计出来的目的,是为了让多线程情况下的JAVA程序每个线程都能够正常的工作,每个线程都有自己的程序计数器,用于保存线程的执行情况,这样在进行线程切换的时候就可以在上次执行的基础上继续执行了
方法区即我们常说的永久代(Permanent Generation), 也称为非堆(No-Heap)、是线程共享的一块内存区域,用于存储被 JVM 加载的类信息、常量、静态变量、JIT即时编译器编译后的代码
等数据
字面量和符号引用
,这部分内容将在类加载后存放到方法区的运行时常量池中JDK 8 移动到堆中
)、final常量、基本数据类型的值(如Integer,管理-128–127的常量。)。JDK1.8后永久代被元空间代替,元空间存储在直接内存(系统内存)
,而不在虚拟机当中(不受JVM最大运行内存的限制,只和本地内存的大小有关
) 其他内容比如类元信息、字段、静态属性、方法、常量等都移动到元空间区
。
特点:线程共享
异常规定:当方法无法满足内存分配需求时会抛出OutOfMemoryError异常。
过-XX:PermSize 和 -XX:MaxPermSize
参数限制方法区的大小JDK1.6字符串常量池在方法区
中,1.7将放在方法区
的字符串常量池放到堆
中。**在JDK1.8时彻底去掉了永久代的概念,而在直接内存中划出一块区域作为元空间,运行时常量池、类常量池都移动到元空间。
。
1.避免OOM:
存储类的相关信息(包括类的字节码文件)
, 虽然永久代可以使用PerSize和MaxPerSize
等参数设置永久代的空间大小, 但随着ASM、Cglib等动态生成字节码技术的出现可以修改对象字节码信息后,无法控制类信息的大小, 因此对于永久代的大小指定比较困难,太小容易出现永久代溢出
,太大则容易导致老年代溢出
,即 java.lang.OutOfMemoryError: PermGen
。
系统内存
,由系统的实际可用空间来控制,在一定程度上可以避免OOM的出现,
但是也需要通过指定MaxMetaspaceSize
等参数来控制大小。2.提高GC性能:
永久代的垃圾收集是和老年代捆绑在一起的,
所以无论两者谁满了,都会触发永久代和老年代的垃圾收集。
JDK1.7时永久代的部分数据已经从Java的永久代中转移到了堆中,如:符号引用、字符串常量池
Full GC,减少了GC的时间(因为GC时不需要再扫描永久代中的数据),提高了GC的性能
。在元空间中,只有少量指针指向堆,如类的元数据中指向class对象的指针。3.Hotspot和JRockit合并:
直接内存(Direct Memory): 也叫堆外内存,直接内存并不是Jvm管理的内存,可以这样理解就是Jvm以外的机器内存
,比如,你有4G的内存,Jvm占用了1G,则其余的3G就是直接内存
NIO(New Input/Output)
类,引入了一种基于通道(Channel)
与缓冲区(Buffer)
的I/O方式,它可以使用Native函数库
直接分配堆外内存
,然后通过一个存储在Java堆
中的DirectByteBuffer对象
作为这块内存的引用进行操作。通
Java堆和Native堆
中来回复制数据。
对象在内存中存储的布局可以分为3块区域:对象头(Header)
、实例数据(Instance Data)
和 对齐填充(Padding)
HotSpot虚拟机的对象头包括2部分信息:
Mark Word
第一部分markword,用于存储对象自身的运行时数据
,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,
对象头的另外一部分是klass类型指针
(Klass Pointer),即对象指向它的Class类元数据的指针
,虚拟机通过这个指针来确定这个对象是哪个类的实例.
数组长度(只有数组对象有): 如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.
实例数据:
是对象真正存储的有效信息
,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。对齐填充
当对象实例数据部分没有对齐时,就需要通过对齐填充来补全
。类加载其实最终是以Class对象
的形式存储在方法区
中的,math和math2都是由同一个类new出来的,当对象被new时,都会在对象头中存储一个指向类元信息的指针,这就是Klass Pointer类型指针
new
关键字->
调用了构造方法Class
的newInstance
方法->
调用了构造方法Constructor
类的newInstance
方法->
调用了构造方法clone
方法->
没有调用构造方法反序列化
->`没有调用构造方法虚拟机遇到一条new指令时
,先检查常量池是否已经加载相应的类
,如果没有,必须先执行相应的类加载
。类加载通过后,接下来分配内存。若Java堆中内存是绝对规整的,使用 “指针碰撞“ 方式分配内存;如果不是规整的,就从空闲列表中分配,叫做 ”空闲列表“ 方式。
CAS同步处理
,或者本地线程分配缓冲
(Thread Local Allocation Buffer, TLAB)。然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码…),最后执行
方法。类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整
,有2种方式:
规整
,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离
,这样便完成分配内存工作。不规整
的,已使用的内存和空闲的内存相互交错, 那就没办法简单的进行指针碰撞了, 必须由由虚拟机维护一个列表
来记录那些内存是可用的
,在分配的时候从列表找到一块足够大的内存分配给对象,并在分配后更新列表记录。对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置
,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。
解决这个问题有两种方案:
同步处理
(采用 CAS + 失败重试
来保障更新操作的原子性);按照线程划分在不同的空间之中进行
,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲
(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。通过-XX:+/-UserTLAB参数来设定虚拟机是否使用TLAB。面试题:(Java实习生)每日10道面试题打卡——JVM篇