Day12 JVM 1 JVM体系结构 类加载器 沙箱安全机制 Native 栈 堆 JProfiler
uwupu 啦啦啦啦啦

JVM体系结构

JVM完整架构图

image

JVM简图

image

Java程序执行过程:java文件编编译 —> class字节码文件 —> 类加载器 —> 运行时数据区;

其中:

  • Java栈、本地方法栈和程序计数器不能进行垃圾回收;
  • JVM调优一般指的是调优方法区

类加载器

image

分类

  1. 虚拟机自带的加载器
  2. 启动类(根)加载器
  3. 扩展类加载器
  4. 应用程序加载器
  5. 用户自定义类加载器

类加载器加载的文件

Bootstrap ClassLoader (启动类加载器) :主要负责加载Java核心类库,目录:/lib下的rt.jar、resources.jar、charsets.jar和class等;

Extention ClassLoader(扩展类加载器):主要负责加载目录/jre/lib/ext目录下的jar包文件和class文件;

Appliation ClassLoader(应用程序类加载器):主要负责加载当前应用的classpath下的所有类;

User ClassLoader(用户自定义类加载器):科技在指定路径的class文件。

自定义一个类,查看其加载器

1
2
3
4
5
6
7
8
9
10
11
12
public class Student {
@Override
public String toString() {
return "Student{}";
}

public static void main(String[] args) {
Student student = new Student();
System.out.println(student.getClass().getClassLoader());
System.out.println(student);
}
}
1
2
3
4
sun.misc.Launcher$AppClassLoader@18b4aac2
Student{}

Process finished with exit code 0

自定义类的类加载器为AppClassLoader应用程序类加载器。

通过getParent(),探索类加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ClassLoader学习 {
public static void main(String[] args) {
Dog dog1 = new Dog();
Dog dog2 = new Dog();

System.out.println(dog1.hashCode());
System.out.println(dog2.hashCode());
/**
* 460141958
* 1163157884
* 表明 dog1和dog2不是同一个对象
*/
Class<? extends Dog> aClass = dog1.getClass();
Class<? extends Dog> aClass2 = dog2.getClass();

System.out.println(aClass.hashCode());
System.out.println(aClass2.hashCode());
/**
* 1956725890
* 1956725890
* 表明aClass和aClass2是同一个对象
*/

ClassLoader classLoader = dog1.getClass().getClassLoader();

System.out.println(classLoader);//AppClassLoader 应用程序类加载器 java.lang.ClassLoader
System.out.println(classLoader.getParent());//ExtClassLoader 扩展类加载器 \jre\lib\ext
System.out.println(classLoader.getParent().getParent());//null rt.jar
//null 1. 不存在 2. java程序获取不到

}
}

双亲委派机制

介绍

当一个类加载器收到了类加载的请求,它不会直接去加载指定的类, 而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。如果都不能加载,就会触发findclass,抛出异常classNotFoundException

  1. 类加载器收到类加载请求;
  2. 将请求委托给父类加载器去完成;
  3. 启动加载器检查是否能够加载当前这个类,能加载则加载,使用当前的加载器;若不能,则通知子加载器去加载这个类;
  4. 若都不能加载,则抛出异常classNotFoundException

image

存在的意义

  1. 通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某个类时,子加载器就不会再重新加载这个类;
  2. 通过双亲委派的方式,保证了安全性。Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人故意破坏JDK。就可以避免有自人定义一个有破坏功能的java.lang.Integer类被加载。这样可以有效方式核心Java API被篡改。

尝试定义一个String类,并运行使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class String {
/**
* 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
* public static void main(String[] args)
* 否则 JavaFX 应用程序类必须扩展javafx.application.Application
*/
/**
* 当一个类加载器收到了类加载的请求,它不会直接去加载指定的类, 而是把这个请求委托给自己的父加载器去加载。
* 只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
*/
//双亲委派机制 安全
//1. Application -> Extention -> Bootstrap
//执行过程中,依据上面的顺序依次寻找String的类,优先执行BOOT,然后Exc,最后App。
//由于String在BOOT中找到String的类,所以运行BOOT中的String类;
//而BOOT中的String类没有main方法,所以出现错误。
public String get(){
return "Hello World";
}

public static void main(String[] args) {
String s = new String();
s.get();
}
}
1
2
3
4
5
6
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

Process finished with exit code 1

Application -> Extention -> Bootstrap

