-
- 1.1. JVM内存结构
- 1.1.1. 简单介绍java内存结构/JVM,虚拟内存,物理内存的区别?
- 1.1.2. java对象一定分配在堆上吗?讲讲内存逃逸?
- 1.1.3. 什么是符号引用?对象访问的两种方式?
- 1.1.4. 解析和分派
- 1.2. 类加载机制
- 1.2.1. new一个对象底层都发生了什么?
- 1.2.2. 类文件结构?(会考吗)
- 1.2.3. 类加载器有哪些?双亲委派?
- 1.3. GC原理
- 1.3.1. 什么是强、软、弱、虚引用?
- 1.3.2. 死亡对象判断算法有哪些?
- 1.3.3. 有哪些可以作为GC Root?
- 1.3.4. GC的触发时机
- 1.3.5. 垃圾收集算法
- 1.3.6. CMS/G1/ZGC的区别?
- 1.4. JVM调优
- 1.4.1. JVM有哪些常用的参数?
- 1.4.2. Minor GC运行很频繁的原因?
- 1.4.3. Minor GC运行时间太慢的原因?
- 1.4.4. Full GC运行很频繁的原因?
- 1.4.5. gc一定会停顿吗?
- 1.1. JVM内存结构
1. JVM
1.1. JVM内存结构
1.1.1. 简单介绍java内存结构/JVM,虚拟内存,物理内存的区别?
图示(JDK1.7):

图示(JDK1.8):

共享的内存:方法区(1.8之后改为元空间,从虚拟机内存转移到本地内存,里面有运行时常量池),堆(位于虚拟机内存中,内含字符串常量池)
私有的内存:每个线程有单独的虚拟机栈,本地方法栈,程序计数器
简单介绍java各个内存区域
- 堆内存:JVM在创建时向Linux申请一大块内存用于自动内存管理,根据GC的方式不同,堆内存的分区也不同,例如JDK8中默认的Parallel Scavenge会把堆内存分为新生代和老年代,两者的比例为2:1,新生代中又分了一个Eden和两个Suvivor,其比例默认为8:1:1,而G1GC则会被堆内存分为1,2,4,8M不等。
- 栈:虚拟机存放了java方法执行中的局部变量表,操作数栈,方法出口等信息
- 方法区:JDK7之前由永久代(PermGen)实现,JDK8之后由元数据区(metaspace)实现。存放了<1>类信息,包括类的接口信息,父类信息。<2>JIT之后的CodeCache。<3>常量池。<4>静态变量:包括类级别变量和接口中的变量,全部方法
方法区的PermGen|metaspace的区别?JDK8为什么要把方法区移到metaspace?
方法区是jvm规范,PermGen和metaSpace是方法区的实现
原因:
- 永久代存在虚拟机内存中,会为GC带来不必要的复杂性,而metaspace存放在本地内存中
- 类及方法的信息比较难确定大小,因此对于永久代的大小指定比较困难,太小容易永久代溢出,太大则容易导致老年代溢出
静态变量存储在哪?
静态方法(实际上所有方法)以及静态变量都存储在方法区,因为它们是反射数据的一部分(类相关数据,而不是与实例相关的)。
程序计数器是什么?程序计数器什么时候为空?
- PC是程序流的指示器,分支,循环,跳转等基础功能都需要依赖这个程序计数器,是线程私有的
- 如果是执行java方法,则存储JVM指令地址。否则如果执行native方法,则是undefined
1.1.2. java对象一定分配在堆上吗?讲讲内存逃逸?
逃逸分析是指JVM会收集对象的信息来判断对象的作用域有没有逃出当前方法,例如在方法中声明了一个StringBuffer sb且调用了sb.toString(),那么sb就没有发生逃逸
DEMO:
public static StringBuffer escape(String s1, String s2){
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(s1);
stringBuffer.append(s2);
return stringBuffer;
}
public void noEscape(String s1,String s2){
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(s1);
stringBuffer.append(s2);
}
对于未发生逃逸的变量,JVM会进行三种优化:
- 同步消除:将synchronized关键字消除
- 栈上分配:将对象由堆上分配转为栈上分配
- 分离对象:如果原来new了一个对象,那么并不会创建该对象,而是创建其中的基础变量
1.1.3. 什么是符号引用?对象访问的两种方式?
符号引用和直接引用
符号引用是解析阶段使用的,可以任意字面量的形式,被引用的类、方法或者变量还没有被加载到内存。
直接引用则是具体内存的指针,被引用的类、方法或者变量已经被加载到内存中了。
类加载的解析阶段就是完成常量池中的符号引用转化为直接引用。
JVM是如何区分是引用类型还是基本类型?他们的存放位置有什么不同?
当声明的是局部变量的时候,JVM 会将基本类型变量名和值放在栈帧中,将引用类型的内存地址放在栈帧中,数据在堆中
对象的访问定位有哪两种方式?
- 句柄
在堆中开辟的句柄池,到对象实例的指针和对象元数据的指针
优点:gc回收,当有多个reference指向同一个句柄池,如果需要对象移动,只需要改变到对象实例数据的指针

