目录
写在前面
此学习笔记为个人学习笔记,部分内容参考自业内官网、书籍、网站、他人博客等,欢迎交流与指正。
一、关于JVM与Java
(一)Java语言的特点
平台无关性
https://blog.youkuaiyun.com/Sunhongyu51/article/details/105154098
GC
语言特性(泛型,反射,入表达式)
面向对象(封装继承多态)
类库(集合、并发、网络、IO/NIO等)
异常处理等
二、类加载与反射
(一)JVM如何加载Class文件
JVM是一种抽象化计算机,jvm有完善的硬件架构,如处理器、堆栈、寄存器等。还具有相应的指令系统,
JVM是一个内存中的虚拟机,jvm的存储就是内存,我们写的所有类、常量、变量、方法都在内存中。
(二)什么是反射
定义:
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。(百度百科)
测试代码:
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
Class c=Class.forName("com.interview.javabasic.reflect.Robot");
Robot r=(Robot) c.newInstance();
System.out.println("Class name is : " + c.getName());
Method throwHiMethod=c.getDeclaredMethod("throwHi", String.class);
throwHiMethod.setAccessible(true);
Object str=throwHiMethod.invoke(r,"shy");
System.out.println("str is :" + str);
Method sayHiMethod = c.getMethod("sayHi",String.class);
sayHiMethod.invoke(r,"woshisayhi");
Field name = c.getDeclaredField("name");
name.setAccessible(true);
name.set(r,"shyname");
sayHiMethod.invoke(r,"woshisayhi");
}
三、ClassLoader和双亲委派机制
(一)谈谈ClassLoader
举例自定义一个Robot类
自定义一个ClassLoader:
两个关键函数:findClass,defineClass
findClass根据名称或者路径去加载.class字节码,然后它会调用defineClass去解析定义.class字节流,返回Class对象
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();
}
}
作用:
实现findClass时,不仅可以实现在自定义路径下加载class文件,通过defineClass方法,只要传入合法的二进制流,便能通过不同形式去加载。比如访问某个远程网络去获取二进制流来生成我们需要的类;或者对某些敏感的class文件进行加密,在findClass中对其解密;此外还可以修改二进制流来改变类的信息;
补充:可以去了解用来改变二进制流的ASM(字节码增强技术);思考AOP技术的实现与其的相同之处
(二)双亲委派机制
1.什么是双亲委派机制
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
2.为什么要使用双亲委派机制加载类?(作用)
1.(避免多份同样字节码的加载)
防止加载同一个.class,所以通过委托问上面是否加载,加载了就不再加载,保证数据安全。
2.保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
3.流程
4.类加载器的类别
1.BootstrapClassLoader(启动类加载器)
c++编写,加载java核心库 java.*,构造ExtClassLoader和AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
2.ExtClassLoader (标准扩展类加载器)
java编写,加载扩展库,如classpath中的jre ,javax.*或者java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。
3.AppClassLoader(系统类加载器)
java编写,加载程序所在的目录,如user.dir所在的位置的class
4.CustomClassLoader(用户自定义类加载器)
java编写,用户自定义的类加载器,可加载指定路径的class文件
5.源码分析
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//涉及到有可能多个线程加载同一个类,为了避免冲突使用同步锁
synchronized (getClassLoadingLock(name)) {
// 首先检查这个classsh是否已经加载过了,如果已经加载过,跳过两个if,直接返回class
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//如果得到c即加载过,则返回c,否则尝试在bootStrapClassloader的目录下扫描查看是否存在class文件,如果有,将其装载进来;如果没有,返回上一层(ExtClassLoader)
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
6.补充
Java中使用.getClassLoader().getParent()方法时:
CustomClassLoader的Parent是AppClassLoader
AppClassLoader的Parent是ExtClassLoader
ExtClassLoader的Parent是的Parent是null,因为BootstrapClassLoader是C++的类,java中无法得到。
测试代码:
输出:
7.loadClass和forName的区别
类的加载方式:
- 隐式加载:new
- 显式加载:loadClass和forName等
显式加载创建对象需使用newInstance方法,且不支持传入参数,需要通过反射,调用构造器的newInstance方法才能传参。
loadClass和forName的区别:
首先他们在运行时都能知道任意一个类的属性和方法
在比较区别前先要了解类装载过程:(从现在开始,装载是类的生成过程,加载是装载的一个过程)
装载过程分为三步:加载,链接(校验、准备、解析),初始化。 解析是可选的。
loadClass方法中有一个参数是resolve,true代表要链接该类,false不链接。(默认false)
forName方法中有一个参数是initialize,true代表要初始化该类,false不初始化。(默认true)
所以区别是:
Class.forName得到的class是已经初始化完成的
Classloder.loaderClass得到的class是还没有链接的
使用场景:
有些情况是只需要知道这个类的存在而不需要初始化的情况使用Classloder.loaderClass,而有些时候又必须执行初始化就选择Class.forName
例如在加载数据库驱动时就是使用Class.froName(“com.mysql.jdbc.Driver”):
下面我们来看看Driver的源代码:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
} <br>
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can\'t register driver!");
}
}
}
从Driver的源码中我们可以看出Driver这个类只有一个static块,这样我们需要初始化后才能得到DriverManager,所以我们选择使用Class.forName()
四、JVM内存模型
(一)运行时数据区域
1.程序计数器
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
JVM的所线程是通过线程轮流切换并分配处理器,每个线程需要独立的计数器。
2.Java虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M:
java -Xss2M HackTheJava
该区域可能抛出以下异常:
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;(使用递归方法可能会发生,可限制递归数量或使用循环代替递归)
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。(虚拟机栈过多)
此段代码会死机,慎用!
3本地方法栈
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
4.堆
所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
- 新生代(Young Generation)
- 老年代(Old Generation)
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms1M -Xmx2M HackTheJava
5.方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。
6.运行时常量池
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
7.直接内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
(二)常见问题
1.-Xms -Xmx -Xss的含义
一般来说初始值与最大值设为相同,防止出现内存抖动
2.Java内存模型中堆和栈的区别——内存分配策略
静态存储不允许程序代码中有可变数据结构,也不允许出现嵌套 、递归结构
栈式存储在运行时需要知道空间需求
堆式存储动态分配
3.元空间、堆、线程独占部分之间的联系——内存角度
4.intern()方法在不同jdk版本的区别