主题 | 链接 |
---|---|
Java基础知识 | 面试题 |
Java集合容器 | 面试题 |
Java并发编程 | 面试题 |
Java底层知识 | 面试题 |
Java常用框架 | 面试题 |
计算机网络 | 面试题 |
数据库 | 面试题 |
RabbitMQ | 面试题 |
Redis | 面试题 |
Elasticsearch | 面试题 |
Zookeeper | 面试题 |
系统设计 | 面试题 |
文章目录
- JVM内存结构
- Java内存模型
- 垃圾回收
-
- 如何判断一个对象是垃圾?
- 用什么方法来判断这个对象是垃圾?
- 什么是引用计算算法?
- 引用计数算法的优缺点是什么?
- 什么是可达性分析算法?
- 哪些对象可以被当做GC Root ?
- 垃圾回收算法
- 什么是标记清除算法?
- 什么是复制算法?
- 什么是标记-整理算法?
- 什么是分代收集算法?
- GC有哪些分类?
- 年轻代的特点?
- 什么样的对象会晋升到老年代?
- 常用的调优参数有哪些?
- 什么是老年代?
- 老年代垃圾回收的时候, 发生了什么?
- 什么时候会触发Full GC?
- 什么是 Stop-the-World?
- 垃圾收集器里面 Safepoint(安全点) 是什么?
- 常见的垃圾收集器有哪些?
- 什么是 G1 (Garbage First)收集器?
- G1 收集器对空间的划分是怎样的?
- Java中的强引用,软引用,弱引用,虚引用有什么用?
- 引用队列是干嘛的?
- Java对象模型
- 类加载机制
- 编译与反编译
- 什么是JIT
JVM内存结构
class文件格式
- JVM不会理解我们写的Java源文件, 我们必须把Java源文件编译成class文件, 才能被JVM识别, 对于JVM而言,class文件相当于一个接口
- class文件是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间。
- class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。 可以把u1, u2, u3, u4看做class文件数据项的“类型” 。
- magic:在class文件开头的四个字节, 存放着class文件的魔数, 这个魔数是class文件的标志,他是一个固定的值: 0XCAFEBABE 。 也就是说他是判断一个文件是不是class格式的文件的标准, 如果开头四个字节不是0XCAFEBABE, 那么就说明它不是class文件, 不能被JVM识别。
- minor_version 和 major_version:紧接着魔数的四个字节是class文件的此版本号和主版本号。
Java程序编译和运行的过程
//MainApp.java
public class MainApp {
public static void main(String[] args) {
Animal animal = new Animal("Puppy");
animal.printName();
}
}
//Animal.java
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void printName() {
System.out.println("Animal ["+name+"]");
}
}
程序运行的详细步骤:
- 在编译好java程序得到MainApp.class文件后,在命令行上敲java AppMain。系统就会启动一个jvm进程,jvm进程从classpath路径中找到一个名为AppMain.class的二进制文件,将MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp类的加载。
- 然后JVM找到AppMain的主函数入口,开始执行main函数。
- main函数的第一条命令是Animal animal = new Animal(“Puppy”);就是让JVM创建一个Animal对象,但是这时候方法区中没有Animal类的信息,所以JVM马上加载Animal类,把Animal类的类型信息放到方法区中。
- 加载完Animal类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Animal实例分配内存, 然后调用构造函数初始化Animal实例,这个Animal实例持有着指向方法区的Animal类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用。
- 当使用animal.printName()的时候,JVM根据animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息的方法表,获得printName()函数的字节码的地址。
- 开始运行printName()函数。
运行时数据区:堆、栈、方法区、直接内存、运行时常量池
- 程序计数器:线程私有,如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址,如果线程正在执行一个Native方法,这个计数器值为空(Undefined),此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
- Java虚拟机栈:线程私有,为虚拟机执行Java方法(字节码)服务,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果虚拟机栈可以动态扩展,如果扩展时无法申请足够的内存,就会抛出OutOfMemoryError异常
- 本地方法栈:线程私有,为虚拟机使用Native方法服务,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果虚拟机栈可以动态扩展,如果扩展时无法申请足够的内存,就会抛出OutOfMemoryError异常
- Java堆:线程共享,Java虚拟机管理的内存中最大的一块,仅用于存放对象实例,Java堆是垃圾收集器管理的主要区域,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。Java堆既可以实现成固定大小的,也可以是可扩展的(通过X-Xmx和Xms控制)
- 方法区:线程共享,用于存储被虚拟机加载的类信息、常量、静态常量,即编译器编译后的代码等数据。当方法区无法申请到内存时,抛出OutOfMemoryError异常
- 运行常量池:运行常量池是方法区的一部分.,常量池用于存放编译期生成的各种字面量和符号引用,一般来说,还会把翻译出来的直接引用也存储在运行时常量池,当常量池无法申请到内存时会抛出OutOfMemoryError异常
- 直接内存:不属于虚拟机运行时数据区的一部分,内存直接分配不受Java堆大小的限制,但是受本机总内存大小和处理器寻址空间的限制,动态扩展时可能出现OutOfMemoryError异常
堆和栈区别
- 栈内存用来存储局部变量和方法调用。,而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
- 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
- 如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出java.lang.StackOverFlowError。而如果是堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError。
- 栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满。如果递归没有及时跳出,很可能发生StackOverFlowError问题。可以通过-Xss选项设置栈内存的大小。-Xms选项可以设置堆的开始时的大小,-Xmx选项可以设置堆的最大值。
Java中的对象一定在堆上分配吗?
- 随着JIT编译器的发展,在编译期间,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。但是这并不是绝对的。
- JVM在内存新生代Eden Space中开辟了一小块区域,由线程私有,称作TLAB(Thread-local allocation buffer),默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
- 虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定;通常默认的TLAB区域大小是Eden区域的1%