Объединение ЦП, чтобы понять, как выполняется строка кода Java
Согласно мысли фон Неймана, компьютер использует двоичную систему как основу системы счисления и должен включать в себя: арифметическое устройство, контроллер, запоминающее устройство и устройство ввода и вывода, как показано на следующем рисунке.
(Картинка взята с Baidu)
Давайте сначала проанализируем принцип работы ЦП.Большинство современных микросхем ЦП объединяют блок управления, арифметический блок и блок памяти.Блок управления является центром управления ЦП, и ЦП должен знать, что делать дальше, то есть что инструкции для выполнения. Блок управления также включает в себя: регистр инструкций (IR), декодер инструкций (ID) и контроллер операций (OC).
Когда программа загружается в память, инструкции находятся в памяти. В это время память не зависит от основного запоминающего устройства вне ЦП, то есть от карты памяти в ПК. Регистр IP указателя инструкций указывает на следующий элемент в памяти для выполнения.Адрес инструкции, блок управления загружает инструкцию в основную память в регистр инструкций в соответствии с точкой регистра IP.Этот регистр инструкций также является устройством хранения, но он интегрирована в ЦП.После того, как инструкция достигает ЦП из основной памяти, это просто строка 010101 Двоичная строка также должна быть декодирована декодером и проанализирована.
Какой код операции, где операнды, а затем конкретный операционный блок выполняет арифметические операции (сложение, вычитание, умножение и деление), логические операции (сравнение, смещение), регистрацию), декодирование (выборку операнда из оперативной памяти и размещение в кэш L1), выполнить (операцию).
Вот объяснение блока хранения SRAM, встроенного в ЦП на приведенном выше рисунке, который просто соответствует DRAM в основной памяти.RAM — это память с произвольным доступом, то есть к данным можно получить доступ, указав адрес, а хранилище доступ к среде диска должен быть последовательным, а ОЗУ делится на два типа: динамическое и статическое.Статическое ОЗУ имеет низкую интеграцию, как правило, небольшую емкость и высокую скорость, в то время как динамическое ОЗУ имеет высокую интеграцию и в основном реализуется за счет зарядки и разрядки конденсаторов. скорость не такая высокая, как у статической ОЗУ.Динамическая ОЗУ используется в качестве основной памяти, а статическая ОЗУ используется в качестве кеша (кеша) между ЦП и основной памятью, чтобы скрыть разницу в скорости между ЦП и основной памятью. , то есть кэши L1 и L2, которые мы часто видим.Скорость кэша первого уровня становится ниже, а емкость становится больше.
На следующем рисунке показана иерархическая архитектура памяти и процесс доступа ЦП к основной памяти.Здесь есть две точки зрения.Одним из них является протокол когерентности кэша, введенный между многоуровневыми кэшами для обеспечения согласованности данных.Подробнее см. В этой статье еще один момент знаний заключается в том, что отображение между кешем и основной памятью, первое, что нужно уяснить, это то, что единицей кеша кеша является строка кеша, которая соответствует блоку памяти в основной памяти, а не переменной , Это происходит главным образом из-за того, что ** Ограничение пространства для доступа к ЦП: доступ к определенной единице хранения, к которому осуществляется доступ, вероятно, будет снова осуществлен через короткий период времени, и ограничение пространства: доступ к определенной единице хранения в течение короткого периода времени, Также осуществляется доступ к соседним ячейкам памяти.**
Существует множество методов сопоставления, например, номер строки кэша = номер блока основной памяти mod cache общее количество строк, так что каждый раз, когда получается адрес основной памяти, номер блока в основной памяти может быть рассчитан в соответствии с этим адресом. номер в .
Далее поговорим о выполнении инструкций ЦП.Адресация, декодирование и выполнение - это процесс выполнения инструкции.Все инструкции будут выполняться в строгом соответствии с этим порядком, но на самом деле можно распараллелить несколько инструкций.Для одноядерного ЦП Для Например, только одна инструкция может занимать исполнительный блок для одновременного выполнения.Выполнение, упомянутое здесь, является третьим шагом в трех шагах обработки инструкций ЦП (выборка инструкции, декодирование и выполнение), то есть вычислительная задача вычислительного блока, поэтому для повышения скорости обработки инструкций ЦП необходимо обеспечить, чтобы подготовительная работа операционного блока перед выполнением была завершена, чтобы операционный блок всегда мог быть в работе, а в последовательном процесс только что, операционный блок простаивает при выборке и декодировании. , и если инструкции выборки и декодирования не попадают в кеш, к ним также нужно обращаться из основной памяти, а скорость основной памяти не на уровне на том же уровне, что и ЦП, поэтомуконвейер инструкцийЭто может значительно повысить скорость обработки ЦП.На следующем рисунке показан пример конвейера с 3 этапами, в то время как текущие процессоры Pentium являются конвейерами с 32 этапами.Конкретный метод состоит в том, чтобы разделить вышеупомянутые три процесса на более мелкие части.
В дополнение к конвейеру инструкций, у ЦП также есть средства оптимизации скорости, такие как предсказание ветвлений и выполнение не по порядку.Ну, вернемся к теме, как выполняется строка кода Java.
Чтобы строка кода выполнялась, она должна иметь исполняемый контекст, включая ресурсы памяти, такие как регистры инструкций, регистры данных, пространство стека и т. д., а затем эта строка кода должна быть распознана планировщиком задач как поток выполнения. операционной системы и дать ему Распределить ресурсы ЦП. Конечно, инструкция, представленная этой строкой кода, должна быть декодирована и распознана ЦП, поэтому строка кода Java должна интерпретироваться как соответствующая инструкция ЦП для выполнения. Давайте взгляните на System.out.println("Hello world") Процесс перевода этой строки кода.
Java — это язык высокого уровня, который не может работать непосредственно на оборудовании, но должен работать на виртуальной машине, которая может распознавать характеристики языка Java, а код Java должен быть преобразован в последовательность инструкций, которую виртуальная машина может распознать посредством компилятор Java. , также известный как байт-код Java, причина, по которой он называется байт-кодом, заключается в том, что инструкция операции (OpCode) байт-кода Java фиксируется как один байт, следующее System.out.println("Hello world") После компиляция байт-кода
0x00: b2 00 02 getstatic Java .lang.System.out
0x03: 12 03 ldc "Hello, World!"
0x05: b6 00 04 invokevirtual Java .io.PrintStream.println
0x08: b1 return
Крайний левый столбец — смещение, средний столбец — байт-код, прочитанный виртуальной машиной, крайний правый столбец — код языка высокого уровня, далее — машинная инструкция, преобразованная в язык ассемблера, средний — машинный код , а третий столбец соответствует машинным инструкциям, последний столбец — соответствующий ассемблерный код
0x00: 55 push rbp
0x01: 48 89 e5 mov rbp,rsp
0x04: 48 83 ec 10 sub rsp,0x10
0x08: 48 8d 3d 3b 00 00 00 lea rdi,[rip+0x3b]
; 加载 "Hello, World!\n"
0x0f: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
0x16: b0 00 mov al,0x0
0x18: e8 0d 00 00 00 call 0x12
; 调用 printf 方法
0x1d: 31 c9 xor ecx,ecx
0x1f: 89 45 f8 mov DWORD PTR [rbp-0x8],eax
0x22: 89 c8 mov eax,ecx
0x24: 48 83 c4 10 add rsp,0x10
0x28: 5d pop rbp
0x29: c3 ret
После того, как JVM загрузит байт-код в файл класса через загрузчик классов, он будет интерпретирован интерпретатором в инструкции по сборке и, наконец, переведен в машинные инструкции, которые может распознать ЦП.Интерпретатор реализуется программным обеспечением, в основном для достижения того же копирование. Байт-код Java может работать на разных аппаратных платформах, а преобразование инструкций по сборке в машинные инструкции осуществляется непосредственно аппаратно. Этот шаг выполняется очень быстро. Конечно, JVM также может преобразовывать некоторые горячие коды (метод) для повышения эффективности работы .Код внутри) компилируется в машинные инструкции за один раз, а затем выполняется, то есть компиляция точно в срок (JIT), соответствующая выполнению интерпретации.Когда JVM запускается, режим выполнения может управляться -Xint и - Xкомп.
На программном уровне после загрузки файла класса в виртуальную машину информация о классе будет храниться в области методов, а код в области методов будет выполняться во время фактической работы.Все потоки в JVM совместно используют память кучи и область метода, и каждый поток имеет свой собственный независимый стек методов Java, собственный стек методов (для собственных методов), регистр ПК (для хранения местоположения выполнения потока), когда вызывается метод, виртуальная машина Java помещает стек в стек методов, соответствующий текущему потоку. Фрейм используется для хранения операндов байт-кода Java и локальных переменных. После выполнения этого метода всплывает кадр стека. Поток будет непрерывно выполнять несколько методов, соответствующих отправке и извлечению различных кадров стека. , JVM интерпретирует процесс выполнения.
прерывать
Как я только что упомянул, пока ЦП включен, он похож на вечный двигатель, который продолжает получать инструкции, выполнять вычисления и повторяться, а прерывание — это душа операционной системы, отсюда и название. иначе, например, во время выполнения системы возникает фатальная ошибка, и выполнение должно быть прекращено.Например, если пользовательская программа вызывает метод системного вызова, такой как mmp и т. д., ЦП будет переключать контексты через прерывания и переходить в пространство ядра, например, ожидание. Программа, введенная пользователем, блокируется, и когда пользователь завершает ввод с клавиатуры и данные ядра готовы, будет отправлен сигнал прерывания, чтобы разбудить пользовательскую программу, чтобы удалить данные из ядра, в противном случае ядро может переполнить данные, когда диск сообщает. Фатальное исключение также уведомит ЦП через прерывание, и таймер также отправит прерывание часов, чтобы уведомить ЦП, когда таймер завершает такт часов.
Типы прерываний, мы не будем их здесь подразделять.Прерывания немного похожи на управляемое событиями программирование, о котором мы часто говорим, и как реализован этот механизм уведомления о событиях?Реализация аппаратных прерываний подключается к ЦП через провод для передачи сигналов прерывания. , в программном обеспечении будут специальные инструкции, такие как выполнение системного вызова для создания потока, и каждый раз, когда ЦП выполняет инструкцию, он будет проверять, есть ли прерывание в регистре прерывания, и если поэтому извлеките его и выполните соответствующий обработчик прерывания.
Ловушка в ядре: когда мы разрабатываем программное обеспечение, мы будем учитывать частоту переключения контекста программы. Слишком высокая частота определенно повлияет на производительность выполнения программы, а ловушка в ядре предназначена для ЦП. Выполнение ЦП изменяется из пользовательского режима. в режим ядра, который раньше был «Программа пользователя использует ЦП, а теперь программа ядра использует ЦП. Это переключение генерируется системным вызовом. Системный вызов предназначен для выполнения базовой программы операционной системы. разработчик Linux, чтобы защитить операционную систему, использует состояние выполнения процесса, так как режим ядра и пользовательский режим разделены.В одном и том же процессе ядро и пользователь используют одно и то же адресное пространство.Как правило, виртуальные адреса 4G используется, из которых 1G для режима ядра и 3G для пользовательского режима.При разработке программы мы должны попытаться уменьшить пользовательский режим до пользовательского режима.Переключение состояния ядра, например создание потока, является системным вызовом, Итак, у нас есть реализация пула потоков.
Понимание модели памяти JVM с точки зрения управления памятью Linux
контекст процесса
Мы можем понимать программу как набор исполняемых инструкций, и после запуска программы операционная система выделит ей ЦП, память и другие ресурсы, и эта работающая программа — это то, что мы называем процессом, а процесс — это работа операционной системы. обработкаЭто абстракция программы, работающей в JVM, а ресурсы памяти и ЦП, выделенные для процесса, являются контекстом процесса, который сохраняет выполняемые в данный момент инструкции и значения переменных.После запуска JVM он также обычный процесс в linux Физический объект и среда, которая поддерживает выполнение процесса, вместе называются контекстом, и переключение контекста должно заменить текущий процесс и заменить его новым процессом для запуска на процессоре, чтобы чтобы разрешить одновременное выполнение нескольких процессов Переключение контекста может происходить из планирования операционной системы, оно также может исходить из программы, например, при чтении ввода-вывода оно будет переключаться между кодом пользователя и кодом операционной системы.
виртуальное хранилище
Когда мы запускаем несколько JVM-исполнений одновременно: System.out.println(new Object()); напечатает хэш-код этого объекта, хэш-код по умолчанию соответствует адресу памяти, и, наконец, обнаружено, что все они напечатаны Java .lang .Object@4fca772d , то есть адреса памяти, возвращаемые несколькими процессами, фактически совпадают.
На приведенном выше примере мы можем доказать, что каждый процесс в Linux имеет отдельное адресное пространство.Перед этим давайте разберемся, как ЦП обращается к памяти?
Допустим, у нас пока нет виртуальных адресов, только физические адреса.Когда компилятор компилирует программу, ему нужно преобразовать язык высокого уровня в машинные инструкции.Тогда ЦП должен указывать адрес при обращении к памяти.Если этот адрес является абсолютный физический адрес,Тогда программа должна быть помещена в фиксированное место в памяти,и этот адрес нужно подтверждать при компиляции.Каждый должен задуматься о том как это жалко.Если я хочу запускать две офисные ворд программы одновременно , то они будут Если вы оперируете одним и тем же куском памяти, это будет беспорядок Великие компьютерные предшественники разработали его, чтобы позволить ЦП
Доступ к памяти осуществляется с использованием базового адреса сегмента + адреса смещения в сегменте. Базовый адрес сегмента подтверждается при запуске программы. Хотя этот базовый адрес сегмента по-прежнему является абсолютным физическим адресом, можно запускать несколько программ одновременно. ЦП использует этот метод. Чтобы получить доступ к памяти таким образом, вам нужен регистр базового адреса сегмента и регистр адреса смещения сегмента для хранения адреса, и, наконец, добавьте два адреса и отправьте их на адресную шину. И память сегментация, которая эквивалентна тому, что каждый процесс будет выделять сегмент памяти, более того, этот сегмент памяти должен быть непрерывным пространством, и несколько сегментов памяти поддерживаются в основной памяти Когда процессу требуется больше памяти и он превышает физическую память, он необходимо изменить сегмент памяти, который обычно не используется на жесткий диск.Когда памяти достаточно, она загружается с жесткого диска, то есть своп.Каждый своп должен оперировать данными всего сегмента.
Прежде всего, непрерывное адресное пространство очень ценно.Например, 50M памяти не сможет поддерживать 5 программ, которым требуется 10M памяти для запуска, когда между сегментами памяти есть промежутки.Как сделать адреса внутри сегмента прерывистыми? Ответ — подкачка памяти.
В защищенном режиме каждый процесс имеет свое собственное независимое адресное пространство, поэтому базовый адрес сегмента является фиксированным, и необходимо указать только адрес смещения в сегменте, и этот адрес смещения называется линейным адресом, а линейный адрес — непрерывным. , а подкачка памяти связывает непрерывные линейные адреса с физическими адресами после подкачки, так что логически непрерывные линейные адреса могут соответствовать прерывистым физическим адресам.Физическое адресное пространство может совместно использоваться несколькими процессами, и это отношение отображения будет поддерживаться через страницу Таблица. Размер стандартной страницы обычно составляет 4 КБ. После подкачки физическая память делится на несколько страниц данных по 4 КБ. Когда процесс обращается за памятью, он может быть сопоставлен с несколькими физическими будет принимать страницу как наименьшую единицу, и когда ему нужно обменяться с жестким диском, он также будет использовать страницу как единицу.
Современные компьютеры в основном используют технологию виртуального хранилища.Виртуальное хранилище заставляет каждый процесс думать, что он владеет всем пространством памяти.На самом деле это виртуальное пространство является абстракцией оперативной памяти и диска.Преимущество этого заключается в том,что каждый процесс имеет согласованный виртуальный адрес пространства, что упрощает управление памятью, процессу не нужно конкурировать с другими процессами за место в памяти, поскольку оно является эксклюзивным, а также защищает соответствующий процесс от уничтожения другими процессами, кроме того, он рассматривает основную память как кэш диска, а в основной памяти хранится только активная память.Сегмент программы и сегмент данных, когда нет данных в основной памяти, происходит прерывание ошибки страницы, и затем он загружается с диска.Когда недостаточно физической памяти, произойдет подкачка на диск.В таблице страниц сохраняется отображение виртуальных адресов и физических адресов.Таблица представляет собой массив, и каждый элемент представляет собой отношение отображения страницы.Это отношение отображения может быть с адрес основной памяти, или с диском.Таблица страниц хранится в основной памяти.Назовем таблицу страниц, хранящуюся в высокоскоростном буферном кеше.Для быстрой таблицы TLAB.
Бит загрузки Указывает, находится ли страница в основной памяти, если страница адреса представляет каждую страницу, данные все еще находятся на диске.
Место хранения Создайте сопоставление между виртуальными страницами и физическими страницами для преобразования адресов. Если оно равно null, это означает нераспределенную страницу.
Бит модификации используется для хранения того, были ли данные изменены.
Биты разрешения используются для управления наличием разрешения на чтение и запись.
Бит запрета кеша в основном используется для обеспечения согласованности данных кеша на диске основной памяти.
карта памяти
В нормальных условиях процесс чтения файла заключается в том, чтобы сначала прочитать данные с диска с помощью системного вызова, сохранить их в буфере ядра операционной системы, а затем скопировать из буфера ядра в пространство пользователя и отобразить память. заключается в копировании файла на диске. Прямое сопоставление с виртуальным пространством хранения пользователя, поддержание сопоставления виртуальных адресов с диском через таблицу страниц и чтение файлов с помощью сопоставления памяти. Извлечение данных в память снижает накладные расходы на системные вызовы. это похоже на файл на диске, с которым можно напрямую манипулировать.Кроме того, из-за использования виртуального хранилища нет необходимости в постоянном пространстве основной памяти для хранения данных.
В Java мы используем MappedByteBuffer для реализации отображения памяти, которая представляет собой память вне кучи.После отображения она не занимает сразу физическую память.Вместо этого при доступе к странице данных сначала проверьте таблицу страниц и обнаружите, что она не была Если есть исключение page fault, то данные загружаются с диска в память, поэтому у некоторых промежуточных программ с высокими требованиями к реальному времени, таких как RocketMQ, сообщение хранится в файле размером 1G, в Чтобы ускорить скорость чтения и записи, этот файл будет после сопоставления с памятью записывать один бит данных на каждой странице, чтобы весь файл 1G мог быть загружен в память, и ни одна страница не будет отсутствовать, когда на самом деле чтение и запись.Это называется предварительным прогревом файла внутри RocketMQ.
Ниже мы вставляем фрагмент кода для модуля хранения сообщений RocketMQ, который находится в классе MappedFile.Этот класс является основным классом хранилища сообщений RocketMQ.Если вам интересно, вы можете изучить его самостоятельно.Когда 1000 страниц данных горячие , привилегия ЦП аннулируется.
private void init(final String fileName, final int fileSize) throws IOException {
this.fileName = fileName;
this.fileSize = fileSize;
this.file = new File(fileName);
this.fileFromOffset = Long.parseLong(this.file.getName());
boolean ok = false;
ensureDirOK(this.file.getParent());
try {
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
TOTAL_MAPPED_FILES.incrementAndGet();
ok = true;
} catch (FileNotFoundException e) {
log.error("create file channel " + this.fileName + " Failed. ", e);
throw e;
} catch (IOException e) {
log.error("map file " + this.fileName + " Failed. ", e);
throw e;
} finally {
if (!ok && this.fileChannel != null) {
this.fileChannel.close();
}
}
}
//文件预热,OS_PAGE_SIZE = 4kb 相当于每 4kb 就写一个 byte 0 ,将所有的页都加载到内存,真正使用的时候就不会发生缺页异常了
public void warmMappedFile(FlushDiskType type, int pages) {
long beginTime = System.currentTimeMillis();
ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
int flush = 0;
long time = System.currentTimeMillis();
for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
byteBuffer.put(i, (byte) 0);
// force flush when flush disk type is sync
if (type == FlushDiskType.SYNC_FLUSH) {
if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
flush = i;
mappedByteBuffer.force();
}
}
// prevent gc
if (j % 1000 == 0) {
log.info("j={}, costTime={}", j, System.currentTimeMillis() - time);
time = System.currentTimeMillis();
try {
// 这里sleep(0),让线程让出 CPU 权限,供其他更高优先级的线程执行,此线程从运行中转换为就绪
Thread.sleep(0);
} catch (InterruptedException e) {
log.error("Interrupted", e);
}
}
}
// force flush when prepare load finished
if (type == FlushDiskType.SYNC_FLUSH) {
log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}",
this.getFileName(), System.currentTimeMillis() - beginTime);
mappedByteBuffer.force();
}
log.info("mapped file warm-up done. mappedFile={}, costTime={}", this.getFileName(),
System.currentTimeMillis() - beginTime);
this.mlock();
}
Расположение объектов в памяти в JVM
В linux, если вы знаете начальный адрес переменной, вы можете прочитать значение переменной, потому что первые 8 бит от начального адреса записывают размер переменной, то есть вы можете найти конечный адрес. В Java мы можем передать метод Field.get(object) для получения значения переменной, то есть отражение, наконец, реализовано через класс UnSafe, мы можем проанализировать конкретный код
Field 对象的 getInt方法 先安全检查 ,然后调用 FieldAccessor
@CallerSensitive
public int getInt(Object obj)
throws IllegalArgumentException, IllegalAccessException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
return getFieldAccessor(obj).getInt(obj);
}
获取field在所在对象中的地址的偏移量 fieldoffset
UnsafeFieldAccessorImpl(Field var1) {
this.field = var1;
if(Modifier.isStatic(var1.getModifiers())) {
this.fieldOffset = unsafe.staticFieldOffset(var1);
} else {
this.fieldOffset = unsafe.objectFieldOffset(var1);
}
this.isFinal = Modifier.isFinal(var1.getModifiers());
}
UnsafeStaticIntegerFieldAccessorImpl 调用unsafe中的方法
public int getInt(Object var1) throws IllegalArgumentException {
return unsafe.getInt(this.base, this.fieldOffset);
}
Через приведенный выше код мы можем читать и записывать значение атрибута через смещение атрибута относительно начального адреса объекта.Это также принцип отражения Java.Этот режим используется во многих сценариях в jdk, таких как LockSupport. Блокирующий объект устанавливается в парке. Тогда какие правила используются для определения смещения атрибута? Воспользуемся этой возможностью, чтобы проанализировать расположение памяти объектов Java
В виртуальной машине Java каждый объект Java имеет заголовок объекта, который состоит из поля тега и указателя типа. Поле тега используется для хранения хэш-кода объекта, информации GC, информации удерживаемой блокировки и типа. класс Класс объекта.В 64-битной операционной системе поле тега занимает 64 бита, и указатель типа также занимает 64 бита, что означает, что объект Java находится в
Если нет атрибута, он будет занимать 16 байт пространства.В текущей JVM сжатый указатель включен по умолчанию, так что указатель типа может занимать только 32 бита, поэтому заголовок объекта занимает 12 байт, а сжатый указатель может воздействовать на заголовок объекта, и Поля ссылочных типов.JVM переупорядочивает поля для выравнивания памяти.Выравнивание здесь в основном относится к начальному адресу объектов в куче виртуальной машины Java, который кратен 8. Если объект использует менее 8N байт, то остальные будут заполнены, а смещение атрибутов, унаследованных подклассом, такое же, как и у родительского класса.
Взяв в качестве примера Лонга, у него есть только одно нестатическое значение атрибута, и хотя заголовок объекта занимает всего 12 байт, смещение значения атрибута может быть только 16, из которых 4 байта могут быть потрачены только впустую, поэтому перестановка полей заключается в том, чтобы избежать потери памяти, нам трудно проанализировать фактическое пространство, занимаемое объектом Java, до загрузки байт-кода Java.Мы можем оценить размер объекта только путем рекурсии всех свойств родительского класса и фактического Размер можно получить с помощью инструментария в агенте Java.
Конечно, еще одна причина выравнивания памяти — позволить полям появляться только в строке кеша одного и того же процессора.Если поля не выровнены, часть поля может быть в строке кеша 1, а оставшаяся половина — в строке кеша. 2. , так что чтение этого поля должно заменить две строки кэша, а запись поля приведет к тому, что другие данные, кэшированные в двух строках кэша, станут недействительными, что повлияет на производительность программы.
Выравнивание памяти позволяет избежать ситуации, когда поле существует в двух строках кеша одновременно, но полностью избежать проблемы ложного разделения кеша все же невозможно, то есть в одной строке кеша хранится несколько переменных, и эти переменные хранятся параллельно в многоядерных процессорах.Когда один из процессоров записывает данные, строка кэша, соответствующая этому полю, становится недействительной, в результате чего другие поля этой строки кэша также становятся недействительными.
В Disruptor, заполнив несколько бессмысленных полей, размер объекта составляет всего 64 байта, а размер строки кеша - 64 байта, так что строка кеша будет использоваться только для этой переменной, что позволяет избежать строки кеша. Псевдо-совместное использование, но в jdk7 метод дает сбой, потому что недопустимое поле очищается, а заполнения поля можно избежать, только унаследовав поле родительского класса, в то время как jdk8 предоставляет аннотации
@Contended, чтобы указать, что эта переменная или объект будет иметь эксклюзивную строку кэша.Чтобы использовать эту аннотацию, вы должны добавить параметр -XX:-RestrictContended при запуске JVM, который фактически обменивает пространство на время.
jdk6 --- 32 位系统下
public final static class VolatileLong
{
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // 填充字段
}
jdk7 通过继承
public class VolatileLongPadding {
public volatile long p1, p2, p3, p4, p5, p6; // 填充字段
}
public class VolatileLong extends VolatileLongPadding {
public volatile long value = 0L;
}
jdk8 通过注解
@Contended
public class VolatileLong {
public volatile long value = 0L;
}
NPTL и модель многопоточности Java
Согласно определению учебников, процесс — это наименьшая единица управления ресурсами, а поток — наименьшая единица планирования и выполнения ЦП.Появление потоков — уменьшение переключения контекста процессов (переключение контекста потоков значительно меньше, чем у процессов), а также лучшую адаптацию. Среда ядра процессора, например, несколько потоков в рамках одного процесса могут выполняться на разных процессорах, а поддержка многопоточности может быть реализована как в ядре Linux, так и вне ядра ядра. Если он размещен за пределами ядра, его нужно только завершить. Переключение работающего стека имеет небольшие накладные расходы на планирование, но этот метод не может адаптироваться к многопроцессорной среде. Базовый процесс по-прежнему выполняется на одном ЦП. Кроме того, из-за высоких требований к пользовательскому программированию текущие основные операционные системы поддерживают потоки в ядре.В Linux поток — это упрощенный процесс, который оптимизирует только накладные расходы на планирование потоков.В JVM потоки и потоки ядра находятся в ядре. взаимно-однозначное соответствие, а планирование потоков полностью передано ядру.
Когда Thread.run, он создаст поток ядра через системный вызов fork(), этот метод будет переключаться между режимом пользователя и режимом ядра, производительность, конечно, не такая высокая, как у потока в пользовательском режиме, потому что поток ядра используется напрямую, поэтому максимальное количество потоков, которые могут быть созданы, также контролируется ядром Текущая модель потоков в Linux — NPTL (Native POSIX Thread Library), которая использует режим «один к одному», совместима со стандартами POSIX. , и не использует потоки управления, что может быть лучше при работе с многоядерными процессорами.
состояние потока
Для процесса есть три состояния: готов, запущен и заблокирован.В JVM есть четыре типа блокировки.Мы можем создать файл дампа через jstack для просмотра состояния потока.
ЗАБЛОКИРОВАНО (на мониторе объектов) При получении блокировки через блок синхронизации synchronized(obj) подождите, пока другие потоки снимут блокировку объекта, в файле дампа будет показано ожидание блокировки
TIMED WAITING (на мониторе объектов) и WAITING (на мониторе объектов) После получения блокировки вызовите object.wait(), чтобы дождаться, когда другие потоки вызовут object.notify(), разница между ними заключается в том, есть ли тайм-аут
Программа TIMED WAITING (спящая) вызывает thread.sleep(), где, если sleep(0) не переходит в состояние блокировки, она напрямую переключается из режима работы в режим готовности.
Программы TIMED WAITING (парковка) и WAITING (парковка) вызывают Unsafe.park(), поток приостанавливается, ожидая выполнения условия, ожидая выполнения условия
В стандарте POSIX thread_block принимает параметр stat, который также имеет три типа: TASK_BLOCKED, TASK_WAITING, TASK_HANGING, и планировщик будет планировать только потоки, статус потока которых READY.Другой момент заключается в том, что блокировка потока является собственной операцией потока. эквивалентно тому, что поток активно отдает квант времени ЦП, поэтому после пробуждения потока его оставшийся квант времени не изменится, и поток может работать только в оставшемся квант времени. В конце состояние потока будет преобразован из RUNNING в READY, ожидая следующего планирования планировщика.
Что ж, давайте разберем нить.По поводу пакета Java concurrent, ядро находится в AQS.Нижний слой реализован через метод cas класса UnSafe и метод park.Позже найдем время разобрать его отдельно.Сейчас мы смотрите на линукс.Схема синхронизации процессов.
POSIX расшифровывается как Portable Operating System Interface of UNIX (сокращенно POSIX), а стандарт POSIX определяет стандарт интерфейса, который операционная система должна предоставлять для приложений.
Операция CAS требует поддержки ЦП, а сравнение и обмен выполняются как одна инструкция.CAS обычно имеет три параметра, расположение в памяти, ожидаемое исходное значение и новое значение.Поэтому compareAndSwap в классе UnSafe использует смещение атрибута относительно начальный адрес объекта для размещения в памяти.
синхронизация потоков
Основная причина синхронизации потоков заключается в том, что для доступа к общим ресурсам требуется несколько операций, и процесс выполнения этих нескольких операций не является атомарным, разделенным планировщиком задач, и другие потоки уничтожат общие ресурсы, поэтому необходимо сделать потоки в критической секции Синхронизация, здесь мы сначала разъясняем концепцию, то есть критическую секцию, которая относится к инструкциям, когда несколько задач обращаются к общим ресурсам, таким как память или файлы, которые являются инструкциями, а не ресурсами, к которым нужно получить доступ.
POSIX определяет пять объектов синхронизации, блокировки взаимного исключения, условные переменные, спин-блокировки, блокировки чтения-записи и семафоры.Эти объекты также имеют соответствующие реализации в JVM, и не все из них используют API, определенные POSIX, которые реализованы в Java. Он более гибкий и позволяет избежать накладных расходов на вызов собственных методов. Конечно, нижний уровень в конечном итоге полагается на мьютекс мьютекса pthread для достижения. Это системный вызов с большими накладными расходами, поэтому JVM автоматически обновляется и обновляет замок. Реализация на основе AQS будет проанализирована позже. Здесь мы в основном говорим о ключевом слове synchronized.
При объявлении блока синхронизированного кода скомпилированный байт-код будет содержать monitorenter и несколько monitorexit (несколько путей выхода, нормальные и ненормальные условия), а при выполнении monitorenter будет проверять, равен ли счетчик целевого объекта блокировки 0, если он равен 0, установите удерживающий поток объекта блокировки на себя, затем добавьте 1 к счетчику, чтобы получить блокировку, если он не равен 0, проверьте, является ли удерживающий поток объекта блокировки самим собой, и если это так, добавьте 1 к счетчику, чтобы получить его Блокировка, если нет, заблокируйте и подождите.При выходе счетчик уменьшается на 1. Когда он уменьшается до 0, поток, удерживающий метку потока объекта блокировки, очищается.Видно, что синхронизировано поддерживает реентерабельность.
Только что упомянули, что блокировка потока — это системный вызов с большими накладными расходами, поэтому JVM разработала адаптивную блокировку вращения, то есть, когда блокировка не получена, ЦП возвращается в состояние вращения и ждет, пока другие потоки снимут блокировку. и время вращения в основном Посмотрите, сколько времени потребовалось для получения блокировки в прошлый раз. Например, последнее вращение в течение 5 миллисекунд не захватило блокировку. На этот раз это 6 миллисекунд. Вращение приведет к тому, что ЦП будет работать всухую. Другим побочным эффектом является механизм несправедливой блокировки, поскольку поток Spin автоматически получает блокировку, в то время как другие блокирующие потоки все еще находятся в ожидании. к нескольким потокам и блокировкам будет доступ только один Ситуация использования потока Последние две блокировки эквивалентны отказу от вызова базовой реализации семафора (семафор используется для управления потоком A для снятия блокировки, например вызовом wait(), и поток B может получить блокировку, которая может быть реализована только ядром, последние два не должны контролироваться базовым семафором, потому что в сцене нет конкуренции), но они поддерживают отношения удержания блокировки в пользовательском пространстве. , поэтому они более эффективны.
Как показано на рисунке выше, если поток войдет в monitorenter, он поместит себя в очередь entryset объекта objectmonitor и затем заблокируется.Если текущий удерживающий поток вызовет метод ожидания, блокировка будет снята, а затем будет инкапсулирована. в objectwaiter и помещается в очередь ожидания объекта objectmonitor. В это время поток в очереди набора записей будет конкурировать за блокировку и входить в активное состояние. Если поток вызывает метод уведомления, он достанет первый объект ожидания объекта waitset и положить его в entryset (на этот раз по стратегии может быть Spin first), когда поток, вызывающий notify, выполняет moniterexit для снятия блокировки, потоки в entryset начинают конкурировать за блокировку и входят в активное состояние.
Чтобы защитить приложение от вмешательства конкуренции данных, модель памяти Java определяет «происходит до» для описания видимости двух операций в памяти, то есть операция X происходит до операции Y, тогда результат операции X виден пользователю. Y. В JVM существует правило «происходит до» для реализации volatile и блокировок. Нижний уровень JVM ограничивает переупорядочивание компилятора, вставляя барьер памяти. Взяв volatile в качестве примера, барьер памяти не позволит оператору раньше операция записи изменчивого поля должна быть переупорядочена на операцию записи. Позже оператор после чтения изменчивого поля также не может быть переупорядочен перед оператором чтения. Инструкции, которые вставляют барьеры памяти, будут иметь разные эффекты в зависимости от типа инструкции. Например, после того, как monitorexit снимет блокировку, кэш будет принудительно обновляться, в то время как volatile Соответствующий барьер памяти будет принудительно сбрасываться в основную память после каждой записи, и из-за характеристик поля volatile компилятор не может этого сделать. выделите его в регистр, чтобы он каждый раз считывался из основной памяти, поэтому volatile подходит для многократного чтения. кеш постоянно обновляется из-за частых записей, это повлияет на производительность.
Что касается подходящего количества потоков для установки в приложении, наша общая практика заключается в том, чтобы установить максимальное количество ядер ЦП * 2. Когда мы пишем код, мы можем не знать, в какой аппаратной среде работать. Мы можем использовать Runtime. getRuntime(). availableProcessors() для получения ядер ЦП,
Однако конкретное количество потоков, которое необходимо установить, в основном связано со временем блокировки в задачах, выполняемых в потоках.Если все задачи требуют интенсивных вычислений, то для достижения максимальной загрузки ЦП необходимо установить только потоки с числом ядер ЦП. использование.Если он слишком велик, это влияет на производительность из-за переключения контекста потока.Если в задаче есть блокирующая операция, ЦП может быть разрешено выполнять задачи в других потоках во время блокировки.Мы можем передать количество потоков = количество ядер / (1 - показатель блокировки) Эта формула используется для расчета наиболее подходящего количества потоков. Уровень блокировки можно получить, рассчитав общее время выполнения и время блокировки задачи. В настоящее время существует большое количество вызовов RPC в архитектуре микросервиса, поэтому использование многопоточности может значительно повысить эффективность выполнения Мы можем использовать мониторинг распределенных ссылок для подсчета времени, затрачиваемого вызовами RPC, и эта часть времени — это время, заблокированное в Конечно, чтобы добиться максимальной эффективности, нам нужно установить разные значения, а затем протестировать.
Как реализовать запланированные задачи в Java
Таймеры уже являются неотъемлемой частью современного программного обеспечения, например, проверка статуса каждые 5 секунд, есть ли новая электронная почта, реализация будильника и т. д. Уже есть готовые API на Java для использования, но если вы хотите проектируйте более эффективно, для более точных задач таймера вам необходимо понимать базовые знания об оборудовании.Например, для реализации промежуточного программного обеспечения распределенного планирования задач вам может потребоваться рассмотреть проблему синхронизации часов между приложениями.
В Java есть два способа реализовать временные задачи: один — с помощью класса таймера, а другой — с помощью ScheduledExecutorService в JUC. Интересно, интересно ли вам, как JVM реализует временные задачи. Продолжаете ли вы опрашивать время, чтобы узнать, Когда время истекло, если время истекло, будет вызвана соответствующая задача обработки, но такой непрерывный опрос без освобождения ЦП определенно не рекомендуется, или поток блокируется, и когда время истекло, чтобы проснуться нить, как JVM узнает, что время вышло и как проснуться???
Прежде всего, давайте посмотрим на JDK и обнаружим, что есть около 3 API, связанных со временем, и эти 3 места также различают точность времени:
-
object.wait(long миллисекунда) Параметр в миллисекундах, который должен быть больше или равен 0. Если он равен 0, он будет заблокирован до тех пор, пока не проснутся другие потоки.Класс таймера реализуется методом wait(). Давайте рассмотрим еще один метод ожидания.
public final void wait(long timeout, int nanos) throws InterruptedException { if (timeout < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos > 0) { timeout++; } wait(timeout); }
Этот метод предназначен для обеспечения тайм-аута, который может поддерживать наносекунды, но добавляет примерно 1 миллисекунду.
-
Thread.sleep(длинная миллисекунда) В настоящее время ЦП обычно освобождается таким образом.Если параметр равен 0, это означает, что ЦП освобождается для потока с более высоким приоритетом, и он переключается из состояния выполнения в состояние готовности к выполнению. подождите, пока ЦП запланирует, Он также предоставляет Он может поддерживать реализацию метода наносекунд, а отличие от ожидания в том, что оно разделено на 500000, нужно ли добавить 1 миллисекунду.
public static void sleep(long millis, int nanos) throws InterruptedException { if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos >= 500000 || (nanos != 0 && millis == 0)) { millis++; } sleep(millis); }
Метод, вызываемый LockSupport.park(long nans) Condition.await(), и condition.await(), используемый ScheduledExecutorService для блокировки определенного периода тайм-аута, и другие методы с параметрами тайм-аута также реализуются через него. таймеры Все достигается с помощью этого метода, который также предоставляет логическое значение для определения точности времени
Как System.currentTimeMillis(), так и System.nanoTime() зависят от базовой операционной системы: первый находится на миллисекундном уровне, а частота тестируемой платформы Windows может превышать 10 мс, а второй — наносекундном уровне с частотой около 100 нс, поэтому, если вы хотите получить более точное время, рекомендуется использовать последний
Что ж, API сделано, давайте посмотрим, как реализован нижний слой таймера.В современных ПК есть три реализации аппаратных часов, и все они завершают синхронизацию тактового сигнала через вход прямоугольного сигнала, генерируемого вибрацией кристалла. .
Часы реального времени RTC, устройство, используемое для хранения системного времени в течение длительного времени, могут полагаться на батарею в материнской плате, чтобы продолжать отсчет времени, даже если она выключена.При запуске Linux он будет считывать время и дату с часов реального времени. в качестве начального значения, а затем использовать другое время во время работы, чтобы сохранить системное время
Программируемый интервальный таймер PIT, счетчик будет иметь начальное значение, и начальное значение будет уменьшаться на 1 каждый раз, когда проходит тактовый цикл.Когда начальное значение уменьшается до 0, тактовое прерывание отправляется на ЦП по проводу, и ЦП сможет выполнить соответствующую программу прерывания, то есть вызвать соответствующую задачу
Счетчик меток времени TSC, все ЦП Intel8086 содержат регистр, соответствующий счетчику меток времени, значение этого регистра будет увеличиваться на 1 каждый раз, когда ЦП получает сигнал прерывания одного тактового цикла.Он имеет более высокую точность, чем PIT, но не может программировать , могу только читать.
Тактовый цикл: Как долго аппаратный таймер генерирует тактовые импульсы, а частота тактового цикла — это количество тактовых импульсов, генерируемых за 1 секунду.В настоящее время обычно это 1193180.
Такт часов: когда начальное значение в PIT уменьшается до 0, будет сгенерировано прерывание часов, и это начальное значение задается программированием.
Когда Linux запускается, он сначала получает начальное время через RTC, а затем ядро поддерживает дату через такт таймера в PIT и регулярно записывает дату в RTC, в то время как таймер прикладной программы в основном устанавливается путем установки начальное значение PIT.Значение задано.Когда начальное значение уменьшено до 0, это означает, что функция обратного вызова будет выполнена.Будут ли у вас тут какие-то сомнения,чтобы программа-таймер могла быть только одна одновременно, и мы в прикладной программе, и еще между приложениями,
Задач по таймеру должно быть много. На самом деле мы можем обратиться к реализации ScheduledExecutorService. Нам нужно только отсортировать эти запланированные задачи по времени. Более ранние задачи, которые должны быть выполнены, помещаются на передний план. Когда приходит первая задача , вторая задача относительно Значение текущего времени, ведь ЦП может одновременно выполнять только одну задачу.Что касается точности времени, то мы не можем сделать это с полной точностью на программном уровне.Ведь, планирование ЦП не полностью контролируется пользовательской программой.Конечно, большая зависимость от оборудования.Частота тактового цикла, текущий TSC может улучшить более высокую точность.
Теперь мы знаем, что время ожидания в Java достигается установкой начального значения через программируемый интервальный таймер и последующим ожиданием сигнала прерывания.На точность влияет аппаратный тактовый цикл, который обычно находится на уровне миллисекунд.В конце концов, скорость света в 1 наносекунде составляет всего 3 метра, поэтому реализация с наносекундными параметрами в JDK — грубая практика, ожидание появления таймера с большей точностью, а получение текущего времени System.currentTimeMillis() будет эффективнее, но это миллисекундная точность, он считывает дату, поддерживаемую ядром Linux, и System.nanoTime() будет предпочтительно использовать TSC, и производительность немного ниже, но она находится в наносекундах, а класс Random использует nanoTime для генерации семян для предотвратить конфликты.
Как Java взаимодействует с внешними устройствами
К внешним устройствам компьютера относятся мышь, клавиатура, принтер, сетевая карта и т. д. Обычно мы называем передачу информации между внешним устройством и основной памятью операцией ввода-вывода. В соответствии с характеристиками операции ее можно разделить на вывод устройство, устройство ввода, устройства хранения.Современные устройства используют каналы для взаимодействия с основной памятью.Канал — это устройство, специально используемое для обработки задач ввода/вывода.Когда ЦП встречает запрос ввода/вывода при обработке основной программы, он запускает выбранное устройство на указанном канале. Запуск прошел успешно, и канал начинает управлять работой устройства
, и ЦП может продолжать выполнять другие задачи.После завершения операции ввода-вывода канал выдает прерывание для завершения операции ввода-вывода, и процессор переключается на обработку событий после завершения ввода-вывода.Другие способы обработки ввода-вывода, таких как опрос, прерывания, DMA , с точки зрения производительности, нет канала, поэтому он не будет здесь представлен.Конечно, связь между Java-программами и внешними устройствами также осуществляется через системные вызовы, и мы тут дальше не пойдет.
об авторе
Сяоцян, технический инженер-разработчик Tongbanjie Capital, присоединился к Tongbanjie в июне 2015 года. В настоящее время отвечает за развитие, связанное с поселением и расчетом средств на улице Тонгбан.
Для получения более интересного контента отсканируйте код, чтобы подписаться на общедоступную учетную запись WeChat «Tongbanjie Technology».