- 直接指针
直接指针可以直接访问实例对象,对象头中存放Class MetaData,减少了一次内存开销,但是增大了gc回收器的实现难度
优点:减少一次内存开销
缺点:增大了gc的难度,还是考虑多个reference指向同一个对象实例数据,所有reference都要改

1.1.4. 解析和分派
解析
像静态方法、私有方法、实例构造、父类方法(final方法)这些,指令调用是invokespecial或者invokestatic,在类加载后其方法就被解析为直接引用了,所调用的方法就已经确定了。
分派
分为静态分派和动态分派
-
静态分派比如方法重载,参数根据静态类型确定
-
动态分派比如方法重写,调用方根据动态运行时确定,即先完成符号引用到直接引用的解析得到调用方的实际类型,然后通过虚函数表vtable得到要调用的方法
vtable
vtable虚函数表主要是加快查找具体调用类型的方法签名,主要作用是如果子类没有重写父类方法,则方法入口地址是一样的。即使重写了,索引号也是一样的。这样可以快速定位。
1.2. 类加载机制
1.2.1. new一个对象底层都发生了什么?
<1>类加载检查:查找该类有没有被加载过,如果没有,则执行类加载过程
<2>分配内存:两种方式,指针碰撞与空闲列表
<3>初始化零值
<4>设置对象头:例如设置对象头的类数据类型引用
<5>执行init方法:调用init方法执行构造方法
类加载的过程
- 加载:获取此类的源信息并转化为方法区的运行时数据结构,即在方法区内存中生成一个java.lang.Class对象,作为这个类各种数据的访问入口。加载与双亲委派机制有关,优先调用父类的加载器去加载。
- 链接:包括三个子阶段:
- 验证:<1>确保class的信息格式符合虚拟机的需求且安全
- 准备:<1>为类中的静态字段分配内存,并设置初始值为默认值0 <2>这里不包含final修饰的变量,因为final变量在编译的时候已经确定了 <3>不会为实例变量分配内存初始化,因为实例变量是在堆中分配内存的
- 解析:<1>将常量池中的符号引用转化为直接引用
- 初始化:<1>执行类构造器clinit()方法,JVM会自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来, 需要区分init()方法
类加载的顺序?
父类的静态成员变量,父类的静态代码块->子类的静态成员变量,子类的静态代码块,父类的成员变量,父类非静态代码块,父类构造器->子类的成员变量,子类的非静态代码块,子类的构造器
why?
子类先加载父类,Clinit()是class加载时候调用的,是先于new创建实例的
new过程不知道为什么先是成员变量,再是非静态代码块,再是构造函数
类加载的时机
- new、getstatic、putstatic、invokestatic指令
- 反射
- 父类为初始化
- 虚拟机启动,main()
分配内存的两种方式?如何保证线程安全的?
两种方式
指针碰撞:前提是内存分为空闲的和非空闲的,只需要挪动划分区域的指针,适合空间压缩整理的垃圾回收器。
空闲列表:列表记录哪些空间是空闲的,适合基于清除算法的垃圾回收器。
如何保证并发安全的?
-
TLAB:为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述CAS进行内存分配
-
在堆上分配内存时,会使用CAS + 重试来申请内存
1.2.2. 类文件结构?(会考吗)
ClassFile {
u4 magic; //Class ⽂件的标志
u2 minor_version;//Class 的副版本号
u2 major_version;//Class 的主版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//⽗类
u2 interfaces_count;//接⼝数量
u2 interfaces[interfaces_count];//⼀个类可以实现多个接⼝
u2 fields_count;//Class ⽂件的字段属性
field_info fields[fields_count];//⼀个类会可以有个字段
u2 methods_count;//Class ⽂件的⽅法数量
method_info methods[methods_count];//⼀个类可以有个多个⽅法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
- 魔数cafebabe
- 副版本号和主版本号
- 当前类和父类的信息 this_class,super_class
- 常量池数量和常量池 constant_pool
- class访问标记access_flags
- 字段数量和字段(字段是开发者定义的,例如private int a),fields
- 属性数量和属性(属性属于JVM添加的,例如exceptions表示该方法可能抛出的异常),attributes
- 接口数量和接口 interfaces
- 方法数量和方法 methods
1.2.3. 类加载器有哪些?双亲委派?
类加载器有哪些?
- 启动类加载器:C++实现,嵌入JVM内部,用来加载java核心类库,用于提供JVM自身需要的类,并不继承自java.lang.ClassLoader,唯一没有父加载器
- 扩展类加载器:由java语言编写,继承自ClassLoader,父类是启动类加载器,主要负责加载JRE/lib/ext目录下的jar包和类。例如JMX类提供了对java应用程序的监控。
- 应用程序类加载器:继承自ClassLoader,负责加载当前应用classpath下的jar包和类
用户自定义类加载器的实现步骤?
继承自抽象类ClassLoader
几个关键方法的伪代码:
loadClass()
protected loadClass() {
// 先从缓存中查找
Class<?> c = findLoadedClass(name);
if(c == null) {
c = parent.loadClass(...);
// 委派给父类去加载。如果没有父类,则交给启动类加载器去加载
}
if(c == null) {
c = findClass(...);
// 如果父类加载器也没加载成功,则用自定义的findClass()去加载
}
}
findClass()
默认ClassLoader接口没有实现findClass(),会抛出异常。
一种实现方式:
protected Class<?> findClass() {
// 获取到Class文件的字节流
byte[] classData = getClassData(...);
// 将byte字节流转化为Class对象
return defineClass(...);
}
defineClass()
将byte字节流转化为JVM能识别的Class对象
resolveClass()
解析Class对象
自定义类加载器的步骤
如果要打破双亲委派原则,继承ClassLoader重写loadClass()方法。如果不打破双亲委派原则,继承自ClassLoader并重写findClass()方法。
为什么需要用户自定义类加载器?
- 需要隔离加载类:应用引入中间件,中间件和应用模块是分开的,要隔离的话就要把这个类加载到不同的环境,确保应⽤中使⽤的Jar包和中间件使⽤的Jar包是不冲突的。再者一些中间件如应用服务器本身就有不同应用环境隔离的需求。方法就是每个应用自定义类加载器并打破双亲委派原则,以实现隔离。
- 扩展加载源:DriverManager是启动类加载器加载的,不能加载各种数据源的驱动,反委派
双亲委派的好处?
可以避免类的重复加载,确保一个类的全局唯一性
1.3. GC原理
1.3.1. 什么是强、软、弱、虚引用?
强引用
强引用是在程序代码中普遍存在的,例如Object obj = new Object();这类引用就是强引用。只要强引用还存在,就永远不会被回收。
软引用
有用但非必需的对象,系统要OOM之前,会把软引用回收掉,SoftReference