六、JVN
1、谈谈对Java的理解
- 平台无关性(一次编译,到处运行)
- GC(垃圾回收机制,释放堆内存)
- 语言特性(泛型、反射,lamada表达式)
- 面向对象(封装,继承,多态)
- 类库(Java本生自带的集合,并发库,网络库,IO)
- 异常处理
2、平台无关性如何实现?
Compile Once, Run Anywhere如何实现?
- 编译时
- 运行时
示例演示:
javap是jdk自带的反汇编译器,可以查看编译器返回的字节码,了解编译器内部工作的机制
在liunx平台下运行:
Java文件运行流程:
平台无关性流程:
Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
为什么JVM不直接将源码解析成机器码去执行?
- 准备工作:每次执行都需要各种检查
- 兼容性:也可以将别的语言解析成字节码
3、JVM如何加载.class文件
JVM是内存中的虚拟机
JVM架构:
- Class Loader :依据特定格式,加载class文件到内存
- Execution Engine :对命令进行解析
- Native Interface :融合不同开发语言的原生库为Java所用
- Runtime Data Area : JVM内存空间结构模型
4、什么是反射?
定义:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
代码示例:
(1)Robot
类
package com.interview.javabasic.reflect;
public class Robot {
// 私有变量
private String name;
// 公共方法
public void sayHi(String helloSentence){
System.out.println(helloSentence + " " + name);
}
// 私有方法
private String throwHello(String tag){
return "Hello " + tag;
}
static {
System.out.println("Hello Robot");
}
}
(2)反射示例主类
package com.interview.javabasic.reflect;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* 反射示例
*/
public class ReflectSample {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
// 获取类
Class rc = Class.forName("com.interview.javabasic.reflect.Robot");
// 获取类实例
Robot r = (Robot) rc.newInstance();
// 打印类名称
System.out.println("Class name is " + rc.getName());
// 通过类获取方法 getDeclaredMethod获取该类的所有方法,不能获得该类实现的或者继承的方法
Method getHello = rc.getDeclaredMethod("throwHello", String.class);
// 私有变量或方法必须设置
getHello.setAccessible(true);
// 获取方法内容
Object str = getHello.invoke(r, "Bob");
System.out.println("getHello result is " + str);
// getMethod只能获得该类的公共方法,也能获得该类继承或者实现的方法
Method sayHi = rc.getMethod("sayHi", String.class);
sayHi.invoke(r, "Welcome");
Field name = rc.getDeclaredField("name");
name.setAccessible(true);
name.set(r, "Alice");
sayHi.invoke(r, "Welcome");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));
}
}
5、ClassLoader
类从编译到执行的过程:
- 编译器将Robot.java源文件编译为Robot.class字节码文件
- ClassLoader将字节码转换为JVM中的Class < Robot>对象
- JVM利用Class
对象实例化为Robot对象
谈谈ClassLoader?
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的。ClassLoader负责通过将Class 文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
ClassLoader的种类
BootStrapClassLoader : C+ +编写,加载核心库java.*
ExtClassLoader : Java编写,加载扩展库javax.*
System.out.println(System.getProperty("java.ext.dirs")); // 打印结果 /Users/cyh/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
AppClassLoader : Java编写,加载程序所在目录
System.out.println(System.getProperty("java.class.path")); // 打印结果 /Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/tools.jar:/Users/cyh/Downloads/javabasic/out/production/javabasic:/Users/cyh/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/192.5728.98/IntelliJ IDEA.app/Contents/lib/idea_rt.jar
自定义ClassLoader : Java编写,定制化加载
自定义ClassLoader类的实现方法:(不太明白6-5)
protected Class<?> findClass (String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null) ;
}
自定义ClassLoader类示例:
package com.interview.javabasic.reflect;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
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";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(name));
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}
主类:
package com.interview.javabasic.reflect;
public class ClassLoaderChecker {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader m = new MyClassLoader("/Users/cyh/Desktop/", "myClassLoader");
Class c = m.loadClass("WaLi");
System.out.println(c.getClassLoader());
System.out.println(c.getClassLoader().getParent());
System.out.println(c.getClassLoader().getParent().getParent());
System.out.println(c.getClassLoader().getParent().getParent().getParent());
c.newInstance();
}
}
输出:
ClassLoader的双亲委派机制
图片来源:https://blog.nowcoder.net/n/0597892d34ae4023add7406cbbe388d0
为什么要使用双亲委派机制去加载类?
- 避免多份同样字节码的加载
查看ClassLoader下的关于native的方法:
打开openjdk中查看c源码;https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/777e3d4780b8/src/share/native/java/lang/ClassLoader.c
类的加载方式
- 隐式加载: new
- 显式加载: loadClass , forName等
loadClass和forName的区别
- Class.forName得到的class是已经初始化完成的
- Classloder.loadClass得到的class是还没有链接的
代码示例:
package com.interview.javabasic.reflect;
public class LoadDifference {
public static void main(String[] args) throws ClassNotFoundException {
// 还没有链接,不能执行静态代码块,所以什么也打印不出来
ClassLoader cl = Robot.class.getClassLoader();
// 已经初始化完成,可以直接打印出"Hello Robot"
Class r = Class.forName("com.interview.javabasic.reflect.Robot");
// Class.forName("com.mysql.jdbc.Driver");
}
}
6、Java内存模型
内存简介
JVM内存模型-JDK8
Java内存模型之线程私有部分
(1)程序计数器( Program Counter Register )
当前线程所执行的字节码行号指示器(逻辑)
改变计数器的值来选取下一条需要执行的字节码指令
和线程是一对一的关系即“线程私有”
对Java方法计数,如果是Native方法则计数器值为Undefined
不会发生内存泄露
(2)JAVA虚拟机栈
局部变量表和操作数栈
- 局部变量表:包含方法执行过程中的所有变量
- 操作数栈:入栈、出栈、复制、交换、产生消费变量
示例演示:
package com.interview.javabasic.jvm.model;
public class ByteCodeSample {
public static int add(int a, int b) {
int c = 0;
c = a + b;
return c;
}
}
执行及反编译命令:
javac ByteCodeSample.java
javap -verbose ByteCodeSample.class
递归为什么会引发java.lang.StackOverflowError异常?
- 递归过深,栈帧数超出虚拟栈深度
示例代码:
package com.interview.javabasic.jvm.model;
public class Fibonacci {
//F(0)=0,F(1)=1,当n>=2的时候,F(n) = F(n-1) + F(n-2),
//F(2)=F(1) + F(0) = 1, F(3) = F(2) + F(1) = 1+1 = 2
//F(0)-F(N) 依次为 0,1,1,2,3,5,8,13,21,34...
public static int fibonacci(int n){
if(n == 0) {return 0;}
if(n == 1) {return 1;}
return fibonacci(n - 1) + fibonacci(n - 2);
}
public static void main(String[] args) {
System.out.println(fibonacci(1000000));
}
}
运行结果:
虚拟机栈过多会引发java.lang.OutOfMemoryError异常
(3)本地方法栈
- 与虚拟机栈相似,主要作用于标注了native的方法
Java内存模型之线程共享部分(不太明白)
(1)元空间( MetaSpace )
元空间( MetaSpace )与永久代( PermGen )的区别
- 元空间使用本地内存,而永久代使用的是jvm的内存(java.lang.OutOfMemoryError : PermGen space)
MetaSpace相比PermGen的优势
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出
- 类和方法的信息大小难易确定,给永久代的大小指定带来困难
- 永久代会为GC带来不必要的复杂性
- 方便HotSpot与其他JVM如Jrockit的集成
(2)Java堆(Heap)
- 对象实例的分配区域
- GC管理的主要区域
7、JVM三大性能调优参数-Xms -Xmx -Xss的含义
示例:java -Xms128m -Xmx128m -Xss256k -jar xxx.jar
- -Xss:规定了每个线程虚拟机栈(堆栈)的大小(一般情况下256k足够,此配置将会影响此进程中并发线程数的大小)
- -Xms:堆的初始值(即该进程刚创建出来时专属Java堆的大小,一旦对象容量超过Java堆的初始容量,将会自动扩容到-Xmx大小)
- -Xmx:堆能达到的最大值(一般将-Xms和-Xmx设置成同样的大小,因为当k不够用要发生扩容时,会发生内存抖动,影响程序运行时的稳定性)
Java内存模型中堆和栈的区别-内存分配策略
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求
- 栈式存储:数据区需求在编译时末知,运行时模块入口前确定
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配
Java内存模型中堆和栈的联系
- 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址
Java内存模型中堆和栈的区别
- 管理方式:栈自动释放,堆需要GC
- 空间大小:栈比堆小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
- 效率:栈的效率比堆高
元空间、堆、线程独占部分间的联系—内存角度
示例代码:
package com.interview.javabasic.jvm.model;
public class HelloWorld {
private String name;
public void sayHello() {
System.out.println("hello" + name);
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
int a = 1;
HelloWorld hw = new HelloWorld();
hw.setName("test");
hw.sayHello();
}
}
分析:
不同JDK版本之间的intern()方法的区别一JDK6 VS JDK6+
String S = new String( original: "a");
s. intern() ;
JDK6:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
JDK6+:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中.
不存在,则在池中创建该字符串并返回其引用。
永久代错误测试代码:
package com.interview.javabasic.jvm.model;
import java.util.Random;
public class PermGenErrTest {
public static void main(String[] args) {
for(int i=0; i <= 1000; i++){
//将返回的随机字符串添加到字符串常量池中
getRandomString(1000000).intern();
}
System.out.println("Mission Complete!");
}
//返回指定长度的随机字符串
private static String getRandomString(int length) {
//字符串源
String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for ( int i = 0; i < length; i++){
// Returns a random number between 0 (inclusive) and 61 (exclusive),str一共62个字符
int number = random.nextInt(62);
// 表示获取str字符串中序号对应的字符追加到sb中
sb.append(str.charAt(number));
}
return sb.toString();
}
}
JVM参数配置(Run->EditConfigurations):
-XX:MaxPermSize=6M -XX:PermSize=6M
运行结果:
- jdk6
- jdk6+(jdk8)
不同jdk版本intern()方法代码测试:
package com.interview.javabasic.jvm.model;
public class InternDifference {
public static void main(String[] args) {
String s = new String("a"); // new的string对象在java堆中
s.intern();
String s2 = "a"; // “”中声明的字符串直接在常量池中创建
System.out.println(s == s2); // ==比较地址
String s3 = new String("a") + new String("a");
s3.intern();
String s4 = "aa";
System.out.println(s3 == s4);
}
}
JDK6版本的intern()方法:
- 运行结果:
false false
- 解析:
JDK6+版本的intern()方法:
- 运行结果:
false true
- 解析:jdk6+是能把字符串的引用放入常量池中的
8、指针压缩
1、指针压缩是如何实现的?如何证明开启指针压缩节省了内存?
默认情况下,Java虚拟机中对象的起始地址需要对齐至8的倍数(即为内存对齐,对应虚拟机选项 -XX:ObjectAlignmentInBytes
,默认值为 8)。如果一个对象用不到8字节,那么空白的那部分空间就白白浪费掉了。这些浪费掉的空间我们称之为对象之间的填充。默认情况下,Java虚拟机中32位的指针可以寻址到2的35次方,也就是32GB的内存空间(超过32位会关闭压缩指针)。在对压缩指针解引用时,我们需要将其左移3位,再加上一个固定的偏移量,便可以寻址到32GB地址空间为64位指针了。此外,我们可以配置刚刚提到的内存对齐选项(-XX:ObjectAlignmentInBytes
)来进一步提升内存寻址范围。但是,这也可能增加对象填充,导致压缩指针没有打到节省空间效果。
在64位的虚拟机中,对象头的标记字段占64位,而类型指针又占64位。也就是说一个对象额外占用的字节就是16个字节。以Integer对象为例,它仅有一个int类型的私有字段,占4个字节。因此,每个Integer的额外开销至少400%,这也就是Java为什么要引入基本数据类型的原因之一。为了减少内存开销,64位Java虚拟机引入了压缩指针概念(对应虚拟机选项-XX:+UseCompressedOops
,默认开启),将堆中原本64位的Java对象指针压缩成32位的。这样一来,对象头的类型指针也会被压缩成32位,使得对象头大小从16字节降低为12字节。压缩指针不仅可以作用对象头的类型指针,还可以作用引用类型的字段,引用类型的数组。
2、开启指针压缩后,一个oop所能表示的最大堆空间是多少?不开呢?
开启指针压缩后,一个oop所能表示的最大堆空间是32 GB,当最大堆大小超过32 GB时,JVM将自动关闭oop压缩。不开启指针压缩后,一个oop所能表示的最大堆空间是4GB。
3、如何扩展开启指针压缩后一个oop所能表示的最大堆空间?
当Java堆大小大于32GB时,也可以使用压缩指针。虽然默认对象对齐是8个字节,但可以使用 -XX:ObjectAlignmentInBytes
配置字节值。指定的值应为2的幂,并且必须在8和256的范围内。例如,当对象对齐为16个字节时,通过压缩指针最多可以使用64 GB的堆空间。请注意,随着对齐值的增加,对象之间未使用的空间也会增加。
常用命令
启用指针压缩:-XX:+UseCompressedOops
禁止指针压缩:-XX:-UseCompressedOops