JVM内存结构和Java内存模型都是面试的热点问题,名字看感觉都差不多,网上有些博客也都把这两个概念混着用,实际上他们之间差别还是挺大的。
通俗点说,JVM内存结构是与JVM的内部存储结构相关,而Java内存模型是与多线程编程相关,本文针对这两个总是被混用的概念展开讲解。
说到JVM内存结构,就不会只是说内存结构的5个分区,而是会延展到整个JVM相关的问题,所以先了解下JVM的构成。
栈:线程运行需要的内存空间
栈帧:每一个方法运行需要的内存(包括参数,局部变量,返回地址等信息)
常见问题解析
垃圾回收是否涉及栈内存:不涉及,垃圾回收只涉及堆内存
栈内存分配越大越好吗:内存一定时,栈内存越大,线程数就越少,所以不应该过大
方法内的局部变量是否是线程安全的:
栈内存溢出(StackOverflowError)
栈帧过多
栈帧过大
线程运行诊断
CPU占用过高(定位问题)
程序运行很长时间没有结果(死锁问题)
通过new关键字创建的对象都会使用堆内存
堆是线程共享的
堆中有垃圾回收机制
堆内存溢出(OutOfMemoryError)
堆内存诊断
命令行方式
jconsole
jvisualvm
StringTable特性
常量池中的字符串仅是字符,第一次使用时才变为对象
利用串池机制,避免重复创建字符串
字符串常量拼接原理是StringBuilder(1.8)
字符串常量拼接原理是编译器优化
StringTable在1.6中存放在永久代,在1.8中存放在堆空间
intern方法主动将串池中没有的字符串对象放入串池
1.8中:尝试放入串池,如果有就不放入,只返回一个引用;如果没有就放入串池,同时返回常量池中对象引用
1.6中:尝试放入串池,如果有就不放入,只返回一个引用;如果没有就复制一个放进去(本身不放入),同时返回常量池中的对象引用
字符串常量池分析(1.8环境)
String s1 = "a";
String s2 = "b";
String s3 = "a"+"b";
String s4 = s1+s2;
String s5 = "ab";
String s6 = s4.intern();
System.out.println(s3==s4);// s3在常量池中,s4在堆上(intern尝试s4放入常量池,因为ab存在了就拒绝放入返回ab引用给s6,s4还是堆上的)
System.out.println(s3==s5);// s3在常量池中,s4也在常量池中(字符串编译期优化)
System.out.println(s3==s6);// s3在常量池中,s6是s4的intern返回常量池中ab的引用,所以也在常量池中
String x2 = new String("c")+new String("d");
String x1 = "cd";
x2.intern();
System.out.println(x1==x2);//x2调用intern尝试放入常量池,但常量池中已经有cd了,所以只是返回一个cd的引用,而x2还是堆上的引用
JVM调优三大参数(如: java -Xms128m -Xmx128m -Xss256k -jar xxxx.jar)
JVM内存结构中堆和栈的区别
判断对象的引用数量来决定对象是否可以被回收
每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
优点:执行效率高,程序执行受影响小
缺点:无法检测出循环引用的情况,导致内存泄露
Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活对象
扫描堆中的对象,看是否能沿着GC Root对象为起点的引用链找到该对象,找不到则可以回收
哪些对象可以作为GC Root
通过System Class Loader或者Boot Class Loader加载的class对象,通过自定义类加载器加载的class不一定是GC Root
虚拟机栈中的引用的对象
本地方法栈中JNI(natice方法)的引用的对象
方法区中的常量引用的对象
方法区中的类静态属性引用的对象
处于激活状态的线程
正在被用于同步的各种锁对象
GC保留的对象,比如系统类加载器等。
安全点(SafePoint)
分析过程中对象引用关系不会发生改变的点
产生安全点的地方:
安全点的数量应该设置适中
串行(SerialGC)
吞吐量优先(ParallelGC)
响应时间优先(CMS -XX:+UseConcMarkSweepGC 标记清除算法)
多线程的垃圾回收器
堆内存较大,多核CPU,Server模式下默认的老年代垃圾回收器
尽可能让单次STW暂停时间最短
部分时期内可以并发执行
执行流程
G1(-XX:+UseG1GC 复制+标记清除算法)
垃圾回收阶段
Full GC
SerialGC
ParallelGC
CMS
新生代内存不足发生的垃圾收集:minor GC
老年代内存不足
G1
新生代内存不足发生的垃圾收集:minor GC
老年代内存不足,达到阈值时进入并发标记和混合收集阶段
强引用
软引用
仅有【软引用】引用该对象时,在垃圾回收后,内存仍不足时会再次发起垃圾回收,回收软引用对象
可以配合引用队列来释放软引用自身
创建一个软引用:SoftReference ref = new SoftReference<>(new Object());
软引用被回收后,仍然还保留一个null,如将软引用加入集合,回收后遍历集合仍然还存在一个null
弱引用
虚引用
终结器引用
加载
链接
校验:检查加载的的Class的正确性和安全性
准备:为类变量分配存储空间并设置类变量初始值
解析:JVM将常量池内的符号引用转换为直接引用
初始化
需求场景
步骤
案例演示
创建自定义类加载器
public class MyClassLoader extends ClassLoader {
private String path;
private String classLoaderName;
public MyClassLoader(String path, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}
//用于寻找类文件
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
//用于加载类文件
private byte[] loadClassData(String name) {
name = path + name + ".class";
try (InputStream in = new FileInputStream(new File(name));
ByteArrayOutputStream out = new ByteArrayOutputStream();) {
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
return out.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
调用自定义类加载器加载类
public class MyClassLoaderChecker {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyClassLoader m = new MyClassLoader("C:\\Users\\73787\\Desktop\\","myClassLoader");
Class<?> c = m.loadClass("Robot");
System.out.println(c.getClassLoader());
c.newInstance();
}
}
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
第三方应用开发过程中,会需要某个类的某个成员变量、方法或是属性是私有的或者只对系统应用开放,就可以通过Java的反射机制来获取所需的私有成员或者方法
代表类的实体,在运行的Java应用程序中表示类和接口
Filed代表类的成员变量(属性)
定义一个Robot类
public class Robot {
//私有属性
private String name;
//公有方法
public void sayHi(String hello){
System.out.println(hello+" "+name);
}
//私有方法
private String thorwHello(String tag){
return "hello "+tag;
}
}
编写一个反射应用类,针对私有的属性和方法必须设置setAccessible(true)才能进行访问
public class ReflectSample {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
//加载类
Class<?> rc = Class.forName("leetcode.Robot");
//获取类实例
Robot r = (Robot)rc.newInstance();
//打印类名
System.out.println(rc.getName());
//加载一个私有方法
Method getHello = rc.getDeclaredMethod("thorwHello",String.class);
getHello.setAccessible(true);
Object bob = getHello.invoke(r, "bob");
System.out.println(bob);
//加载一个公有方法
Method sayHi = rc.getMethod("sayHi",String.class);
Object welcome = sayHi.invoke(r,"welcome");
//加载一个私有属性
Field name = rc.getDeclaredField("name");
name.setAccessible(true);
name.set(r,"tom");
sayHi.invoke(r,"welcome");
}
}
JVM实现不同会造成“翻译”的效果不同,不同CPU平台的机器指令有千差万别,无法保证同一份代码并发下的效果一致。所以需要一套统一的规范来约束JVM的翻译过程,保证并发效果一致性
/**
* 〈可见性问题分析〉
*
* @author Chkl
* @create 2020/3/4
* @since 1.0.0
*/
public class FieldVisibility {
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
循环创建两类线程,一个线程用于做值的交换,一个线程用于打印值
比较直观的三种结果
实际上除了很容易想到的三种情况外还有一种特殊情况:b = 3 , a = 1
计算:
a = 3;
b = 2;
a = a + 1;
重排序优化前的instructions
load a
set to 3
store 3
load b
set to 2
store b
load a
set to 4
store a
经过重排序处理后
load a
set to 3
set to 4
store a
load b
set to 2
store b
上述少了两个指令,优化了性能
什么是volatile
什么时候适合用vilatile
volatile的作用
volatile的性能
什么是happens-before规则:前一个操作的结果可以被后续的操作获取。
如果有用,点个赞再走吧