执行过程中,依据上面的顺序依次寻找String的类,优先执行Bootstrap,然后Extention,最后Application。
由于String在BOOT中找到String的类,所以运行BOOT中的String类;
而BOOT中的String类没有main方法,所以出现错误。

沙箱安全机制

Java安全模型的核心是Java沙箱(sandbox)。

介绍

沙箱是一个限制程序运行的环境,沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源的访问,通过这样的错误来保证代码的有效隔离,防止对本地系统造成破坏。

Java沙箱主要限制系统资源访问

历史

在Java中,执行程序分为本地代码远程代码两种。

本地代码默认视为可信任的,而远程代码被看做是不受信任的

  • 对于受信任的本地代码,可以访问一切本地资源;
  • 对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(SandBox机制)。

Java1.0 安全模型

image

Java 1.1 安全模型

image

Java1.6安全模型 (目前最新的安全模型)

image

在当前最新的安全机制实现,引入了域(Domain)的概念,虚拟机会把所有代码加载到不同的系统域和应用域;

系统域部分专门负责与关键资源进行交互,各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。

虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限。

组成沙箱的基本组件

  • 字节码校验器(bytecode verifier):确保java类文件遵循java语言规范。但并不是所有的类文件都会经过字节码校验,比如核心类。
  • 类装载器(class loader):
    • 防止恶意代码去干涉正常的代码 //双亲委派机制
    • 守护了被信任的类库边界;//双亲委派机制
    • 代码归入保护域,确定了代码可以进行哪些操作。 //沙箱安全机制

类加载器采用双亲委派模式

通过包区分了访问域,外层恶意的类通过内置代码无法获得权限访问到内置类,破坏代码因此无法生效。

  • 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。

  • **安全管理器(security manager)**:是核心API和操作系统之间的主要接口,实现权限控制,比存取控制器优先级高。

  • 安全软件包(security package):java.scurity下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:

    • 安全提供者
    • 消息摘要
    • 数字签名
    • 加密
    • 鉴别

Native、方法区

Thread.start()简述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)//线程的状态不是启动状态
throw new IllegalThreadStateException();

/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);//将线程加入线程组

boolean started = false;//默认线程未启动
try {
start0();//启动线程
started = true;//若启动成功,表示没有抛出异常,标志位设true。
} finally {
try {
if (!started) {
group.threadStartFailed(this);//向group表示线程执行失败
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}

private native void start0();//启动线程,是一个本地方法

Native

Java里用native修饰的方法,不在java作用范围内,调用的是底层c语言的库。

使用native修饰的方法,会进入本地方法栈,调用JNI(本地方法接口),JNI调用本地方法库。

JNI本地方法接口:,扩展Java的使用,融合不同的编程语言为Java所用。(最初是C、C++)

Native Method Stack本地方法栈:登记Native方法。

PC寄存器

程序计数器:Program Counter Register

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也是将要执行的指令代码)。

当执行引擎读取下一条指令,PC计数器会+1。

PC寄存器是一个非常小的内存空间。(小到可以忽略不计)

方法区

Method Area

方法区被所有线程共享。

image

所有定义的方法的信息都保存在该区域。

属于共享区间

内容

静态变量,常量,类信息(构造方法、接口定义),运行时常量池都在方法区中;

  • static , final , Class ,常量池。

实例变量存在于堆内存中,与方法区无关。

JVM中内存分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class JVM内存分配部分 {
public static void main(String[] args) {
Person person = new Person(12,"张三");

}
}

class Person{
int age;
String name = "anonymous";

public Person(int age, String name) {
this.age = age;
this.name = name;
}
}

image

持续学习:程序 = 数据结构 + 算法 饭碗:程序 = 框架 + 业务逻辑

栈:先进后出,后进先出。

队列:先进先出。(FIFO:First Input First Output)

通过main方法理解栈

1
2
3
4
5
6
7
8
9
10
public class UnderstandStackByMainMethod {
public static void main(String[] args) {
System.out.println("main start");
hello();
System.out.println("main stop");
}
public static void hello(){
System.out.println("hello");
}
}
1
2
3
4
5
main start
hello
main stop

Process finished with exit code 0

程序运行:main方法开始,然后其他方法,最后main方法结束。

image

在栈中,main先进入栈,然后是hello()入栈,然后hello()出栈,最后main出栈。

JVM栈

栈:主管程序的运行,生命周期和线程同步(线程结束 —> 栈内存释放)。因此,对于栈来说,不存在垃圾回收问题

介绍

image

JVM栈描述的是每个线程Java方法执行的内存模型:每个方法被执行的时候,JVM会同步创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。

栈是运行时单位,堆是存储的单位,即:

栈解决的是运行问题,即程序如何执行,或者如何处理数据。

堆解决的事数据存储问题,即数据怎么放,放哪儿。

特点

  • 访问速度快,仅次于程序计数器
  • 线程私有
  • 存在OOM,不存在GC

存放类型

8种数据类型、对象的引用,实例的方法。

其他

  • Java虚拟机栈是线程私有的,生命周期与线程相同。(随线程而生,随线程而灭。)

  • 如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出StackOverflowError异常;

  • 若虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

    (当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)

  • Java虚拟机栈描述的是Java方法执行的内存模型

Heap,一个JVM只有一个堆内存,堆内存的大小可以调节。

堆里面放什么

类,方法,常量,变量,保存引用类型的指向类型(真实对象)。

分三个区域

Java 7及之前堆内存逻辑上分为3个部分:新生区 + 养老区 + 永久区

Java 8及之后堆内存逻辑上分为3个部分:新生区 + 养老区 + 元空间

  • 新生区 Young Generation Space Young/New
    • Eden区 伊甸区
    • Survivor区 幸存区
  • Tenure generation Space 养老区 Old/Tenure
  • Permanent Space 永久区 Perm
  • Meta Space 元空间 Meta

image

image

GC垃圾回收主要在伊甸园区和老年区。

在JDK8之后,永久存储区改为元空间

若内存满了,会触发OOM:java.lang.OutOfMemoryError: java heap space。

新生区的内存比例: Eden : from : to = 8 : 1 : 1

新生区

Eden

新创建的对象会放在Eden区,每经历一次GC,位于Eden区存活的对象会被移到幸存区的from区。

幸存区

  • 幸存区分为from和to两部分,两部分会互相交换;
  • 分辨from分区和to分区:一般情况下,空的为to分区。
  • 每次GC,都会把Eden中没有被清理的对象移到幸存区中。
  • 一旦Eden区被GC后,一部分被清理,没有被清理的移到幸存区。

计算过程

  • 每个对象都被定义有寿命

  • 当寿命达到指定值,就会被移入老年代;

永久区

这个区域常驻内存,用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境。

这个区域不存在垃圾回收关闭VM虚拟机会释放这个区域的内存。

永久区满的情况

  • 一个启动类,加载了大量的第三方jar包;
  • Tomcat部署了太多的应用,或者大量动态生成的反射类。

不断地加载,若内存满,就会出现OOM。

版本历史

Jdk1.6之前:永久代,常量池在方法区;

Jdk1.7:永久代,但是慢慢退化了,去永久代,常量池在堆中;

jdk1.8之后:无永久代,常量池在元空间。

用代码表现内存溢出

VM参数:-Xms8M -Xmx8M -XX:+PrintGCDetails

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GetJVMMaxMemoery {
public static void main(String[] args) {
long l = Runtime.getRuntime().maxMemory();
long total = Runtime.getRuntime().totalMemory();
System.out.println("max = "+l/(double)1024/1024+" MB");
System.out.println("total = "+total/(double)1024/1024+" MB");

//溢出用代码
StringBuilder str = new StringBuilder("asdqwe");
while (true){
str.append("qweasdwa");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[GC (Allocation Failure) [PSYoungGen: 1536K->488K(2048K)] 1536K->716K(7680K), 0.0007954 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
max = 7.5 MB
total = 7.5 MB
[Full GC (Ergonomics) [PSYoungGen: 1477K->0K(2048K)] [ParOldGen: 5604K->3731K(5632K)] 7082K->3731K(7680K), [Metaspace: 3242K->3242K(1056768K)], 0.0061693 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 3731K->3731K(7680K), 0.0004458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 3731K->3713K(5632K)] 3731K->3713K(7680K), [Metaspace: 3242K->3242K(1056768K)], 0.0055763 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 2048K, used 75K [0x00000000ffd80000, 0x0000000100000000, 0x0000000100000000)
eden space 1536K, 4% used [0x00000000ffd80000,0x00000000ffd92f30,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 5632K, used 3713K [0x00000000ff800000, 0x00000000ffd80000, 0x00000000ffd80000)
object space 5632K, 65% used [0x00000000ff800000,0x00000000ffba0518,0x00000000ffd80000)
Metaspace used 3272K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.yn.GetJVMMaxMemoery.main(GetJVMMaxMemoery.java:13)

Process finished with exit code 1

JProfiler工具

  • 分许Dump内存文件,快速定位内存泄露。

其他

Java程序运行的部分参数

  • -Xms 初始化分配内存大小 -Xms1G、-Xms512M

  • -Xmx 设置最大分配内存 -Xmx1024M、-Xmx2G

  • -XX:PrintGCDetails 输出内存分配

  • -XX:HeapDumpOnOutOfMemoryError 输出栈溢出信息

  • -XX:MaxTenuringThreshold = 15 通过这个参数可以设定对象经过GC多少次仍然存活后晋升到老年代的最大阈值。默认:15。

Java代码校验过程

java代码首先会经过编译器校验,然后在解释器解释前会被字节码校验器校验

对于字节码校验器,运行的代码可能来自本地计算机,也有可能来自远程计算机,本地计算机的代码经过编译器校验,但远程计算机的代码不一定被编译器校验,所以解释前必须经过字节码校验器再次校验。

来源

https://www.oracle.com/java/technologies/security-in-java.html

What about the concept of a “hostile compiler”? Although the Java compiler ensures that Java source code doesn’t violate the safety rules, when an application such as the HotJava Browser imports a code fragment from anywhere, it doesn’t actually know if code fragments follow Java language rules for safety: the code may not have been produced by a known-to-be trustworthy Java compiler. In such a case, how is the Java run-time system on your machine to trust the incoming bytecode stream? The answer is simple: the Java run-time system doesn’t trust the incoming code, but subjects it to bytecode verification.

The tests range from simple verification that the format of a code fragment is correct, to passing each code fragment through a simple theorem prover to establish that it plays by the rules:

  • it doesn’t forge pointers,
  • it doesn’t violate access restrictions,
  • it accesses objects as what they are (for example, InputStream objects are always used as InputStreams and never as anything else).

A language that is safe, plus run-time verification of generated code, establishes a base set of guarantees that interfaces cannot be violated.

不准确的翻译

“恶意编译器”是什么?即使Java编译器确保Java源代码不会避开安全规则,但当代码片段导入来自像HotJava这样的浏览器,不能保证代码片段在安全上是否遵循Java语言规则:代码可能被不被信任的Java编译器编译。因此,电脑上的Java运行时系统如何相信输入的字节流?答案很简单:Java运行时系统不信任输入的代码,而是把它交给字节码校验器

首先进行简单的代码格式正确性验证,然后每段代码都要通过一个简易的规则检测器,确保符合以下规则:

  • 不会伪造指针
  • 不违反访问显示
  • 按原样访问对象(例如InputStream对象仅仅被访问作InputStream而不是其他的东西)

个人理解

交给Java解释器的代码,可以来自可信的Java编译器,也有可能来自恶意的Java编译器。为保证安全,在Java解释器之前,字节码校验器会对代码再次(在此之前是Java编译器)进行校验,确保不会有不合适的代码被运行。

  • 没有伪造指针
  • 不会违反访问限制
  • 访问对象正确(例如InputStream对象仅仅被访问作InputStream而不是其他的东西)

Java的Robot类:java.awt.Robot

Java中Robot类位于java.awt.Robot,该类用于为测试自动化,自运行演示程序和其他需要控制鼠标和键盘的应用程序生成本机系统输入事件,Robot类的主要目的是便于Java平台实现自动测试。

JNI 本地方法接口

Java Native Interface

三种JVM

  • Sun公司 HotSpot Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
  • BEA JRockit
  • IBM J9VM

学习用主要是HotSpot

一些问题

请谈谈你对

core

  • 请你谈谈你对JVM的理解?Java8虚拟机和之前的变化更新?
  • 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
  • JVM的常用调优参数有哪些?
  • 内存快照如何抓取,怎么分析Dump文件?
  • 谈谈JVM中,类加载器的你的认识?

JVM

  • JVM的内存模型和分区 ,每个区放什么?
  • 堆里面的分区有哪些?Eden、from、to,老年区,说说他们的特点。
  • GC的算法有哪些?标记清除法,标记整理,复制算法,引用计数器,如何使用?
  • 轻GC 和 重GC 分别在什么时候发生?

知识点

JVM位置

JVM体系结构

类加载器

双亲委派机制

沙箱安全机制

Native

PC寄存器

方法区

三种JVM

新生区老年区

堆内存调优

GC

​ 常用算法

JMM

总结

 评论